9.6 创建一个可以探索的3D世界
现在我们知道如何使用纹理和光照创建一个基本的3D模型,现在,该创建我们自己的3D世界了。本节,我们将创建三套缓冲区,立方体缓冲区、墙壁缓冲区、地板缓冲区。我们使用立方体缓冲区渲染在我们的世界中随机放置的木箱,使用墙壁缓冲区用来创建墙壁,使用地板缓冲区用来创建地板和天花板(我们可以重用地板缓冲区来创建天花板,是因为它们的形状完全相同)。接下来,我们为文档添加键盘事件监听器,以便可以使用方向键和鼠标来探索我们的世界。我们开始吧!
操作步骤
按照以下步骤,使用WebGL创建一个到处随机置有木箱,并可以使用键盘和鼠标进行探索的3D世界:
1. 链接到glMatrix库和WebGL包装器:
<script type="text/javascript" src="glMatrix-1.0.1.min.js"> </script>
<script type="text/javascript" src="WebGL.js"> </script>
2. 定义Controller构造函数,该函数初始化视图、WebGL包装器对象和模型对象,附加键盘事件监听器,并加载纹理:
/****
* Controller
*/
function Controller(){
this.view = new View(this);
this.gl = new WebGL("myCanvas");
this.gl.setShaderProgram("TEXTURE_DIRECTIONAL_LIGHTING");
this.model = new Model(this);
this.attachListeners();
var sources = {
crate: "crate.jpg",
metalFloor: "metalFloor.jpg",
metalWall: "metalWall.jpg",
ceiling: "ceiling.jpg"
};
this.mouseDownPos = null;
this.mouseDownPitch = 0;
this.mouseDownYaw = 0;
var that = this;
this.loadTextures(sources, function(){
that.gl.setStage(function(){
that.view.stage();
});
that.gl.start();
});
}
3. 定义loadTextures()方法,该方法加载纹理:
Controller.prototype.loadTextures = function(sources, callback) {
var gl = this.gl;
var context = gl.getContext();
var textures = this.model.textures;
var loadedImages = 0;
var numImages = 0;
for (var src in sources) {
// anonymous function to induce scope
(function() {
var key = src;
numImages++;
textures[key] = context.createTexture();
textures[key].image = new Image();
textures[key].image.onload = function(){
gl.initTexture(textures[key]);
if (++loadedImages >= numImages) {
callback();
}
};
textures[key].image.src = sources[key];
})();
}
};
4. 定义getMousePos()方法,该方法获取鼠标位置:
Controller.prototype.getMousePos = function(evt){
return {
x: evt.clientX,
y: evt.clientY
};
};
5. 定义handleMouseDown()方法,该方法捕获鼠标起始位置,相机的倾斜度和偏转角:
Controller.prototype.handleMouseDown = function(evt){
var camera = this.model.camera;
this.mouseDownPos = this.getMousePos(evt);
this.mouseDownPitch = camera.pitch;
this.mouseDownYaw = camera.yaw;
};
6. 定义handleMouseMove()方法,该方法更新相机:
Controller.prototype.handleMouseMove = function(evt) {
var mouseDownPos = this.mouseDownPos;
var gl = this.gl;
if (mouseDownPos !== null) {
var mousePos = this.getMousePos(evt);
//更新倾斜度
var yDiff = mousePos.y - mouseDownPos.y;
this.model.camera.pitch = this.mouseDownPitch + yDiff / gl.getCanvas().height;
//更新偏转角
var xDiff = mousePos.x - mouseDownPos.x;
this.model.camera.yaw = this.mouseDownYaw + xDiff / gl.getCanvas().width;
}
};
7. 定义handleKeyDown()方法,该方法控制用户在整个世界中移动:
Controller.prototype.handleKeyDown = function(evt) {
var model = this.model;
var keycode = ((evt.which) || (evt.keyCode));
switch (keycode) {
//左箭头键
case 37:
model.sideMovement = model.LEFT;
break;
//上箭头键
case 38:
model.straightMovement = model.FORWARD;
break;
//右箭头键
case 39:
model.sideMovement = model.RIGHT;
break;
//下箭头键
case 40:
model.straightMovement = model.BACKWARD;
break;
}
};
8. 定义handleKeyUp()方法,当用户释放左箭头键或右箭头键,该方法设置侧向运动方向为STILL,当用户释放上箭头键或下箭头键,该方法设置直线运动方向为STILL:
Controller.prototype.handleKeyUp = function(evt){
var model = this.model;
var keycode = ((evt.which) || (evt.keyCode));
switch (keycode) {
//左箭头键
case 37:
model.sideMovement = model.STILL;
break;
//上箭头键
case 38:
model.straightMovement = model.STILL;
break;
//右箭头键
case 39:
model.sideMovement = model.STILL;
break;
//下箭头键
case 40:
model.straightMovement = model.STILL;
break;
}
};
9. 定义attachListeners()方法,该方法为画布和文档附加事件监听器:
Controller.prototype.attachListeners = function(){
var gl = this.gl;
var that = this;
gl.getCanvas().addEventListener("mousedown", function(evt) {
that.handleMouseDown(evt);
}, false);
gl.getCanvas().addEventListener("mousemove", function(evt) {
that.handleMouseMove(evt);
}, false);
document.addEventListener("mouseup", function(evt) {
that.mouseDownPos = null;
}, false);
document.addEventListener("mouseout", function(evt){
// 与mouseup的功能相同
that.mouseDownPos = null;
}, false);
document.addEventListener("keydown", function(evt) {
that.handleKeyDown(evt);
}, false);
document.addEventListener("keyup", function(evt) {
that.handleKeyUp(evt);
}, false);
};
10. 定义Model的构造函数,该函数初始化相机,及木箱、地板、墙壁的缓冲区:
/****
* Model
*/
function Model(controller){
this.controller = controller;
this.cubeBuffers = {};
this.floorBuffers = {};
this.wallBuffers = {};
this.angle = 0;
this.textures = {};
this.cratePositions = [];
// movements
this.STILL = "STILL";
this.FORWARD = "FORWARD";
this.BACKWARD = "BACKWARD";
this.LEFT = "LEFT";
this.RIGHT = "RIGHT";
// camera
this.camera = {
x: 0,
y: 1.5,
z: 5,
pitch: 0,
yaw: 0
};
this.straightMovement = this.STILL;
this.sideMovement = this.STILL;
this.speed = 8; // units per second this.initBuffers();
this.initCratePositions();
}
11. 定义initCratePositions()方法,该方法创建20个位置随机并随机堆放的木箱:
Model.prototype.initCratePositions = function() {
var crateRange = 45;
// randomize 20 floor crates
for (var n = 0; n < 20; n++) {
var cratePos = {};
cratePos.x = (Math.random() * crateRange * 2) - crateRange;
cratePos.y = 0;
cratePos.z = (Math.random() * crateRange * 2) - crateRange;
cratePos.rotationY = Math.random() * Math.PI * 2;
this.cratePositions.push(cratePos);
if (Math.round(Math.random() * 3) == 3) {
var stackedCratePosition = {};
stackedCratePosition.x = cratePos.x;
stackedCratePosition.y = 2.01;
stackedCratePosition.z = cratePos.z;
stackedCratePosition.rotationY = cratePos.
rotationY + ((Math.random() * Math.PI / 8) - Math.PI / 16);
this.cratePositions.push(stackedCratePosition);
}
}
};
12. 定义initCubeBuffers()方法,该方法初始化模型的立方体缓冲区:
Model.prototype.initCubeBuffers = function(){
var gl = this.controller.gl;
this.cubeBuffers.positionBuffer = gl.createArrayBuffer([
-1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, // Front face
-1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, // Back face
-1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1, // Top face
-1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // Bottom face
1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, // Right face
-1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1 // Left face
]);
this.cubeBuffers.normalBuffer = gl.createArrayBuffer([
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // Front face
0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, // Back face
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // Top face
0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, // Bottom face
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // Right face
-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0 // Left face
]);
this.cubeBuffers.textureBuffer = gl.createArrayBuffer([
0, 0, 1, 0, 1, 1, 0, 1, // Front face
1, 0, 1, 1, 0, 1, 0, 0, // Back face
0, 1, 0, 0, 1, 0, 1, 1, // Top face
1, 1, 0, 1, 0, 0, 1, 0, // Bottom face
1, 0, 1, 1, 0, 1, 0, 0, // Right face
0, 0, 1, 0, 1, 1, 0, 1 // Left face
]);
this.cubeBuffers.indexBuffer = gl.createElementArrayBuffer([
0, 1, 2, 0, 2, 3, // Front face
4, 5, 6, 4, 6, 7, // Back face
8, 9, 10, 8, 10, 11, // Top face
12, 13, 14, 12, 14, 15, // Bottom face
16, 17, 18, 16, 18, 19, // Right face
20, 21, 22, 20, 22, 23 // Left face
]);
};
13. 定义initFloorBuffers()方法,该方法初始化地板缓冲区(该缓冲区也可以用于天花板):
Model.prototype.initFloorBuffers = function(){
var gl = this.controller.gl;
this.floorBuffers.positionBuffer = gl.createArrayBuffer([
-50, 0, -50, -50, 0, 50, 50, 0, 50, 50, 0, -50
]);
this.floorBuffers.textureBuffer = gl.createArrayBuffer([
0, 25, 0, 0, 25, 0, 25, 25
]);
this.floorBuffers.indexBuffer = gl.createElementArrayBuffer([
0, 1, 2, 0, 2, 3
]);
// floor normal points upwards
this.floorBuffers.normalBuffer = gl.createArrayBuffer([
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0
]);
};
14. 定义initWallBuffers()方法,该方法初始化墙壁缓冲区:
Model.prototype.initWallBuffers = function(){
var gl = this.controller.gl;
this.wallBuffers.positionBuffer = gl.createArrayBuffer([
-50, 5, 0, 50, 5, 0, 50, -5, 0, -50, -5, 0
]);
this.wallBuffers.textureBuffer = gl.createArrayBuffer([
0, 0, 25, 0, 25, 1.5, 0, 1.5
]);
this.wallBuffers.indexBuffer = gl.createElementArrayBuffer([
0, 1, 2, 0, 2, 3
]);
// floor normal points upwards
this.wallBuffers.normalBuffer = gl.createArrayBuffer([
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1
]);
};
15. 定义initBuffers()方法,该方法初始化立方体、地板和墙壁缓冲区:
Model.prototype.initBuffers = function() {
this.initCubeBuffers();
this.initFloorBuffers();
this.initWallBuffers();
};
16. 定义updateCameraPos()方法,该方法用于在每个动画帧更新相机的位置:
Model.prototype.updateCameraPos = function() {
var gl = this.controller.gl;
if (this.straightMovement != this.STILL) {
var direction = this.straightMovement == this.FORWARD ? -1 : 1;
var distEachFrame = direction * this.speed * gl.getTimeInterval() / 1000;
this.camera.z += distEachFrame * Math.cos(this.camera.yaw);
this.camera.x += distEachFrame * Math.sin(this.camera.yaw);
}
if (this.sideMovement != this.STILL) {
var direction = this.sideMovement == this.RIGHT ? 1 : -1;
var distEachFrame = direction * this.speed * gl.getTimeInterval() / 1000;
this.camera.z += distEachFrame * Math.cos(this.camera.yaw + Math.PI / 2);
this.camera.x += distEachFrame * Math.sin(this.camera.yaw + Math.PI / 2);
}
};
17. 定义View构造函数,该函数设置画布的尺寸:
/*** *
* View
*/
function View(controller){
this.controller = controller;
this.canvas = document.getElementById("myCanvas");
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
18. 定义drawFloor()方法,该方法绘制地板:
View.prototype.drawFloor = function(){
var controller = this.controller;
var gl = controller.gl;
var model = controller.model;
var floorBuffers = model.floorBuffers;
gl.save();
gl.translate(0, -1.1, 0);
gl.pushPositionBuffer(floorBuffers);
gl.pushNormalBuffer(floorBuffers);
gl.pushTextureBuffer(floorBuffers,
model.textures.metalFloor);
gl.pushIndexBuffer(floorBuffers);
gl.drawElements(floorBuffers);
gl.restore();
};
19. 定义drawCeiling()方法,该方法绘制天花板:
View.prototype.drawCeiling = function(){
var controller = this.controller;
var gl = controller.gl;
var model = controller.model;
var floorBuffers = model.floorBuffers;
gl.save();
gl.translate(0, 8.9, 0);
// use floor buffers with ceiling texture
gl.pushPositionBuffer(floorBuffers);
gl.pushNormalBuffer(floorBuffers);
gl.pushTextureBuffer(floorBuffers, model.textures.ceiling);
gl.pushIndexBuffer(floorBuffers);
gl.drawElements(floorBuffers);
gl.restore();
};
20. 定义drawCrates()方法,该方法绘制木箱:
View.prototype.drawCrates = function(){
var controller = this.controller;
var gl = controller.gl;
var model = controller.model;
var cubeBuffers = model.cubeBuffers;
for (var n = 0; n < model.cratePositions.length; n++) {
gl.save();
var cratePos = model.cratePositions[n];
gl.translate(cratePos.x, cratePos.y, cratePos.z);
gl.rotate(cratePos.rotationY, 0, 1, 0);
gl.pushPositionBuffer(cubeBuffers);
gl.pushNormalBuffer(cubeBuffers);
gl.pushTextureBuffer(cubeBuffers,
model.textures.crate);
gl.pushIndexBuffer(cubeBuffers);
gl.drawElements(cubeBuffers);
gl.restore();
}
};
21. 定义drawWalls()方法,该方法绘制墙壁:
View.prototype.drawWalls = function(){
var controller = this.controller;
var gl = controller.gl;
var model = controller.model;
var wallBuffers = model.wallBuffers;
var metalWallTexture = model.textures.metalWall;
gl.save();
gl.translate(0, 3.9, -50);
gl.pushPositionBuffer(wallBuffers);
gl.pushNormalBuffer(wallBuffers);
gl.pushTextureBuffer(wallBuffers, metalWallTexture);
gl.pushIndexBuffer(wallBuffers);
gl.drawElements(wallBuffers);
gl.restore();
gl.save();
gl.translate(0, 3.9, 50);
gl.rotate(Math.PI, 0, 1, 0);
gl.pushPositionBuffer(wallBuffers);
gl.pushNormalBuffer(wallBuffers);
gl.pushTextureBuffer(wallBuffers, metalWallTexture);
gl.pushIndexBuffer(wallBuffers);
gl.drawElements(wallBuffers);
gl.restore();
gl.save();
gl.translate(50, 3.9, 0);
gl.rotate(Math.PI * 1.5, 0, 1, 0);
gl.pushPositionBuffer(wallBuffers);
gl.pushNormalBuffer(wallBuffers);
gl.pushTextureBuffer(wallBuffers, metalWallTexture);
gl.pushIndexBuffer(wallBuffers);
gl.drawElements(wallBuffers);
gl.restore();
gl.save();
gl.translate(-50, 3.9, 0);
gl.rotate(Math.PI / 2, 0, 1, 0);
gl.pushPositionBuffer(wallBuffers);
gl.pushNormalBuffer(wallBuffers);
gl.pushTextureBuffer(wallBuffers, metalWallTexture);
gl.pushIndexBuffer(wallBuffers);
gl.drawElements(wallBuffers);
gl.restore();
};
22. 定义stage()方法,该方法更新相机位置,清除画布,相对于相机位置定位世界,然后绘制地板、墙壁、天花板、和木箱:
View.prototype.stage = function(){
var controller = this.controller;
var gl = controller.gl;
var model = controller.model;
var view = controller.view;
var camera = model.camera;
model.updateCameraPos();
gl.clear();
// set field of view at 45 degrees
// set viewing range between 0.1 and 100 units away.
gl.perspective(45, 0.1, 150.0);
gl.identity();
gl.rotate(-camera.pitch, 1, 0, 0);
gl.rotate(-camera.yaw, 0, 1, 0);
gl.translate(-camera.x, -camera.y, -camera.z);
// enable lighting
gl.enableLighting();
gl.setAmbientLighting(0.5, 0.5, 0.5);
gl.setDirectionalLighting(-0.25, -0.25, -1, 0.8, 0.8, 0.8);
view.drawFloor();
view.drawWalls();
view.drawCeiling();
view.drawCrates();
};
23. 页面加载完成后,初始化Controller:
window.onload = function(){
new Controller();
};
24. 在HTML文档的body部分嵌入canvas标签:
<canvas id="myCanvas" width="" height=""> </canvas>
工作原理
本节使用MVC(模型、视图、控制器)设计模式把绘制逻辑从数据逻辑中分离出来。
Controller类负责管理模型和视图,并管理用户的动作。它使用handleKeyDown()和handleKeyUp()方法来处理箭头键事件,使用handleMouseDown()和 handleMouseMove()处理屏幕拖动事件。除此之外,在模拟开始之前,控制器还负责预加载所有的纹理。
其次,模型负责处理所有的数据设置逻辑。我们的模拟包括立方体、地板、墙壁缓冲区,纹理,木箱的位置,相机的位置、倾斜度和偏转角,用户的运动。木箱的位置使用initCratePositions()方法初始化,用于世界的缓冲区使用initCubeBuffers(),initFloorBuffers()和initWallBuffers()方法进行初始化,相机的位置、倾斜度和偏转角使用updateCameraPos()方法进行更新。
最后,视图负责使用模型数据来渲染3D世界。缓冲区被发送到显卡,并使用drawFloor(), drawCeiling(),drawCrates(),和drawWalls()方法进行渲染。在每个动画帧中,调用stage()方法更新相机位置,清除画布,设置光照,并使用前面提到的方法绘制场景。
了解更多
如果你想扩展本节的内容,这里是一些更多的想法:
- 增加边界条件,以便玩家不能穿透木箱和墙壁
- 使玩家可以跳跃,甚至可以跳到木箱上
- 创建通往其他房间的走廊
- 创建楼梯,以便玩家都探索其他楼层
- 使用HTML5的audio标签增加行走时的声音
现在,你能够使用纹理和光照创建3D模型,也可以它们放在一起形成3D世界的片段,在你和现实版的电子争霸之间的唯一的东西,就是你的想象力。祝你玩的开心!
相关参考
第5章 创建Animation类