[OpenGL ES 05]相對空間變換及顏色
[OpenGL ES 05]相對空間變換及顏色
羅朝輝 (http://www.rzrgm.cn/kesalin/)
本文遵循“署名-非商業用途-保持一致”創作公用協議
這是《OpenGL ES 教程》的第五篇,前四篇請參考如下鏈接:
[OpenGL ES 01]iOS上OpenGL ES之初體驗
[OpenGL ES 02]OpenGL ES渲染管線與著色器
[OpenGL ES 03]3D變換:模型,視圖,投影與Viewport
[OpenGL ES 04]3D變換實踐篇:平移,旋轉,縮放
前言
前面已經花了兩篇文章來講 3D 變換,可 3D 變換實在不是區區兩篇文章能講的透的,為了盡量將這個話題講得全面點,在這篇本來講顏色的文章里再順帶講講相對空間變換這個還沒有提及的話題。相對空間變換類似于為“本地”坐標到“世界”坐標的變換,就是坐標空間之間的變換。你可以想象下這樣一個動作,抬起胳膊同時彎曲手臂,手臂相當于在胳膊的坐標空間中旋轉,而胳膊所在空間又相當于在身體或胳膊的坐標空間中旋轉。就好像空間是嵌套的,一層套一層,外層會影響里層,里層的變換是相對于外層進行的。此外,還將介紹如何在 OpenGL ES 中使用顏色以及背面剔除。
今天將演示這樣一個相對運動與顏色的示例,示例運行效果如下圖所示,源碼在這里:點此查看

一,相對變換
1,創建工程,本文是建立在前文[OpenGL ES 04]3D變換實踐篇:平移,旋轉,縮放的基礎上的,如果你還沒有閱讀過那篇文章,請參考前文如何創建一個 UIView 與 OpenGL View 共存的工程,以及如何通過UI控件來操控 3D 世界的物體。參照效果圖與前文,建立兩個UIView,兩個UISlider 和一個按鈕,并與 UIViewController.h 中的如下屬性或方法關聯:
@property (nonatomic, strong) IBOutlet OpenGLView * openGLView; - (IBAction) OnShoulderSliderValueChanged:(id)sender; - (IBAction) OnElbowSliderValueChanged:(id)sender; - (IBAction) OnRotateButtonClick:(id)sender;
2,記得在 UIViewController.m 中清理 openGLView 資源:
- (void)viewDidUnload
{
[super viewDidUnload];
[self.openGLView cleanup];
self.openGLView = nil;
}
3,實現 UIViewController.m 中相關的事件響應方法,今天讓我們來實現下“測試驅動”,先寫接口的使用,然后再實現該接口:
- (void) OnShoulderSliderValueChanged:(id)sender
{
UISlider * slider = (UISlider *)sender;
float currentValue = [slider value];
NSLog(@" >> current shoulder is %f", currentValue);
self.openGLView.rotateShoulder = currentValue;
}
- (void) OnElbowSliderValueChanged:(id)sender
{
UISlider * slider = (UISlider *)sender;
float currentValue = [slider value];
NSLog(@" >> current elbow is %f", currentValue);
self.openGLView.rotateElbow = currentValue;
}
- (IBAction) OnRotateButtonClick:(id)sender
{
[self.openGLView toggleDisplayLink];
UIButton * button = (UIButton *)sender;
NSString * text = button.titleLabel.text;
if ([text isEqualToString:@"Rotate"]) {
[button setTitle: @"Stop" forState: UIControlStateNormal];
}
else {
[button setTitle: @"Rotate" forState: UIControlStateNormal];
}
}
4,從上面的代碼中可以看出,我們需要兩個公開屬性:rotateShoulder 和 rotateElbow,分別代碼胳膊和手臂的繞 x 軸的旋轉量,然后還需要一個公開方法:toggleDisplayLink,用來控制彩色正方體的旋轉。因此我們需要三個旋轉量,兩個公開的,一個私有的。在 OpenGLView.h 中聲明兩個公開的變量,
@property (nonatomic, assign) float rotateShoulder; @property (nonatomic, assign) float rotateElbow; - (void)render; - (void)cleanup; - (void)toggleDisplayLink;
然后在 OpenGLView.h 的匿名 category 中聲明如下變量:
@interface OpenGLView()
{
KSMatrix4 _shouldModelViewMatrix;
KSMatrix4 _elbowModelViewMatrix;
float _rotateColorCube;
CADisplayLink * _displayLink;
}
和方法:
- (void)updateShoulderTransform; - (void)updateElbowTransform; - (void)resetTransform; - (void)updateRectangleTransform; - (void)updateColorCubeTransform; - (void)drawColorCube; - (void)drawCube:(KSVec4) color;
下面來解釋這些代碼,因為要實現相對運動的效果,我們需要保留每個模型的模型視圖矩陣,_shouldModelViewMatrix 對應的是胳膊的模型視圖矩陣,而 _elbowModelViewMatrix 對應的是手臂的模型視圖矩陣。后者是在前者的基礎上進行的,這是什么意思呢?你還記得前面介紹模型變換,視圖變換,投影變換的次序過程么?這里我們同樣也可以類推下,手臂所在的空間的物體先變換到胳膊所在空間,然后胳膊所在空間(包括從手臂空間變換來的物體)一起經胳膊模型視圖變換到視圖空間中去。updateShoulderTransform 和 updateElbowTransform 分別用來更新胳膊和手臂的模型視圖矩陣,下面我們來看看它們的實現:
- (void) updateShoulderTransform
{
ksMatrixLoadIdentity(&_shouldModelViewMatrix);
ksTranslate(&_shouldModelViewMatrix, -0.0, 0.0, -5.5);
// Rotate the shoulder
//
ksRotate(&_shouldModelViewMatrix, self.rotateShoulder, 0.0, 0.0, 1.0);
// Scale the cube to be a shoulder
//
ksCopyMatrix4(&_modelViewMatrix, &_shouldModelViewMatrix);
ksScale(&_modelViewMatrix, 1.5, 0.6, 0.6);
// Load the model-view matrix
glUniformMatrix4fv(_modelViewSlot, 1, GL_FALSE, (GLfloat*)&_modelViewMatrix.m[0][0]);
}
- (void) updateElbowTransform
{
// Relative to shoulder
//
ksCopyMatrix4(&_elbowModelViewMatrix, &_shouldModelViewMatrix);
// Translate away from shoulder
//
ksTranslate(&_elbowModelViewMatrix, 1.5, 0.0, 0.0);
// Rotate the elbow
//
ksRotate(&_elbowModelViewMatrix, self.rotateElbow, 0.0, 0.0, 1.0);
// Scale the cube to be a elbow
ksCopyMatrix4(&_modelViewMatrix, &_elbowModelViewMatrix);
ksScale(&_modelViewMatrix, 1.0, 0.4, 0.4);
// Load the model-view matrix
glUniformMatrix4fv(_modelViewSlot, 1, GL_FALSE, (GLfloat*)&_modelViewMatrix.m[0][0]);
}
還記得在第三篇文章講過,理解 3D 變換是一個順序,而寫代碼時卻是反序么?在這里也是一樣的,我們先更新胳膊的模型視圖變換,然后在此基礎上再更新手臂的模型變換,也就是首先旋轉胳膊,然后在此基礎上再旋轉手臂。在這里你或許會對那兩個 scale 感到奇怪,這是因為我偷懶了--使用同一個 drawCube 的方法來描繪胳膊和手臂,該 Cube 長寬高均為1,為了讓胳膊和手臂的大小不一樣,所以需要進行不同的縮放,而這個縮放是在各自的本地空間進行的,不影響其它“子”空間。因此這里用來描繪胳膊的縮放動作不應該影響手臂空間,因此,這個縮放沒有保存到 _shouldModelViewMatrix 里去。你也可以單獨描繪不同大小的胳膊和手臂,從而不需要進行縮放,也許這樣更好理解一點。
更新手臂的模型視圖矩陣時,我們是在胳膊的模型視圖矩陣基礎上進行的,也就是說對胳膊的模型變換(在本列中是旋轉)也會對手臂產生影響,這是通過語句:ksCopyMatrix4(&_elbowModelViewMatrix, &_shouldModelViewMatrix);來實現的。然后我們需要將手臂偏移到胳膊上手臂的尾端,前面說過我們在胳膊 Cube 的 x 方向上進行了 1.5 倍的放大,而 Cube 的長寬高均為1,因此胳膊的實際寬是 1.5,因此我們需要右移 1.5 個單位。然后再進行手臂自身的旋轉,這個旋轉是在手臂空間這個"子"空間進行的,不會對胳膊這個“父空間”產生影響。最后的縮放是為了描繪合適大小的手臂,是在本地空間進行的。
在這里還使用到 ksCopyMatrix4 這個數學工具函數,它在 GLESMath 中聲明與定義:
void ksCopyMatrix4(KSMatrix4 *result, const KSMatrix4 * target)
{
memcpy(result, target, sizeof(KSMatrix4));
}
5,drawCube 的實現如下:
- (void)drawCube:(KSVec4) color
{
GLfloat vertices[] = {
0.0f, -0.5f, 0.5f,
0.0f, 0.5f, 0.5f,
1.0f, 0.5f, 0.5f,
1.0f, -0.5f, 0.5f,
1.0f, -0.5f, -0.5f,
1.0f, 0.5f, -0.5f,
0.0f, 0.5f, -0.5f,
0.0f, -0.5f, -0.5f,
};
GLubyte indices[] = {
0, 1, 1, 2, 2, 3, 3, 0,
4, 5, 5, 6, 6, 7, 7, 4,
0, 7, 1, 6, 2, 5, 3, 4
};
glVertexAttrib4f(_colorSlot, color[0], color[1], color[2], color[3]);
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices );
glEnableVertexAttribArray(_positionSlot);
glDrawElements(GL_LINES, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, indices);
}
注意,這里的這個 cube 的旋轉點不是在中心了,而是在 x 為 0 的地方,這時為了讓胳膊和手臂都繞左邊的邊緣旋轉。你能理解這是怎么做到的么?在上面的代碼中還能看到顏色相關的代碼:glVertexAttrib4f(_colorSlot, color[0], color[1], color[2], color[3]);,下面會有詳細介紹。
二,使用顏色
1,顏色簡介
OpenGL 中的顏色一般都采用常見的 RGBA 模式,這四個字母分別表示紅綠藍三原色,以及 alpha,alpha 可粗略理解為透明度,在 OpenGL 中,它們的取值范圍為(0, 1.0)。不同比例的紅綠藍三種成分的組合就可以形成多彩的顏色。我們之所以能夠看到多彩的顏色,是因為在光照的作用下,物體表面反射光線(光子)進入到我們的眼睛里面,不同頻率(能量)的光子刺激視網膜上的感光細胞從而形成視覺。由于人體眼睛的感光細胞對紅綠藍三種頻率的刺激最為敏感,因此圖形學里使用這三種顏色作為原色來組合生成其他顏色。而在這三種顏色中,人體眼睛又對綠色最為敏感,所以在一些顏色格式中,綠色部分比其他部分權重一些,比如16位顏色格式RGB565,綠色占了6位。在 OpenGL 程序執行過程中,我們根據可以設置幾何圖元頂點的顏色,來確定幾何圖元的顏色。這種顏色可能是頂點顯示指定的值(如本文示例),也可能是啟用光照之后由變換矩陣與表面法線以及其他材質屬性的交互效果(后面講光照的時候會講到)。
2,修改著色器
為了在 OpenGL ES 2.0 中使用顏色,我們需要修改著色器,傳入用戶自定義的顏色。修改 FragmentShader.glsl 如下:
precision mediump float;
varying vec4 vDestinationColor;
void main()
{
gl_FragColor = vDestinationColor;
}
修改 VertexShader.glsl 如下:
uniform mat4 projection;
uniform mat4 modelView;
attribute vec4 vPosition;
attribute vec4 vSourceColor;
varying vec4 vDestinationColor;
void main(void)
{
gl_Position = projection * modelView * vPosition;
vDestinationColor = vSourceColor;
}
在《[OpenGL ES 02]OpenGL ES渲染管線與著色器》一文中已經對著色有較詳細的介紹,在這里就不多說了。我們在程序中向頂點著色器傳入 vSourceColor,該變量在頂點著色器中被賦值給頂點著色器的輸出變量 vDestinationColor,而 vDestinationColor 被用作片元著色器的輸入,最終在片元著色器中被賦值給內建變量 gl_FragColor,從而實現顏色賦值。
3,在程序中賦值
首先,和使用 vPosition 一樣,我們在 setupProgram 方法中找到 vDestinationColor 的槽位:
// Get the attribute position slot from program
//
_positionSlot = glGetAttribLocation(_programHandle, "vPosition");
// Get the attribute color slot from program
//
_colorSlot = glGetAttribLocation(_programHandle, "vSourceColor");
然后在描繪模型時使用該槽位來設置模型的顏色:
glVertexAttrib4f(_colorSlot, color[0], color[1], color[2], color[3]);
4,描繪胳膊和手臂
好吧,先讓我們整合上面的講解,看看到目前為止的成果如何(記得將未實現的方法實現,方法體為空即可):
- (void)render
{
KSVec4 colorRed = {1, 0, 0, 1};
KSVec4 colorWhite = {1, 1, 1, 1};
glClearColor(0.0, 1.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
// Setup viewport
//
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
// Draw shoulder
//
[self updateShoulderTransform];
[self drawCube:colorRed];
// Draw elbow
//
[self updateElbowTransform];
[self drawCube:colorWhite];
[_context presentRenderbuffer:GL_RENDERBUFFER];
}
前面說了要先描繪胳膊,在描繪手臂,因為在各自的模型中對 cube 進行了縮放,所以雖然是調用同一個 drawCube 但描繪出來的是不一樣的,在這里設置胳膊是紅色線條的,手臂是白色線條的。運行效果如下:

我們可以通過滑動 Shoulder 這個滑塊來旋轉整個胳膊和手臂,通過滑動 Elbow 這個滑塊來只旋轉手臂。試試看看,相信你會對 3D 變換有更深的理解。
三,平滑著色
1,顏色模式
OpenGL 支持兩種著色模式:單調著色(Flat)與平滑著色(smooth,也稱Gouraud著色)。單調著色就是整個圖元的顏色就是它的任何一個頂點的顏色,比如教程02中的紅色三角形效果;平滑著色下每個頂點都是單獨進行的,頂點之間的點是所有頂點顏色的均勻插值計算而得。下面將演示一個平滑著色的正方體:
- (void) updateColorCubeTransform
{
ksMatrixLoadIdentity(&_modelViewMatrix);
ksTranslate(&_modelViewMatrix, 0.0, -2, -5.5);
ksRotate(&_modelViewMatrix, _rotateColorCube, 0.0, 1.0, 0.0);
// Load the model-view matrix
glUniformMatrix4fv(_modelViewSlot, 1, GL_FALSE, (GLfloat*)&_modelViewMatrix.m[0][0]);
}
- (void) drawColorCube
{
GLfloat vertices[] = {
-0.5f, -0.5f, 0.5f, 1.0, 0.0, 0.0, 1.0, // red
-0.5f, 0.5f, 0.5f, 1.0, 1.0, 0.0, 1.0, // yellow
0.5f, 0.5f, 0.5f, 0.0, 0.0, 1.0, 1.0, // blue
0.5f, -0.5f, 0.5f, 1.0, 1.0, 1.0, 1.0, // white
0.5f, -0.5f, -0.5f, 1.0, 1.0, 0.0, 1.0, // yellow
0.5f, 0.5f, -0.5f, 1.0, 0.0, 0.0, 1.0, // red
-0.5f, 0.5f, -0.5f, 1.0, 1.0, 1.0, 1.0, // white
-0.5f, -0.5f, -0.5f, 0.0, 0.0, 1.0, 1.0, // blue
};
GLubyte indices[] = {
// Front face
0, 3, 2, 0, 2, 1,
// Back face
7, 5, 4, 7, 6, 5,
// Left face
0, 1, 6, 0, 6, 7,
// Right face
3, 4, 5, 3, 5, 2,
// Up face
1, 2, 5, 1, 5, 6,
// Down face
0, 7, 4, 0, 4, 3
};
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(float), vertices);
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(float), vertices + 3);
glEnableVertexAttribArray(_positionSlot);
glEnableVertexAttribArray(_colorSlot);
glDrawElements(GL_TRIANGLES, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, indices);
glDisableVertexAttribArray(_colorSlot);
}
更新Color Cube 模型視圖的代碼 updateColorCubeTransform 應該很好理解,在此就不再累述了,重點來看 drawColorCube 這個方法,記得與前面 drawCube 的方法對比下。首先聲明頂點,這里的頂點不僅包括坐標信息,還包括顏色信息!當然頂點信息還可以包括更多,如法線信息,紋理坐標等。然后我們使用上面加粗藍色的那一行來指定顏色數據的來源,格式:起始數據為 vertices + 3(前面有三個 GL_Float 的位置信息);讀取下個數據的步長 stride 為 7 * sizeof(float),就是說從一個顏色數據到下一個顏色數據的間隔為 7(3 個位置信息 + 4顏色信息);每個顏色信息的了下為 GL_Float,而4個顏色信息拼成一個顏色。然后我們還有使能顏色槽位,這樣顏色才會對頂點起作用。
2,渲染 ColorCube
在 Render 中緊接 Set viewport 之后加入描繪 ColorCube 的代碼:
// Draw color cube
//
[self updateColorCubeTransform];
[self drawColorCube];
編譯運行,你就可以看到一個彩色正方體,每個面上的顏色都是四個頂點均勻插值而成。
四,背面剔除
你也許會發現,這個正方體怎么看起來不像正方體呢?那是因為還沒有對背面進行剔除。默認情況下,OpenGL ES 是不進行背面剔除的,也就是正對我們的面和背對我們的面都進行了描繪,因此看起來就怪了。OpenGL ES 提供了 glFrontFace 這個函數來讓我們設置那那一面被當做正面,默認情況下逆時針方向的面被當做正面(GL_CCW)。我們可以調用 glCullFace 來明確指定我們想要剔除的面(GL_FRONT,GL_BACK, GL_FRONT_AND_BACK),默認情況下是剔除 GL_BACK。為了讓剔除生效,我們得使能之:glEnable(GL_CULL_FACE)。在這里,我們只需要在合適的地方調用 glEnable(GL_CULL_FACE),其他的都采用默認值就能滿足我們目前的需求。在 setProjection 的最后添加:
glEnable(GL_CULL_FACE);
4,為了更好地查看整個正方體,讓我們如教程 04 中那樣進行旋轉動畫:
- (void)toggleDisplayLink
{
if (_displayLink == nil) {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
else {
[_displayLink invalidate];
[_displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
_displayLink = nil;
}
}
- (void)displayLinkCallback:(CADisplayLink*)displayLink
{
_rotateColorCube += displayLink.duration * 90;
[self render];
}
編譯運行,效果如下:

五,總結
在本文中,介紹了相對空間變換的概念,如果你能夠掌握 3D 空間這種層次關系,相信你對 3D 變換會有深的認識,此外還介紹了顏色的使用以及背面剔除。雖然今天介紹的東西并不太多,但每天坦實的一小步,日積月累,我們終將領會3D 寶庫的點點滴滴。
六,引用
《OpenGL 編程指南》
《OpenGL ES 2.0 Programming Guide》
浙公網安備 33010602011771號