第10课 OpenGL 3D世界

加载3D世界,并在其中漫游:

在这一课中,你将学会如何加载3D世界,并在3D世界中漫游。这一课使用第一课的代码,当然在课程说明中我只介绍改变了代码。

这一课是由Lionel Brits (βtelgeuse)所写的。在本课中我们只对增加的代码做解释。当然只添加课程中所写的代码,程序是不会运行的。如果您有兴趣知道下面的每一行代码是如何运行的话,请下载完整的源码,并在浏览这一课的同时,对源码进行跟踪。
好了现在欢迎来到名不见经传的第十课。到现在为止,您应该有能力创建一个旋转的立方体或一群星星了,对3D编程也应该有些感觉了吧?但还是请等一下!不要立马冲动地要开始写个Quake
IV,好不好...:)。只靠旋转的立方体还很难来创造一个可以决一死战的酷毙了的对手....:)。现在这些日子您所需要的是一个大一点的、更复杂些的、动态3D世界,它带有空间的六自由度和花哨的效果如镜像、入口、扭曲等等,当然还要有更快的帧显示速度。这一课就要解释一个基本的3D世界"结构",以及如何在这个世界里游走。
数据结构
当您想要使用一系列的数字来完美的表达3D环境时,随着环境复杂度的上升,这个工作的难度也会随之上升。出于这个原因,我们必须将数据归类,使其具有更多的可操作性风格。在程序清单头部出现了sector(区段)的定义。每个3D世界基本上可以看作是sector(区段)的集合。一个sector(区段)可以是一个房间、一个立方体、或者任意一个闭合的区间。

typedef struct tagSECTOR                        // 创建Sector区段结构
{
    int numtriangles;                        // Sector中的三角形个数
    TRIANGLE* triangle;                        // 指向三角数组的指针
} SECTOR;                                // 命名为SECTOR

一个sector(区段)包含了一系列的多边形,所以下一个目标就是triangle(我们将只用三角形,这样写代码更容易些)。

typedef struct tagTRIANGLE                        // 创建Triangle三角形结构
{
    VERTEX vertex[3];                        // VERTEX矢量数组,大小为3
} TRIANGLE;                                // 命名为 TRIANGLE

三角形本质上是由一些(两个以上)顶点组成的多边形,顶点同时也是我们的最基本的分类单位。顶点包含了OpenGL真正感兴趣的数据。我们用3D空间中的坐标值(x,y,z)以及它们的纹理坐标(u,v)来定义三角形的每个顶点。

typedef struct tagVERTEX                        // 创建Vertex顶点结构
{
    float x, y, z;                            // 3D 坐标
    float u, v;                            // 纹理坐标
} VERTEX;                                // 命名为VERTEX

载入文件 在程序内部直接存储数据会让程序显得太过死板和无趣。从磁盘上载入世界资料,会给我们带来更多的弹性,可以让我们体验不同的世界,而不用被迫重新编译程序。另一个好处就是用户可以切换世界资料并修改它们而无需知道程序如何读入输出这些资料的。数据文件的类型我们准备使用文本格式。这样编辑起来更容易,写的代码也更少。等将来我们也许会使用二进制文件。

问题是,怎样才能从文件中取得数据资料呢?首先,创建一个叫做SetupWorld()的新函数。把这个文件定义为filein,并且使用只读方式打开文件。我们必须在使用完毕之后关闭文件。大家一起来看看现在的代码:

// 先前的定义: char* worldfile = "data\\world.txt";
void SetupWorld()                            // 设置我们的世界
{
    FILE *filein;                            // 工作文件
    filein = fopen(worldfile, "rt");                // 打开文件
    ...
    (读入数据资料))
    ...
    fclose(filein);                            // 关闭文件
    return;                                // 返回
}

下一个挑战是将每个单独的文本行读入变量。这有很多办法可以做到。一个问题是文件中并不是所有的行都包含有意义的信息。空行和注释不应该被读入。我们创建了一个叫做readstr()的函数。这个函数会从数据文件中读入一个有意义的行至一个已经初始化过的字符串。下面就是代码:

void readstr(FILE *f,char *string)                    //  读入一个字符串
{
    do                                // 循环开始
    {
        fgets(string, 255, f);                    // 读入一行
    } while ((string[0] == ‘/‘) || (string[0] == ‘\n‘));        // 考察是否有必要进行处理
    return;                                // 返回
}

