3D 开发主要分为三个步骤:建模、渲染、逻辑控制。
建模,指通过一些基本图元如点、线、三角形、多边形将物体画出来,一般使用 3DMax、MilkShape 等建模工具来完成。具体来说,建模涉及模型的构建、贴纹 理、制作动画等。
渲染,即使用 OpenGL 图形接口将模型在计算机上画出来。
逻辑控制,若要模型动起来,需要根据时间计算模型各个顶点的坐标,这个通过 程序逻辑来控制。
在 3D 开发中有一个常用的速度指标,即帧每秒(f/s),其计算方法如下:
fps = numFrame / Interval
用 3dMax 等工具建模的一般思路是:
建模工具有很多,如 3DMax、Maya、Blender、MilkShape、AC3D 等,其中 MilkShape 短小精悍,容易上手比较推荐新手使用。
Milkshape :建模、贴纹理、加骨骼、做动画
Milkshape 的教程可在这里 下载。
保存模型有很多种格式,如 md2、ms3d、3ds、obj等。
It's a state machine - Setup the stage, lighting, actors... Then draw it!
OpenGL 中的纹理通过一个唯一号引用,通过函数 glBindTexture() 实现。你 可以自己指定这个唯一号,或者通过调用 glGenTextures () 函数生成一个唯一 号。
GLuint texture[1];
glGenTextures(1, &texture[0]);
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexImage2D(GL_TEXTURE_2D, 0, 3, sizeX, sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, pData);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
此时 pdata 里的数据就可以释放了
在实际开发时,往往需要一个纹理管理器提供纹理的装载、释放、获取等功能, 下面的文章介绍了一个纹理管理器的设计和实现。
A Singleton Texture Manager for OpenGL
以上图片来自:http://blogs.msdn.com/danlehen/
核心绘制代码如下所示:
glVertexPointer(3, GL_FLOAT, 0, m_vertices);
glNormalPointer(GL_FLOAT, 0, m_normals);
glBindTexture(GL_TEXTURE_2D, m_textureId);
glTexCoordPointer(2, GL_FLOAT, 0, m_uvs);
glDrawElements(GL_TRIANGLES, m_triangleNums * 3, GL_UNSIGNED_SHORT,m_indices);
或者
glDrawArrays(GL_TRIANGLES, 0, m_triangleNums * 3);
在 android 平台上,纹理图的长宽必须是 2 的幂,纹理图尺寸超过 512x512 时 fps 会显著降低。一般的做法是将所有纹理组合到成一张 256x256 或者 512x512 的大图。
使用纹理有两种方法,一是直接用建模工具映射好,二是通过动态调整纹理坐标 来获取需要的纹理。前者不做介绍,重点介绍下后者。
调整纹理坐标的方法有两种:一是手工修改现有纹理代码,二是通过 glMatrixMode (glTexture) 对纹理坐标进行操作。
glMatrixMode (glTexture) 调整纹理坐标的代码如下:
// move texture
glMatrixMode(GL_TEXTURE);
glPushMatrix();
glLoadIdentity();
// move the texture - this does not work
glTranslatef(offset, 0.0f, 0.0f);
glTranslatef(0.0f, offset, 0.0f);
glTranslatef(0.0f, 0.0f, offset);
glPopMatrix();
注意:
函数原型如下所示:
gluLookAt(GLdoble eyex,GLdouble eyey,GLdouble eyez,
GLdouble centerx,GLdouble centery,GLdouble centerz,
GLdouble upx,GLdouble upy,GLdouble upz);
gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar);
首先得设置 gluPerspective,来看看它的参数都表示什么意思:
我们知道,远处的东西看起来要小一些,近处的东西看起来会大一些,这就是透视 (perspective)原理,如下图所示:
假设那两条线表示公路,理论上讲,它们的两条边是平行的,但现实情况中,它 们在远方(可以无限远)总要相交于一点,实际线段 AB 的长度等于 CD 的长度, 只是在此例中使用了透视角,故会有如上的效果,是不是很接近现实的情况?
结合我们刚才这两个函数:
就是这两个参数的意义了。
再解释下那个"眼睛睁开的角度"是什么意思,首先假设我们现在距离物体有50个 单位距离远的位置,在眼睛睁开角度设置为 45 时,请看大屏幕:
我们可以看到,在远处一个球;现在我们将眼睛再张开点看,将“眼睛睁开的角 度”设置为 178 (180度表示平角,那时候我们将什么也看不到,眼睛睁太大了,眼 大无神)。
我们只看到一个点,因为我们看的范围太大了,这个球本身大小没有改变,但是 它在我们的“视界”内太小了。
反之,我们将眼睛闭小些,改为 1 度看看会出现什么情况呢?
在我们距离该物体 3000 距离远,“眼睛睁开的角度”为 1 时,我们似乎走进了 这个球内,这个是不是类似于相机的焦距?
当我们将“透视角”设置为 0 时,我们相当于闭上双眼,这个世界清静了,我们 什么也看不到了。
现在来看 gluLookAt 函数。
它共接受三对坐标,分别为 eye,center,up。
如果将观察点比喻成我们的眼睛,那么这个 up 则表示我们是正立还是倒立。从 不同角度看,所看的影像大不相同。若需要指明我们现在正立,那么 X,Z 轴为 0,Y 轴为正即可,通常将其设置为 1,只要表示一个向上的向量(方向)即可。 我们指定 0.1f 或 0.00001f 或 1000.0f,效果是一样的,只要能表示方向即可。
球是画在世界坐标系的原点上的,即 O(0,0,0) 坐标上,我们的眼睛位于观察点 A(0,0,100),Z 轴向屏幕里看去的方向为负,屏幕外我们的位置,Z 轴为正值, 其实很好理解,即我们距离原点的距离,设置 100,将观察到如下图所示的影像。
如果我们向前或向后移动,则相应的图像会变大或变小,这里其实就是运用了透 视原理,近处的物体大,远处的物体小,实际物体的大小是不变的。
同理改变 center 坐标(眼睛看去的那个点,可简单理解为视线的终点)也会影响 球的大小,同样可以认为是改变了物体与观察点的距离所致。
以上理解了之后,来做一个测试。
透视图不变,最远处仍为 3000,近处为 0.1。
gluPerspective // 设置透视图
(45, // 透视角设置为 45 度,在Y方向上以角度为单位的视野
(GLfloat)x/(GLfloat)y, // 窗口的宽与高比
0.1f, // 视野透视深度:近点1.0f
3000.0f // 视野透视深度:始点0.1f远点1000.0f
);
将我们的观察点置于 A(0,10,0),将观察位置(视线终点)坐标置于(0,0,0),然 后在原点开始绘图,画一个 V 字形,并将 Z 轴的值从 -1000 递增加到 +1000, 增量为10,代码如下:
glColor3f(0.5f, 0.7f, 1.0f);
glBegin(GL_LINES);
for(int i=-1000;i<=1000;i+=10)
{
glVertex3f(0,0,i);
glVertex3f(10,10,i);
glVertex3f(0,0,i);
glVertex3f(-10,10,i);
}
glEnd();
效果图如下所示:
OpenGL 的重要功能之一是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置。
每次计算屏幕输出时,OpenGL 采取这样的方式进行:
屏幕坐标点 = 3D 模型点 * 几何变换栈矩阵(n...1) * 投影变换栈矩阵 (n...1)。其中 n...1 表示从栈顶到栈底。
更复杂的流程是:
屏幕坐标点 = 3D 模型坐标 -〉 (几何变换矩阵) -〉 人眼坐标 -〉(投影变换矩阵) -〉 正则设备坐标(相对于 OpenGL 屏幕坐标原点)
-〉 校正成窗口坐标(相对于窗口坐标)
简单的讲,OpenGL 中从三维场景到屏幕图形要经历如下所示的变换过程:
模型坐标-〉世界坐标-〉观察坐标-〉投影坐标-〉设备坐标
其中四种坐标经常要在程序中用到:物体坐标(也叫模型坐标、局部坐标),世界坐标,眼坐标(也叫观察坐标)和设备坐标.
物体坐标:
以物体某一点为原点而建立的“世界坐标”,该坐标系仅对该物体适用,用来简化对物体各部分坐标的描述。物体放到场景中时,各部分经历的坐标变换相同,相对位置不变,所以可视为一个整体,与人类的思维习惯一致;
世界坐标:
是OpenGL中用来描述场景的坐标,Z+轴垂直屏幕向外,X+从左到右,Y+轴从下到上,是右手笛卡尔坐标系统。我们用这个坐标系来描述物体及光 源的位置。 OpenGL 中有一个坐标转换矩阵栈 (Modeview),栈顶就是当前坐标变换矩阵,进入 OpenGL 管道的每个坐标 (齐次坐标)都会乘上这个矩阵,结果才是对应点在场景中的世界坐标。将物体放到场景中也就是将物体平移到特定位置、旋转一定角度,这些操作都是坐标变换。
眼坐标:
是以视点为原点,以视线的方向为Z+轴正方向的坐标系中的方向。OpenGL管道会将世界坐标先变换到眼坐标,然后进行裁剪,只有在视线范围(视见 体)之内的场景才会进入下一阶段的计算。 OpenGL 有个投影变换矩阵栈 (Projection),栈顶矩阵就是当前投影变换矩阵,负责将场景各坐标变换到眼坐标。由于所得到的结果是裁剪后的场景部分,称为裁剪坐标。
设备坐标:
OpenGL的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置,这个位置就称为设备坐标。在屏幕、打印机等设备上的坐标是二维坐标。
几何变换栈:为了维护几何变换, OpenGL 维护一个几何变换栈,每次几何变换, 都用相应的矩阵乘对应的栈顶元素,并替换掉上次的栈顶。
对于几何变换栈,有两个操作可以使用:
深入探讨透视投影变换:http://www.cppblog.com/zmj/archive/2008/08/28/58936.html
glOrtho 函数的意义: http://flyingliang.spaces.live.com/blog/cns!9112491EC93C817B!1228.entry
正交投影的特点:
无论物体距离相机多远,投影后的物体大小尺寸不变。这种投影通常用在建筑蓝图绘制和计算机辅助设计等方面,这些行业要求投影后的物体尺寸及相互间的角度不变,以便施工或制造时物体比例大小正确。
透视投影的特点:离观察者越远的对象越小。
// Init *
glClearColor (0.0f, 0.0f, 0.0f, 0.0f); /* Set background color*/
glClearDepth (1.0);
glShadeModel (GL_SMOOTH);
glMatrixMode (GL_PROJECTION); /* 设置投影变换 */
glLoadIdentity ();
gluPerspective (......);
glMatrixMode (GL_MODEVIEW);
//* 清屏 *
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity ();
glTranslatef (x, y, z); /* 平移 */
glRotatef (angle, x, y, z); /* 旋转 */
glBegin (GL_POLYGON);
glColor3f (...);
glVertex3f (...);
... ...
glEnd ();
/* ReSizeScene */
glViewport (0, 0, width, height);
glMatrixMode (GL_PROJECTION);
glLoadIdentity();
gluPerspective(... ...);
glMatrixMode(GL_MODELVIEW);