当前位置: 首页 > 文档资料 > HTML5 Canvas 实战 >

9.6 创建一个可以探索的3D世界

优质
小牛编辑
128浏览
2023-12-01

现在我们知道如何使用纹理和光照创建一个基本的3D模型,现在,该创建我们自己的3D世界了。本节,我们将创建三套缓冲区,立方体缓冲区、墙壁缓冲区、地板缓冲区。我们使用立方体缓冲区渲染在我们的世界中随机放置的木箱,使用墙壁缓冲区用来创建墙壁,使用地板缓冲区用来创建地板和天花板(我们可以重用地板缓冲区来创建天花板,是因为它们的形状完全相同)。接下来,我们为文档添加键盘事件监听器,以便可以使用方向键和鼠标来探索我们的世界。我们开始吧!

3D世界
图9-5 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类