下一步我们读入区段数据。这一课将只处理一个区段,不过实现一个多区段引擎也很容易。让我们将注意力转回SetupWorld()。程序必须知道区段内包含了多少个三角形。我们在数据文件中以下面这种形式定义三角形数量:
接下来是读取三角形数量的代码:

int numtriangles;                            // 区段中的三角形数量
char oneline[255];                            // 存储数据的字符串
...
readstr(filein,oneline);                        // 读入一行数据
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);            // 读入三角形数量

余下的世界载入过程采用了相似的方法。接着,我们对区段进行初始化,并读入部分数据:

// 先前的定义: SECTOR sector1;
char oneline[255];                            // 存储数据的字符串
int numtriangles;                            // 区段的三角形数量
float x, y, z, u, v;                            // 3D 和 纹理坐标
...
sector1.triangle = new TRIANGLE[numtriangles];                // 为numtriangles个三角形分配内存并设定指针
sector1.numtriangles = numtriangles;                    // 定义区段1中的三角形数量

// 遍历区段中的每个三角形
for (int triloop = 0; triloop < numtriangles; triloop++)        // 遍历所有的三角形
{
    // 遍历三角形的每个顶点
    for (int vertloop = 0; vertloop < 3; vertloop++)        // 遍历所有的顶点
    {
        readstr(filein,oneline);                // 读入一行数据

        // 读入各自的顶点数据
        sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);

        // 将顶点数据存入各自的顶点
        sector1.triangle[triloop].vertex[vertloop].x = x;    // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值 x =x
        sector1.triangle[triloop].vertex[vertloop].y = y;    // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值 y =y
        sector1.triangle[triloop].vertex[vertloop].z = z;    // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值  z =z
        sector1.triangle[triloop].vertex[vertloop].u = u;    // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值  u =u
        sector1.triangle[triloop].vertex[vertloop].v = v;    // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值  e=v
    }
}

数据文件中每个三角形都以如下形式声明:
X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3
显示世界
现在区段已经载入内存,我们下一步要在屏幕上显示它。到目前为止,我们所作过的都是些简单的旋转和平移。但我们的镜头始终位于原点(0,0,0)处。任何一个不错的3D引擎都会允许用户在这个世界中游走和遍历,我们的这个也一样。实现这个功能的一种途径是直接移动镜头并绘制以镜头为中心的3D环境。这样做会很慢并且不易用代码实现。我们的解决方法如下:

  • 根据用户的指令旋转并变换镜头位置。
  • 围绕原点,以与镜头相反的旋转方向来旋转世界。(让人产生镜头旋转的错觉)
  • 以与镜头平移方式相反的方式来平移世界(让人产生镜头移动的错觉)。

这样实现起来就很简单. 下面从第一步开始吧(平移并旋转镜头)。

if (keys[VK_RIGHT])                            // 右方向键按下了么?
{
    yrot -= 1.5f;                            // 向左旋转场景
}
if (keys[VK_LEFT])                            // 左方向键按下了么?
{
    yrot += 1.5f;                            // 向右侧旋转场景
}
if (keys[VK_UP])                            // 向上方向键按下了么?
{
    xpos -= (float)sin(heading*piover180) * 0.05f;            // 沿游戏者所在的X平面移动
    zpos -= (float)cos(heading*piover180) * 0.05f;            // 沿游戏者所在的Z平面移动
    if (walkbiasangle >= 359.0f)                    // 如果walkbiasangle大于359度
    {
        walkbiasangle = 0.0f;                    // 将 walkbiasangle 设为0
    }
    else                                // 否则
    {
         walkbiasangle+= 10;                    // 如果 walkbiasangle < 359 ,则增加 10
    }
    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;        // 使游戏者产生跳跃感
}
if (keys[VK_DOWN])                            // 向下方向键按下了么?
{
    xpos += (float)sin(heading*piover180) * 0.05f;            // 沿游戏者所在的X平面移动
    zpos += (float)cos(heading*piover180) * 0.05f;            // 沿游戏者所在的Z平面移动

    if (walkbiasangle <= 1.0f)                    // 如果walkbiasangle小于1度
    {
        walkbiasangle = 359.0f;                    // 使 walkbiasangle 等于 359
    }
    else                                // 否则
    {
        walkbiasangle-= 10;                    // 如果 walkbiasangle > 1 减去 10
    }
    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;        // 使游戏者产生跳跃感
}

