目錄
一、分析拉伸的原因
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、它們的頂點數據均為:2、借助 Matlab 把頂點數據繪制出來:
從圖可以看出,這三個數據形成的其實是一個等邊直角三角形,而在 iOS 模擬器中通過 OpenGL ES 繪制出來的是直角三角形,所以是有問題的,三角形被拉伸了。
3、on-Screen (屏幕) 的像素分布情況:
iPhone6s Plus 屏幕:5.5寸,1920 x 1080 像素分辨率,明顯寬高比不是 1:1 的;
-
OpenGL ES 的屏幕坐標系 與 物理屏幕的坐標系對比:
OpenGL ES 的屏幕坐標系
物理屏幕的坐標系
分析:前者是正方體,后者長方體,不拉伸才怪。
- 首先,OpenGL 最后生成的都是像素信息,再顯示在物理屏幕上;通過 1) 和 2) 可以知道 Y 方向的像素數量大于 X 方向的像素數量,導致真實屏幕所生成的 Y 軸與 X 軸的刻度不一致(就是Y=0.5 > X=0.5),從而引起了最后渲染繪制出來的圖形是向 Y 方向拉伸了的。
動畫演示修復:
所以要做的事情是,把頂點坐標的 Y 坐標變小,而且是要根據當前顯示屏幕的像素比來進行縮小。
Gif 圖片,由 C4D 制作,PS 最終導出;
- 在 Shader 里面,v_Position 的數據類型是 vec4 ,即為4分量的向量數據{x,y,z,w};就是說,要把這個向量通過數學運算變成適應當前屏幕的向量。
二、準備知識,三維變換
-- 建議 --:如果向量、矩陣知識不熟悉的可以看看《線性代數》一書;如果已經有相應的基礎了,可以直接看《3D數學基礎:圖形與游戲開發》,了解 3D 的世界是如何用向量和矩陣知識描述的;若對 3D 知識有一定的認識,可以直接看《OpenGL Programming Guide》8th 的變換知識, 或 《OpenGL Superblble》7th 的矩陣與變換知識,明確 OpenGL 是如何應用這些知識進行圖形渲染的。
注:以下核心知識均來源于,《3D數學基礎:圖形與游戲開發》,建議看一下第8章;
圖片通過 sketch 制作,請放大看
1、4 x 4 方陣
- 它其實就是一個齊次矩陣,是對3D運算的一種簡便記法;
- 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 使用的是什么向量?
- 英文大意:矩陣和矩陣乘法在處理坐標系顯示模型方面是一個非常有用的途徑,而且對于處理線性變換而言也是非常方便的機制。
紅框處的向量就是 v_Position 頂點數據;即 OpenGL 用的是列向量;(木有找到更有力的證據,只能這樣了)
- 左乘右乘問題?
- 英文大意:在我們的視圖模型中,我們想通過一個向量來與矩陣變換進行乘法運算,這里描述了一個矩陣乘法,向量先乘以 A 矩陣再乘以 B 矩陣:
很明顯,例子使用的就是左乘,即 OpenGL 用的是左乘;
圖 1、3 來源于,《OpenGL Programming Guide 8th》第5章第二節
圖 2 來源于,《3D數學基礎:圖形與游戲開發》7.1.8
3、單次三維變換與多次三維變換問題
- OpenGL 的三維變換整體圖:
因為列向量的影響,在做點乘的時候,平移放在下方與右側是完全不一樣的結果,所以進行了適應性修改
- 平移部分的內容:
- 矩陣平移公式
等式左側:A(4x4)方陣點乘{v.x, v.y, v.z, 1.0}是頂點數據列向量;右側就是一個 xyz 均增加一定偏移的列向量
圖片來源于,《OpenGL Superblble》7th, Part 1, Chapter 4. Math for 3D Graphics
- 投影(就是零)
- 所有的變換圖例演示
物體的坐標是否與屏幕坐標原點重疊
- 單次變換(原點重疊)
無變換,即此矩陣與任一向量相乘,不改變向量的所有分量值,能做到這種效果的就是單位矩陣,而我們使用的向量是齊次坐標{x, y, z, w},所以使用 4 x 4 方陣;{w === 1}.
- 縮放
單一的線性變換——縮放,縮放變換是作用在藍色區域的 R(3x3) 方陣的正對角線(從m11(x)->m22(y)->m33(z))中;例子是 X、Y、Z 均放大 3 倍。
- 旋轉
單一的線性變換——旋轉,旋轉變換是作用在藍色區域的 R(3x3) 方陣中;例子是繞 Z 軸旋轉 50 度。
- 平移
單一的線性變換——平移,平移變換是作用在綠色區域的 R(3x1) 矩陣中({m11, m21, m31}對應{x, y, z});例子是沿 X 正方向平移 2.5 個單位。
- 單次變換(原點不重疊)
以上圖片內容來源于《OpenGL Programming Guide》8th, Linear Transformations and Matrices 一小節,使用 skecth 重新排版并導出
- 多次變換
這里的問題就是先旋轉還是后旋轉。旋轉前后,變化的是物體的坐標系(虛線(變換后),實線(變換前)),主要是看你要什么效果,而不是去評論它的對錯。
圖片來源于,《OpenGL Superblble》7th, Matrix Construction and Operators 一節;
4、OpenGL 的變換是在那個階段發生的,如何發生
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);
};
它們都定義在:
注:如果不想自己去寫這些函數,那么可以直接使用 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 代碼分析類
核心的知識是正則表達式,主要是把代碼中的變量解析出來,可以對它們做大規模的處理。有興趣可以看一下,沒有興趣的可以忽略它完全不影響學習和練習本文的內容。