我们先引入关于"矩阵堆栈"的官方说法:
OpenGL的矩阵堆栈指的就是内存中专门用来存放矩阵数据的某块特殊区域。
实际上,在创建、装入、相乘模型变换和投影变换矩阵时,都已用到堆栈操作。一般说来,矩阵堆栈常用于构造具有继承性的模型,即由一些简单目标构成的复杂模型。例如,一辆自行车就是由两个轮子、一个三角架及其它一些零部件构成的。它的继承性表现在当自行车往前走时,首先是前轮旋转,然后整个车身向前平移,接着是后轮旋转,然后整个车身向前平移,如此进行下去,这样自行车就往前走了。
矩阵堆栈对复杂模型运动过程中的多个变换操作之间的联系与独立十分有利。因为所有矩阵操作函数如LoadMatrix()、MultMatrix()、LoadIdentity()等只处理当前矩阵或堆栈顶部矩阵,这样堆栈中下面的其它矩阵就不受影响。堆栈操作函数有以下两个:
PushMatrix(void);
PopMatrix(void);
第一个函数表示将所有矩阵依次压入堆栈中,顶部矩阵是第二个矩阵的备份;压入的矩阵数不能太多,否则出错。
第二个函数表示弹出堆栈顶部的矩阵,令原第二个矩阵成为顶部矩阵,接受当前操作,故原顶部矩阵被破坏;当堆栈中仅存一个矩阵时,不能进行弹出操作,否则出错。
由此看出,矩阵堆栈操作与压入矩阵的顺序刚好相反,编程时要特别注意矩阵操作的顺序。
为了更好地理解这两个函数,我们可以形象地认为glPushMatrix()就是“记住自己在哪”,glPopMatrix()就是“返回自己原来所在地”。
我特意在"机器人"的代码里加入这段演示矩阵堆栈用法的例子, 让朋友们能看得更明白清楚一些.
下面这段测试代码是想让第一个长方体缩放为2*1*1, 沿X轴转45, 第二个长方体缩放为1*1*1,也就是不变形, 置于第一个长方体的左边贴着.
1 public void rtest(ref OpenGL gl, float xPos, float yPos, float zPos) 2 { 3 gl.PushMatrix(); 4 { 5 gl.Color(1f, 0f, 0f); 6 gl.Translate(xPos, yPos, zPos); 7 gl.Scale(2f, 1f, 1f); 8 gl.Rotate(45, 1f, 0f, 0f); 9 DrawCube(ref gl, 0, 0, 0, true); 10 } 11 gl.PopMatrix(); 12 13 gl.PushMatrix(); 14 { 15 gl.Color(0f, 1f, 0f); 16 gl.Translate(xPos - 2f, yPos, zPos); 17 DrawCube(ref gl, 0, 0, 0, true); 18 } 19 gl.PopMatrix(); 20 }
看到的效果就是我想真正想要的.
我们修改下代码, 把PushMatrix()和popMatrix()都注释了, 就像下面这样:
1 public void rtest(ref OpenGL gl, float xPos, float yPos, float zPos) 2 { 3 //gl.PushMatrix(); 4 //{ 5 gl.Color(1f, 0f, 0f); 6 gl.Translate(xPos, yPos, zPos); 7 gl.Scale(2f, 1f, 1f); 8 gl.Rotate(45, 1f, 0f, 0f); 9 DrawCube(ref gl, 0, 0, 0, true); 10 //} 11 //gl.PopMatrix(); 12 13 //gl.PushMatrix(); 14 //{ 15 gl.Color(0f, 1f, 0f); 16 gl.Translate(xPos - 2f, yPos, zPos); 17 DrawCube(ref gl, 0, 0, 0, true); 18 //} 19 //gl.PopMatrix(); 20 }
然后结果是这样的:
显示, 第二个DrawCube()受到了上面那些变换操作的影响, 像是继承了上面的那个长方体的旋转与缩放一样.
因此, 在这里如果两个DrawCube()操作在其首尾各加入栈出栈功能括起来, 那么这两次DrawCube()就相互独立了起来, 不会相互影响.
最后我们给出一个运用"变换"的综合性的例子, 它画了一个手脚能动的机器人, 场景做360度旋转, 可以观察到机器人每个面.
这个例子改编自 徐明亮的《OpenGL游戏编程》这本书里的一个例子, 原书例子是C++的代码.
先贴出源代码:
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Windows.Forms; 9 using SharpGL; 10 11 namespace SharpGLWinformsApplication1 12 { 13 public partial class SharpGLForm : Form 14 { 15 public float angle; // 机器人绕视点旋转的角度 16 float[] legAngle = new float[2]; // 腿的当前旋转角度 17 float[] armAngle = new float[2]; // 胳膊的当前旋转角度 18 19 bool leg1 = true; // 机器人腿的状态,true向前,flase向后 20 bool leg2 = false; 21 bool arm1 = true; 22 bool arm2 = false; 23 24 private float rotation = 0.0f; 25 public SharpGLForm() 26 { 27 InitializeComponent(); 28 angle = 0.0f; // 设置初始角度为0 29 legAngle[0] = legAngle[1] = 0.0f; 30 armAngle[0] = armAngle[1] = 0.0f; 31 } 32 33 private void openGLControl_OpenGLDraw(object sender, PaintEventArgs e) 34 { 35 OpenGL gl = openGLControl.OpenGL; 36 gl.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT); 37 38 gl.LoadIdentity(); 39 gl.Rotate(rotation, 0.0f, 1.0f, 0.0f); 40 41 drawGrid(gl); 42 DrawRobot(ref gl, 0, 0, 0); 43 //rtest(ref gl, 0, 0, 0); 44 rotation += 1.0f; 45 } 46 47 48 49 50 private void openGLControl_OpenGLInitialized(object sender, EventArgs e) 51 { 52 OpenGL gl = openGLControl.OpenGL; 53 gl.ClearColor(0, 0, 0, 0); 54 } 55 56 private void openGLControl_Resized(object sender, EventArgs e) 57 { 58 OpenGL gl = openGLControl.OpenGL; 59 60 gl.MatrixMode(OpenGL.GL_PROJECTION); 61 62 gl.LoadIdentity(); 63 gl.Perspective(60.0f, (double)Width / (double)Height, 0.01, 100.0); 64 gl.LookAt(-5, 5, 15, 0, 0, 0, 0, 1, 0); 65 gl.MatrixMode(OpenGL.GL_MODELVIEW); 66 } 67 68 //测试例子 69 public void rtest(ref OpenGL gl, float xPos, float yPos, float zPos) 70 { 71 gl.PushMatrix(); 72 { 73 gl.Color(1f, 0f, 0f); 74 gl.Translate(xPos, yPos, zPos); 75 gl.Scale(2f, 1f, 1f); 76 gl.Rotate(45, 1f, 0f, 0f); 77 DrawCube(ref gl, 0, 0, 0, true); 78 } 79 gl.PopMatrix(); 80 81 gl.PushMatrix(); 82 { 83 gl.Color(0f, 1f, 0f); 84 gl.Translate(xPos - 2f, yPos, zPos); 85 DrawCube(ref gl, 0, 0, 0, true); 86 } 87 gl.PopMatrix(); 88 } 89 90 91 public void DrawRobot(ref OpenGL Gl, float xPos, float yPos, float zPos) 92 { 93 Gl.PushMatrix(); 94 { 95 Gl.Translate(xPos, yPos, zPos); 96 97 ///绘制各个部分 98 //Gl.LoadIdentity(); 99 DrawHead(ref Gl, 1f, 2f, 0f); // 绘制头部 2*2*2 100 DrawTorso(ref Gl, 1.5f, 0.0f, 0.0f); //躯干, 3*5*2 101 102 Gl.PushMatrix(); 103 { 104 //如果胳膊正在向前运动,则递增角度,否则递减角度 105 if (arm1) 106 armAngle[0] = armAngle[0] + 1f; 107 else 108 armAngle[0] = armAngle[0] - 1f; 109 110 ///如果胳膊达到其最大角度则改变其状态 111 if (armAngle[0] >= 15.0f) 112 arm1 = false; 113 if (armAngle[0] <= -15.0f) 114 arm1 = true; 115 116 //平移并旋转后绘制胳膊 117 Gl.Translate(0.0f, -0.5f, 0.0f); 118 Gl.Rotate(armAngle[0], 1.0f, 0.0f, 0.0f); 119 DrawArm(ref Gl, 2.5f, 0.0f, -0.5f); //胳膊1, 1*4*1 120 } 121 Gl.PopMatrix(); 122 123 Gl.PushMatrix(); 124 { 125 if (arm2) 126 armAngle[1] = armAngle[1] + 1f; 127 else 128 armAngle[1] = armAngle[1] - 1f; 129 130 131 if (armAngle[1] >= 15.0f) 132 arm2 = false; 133 if (armAngle[1] <= -15.0f) 134 arm2 = true; 135 136 137 Gl.Translate(0.0f, -0.5f, 0.0f); 138 Gl.Rotate(armAngle[1], 1.0f, 0.0f, 0.0f); 139 DrawArm(ref Gl, -1.5f, 0.0f, -0.5f); //胳膊2, 1*4*1 140 } 141 Gl.PopMatrix(); 142 143 Gl.PushMatrix(); 144 { 145 ///如果腿正在向前运动,则递增角度,否则递减角度 146 if (leg1) 147 legAngle[0] = legAngle[0] + 1f; 148 else 149 legAngle[0] = legAngle[0] - 1f; 150 151 ///如果腿达到其最大角度则改变其状态 152 if (legAngle[0] >= 15.0f) 153 leg1 = false; 154 if (legAngle[0] <= -15.0f) 155 leg1 = true; 156 157 ///平移并旋转后绘制胳膊 158 Gl.Translate(0.0f, -0.5f, 0.0f); 159 Gl.Rotate(legAngle[0], 1.0f, 0.0f, 0.0f); 160 DrawLeg(ref Gl, -0.5f, -5.0f, -0.5f); //腿部1,1*5*1 161 } 162 Gl.PopMatrix(); 163 164 Gl.PushMatrix(); 165 { 166 if (leg2) 167 legAngle[1] = legAngle[1] + 1f; 168 else 169 legAngle[1] = legAngle[1] - 1f; 170 171 if (legAngle[1] >= 15.0f) 172 leg2 = false; 173 if (legAngle[1] <= -15.0f) 174 leg2 = true; 175 176 Gl.Translate(0.0f, -0.5f, 0.0f); 177 Gl.Rotate(legAngle[1], 1.0f, 0.0f, 0.0f); 178 DrawLeg(ref Gl, 1.5f, -5.0f, -0.5f); //腿部2, 1*5*1 179 } 180 Gl.PopMatrix(); 181 } 182 Gl.PopMatrix(); 183 } 184 185 // 绘制一个手臂 186 void DrawArm(ref OpenGL Gl, float xPos, float yPos, float zPos) 187 { 188 Gl.PushMatrix(); 189 Gl.Color(1.0f, 0.0f, 0.0f); // 红色 190 Gl.Translate(xPos, yPos, zPos); 191 Gl.Scale(1.0f, 4.0f, 1.0f); // 手臂是1x4x1的立方体 192 DrawCube(ref Gl, 0.0f, 0.0f, 0.0f,false); 193 Gl.PopMatrix(); 194 } 195 196 // 绘制一条腿 197 void DrawLeg(ref OpenGL Gl, float xPos, float yPos, float zPos) 198 { 199 Gl.PushMatrix(); 200 Gl.Color(1.0f, 1.0f, 0.0f); // 黄色 201 Gl.Translate(xPos, yPos, zPos); 202 Gl.Scale(1.0f, 5.0f, 1.0f); // 腿是1x5x1长方体 203 DrawCube(ref Gl, 0.0f, 0.0f, 0.0f,false); 204 Gl.PopMatrix(); 205 } 206 207 // 绘制头部 208 void DrawHead(ref OpenGL Gl, float xPos, float yPos, float zPos) 209 { 210 Gl.PushMatrix(); 211 Gl.Color(1.0f, 1.0f, 1.0f); // 白色 212 Gl.Translate(xPos, yPos, zPos); 213 Gl.Scale(2.0f, 2.0f, 2.0f); //头部是 2x2x2长方体 214 DrawCube(ref Gl, 0.0f, 0.0f, 0.0f,false); 215 Gl.PopMatrix(); 216 } 217 218 // 绘制机器人的躯干 219 void DrawTorso(ref OpenGL Gl, float xPos, float yPos, float zPos) 220 { 221 Gl.PushMatrix(); 222 Gl.Color(0.0f, 0.0f, 1.0f); // 蓝色 223 Gl.Translate(xPos, yPos, zPos); 224 Gl.Scale(3.0f, 5.0f, 2.0f); // 躯干是3x5x2的长方体 225 DrawCube(ref Gl, 0.0f, 0.0f, 0.0f,false); 226 Gl.PopMatrix(); 227 } 228 229 void drawGrid(OpenGL gl) 230 { 231 //绘制过程 232 gl.PushAttrib(OpenGL.GL_CURRENT_BIT); //保存当前属性 233 gl.PushMatrix(); //压入堆栈 234 gl.Translate(0f, -20f, 0f); 235 gl.Color(0f, 0f, 1f); 236 237 //在X,Z平面上绘制网格 238 for (float i = -50; i <= 50; i += 1) 239 { 240 //绘制线 241 gl.Begin(OpenGL.GL_LINES); 242 { 243 if (i == 0) 244 gl.Color(0f, 1f, 0f); 245 else 246 gl.Color(0f, 0f, 1f); 247 248 //X轴方向 249 gl.Vertex(-50f, 0f, i); 250 gl.Vertex(50f, 0f, i); 251 //Z轴方向 252 gl.Vertex(i, 0f, -50f); 253 gl.Vertex(i, 0f, 50f); 254 255 } 256 gl.End(); 257 } 258 gl.PopMatrix(); 259 gl.PopAttrib(); 260 } 261 262 internal void DrawCube(ref OpenGL Gl, float xPos, float yPos, float zPos,bool isLine) 263 { 264 Gl.PushMatrix(); 265 Gl.Translate(xPos, yPos, zPos); 266 if (isLine) 267 Gl.Begin(OpenGL.GL_LINE_STRIP); 268 else 269 Gl.Begin(OpenGL.GL_POLYGON); 270 271 // 顶面 272 Gl.Vertex(0.0f, 0.0f, 0.0f); 273 Gl.Vertex(0.0f, 0.0f, -1.0f); 274 Gl.Vertex(-1.0f, 0.0f, -1.0f); 275 Gl.Vertex(-1.0f, 0.0f, 0.0f); 276 277 // 前面 278 Gl.Vertex(0.0f, 0.0f, 0.0f); 279 Gl.Vertex(-1.0f, 0.0f, 0.0f); 280 Gl.Vertex(-1.0f, -1.0f, 0.0f); 281 Gl.Vertex(0.0f, -1.0f, 0.0f); 282 283 // 右面 284 Gl.Vertex(0.0f, 0.0f, 0.0f); 285 Gl.Vertex(0.0f, -1.0f, 0.0f); 286 Gl.Vertex(0.0f, -1.0f, -1.0f); 287 Gl.Vertex(0.0f, 0.0f, -1.0f); 288 289 // 左面 290 Gl.Vertex(-1.0f, 0.0f, 0.0f); 291 Gl.Vertex(-1.0f, 0.0f, -1.0f); 292 Gl.Vertex(-1.0f, -1.0f, -1.0f); 293 Gl.Vertex(-1.0f, -1.0f, 0.0f); 294 295 // 底面 296 Gl.Vertex(0.0f, 0.0f, 0.0f); 297 Gl.Vertex(0.0f, -1.0f, -1.0f); 298 Gl.Vertex(-1.0f, -1.0f, -1.0f); 299 Gl.Vertex(-1.0f, -1.0f, 0.0f); 300 301 302 // 后面 303 Gl.Vertex(0.0f, 0.0f, 0.0f); 304 Gl.Vertex(-1.0f, 0.0f, -1.0f); 305 Gl.Vertex(-1.0f, -1.0f, -1.0f); 306 Gl.Vertex(0.0f, -1.0f, -1.0f); 307 Gl.End(); 308 Gl.PopMatrix(); 309 } 310 311 312 313 } 314 }
这个代码朋友们主要需注意下面两个重点:
(1) 机器人的6个部分的坐标为什么要取下面这样的值?
因为画每个部分都用了入栈出栈, 因此每个部分都是独立相对于原点来计算的, 计算的时候你还得参考 长*高*宽 的信息, 我已经把它标注到注释里了.
DrawHead(ref Gl, 1f, 2f, 0f); // 绘制头部 2*2*2 DrawTorso(ref Gl, 1.5f, 0.0f, 0.0f); //躯干, 3*5*2 DrawArm(ref Gl, 2.5f, 0.0f, -0.5f); //胳膊1, 1*4*1 DrawArm(ref Gl, -1.5f, 0.0f, -0.5f); //胳膊2, 1*4*1 DrawLeg(ref Gl, -0.5f, -5.0f, -0.5f); //腿部1,1*5*1 DrawLeg(ref Gl, 1.5f, -5.0f, -0.5f); //腿部2, 1*5*1
(2) 好好体会堆栈的操作, 主要在画机器人的函数DrawRobot()里.
程序运行时截取了一帧,效果如下:
OpenGL的"变换" 主题终于彻底讲完了! 最初笔者接触这些内容时, 感觉术语太多, 枯燥无趣. 但是它是OpenGL真正最基本的入门功夫. 就像 徐明亮在《OpenGL游戏编程》这本书里说的: 说完OpenGL变换的知识后, 读者已经有足够的基础可以开始编写游戏了!
我当时还郁闷, 就学这点知识就可以开始写游戏? 那材质灯光呢? 开什么玩笑?
现在我就同意这一说法了, 因为"变换"的知识是重中之重, 请引起朋友们足够的重视, 踏实把它学好! 不要像笔者一样浮澡走弯路!