这个实现很简单。当左右方向键按下后,旋转变量yrot
相应增加或减少。当前后方向键按下后,我们使用sine和cosine函数重新生成镜头位置(您需要些许三角函数学的知识:-)。Piover180
是一个很简单的折算因子用来折算度和弧度。
接着您可能会问:walkbias是什么意思?这是NeHe的发明的单词:-)。基本上就是当人行走时头部产生上下摆动的幅度。我们使用简单的sine正弦波来调节镜头的Y轴位置。如果不添加这个而只是前后移动的话,程序看起来就没这么棒了。
现在,我们已经有了下面这些变量。可以开始进行步骤2和3了。由于我们的程序还不太复杂,我们无需新建一个函数,而是直接在显示循环中完成这些步骤。

int DrawGLScene(GLvoid)                            // 绘制 OpenGL 场景
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);        // 清除 场景 和 深度缓冲
    glLoadIdentity();                        // 重置当前矩阵

    GLfloat x_m, y_m, z_m, u_m, v_m;                // 顶点的临时 X, Y, Z, U 和 V 的数值

    GLfloat xtrans = -xpos;                        // 用于游戏者沿X轴平移时的大小
    GLfloat ztrans = -zpos;                        // 用于游戏者沿Z轴平移时的大小
    GLfloat ytrans = -walkbias-0.25f;                // 用于头部的上下摆动

    GLfloat sceneroty = 360.0f - yrot;                // 位于游戏者方向的360度角

    int numtriangles;                        // 保有三角形数量的整数
    glRotatef(lookupdown,1.0f,0,0);                    // 上下旋转
    glRotatef(sceneroty,0,1.0f,0);                    // 根据游戏者正面所对方向所作的旋转

    glTranslatef(xtrans, ytrans, ztrans);                // 以游戏者为中心的平移场景
    glBindTexture(GL_TEXTURE_2D, texture[filter]);            // 根据 filter 选择的纹理

    numtriangles = sector1.numtriangles;                // 取得Sector1的三角形数量

    // 逐个处理三角形
    for (int loop_m = 0; loop_m < numtriangles; loop_m++)        // 遍历所有的三角形
    {
        glBegin(GL_TRIANGLES);                    // 开始绘制三角形
            glNormal3f( 0.0f, 0.0f, 1.0f);            // 指向前面的法线
            x_m = sector1.triangle[loop_m].vertex[0].x;    // 第一点的 X 分量
            y_m = sector1.triangle[loop_m].vertex[0].y;    // 第一点的 Y 分量
            z_m = sector1.triangle[loop_m].vertex[0].z;    // 第一点的 Z 分量
            u_m = sector1.triangle[loop_m].vertex[0].u;    // 第一点的 U  纹理坐标
            v_m = sector1.triangle[loop_m].vertex[0].v;    // 第一点的 V  纹理坐标
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);    // 设置纹理坐标和顶点

            x_m = sector1.triangle[loop_m].vertex[1].x;    // 第二点的 X 分量
            y_m = sector1.triangle[loop_m].vertex[1].y;    // 第二点的 Y 分量
            z_m = sector1.triangle[loop_m].vertex[1].z;    // 第二点的 Z 分量
            u_m = sector1.triangle[loop_m].vertex[1].u;    // 第二点的 U  纹理坐标
            v_m = sector1.triangle[loop_m].vertex[1].v;    // 第二点的 V  纹理坐标
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);    // 设置纹理坐标和顶点

            x_m = sector1.triangle[loop_m].vertex[2].x;    // 第三点的 X 分量
            y_m = sector1.triangle[loop_m].vertex[2].y;    // 第三点的 Y 分量
            z_m = sector1.triangle[loop_m].vertex[2].z;    // 第三点的 Z 分量
            u_m = sector1.triangle[loop_m].vertex[2].u;    // 第二点的 U  纹理坐标
            v_m = sector1.triangle[loop_m].vertex[2].v;    // 第二点的 V  纹理坐标
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);    // 设置纹理坐标和顶点
        glEnd();                        // 三角形绘制结束
    }
    return TRUE;                            // 返回
}

