[OpenGL ES 05]相对空间变换及颜色
罗朝辉 ()
本文遵循“”创作公用协议
这是《OpenGL ES 教程》的第五篇,前四篇请参考如下链接:
前言
前面已经花了两篇文章来讲 3D 变换,可 3D 变换实在不是区区两篇文章能讲的透的,为了尽量将这个话题讲得全面点,在这篇本来讲颜色的文章里再顺带讲讲相对空间变换这个还没有提及的话题。相对空间变换类似于为“本地”坐标到“世界”坐标的变换,就是坐标空间之间的变换。你可以想象下这样一个动作,抬起胳膊同时弯曲手臂,手臂相当于在胳膊的坐标空间中旋转,而胳膊所在空间又相当于在身体或胳膊的坐标空间中旋转。就好像空间是嵌套的,一层套一层,外层会影响里层,里层的变换是相对于外层进行的。此外,还将介绍如何在 OpenGL ES 中使用颜色以及背面剔除。
今天将演示这样一个相对运动与颜色的示例,示例运行效果如下图所示,源码在这里:
一,相对变换
1,创建工程,本文是建立在前文的基础上的,如果你还没有阅读过那篇文章,请参考前文如何创建一个 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;}
在《》一文中已经对着色有较详细的介绍,在这里就不多说了。我们在程序中向顶点着色器传入 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》