OpenGL ES 2.0 (iOS)[02]:修復三角形的顯示


目錄

一、分析拉伸的原因

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

二、準備知識,三維變換

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

三、OpenGL 下的三維變換

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

四、修復拉伸問題

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

一、分析拉伸的原因

1、修復前后照片對比

問題與目標

圖片通過 sketch 制作

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

1、它們的頂點數據均為:
頂點數組

VFVertex

2、借助 Matlab 把頂點數據繪制出來:


分布圖

從圖可以看出,這三個數據形成的其實是一個等邊直角三角形,而在 iOS 模擬器中通過 OpenGL ES 繪制出來的是直角三角形,所以是有問題的,三角形被拉伸了。

3、on-Screen (屏幕) 的像素分布情況:

  1. iPhone6s Plus 屏幕:5.5寸,1920 x 1080 像素分辨率,明顯寬高比不是 1:1 的;

  2. OpenGL ES 的屏幕坐標系 與 物理屏幕的坐標系對比:


    OpenGL ES 的屏幕坐標系

    物理屏幕的坐標系

分析:前者是正方體,后者長方體,不拉伸才怪。

  1. 首先,OpenGL 最后生成的都是像素信息,再顯示在物理屏幕上;通過 1) 和 2) 可以知道 Y 方向的像素數量大于 X 方向的像素數量,導致真實屏幕所生成的 Y 軸與 X 軸的刻度不一致(就是Y=0.5 > X=0.5),從而引起了最后渲染繪制出來的圖形是向 Y 方向拉伸了的。

動畫演示修復:


FixTriangle.gif

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

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

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

二、準備知識,三維變換

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

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

4x4 整體

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

1、4 x 4 方陣

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

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

線性變換
  • n,是標準化向量,而向量標準化就是指單位化:
    normalied

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

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

  • k,是一個常數;

  • a,是一個弧度角;

1) 線性縮放

線性縮放
  • XYZ 方向的縮放:


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

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

2) 線性旋轉

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


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


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


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

3、平移

平移

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

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

4、向量(四元數)

四元數

a.向量,即4D向量,也稱齊次坐標{x, y, z, w}; 4D->3D,{x/w, y/w, z/w};

b.四元數,[ w, v ]或[ w, (x,y,z) ]兩種記法,其中 w 就是一個標量,即一個實數;

c.點乘

矩陣乘法,點乘

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

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

5、w 與 其它

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

  • W
    w

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

  • 投影
    投影

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


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


三、OpenGL 下的三維變換

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

1、OpenGL 的坐標系

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


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

  • 旋轉的正方向


    右手坐標

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

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

  • 這里討論的核心是,gl_Position 接收的是 行向量,還是列向量?
行向量
列向量
  • 討論行列向量的目的是明確,3D 矩陣變換在做乘法的時候是使用左乘還是右乘;

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

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

  • 那么 OpenGL 使用的是什么向量?
圖1,列向量
  • 英文大意:矩陣和矩陣乘法在處理坐標系顯示模型方面是一個非常有用的途徑,而且對于處理線性變換而言也是非常方便的機制。
圖2

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

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

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

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

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

多次變換
  1. OpenGL 的三維變換整體圖:
4x4 整體 OpenGL

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

  • 平移部分的內容:
4X4方陣 OpenGL
平移 OpenGL
  • 矩陣平移公式

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

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

  • 投影(就是零)
投影 OpenGL
  1. 所有的變換圖例演示

物體的坐標是否與屏幕坐標原點重疊

Linaer Transforms
  • 單次變換(原點重疊)
Identity

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

  • 縮放
Scale

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

  • 旋轉
Rotate

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

  • 平移
Translation

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

  • 單次變換(原點不重疊)
Translation&Scale
Translation&Rotate

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

  1. 多次變換
連續變換

這里的問題就是先旋轉還是后旋轉。旋轉前后,變化的是物體的坐標系(虛線(變換后),實線(變換前)),主要是看你要什么效果,而不是去評論它的對錯。

圖片來源于,《OpenGL Superblble》7th, Matrix Construction and Operators 一節;

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

3D變換

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

變換轉換

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

詳細過程

作為了解即可

以上圖片均來源于,《OpenGL Programming Guide》8th, 5. Viewing Transformations, Clipping, and Feedback 的 User Transformations 一節;


四、修復拉伸問題

1、改寫 Shader Code

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

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

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

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

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

核心步驟

1、在 glLinkProgram 函數之后,利用 glGetUniformLocation 函數得到 uniform 變量的 location (內存標識符);

2、從 Render Buffer 得到屏幕的像素比(寬:高)值,即為縮小的值;

3、使用 Shader Program , 調用 glUseProgram 函數;

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

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

  • 如何給 uniform 變量賦值?

1、得到 uniform 的內存標識符

要在 glLinkProgram 后,再獲取 location 值,因為只有鏈接后 Program 才會 location 的值

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

補充:

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

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

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

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

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

2、得到屏幕的像素比

- (CGFloat)windowScaleFactor {
    
    CGSize renderSize = [self renderBufferSize];
    float scaleFactor = (renderSize.width / renderSize.height);
    
    return scaleFactor;
    
}

補充:renderBufferSize

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

3、使用 Shader Program

- (void)useShader {
    
    glUseProgram(self.shaderProgramID);
    
}

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

 VFMatrix4 scaleMat4 = VFMatrix4MakeScaleY(scale);

擴展1:

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

它們都定義在:


VFMath

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

數學函數

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

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

- (void)makeScaleToFitCurrentWindowWithScale:(float)scale {
    
    NSDictionary *vertexShaderValueInfos = self.shaderCodeAnalyzer.shaderFileValueInfos[@"VFVertexShader"];
    ValueInfo_Dict *uniforms = vertexShaderValueInfos[UNIFORM_VALUE_DICT_KEY];
//    NSLog(@"uniforms %@", [uniforms allKeys]);
    
    // v_Projection 投影
//    VFMatrix4 scaleMat4 = VFMatrix4Identity;
    VFMatrix4 scaleMat4 = VFMatrix4MakeScaleY(scale);
    VFMatrix4 transMat4 = VFMatrix4Identity; //VFMatrix4MakeTranslationX(0.3)
    glUniformMatrix4fv((GLint)uniforms[@"v_Projection"].location,   // 定義的 uniform 變量的內存標識符
                       1,                                           // 不是 uniform 數組,只是一個 uniform -> 1
                       GL_FALSE,                                    // ES 下 只能是 False
                       (const GLfloat *)scaleMat4.m1D);             // 數據的首指針
    
    glUniformMatrix4fv((GLint)uniforms[@"v_Translation"].location,   // 定義的 uniform 變量的內存標識符
                       1,                                           // 不是 uniform 數組,只是一個 uniform -> 1
                       GL_FALSE,                                    // ES 下 只能是 False
                       (const GLfloat *)transMat4.m1D);             // 數據的首指針

}

擴展2:

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


3、完整工程:Github: DrawTriangle_Fix

glsl 代碼分析類


核心的知識是正則表達式,主要是把代碼中的變量解析出來,可以對它們做大規模的處理。有興趣可以看一下,沒有興趣的可以忽略它完全不影響學習和練習本文的內容。


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容