搞定!我们已经完成了自己的第一帧画面。这绝对算不上什么Quake,但咳...,我们绝对也不是Carmack或者Abrash。运行程序时,您可以按下F、B、
PgUp 和 PgDown 键来看看效果。PgUp /
PgDown简单的上下倾斜镜头。如果NeHe决定保留的话,程序中使用的纹理取自于我的学校ID证件上的照片,并且做了浮雕效果....:)。
现在您也许在考虑下一步该做什么。但还是不要考虑使用这些代码来实现完整的3D引擎,写这个程序的目的也并非如此。您也许希望您的游戏中不止存在一个Sector,尤其是实现类似入口这样的部分,您还可能需要使用多边形(超过3个顶点)。程序现在的代码实现允许载入多个Sector并剔除了背面(背向镜头不用绘制的多边形)。将来我会写个这样的教程,但这需要更多的数学知识基础。

时间: 2024-10-13 07:53:00

第10课 OpenGL 3D世界的相关文章

NeHe OpenGL教程 第十课:3D世界

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第十课:3D世界 加载3D世界,并在其中漫游: 在这一课中,你将学会如何加载3D世界,并在3D世界中漫游.这一课使用第一课的代码,当然在课程说明中我只介绍改变了代码. 这一课是由Lionel Brits (βtelgeuse)所写的

第05课 OpenGL 3D空间

3D空间: 我们使用多边形和四边形创建3D物体,在这一课里,我们把三角形变为立体的金子塔形状,把四边形变为立方体. 在上节课的内容上作些扩展,我们现在开始生成真正的3D对象,而不是象前两节课中那样3D世界中的2D对象.我们给三角形增加一个左侧面,一个右侧面,一个后侧面来生成一个金字塔(四棱锥).给正方形增加左.右.上.下及背面生成一个立方体. 我们混合金字塔上的颜色,创建一个平滑着色的对象.给立方体的每一面则来个不同的颜色. int DrawGLScene(GLvoid) // 此过程中包括所有

NeHe OpenGL教程 第四十四课:3D光晕

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第四十四课:3D光晕 3D 光晕 当镜头对准太阳的时候就会出现这种效果,模拟它非常的简单,一点数学和纹理贴图就够了.好好看看吧. 大家好,欢迎来到新的一课,在这一课中我们将扩展glCamera类,来实现镜头光晕的效果.在日常生活中,

NeHe OpenGL教程 第五课:3D空间

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. 第五课:3D空间 3D空间: 我们使用多边形和四边形创建3D物体,在这一课里,我们把三角形变为立体的金子塔形状,把四边形变为立方体. 在上节课的内容上作些扩展,我们现在开始生成真正的3D对象,而不是象前两节课中那样3D世界中的2D对象.我们给三角形增加一

第1部分: 游戏引擎介绍, 渲染和构造3D世界

原文作者:Jake Simpson译者: 向海Email:[email protected] ------------------------------------------------------------第1部分: 游戏引擎介绍, 渲染和构造3D世界 介绍 自Doom游戏时代以来我们已经走了很远. DOOM不只是一款伟大的游戏,它同时也开创了一种新的游戏编程模式: 游戏 "引擎". 这种模块化,可伸缩和扩展的设计观念可以让游戏玩家和程序设计者深入到游戏核心,用新的模型,场景和

使用ivx的3D世界实现跑马灯效果的经验总结

之前的案例涉及的动画效果都是平面展示,但是ivx中也可以通过3D世界组件展示3D的效果.今天我们就以跑马灯为例来讲一下ivx中的3D世界是如何使用的.一.3D世界3D世界最基础的组成部分就是坐标系和摄像机.坐标系是一个空间直角坐标系,3D世界下的所有组件都会有一个XYZ坐标来决定它在3D世界中的位置,而摄像机负责控制我们的视角,下图中红圈处就是摄像机的位置,黄线框起来的区域就是我们的视角范围.另外我们还可以在3D世界中添加各种光源,字体,图片,图片序列和物体模型这些具有展示效果的组件,除此之外还

Spark3000门徒第10课Java开发Spark实战总结

今晚听了王家林老师的第10课Java开发Spark实战,课后作业是:用Java方式采用Maven开发Spark的WordCount并运行在集群中 先配置pom.xml <groupId>com.dt.spark</groupId> <artifactId>SparkApps</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging

高并发之Memcached实战第10课-“Memcached Get获取数据”部分代码分享2

高并发之Memcached实战第10课-"Memcached Get获取数据"部分代码分享2 一.Memcached客户端读写在同一个程序的逻辑: MemcachedClient mcc = new MemcachedClient(list); if(mcc.get("something")==null) { if(!DataFactory.Exist(somethingObject)) { DataFactory.StoreInDB(somethingObject

安卓学习第10课——listview

1.普通listview <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" androi