在这一课中,你将学会如何加载3D世界,并在3D世界中漫游。
现在这些日子您所需要的是一个大一点的、更复杂些的、动态3D世界,它带有空间的六自由度和花哨的效果如镜像、入口、扭曲等等,当然还要有更快的帧显示速度。这一课就要解释一个基本的3D世界"结构",以及如何在这个世界里游走。
数据结构
当您想要使用一系列的数字来完美的表达3D环境时,随着环境复杂度的上升,这个工作的难度也会随之上升。出于这个原因,我们必须将数据归类,使其具有更多的可操作性风格。在程序清单头部出现了sector(区段)的定义。每个3D世界基本上可以看作是sector(区段)的集合。一个sector(区段)可以是一个房间、一个立方体、或者任意一个闭合的区间。
这里我们在渲染类Render里面定义了三个内部类,内部类的代码如下所示:
- /**
- * 定义顶点
- * 顶点包含了OpenGL真正感兴趣的数据。
- * 我们用3D空间中的坐标值(x,y,z)以及它们的纹理坐标(u,v)来定义三角形的每个顶点。
- */
- private static class Vertex {
- public float x, y, z;
- public float u, v;
- }
- /**
- * 定义三边形
- * 三角形本质上是由一些(两个以上)顶点组成的多边形,
- * 顶点同时也是我们的最基本的分类单位。
- */
- private static class Triangle {
- public Vertex[] vertex = new Vertex[3];
- public Triangle() {
- for (int i = 0; i < 3; i++)
- vertex[i] = new Vertex();
- }
- }
- /**
- * 区段定义
- * 一个sector(区段)包含了一系列的多边形,
- * 所以下一个目标就是triangle(我们将只用三角形,这样写代码更容易些)。
- */
- private static class Sector {
- public int numtriangles;
- public Triangle[] triangles;
- public Sector(int inTri) {
- //确定了三边形的数量
- numtriangles = inTri;
- triangles = new Triangle[inTri];
- //对三边形赋值
- for (int i = 0; i < inTri; i++)
- triangles[i] = new Triangle();
- }
- }
/**
* 定义顶点
* 顶点包含了OpenGL真正感兴趣的数据。
* 我们用3D空间中的坐标值(x,y,z)以及它们的纹理坐标(u,v)来定义三角形的每个顶点。
*/
private static class Vertex {
public float x, y, z;
public float u, v;
}
/**
* 定义三边形
* 三角形本质上是由一些(两个以上)顶点组成的多边形,
* 顶点同时也是我们的最基本的分类单位。
*/
private static class Triangle {
public Vertex[] vertex = new Vertex[3];
public Triangle() {
for (int i = 0; i < 3; i++)
vertex[i] = new Vertex();
}
}
/**
* 区段定义
* 一个sector(区段)包含了一系列的多边形,
* 所以下一个目标就是triangle(我们将只用三角形,这样写代码更容易些)。
*/
private static class Sector {
public int numtriangles;
public Triangle[] triangles;
public Sector(int inTri) {
//确定了三边形的数量
numtriangles = inTri;
triangles = new Triangle[inTri];
//对三边形赋值
for (int i = 0; i < inTri; i++)
triangles[i] = new Triangle();
}
}
下面是渲染类的全部的代码:
- package demos.nehe.lesson10;
- import demos.common.ResourceRetriever;
- import demos.common.TextureReader;
- import javax.media.opengl.GL;
- import javax.media.opengl.GLAutoDrawable;
- import javax.media.opengl.GLEventListener;
- import javax.media.opengl.glu.GLU;
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.util.StringTokenizer;
- import javax.media.opengl.GL2;
- import javax.media.opengl.glu.gl2.GLUgl2;
- class Renderer implements GLEventListener {
- //常量
- private final float PI_180 = (float) (Math.PI / 180.0);
- //启用混合的开关
- private boolean blendingEnabled; // Blending ON/OFF
- //
- private float heading;
- private float xpos;
- private float zpos;
- //设置是否可以在上下左右移动或旋转视角
- private boolean stepForward;
- private boolean stepBackward;
- private boolean turnRight;
- private boolean turnLeft;
- /**
- * 当左右方向键按下后,旋转变量yrot相应增加或减少。
- * 当前后方向键按下后,我们使用sine和cosine函数重新生成镜头位置(您需要些许三角函数学的知识:-)。
- * Piover180是一个很简单的折算因子用来折算度和弧度。
- *
- */
- //Y轴方向上的旋转量
- private float yrot; // Y Rotation
- /**
- * walkbias:基本上就是当人行走时头部产生上下摆动的幅度。
- * 我们使用简单的sine正弦波来调节镜头的Y轴位置。
- * 如果不添加这个而只是前后移动的话,程序看起来就没这么棒了。
- */
- private float walkbias = 0;
- private float walkbiasangle = 0;
- private float lookupdown = 0.0f;
- private boolean lookUp;
- private boolean lookDown;
- //选择过滤的种类
- private int filter;
- //使用的纹理贴图
- private int[] textures = new int[3];
- //我们的模型存储
- private Sector sector1;
- private GLU glu = new GLUgl2();
- /**
- * 下面是相关的设置启动上面的上下左右及其混合
- */
- public void toggleBlending() {
- blendingEnabled = !blendingEnabled;
- }
- public void switchFilter() {
- filter = (filter + 1) % 3;
- }
- public void stepForward(boolean step) {
- stepForward = step;
- }
- public void stepBackward(boolean step) {
- stepBackward = step;
- }
- public void turnRight(boolean turn) {
- turnRight = turn;
- }
- public void turnLeft(boolean turn) {
- turnLeft = turn;
- }
- public void lookUp(boolean look) {
- lookUp = look;
- }
- public void lookDown(boolean look) {
- lookDown = look;
- }
- /**
- * 导入3D的模型
- * 载入文件
- * 在程序内部直接存储数据会让程序显得太过死板和无趣。
- * 从磁盘上载入世界资料,会给我们带来更多的弹性,
- * 可以让我们体验不同的世界,而不用被迫重新编译程序。
- * 另一个好处就是用户可以切换世界资料并修改它们而无需知道程序如何读入输出这些资料的。
- * 数据文件的类型我们准备使用文本格式。
- * 这样编辑起来更容易,写的代码也更少。等将来我们也许会使用二进制文件。
- * @throws IOException
- */
- private void setupWorld() throws IOException {
- BufferedReader in = null;
- try {
- /**
- * 文件格式
- * 数据文件中每个三角形都以如下形式声明:
- * X1 Y1 Z1 U1 V1
- * X2 Y2 Z2 U2 V2
- * X3 Y3 Z3 U3 V3
- */
- in = new BufferedReader(new InputStreamReader(ResourceRetriever.getResourceAsStream("demos/data/models/world.txt")));
- String line = null;
- while ((line = in.readLine()) != null) {
- //如果一行为空
- if (line.trim().length() == 0 || line.trim().startsWith("//"))
- continue;
- //读取第一行 第一行里以字符串“NUMPOLLIES”开头,记录了这个区段里的三边形的数量
- if (line.startsWith("NUMPOLLIES")) {
- int numTriangles;
- //将读取的数量的字符串转化为整形
- numTriangles = Integer.parseInt(line.substring(line.indexOf("NUMPOLLIES") + "NUMPOLLIES".length() + 1));
- //构建区段将顶点和面的信息存储在区段里并保存在内存中
- sector1 = new Sector(numTriangles);
- break;
- }
- }
- //将顶点信息和面的信息保存在区段中
- for (int i = 0; i < sector1.numtriangles; i++) {
- for (int vert = 0; vert < 3; vert++) {
- //去除空格行
- while ((line = in.readLine()) != null) {
- if (line.trim().length() == 0 || line.trim().startsWith("//"))
- continue;
- break;
- }
- //如果不为空格行
- if (line != null) {
- StringTokenizer st = new StringTokenizer(line, " ");
- //由文件txt中存储的格式,将每行中的数据转换为浮点型,储存到三边形的顶点数组中
- sector1.triangles[i].vertex[vert].x = Float.valueOf(st.nextToken()).floatValue();
- sector1.triangles[i].vertex[vert].y = Float.valueOf(st.nextToken()).floatValue();
- sector1.triangles[i].vertex[vert].z = Float.valueOf(st.nextToken()).floatValue();
- sector1.triangles[i].vertex[vert].u = Float.valueOf(st.nextToken()).floatValue();
- sector1.triangles[i].vertex[vert].v = Float.valueOf(st.nextToken()).floatValue();
- }
- }
- }
- } finally {
- if (in != null)
- in.close();
- }
- }
- /**
- * 导入纹理图片,纹理图片banding
- * @param gl
- * @param glu
- * @throws IOException
- */
- private void loadGLTextures(GL2 gl, GLU glu) throws IOException {
- TextureReader.Texture texture = TextureReader.readTexture("demos/data/images/mud.png");
- //Create Nearest Filtered Texture
- //启用Nearest 过滤
- gl.glGenTextures(3, textures, 0);
- gl.glBindTexture(GL2.GL_TEXTURE_2D, textures[0]);
- gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
- gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
- gl.glTexImage2D(GL2.GL_TEXTURE_2D,
- 0,
- 3,
- texture.getWidth(),
- texture.getHeight(),
- 0,
- GL.GL_RGB,
- GL.GL_UNSIGNED_BYTE,
- texture.getPixels());
- //Create Linear Filtered Texture
- //启用线性过滤的纹理
- gl.glBindTexture(GL2.GL_TEXTURE_2D, textures[1]);
- gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
- gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
- gl.glTexImage2D(GL2.GL_TEXTURE_2D,
- 0,
- 3,
- texture.getWidth(),
- texture.getHeight(),
- 0,
- GL.GL_RGB,
- GL.GL_UNSIGNED_BYTE,
- texture.getPixels());
- //启用 mipmapped滤波方式
- gl.glBindTexture(GL2.GL_TEXTURE_2D, textures[2]);
- gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
- gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_NEAREST);
- glu.gluBuild2DMipmaps(GL2.GL_TEXTURE_2D,
- 3,
- texture.getWidth(),
- texture.getHeight(),
- GL.GL_RGB,
- GL.GL_UNSIGNED_BYTE,
- texture.getPixels());
- }
- public void init(GLAutoDrawable drawable) {
- GL2 gl = drawable.getGL().getGL2();
- try {
- //导入纹理图片并banding
- loadGLTextures(gl, glu);
- //导入模型顶点和三边形
- setupWorld();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- //启用纹理映射
- gl.glEnable(GL2.GL_TEXTURE_2D);
- //设置混合
- gl.glBlendFunc(GL2.GL_SRC_ALPHA, GL.GL_ONE);
- //启用高斯模糊
- gl.glShadeModel(GL2.GL_SMOOTH);
- //设置背景颜色
- gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
- //启用深度缓存
- gl.glClearDepth(1.0);
- //启用深度测试
- gl.glEnable(GL.GL_DEPTH_TEST);
- gl.glDepthFunc(GL.GL_LEQUAL); //The Type Of Depth Test To Do
- //启用
- gl.glHint(GL2.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST); // Really Nice Perspective Calculations
- }
- private void update() {
- /**
- * 这个实现很简单。当左右方向键按下后,旋转变量yrot相应增加或减少。
- * 当前后方向键按下后,我们使用sine和cosine函数重新生成镜头位置
- * (您需要些许三角函数学的知识:-)。
- * Piover180是一个很简单的折算因子用来折算度和弧度。
- * walkbias是什么意思?这是NeHe的发明的单词:-)。
- * 基本上就是当人行走时头部产生上下摆动的幅度。
- * 我们使用简单的sine正弦波来调节镜头的Y轴位置。
- * 如果不添加这个而只是前后移动的话,程序看起来就没这么棒了。
- */
- if (stepForward) {
- xpos -= (float) Math.sin(heading * PI_180) * 0.05f;
- zpos -= (float) Math.cos(heading * PI_180) * 0.05f;
- if (walkbiasangle >= 359.0f) {
- walkbiasangle = 0.0f;
- } else {
- walkbiasangle += 10;
- }
- walkbias = (float) Math.sin(walkbiasangle * PI_180) / 20.0f;
- }
- if (stepBackward) {
- xpos += (float) Math.sin(heading * PI_180) * 0.05f;
- zpos += (float) Math.cos(heading * PI_180) * 0.05f;
- if (walkbiasangle <= 1.0f) {
- walkbiasangle = 359.0f;
- } else {
- walkbiasangle -= 10;
- }
- walkbias = (float) Math.sin(walkbiasangle * PI_180) / 20.0f;
- }
- if (turnRight) {
- heading -= 1.0f;
- yrot = heading;
- }
- if (turnLeft) {
- heading += 1.0f;
- yrot = heading;
- }
- if (lookUp) {
- lookupdown -= 1.0f;
- }
- if (lookDown) {
- lookupdown += 1.0f;
- }
- }
- /**
- * 绘制模型
- * @param drawable
- */
- public void display(GLAutoDrawable drawable) {
- //更新
- update();
- GL2 gl = drawable.getGL().getGL2();
- //启用颜色缓存和深度缓存
- gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); //Clear The Screen And The Depth Buffer
- //重置观察模型
- gl.glLoadIdentity(); //Reset The View
- //如果混合启用就开启混合
- if (!blendingEnabled) {
- gl.glDisable(GL.GL_BLEND);
- gl.glEnable(GL.GL_DEPTH_TEST);
- } else {
- gl.glEnable(GL.GL_BLEND);
- gl.glDisable(GL.GL_DEPTH_TEST);
- }
- // 顶点的临时 X, Y, Z, U 和 V 的数值
- float x = 0,y = 0,z = 0,u = 0,v = 0;
- //平移的三维分量
- 用于游戏者沿X轴平移时的大小
- float xtrans = -xpos;
- // 用于游戏者沿Z轴平移时的大小
- float ztrans = -zpos;
- // 用于头部的上下摆动
- float ytrans = -walkbias - 0.25f;
- // 位于游戏者方向的360度角
- float sceneroty = 360.0f - yrot;
- // 上下旋转
- gl.glRotatef(lookupdown, 1.0f, 0, 0);
- // 根据游戏者正面所对方向所作的旋转
- gl.glRotatef(sceneroty, 0, 1.0f, 0);
- // 以游戏者为中心的平移场景
- gl.glTranslatef(xtrans, ytrans, ztrans);
- // 根据 filter 选择的纹理
- gl.glBindTexture(GL2.GL_TEXTURE_2D, textures[filter]);
- // 处理各个三边形
- for (int i = 0; i < sector1.numtriangles; i++) {
- //绘制三边形
- gl.glBegin(GL2.GL_TRIANGLES);
- // 指向前面的法线
- gl.glNormal3f(0.0f, 0.0f, 1.0f);
- //第一个顶点的各分量
- x = sector1.triangles[i].vertex[0].x;
- y = sector1.triangles[i].vertex[0].y;
- z = sector1.triangles[i].vertex[0].z;
- u = sector1.triangles[i].vertex[0].u;
- v = sector1.triangles[i].vertex[0].v;
- gl.glTexCoord2f(u, v);
- gl.glVertex3f(x, y, z);
- //第二个顶点的各分量
- x = sector1.triangles[i].vertex[1].x;
- y = sector1.triangles[i].vertex[1].y;
- z = sector1.triangles[i].vertex[1].z;
- u = sector1.triangles[i].vertex[1].u;
- v = sector1.triangles[i].vertex[1].v;
- gl.glTexCoord2f(u, v);
- gl.glVertex3f(x, y, z);
- //第三个顶点的各分量
- x = sector1.triangles[i].vertex[2].x;