在数据可视化领域利用webgl来创建三维场景或VR已经越来越普遍,各种开发框架也应运而生。今天我们就通过最基本的threejs来完成第一人称视角的场景巡检功能。如果你是一位threejs的初学者或正打算入门,我强烈推荐你仔细阅读本文并在我的代码基础之上继续深入学习。因为它将是你能够在网上找到的最好的免费中文教程,通过本文你可以学习到一些基本的三维理论,threejs的api接口以及你应该掌握的数学知识。当然要想完全掌握threejs可能还有很长的路需要走,但至少今天我将带你入门并传授一些独特的学习技巧。

第一人称视角的场景巡检主要需要解决两个问题,人物在场景中的移动和碰撞检测。移动与碰撞功能是所有三维场景首先需要解决的基本问题。为了方便理解,首先需要构建一个简单的三维场景并在遇到问题的时候向你演示如何解决它。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>平移与碰撞</title>
  6. <script src="js/three.js"></script>
  7. <script src="js/jquery3.4.1.js"></script>
  8. </head>
  9. <body>
  10. <canvas id="mainCanvas"></canvas>
  11. </body>
  12. <script>
  13. let scene, camera, renderer, leftPress, cube;
  14. init();
  15. helper();
  16. createBoxer();
  17. animate();
  18.  
  19. function init() {
  20. // 初始化场景
  21. scene = new THREE.Scene();
  22. scene.background = new THREE.Color(0xffffff);
  23.  
  24. // 创建渲染器
  25. renderer = new THREE.WebGLRenderer({
  26. canvas: document.getElementById("mainCanvas"),
  27. antialias: true, // 抗锯齿
  28. alpha: true
  29. });
  30. renderer.setSize(window.innerWidth, window.innerHeight);
  31.  
  32. // 创建透视相机
  33. camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  34. camera.position.set(0, 40, 30);
  35. camera.lookAt(0, 0, 0);
  36.  
  37. // 参数初始化
  38. mouse = new THREE.Vector2();
  39. raycaster = new THREE.Raycaster();
  40.  
  41. // 环境光
  42. var ambientLight = new THREE.AmbientLight(0x606060);
  43. scene.add(ambientLight);
  44. // 平行光
  45. var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
  46. directionalLight.position.set(1, 0.75, 0.5).normalize();
  47. scene.add(directionalLight);
  48. }
  49.  
  50. function helper() {
  51. var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
  52. grid.material.opacity = 0.1;
  53. grid.material.transparent = true;
  54. scene.add(grid);
  55.  
  56. var axesHelper = new THREE.AxesHelper(30);
  57. scene.add(axesHelper);
  58. }
  59.  
  60. function animate() {
  61. requestAnimationFrame(animate);
  62. renderer.render(scene, camera);
  63. }
  64.  
  65. function createBoxer() {
  66. var geometry = new THREE.BoxGeometry(5, 5, 5);
  67. var material = new THREE.MeshPhongMaterial({color: 0x00ff00});
  68. cube = new THREE.Mesh(geometry, material);
  69. scene.add(cube);
  70. }
  71.  
  72. $(window).mousemove(function (event) {
  73. event.preventDefault();
  74. if (leftPress) {
  75. cube.rotateOnAxis(
  76. new THREE.Vector3(0, 1, 0),
  77. event.originalEvent.movementX / 500
  78. );
  79. cube.rotateOnAxis(
  80. new THREE.Vector3(1, 0, 0),
  81. event.originalEvent.movementY / 500
  82. );
  83. }
  84. });
  85.  
  86. $(window).mousedown(function (event) {
  87. event.preventDefault();
  88. leftPress = true;
  89.  
  90. });
  91.  
  92. $(window).mouseup(function (event) {
  93. event.preventDefault();
  94. leftPress = false;
  95. });
  96. </script>
  97. </html>

很多js的开发人员非常熟悉jquery,我引用它确实让代码显得更加简单。首先我在init()方法里初始化了一个场景。我知道在大部分示例中包括官方提供的demo里都是通过threejs动态的在document下创建一个<canvas/>节点。我强烈建议你不要这样做,因为在很多单页面应用中(例如:Vue和Angular)直接操作DOM都不被推荐。接下来我使用helper()方法创建了两个辅助对象:一个模拟地面的网格和一个表示世界坐标系的AxesHelper。最后我利用createBoxer()方法在视角中央摆放了一个绿色的立方体以及绑定了三个鼠标动作用来控制立方地旋转。如图:

你可以尝试将代码复制到本地并在浏览器中运行,移动鼠标看看效果。接下来,为了让方块移动起来,我们需要添加一些键盘响应事件,以及给方块的“正面”上色。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>平移与碰撞</title>
  6. <script src="js/three.js"></script>
  7. <script src="js/jquery3.4.1.js"></script>
  8. </head>
  9. <body>
  10. <canvas id="mainCanvas"></canvas>
  11. </body>
  12. <script>
  13. let scene, camera, renderer, leftPress, cube;
  14. let left, right, front, back;
  15. init();
  16. helper();
  17. createBoxer();
  18. animate();
  19.  
  20. function init() {
  21. // 初始化场景
  22. scene = new THREE.Scene();
  23. scene.background = new THREE.Color(0xffffff);
  24.  
  25. // 创建渲染器
  26. renderer = new THREE.WebGLRenderer({
  27. canvas: document.getElementById("mainCanvas"),
  28. antialias: true, // 抗锯齿
  29. alpha: true
  30. });
  31. renderer.setSize(window.innerWidth, window.innerHeight);
  32.  
  33. // 创建透视相机
  34. camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  35. camera.position.set(0, 40, 30);
  36. camera.lookAt(0, 0, 0);
  37.  
  38. // 参数初始化
  39. mouse = new THREE.Vector2();
  40. raycaster = new THREE.Raycaster();
  41.  
  42. // 环境光
  43. var ambientLight = new THREE.AmbientLight(0x606060);
  44. scene.add(ambientLight);
  45. // 平行光
  46. var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
  47. directionalLight.position.set(1, 0.75, 0.5).normalize();
  48. scene.add(directionalLight);
  49. }
  50.  
  51. function helper() {
  52. var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
  53. grid.material.opacity = 0.1;
  54. grid.material.transparent = true;
  55. scene.add(grid);
  56.  
  57. var axesHelper = new THREE.AxesHelper(30);
  58. scene.add(axesHelper);
  59. }
  60.  
  61. function animate() {
  62. requestAnimationFrame(animate);
  63. renderer.render(scene, camera);
  64. if (front) {
  65. cube.translateZ(-1)
  66. }
  67. if (back) {
  68. cube.translateZ(1);
  69. }
  70. if (left) {
  71. cube.translateX(-1);
  72. }
  73. if (right) {
  74. cube.translateX(1);
  75. }
  76. }
  77.  
  78. function createBoxer() {
  79. var geometry = new THREE.BoxGeometry(5, 5, 5);
  80. var mats = [];
  81. mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
  82. mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
  83. cube = new THREE.Mesh(geometry, mats);
  84. for (let j = 0; j < geometry.faces.length; j++) {
  85. if (j === 8 || j === 9) {
  86. geometry.faces[j].materialIndex = 1;
  87. } else {
  88. geometry.faces[j].materialIndex = 0;
  89. }
  90. }
  91. scene.add(cube);
  92. }
  93.  
  94. $(window).mousemove(function (event) {
  95. event.preventDefault();
  96. if (leftPress) {
  97. cube.rotateOnAxis(
  98. new THREE.Vector3(0, 1, 0),
  99. event.originalEvent.movementX / 500
  100. );
  101. cube.rotateOnAxis(
  102. new THREE.Vector3(1, 0, 0),
  103. event.originalEvent.movementY / 500
  104. );
  105. }
  106. });
  107.  
  108. $(window).mousedown(function (event) {
  109. event.preventDefault();
  110. leftPress = true;
  111.  
  112. });
  113.  
  114. $(window).mouseup(function (event) {
  115. event.preventDefault();
  116. leftPress = false;
  117. });
  118.  
  119. $(window).keydown(function (event) {
  120. switch (event.keyCode) {
  121. case 65: // a
  122. left = true;
  123. break;
  124. case 68: // d
  125. right = true;
  126. break;
  127. case 83: // s
  128. back = true;
  129. break;
  130. case 87: // w
  131. front = true;
  132. break;
  133. }
  134. });
  135.  
  136. $(window).keyup(function (event) {
  137. switch (event.keyCode) {
  138. case 65: // a
  139. left = false;
  140. break;
  141. case 68: // d
  142. right = false;
  143. break;
  144. case 83: // s
  145. back = false;
  146. break;
  147. case 87: // w
  148. front = false;
  149. break;
  150. }
  151. });
  152. </script>
  153. </html>

我们添加了keydown()事件和keyup()事件用来捕获键盘响应。我们还修改了createBoxer()方法,给朝向我们的那一面涂上红色。你一定发现了BoxGeometry所代表的立方体虽然只有6个面,可是为了给“1个面”上色我们却需要同时在“2个面”的材质上着色。这是因为在三维场景中,“面”的含义表示由空间中3个点所代表的区域,而一个矩形由两个三角形拼接而成。完成以后的样子如下:

随意拖动几下鼠标,我们可能会得到一个类似的状态:

设想一下在第一人称视角的游戏中,我们抬高视角观察周围后再降低视角,地平线是否依然处于水平状态。换句话说,无论我们如何拖动鼠标,红色的那面在朝向我们的时候都不应该倾斜。要解释这个问题,我们首先需要搞清楚三维场景中的坐标系概念。在threejs的世界中存在两套坐标体系:世界坐标系和自身坐标系。世界坐标系是整个场景的坐标系统,通过它可以定位场景中的物体。而自身坐标系就比较复杂,实际上一个物体的自身坐标系除了用来表示物体各个部分的相对关系以外主要用来表示物体的旋转。想象一下月球的自转和公转,在地月坐标系中,月球围绕地球公转,同时也绕着自身的Y轴旋转。在我们上面的场景中,立方体自身的坐标轴会随着自身的旋转而改变,当我们的鼠标自下而上滑动后,Y轴将不再垂直于地面。如果这时我们再横向滑动鼠标让立方体绕Y轴旋转,自然整个面都会发生倾斜。如果你还不理解可以在自己的代码中多尝试几次,理解世界坐标系和自身坐标系对于学习webgl尤其重要。很显然,要模拟第一人称的视角转动我们需要让视角上下移动的旋转轴为自身坐标系的X轴,左右移动的旋转轴固定为穿过自身中心的一条与世界坐标系Y轴保持平行的轴线。理解这个问题很不容易,可是解决它却非常简单。threejs为我们提供了方法,我们只需要修改mousemove()方法:

  1. $(window).mousemove(function (event) {
  2. event.preventDefault();
  3. if (leftPress) {
  4. cube.rotateOnWorldAxis(
  5. new THREE.Vector3(0, 1, 0),
  6. event.originalEvent.movementX / 500
  7. );
  8. cube.rotateOnAxis(
  9. new THREE.Vector3(1, 0, 0),
  10. event.originalEvent.movementY / 500
  11. );
  12. }
  13. });

有了控制视角的方式,接下来我们移动一下方块。新的问题又出现了:盒子的运动方向也是沿着自身坐标系的。就和我们看着月亮行走并不会走到月亮上去的情形一样,如果要模拟第一人称视角的移动,视角的移动方向应该永远和世界坐标系保持平行,那么我们是否可以通过世界坐标系来控制物体的移动呢:

  1. function animate() {
  2. requestAnimationFrame(animate);
  3. renderer.render(scene, camera);
  4. if (front) {
  5. // cube.translateZ(-1)
  6. cube.position.z -= 1;
  7. }
  8. if (back) {
  9. // cube.translateZ(1);
  10. cube.position.z += 1;
  11. }
  12. if (left) {
  13. // cube.translateX(-1);
  14. cube.position.x -= 1;
  15. }
  16. if (right) {
  17. // cube.translateX(1);
  18. cube.position.x += 1;
  19. }
  20. }

很显然也不行,原因是我们应该让物体的前进方向与物体面对的方向保持一致:

尽管这个需求显得如此合理,可是threejs似乎并没有提供有效的解决方案,就连官方示例中提供的基于第一人称的移动也仅仅是通过固定物体Y轴数值的方法实现的。在射击游戏中不能蹲下或爬上屋顶实在不能让玩家接受。为了能够在接下来的变换中分解问题和测试效果,我们在模型上添加两个箭头表示物体的前后方向。

  1. let arrowFront, arrowBack;
  2.  
  3. function animate() {
  4. requestAnimationFrame(animate);
  5. renderer.render(scene, camera);
  6. arrowFront.setDirection(cube.getWorldDirection(new THREE.Vector3()).normalize());
  7. arrowFront.position.copy(cube.position);
  8. arrowBack.setDirection(cube.getWorldDirection(new THREE.Vector3()).negate().normalize());
  9. arrowBack.position.copy(cube.position);
  10. if (front) {
  11. // cube.translateZ(-1)
  12. cube.position.z -= 1;
  13. }
  14. if (back) {
  15. // cube.translateZ(1);
  16. cube.position.z += 1;
  17. }
  18. if (left) {
  19. // cube.translateX(-1);
  20. cube.position.x -= 1;
  21. }
  22. if (right) {
  23. // cube.translateX(1);
  24. cube.position.x += 1;
  25. }
  26. }
  27.  
  28. function createBoxer() {
  29. var geometry = new THREE.BoxGeometry(5, 5, 5);
  30. var mats = [];
  31. mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
  32. mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
  33. cube = new THREE.Mesh(geometry, mats);
  34. for (let j = 0; j < geometry.faces.length; j++) {
  35. if (j === 8 || j === 9) {
  36. geometry.faces[j].materialIndex = 1;
  37. } else {
  38. geometry.faces[j].materialIndex = 0;
  39. }
  40. }
  41. scene.add(cube);
  42. arrowFront = new THREE.ArrowHelper(cube.getWorldDirection(), cube.position, 15, 0xFF0000);
  43. scene.add(arrowFront);
  44. arrowBack = new THREE.ArrowHelper(cube.getWorldDirection().negate(), cube.position, 15, 0x00FF00);
  45. scene.add(arrowBack);
  46. }

修改后的效果如下:

有了箭头的辅助,我们能够以比较直观的方式测试算法是否有效。如果你能够认真读到这里,可能已经迫不及待想继续了,但是还请稍安勿躁。进入下个环节前,我们需要首先了解几个重要的概念。

  1. 三维向量(Vector3):可以表征三维空间中的点或来自原点(0,0,0)的矢量。需要注意,Vector3既可以表示空间中的一个点又可以表示方向。因此为了避免歧义,我建议在作为矢量的时候通过normalize()方法对向量标准化。具体api文档参考
  2. 欧拉角(Euler):表示一个物体在其自身坐标系上的旋转角度,欧拉角也是一个很常见的数学概念,优点是对于旋转的表述相对直观,不过我们在项目中并不常用。
  3. 四元数(Quaternion):四元数是一个相对高深的数学概念,几何含义与欧拉角类似。都可以用来表征物体的旋转方向,优点是运算效率更高。
  4. 四维矩阵(Matrix4):在threejs的世界中,任何一个对象都有它对应的四维矩阵。它集合了平移、旋转、缩放等操作。有时我们可以通过它来完成两个对象的动作同步。
  5. 叉积(.cross() ):向量叉积表示由两个向量所确定的平面的法线方向。叉积的用途很多,例如在第一人称的视角控制下,实现左右平移就可以通过当前视角方向z与垂直方向y做叉积运算获得:z.cross(y)。
  6. 点积(.dot()):与向量叉积不同,向量点积为一个长度数据。vect_a.dot(vect_b)表示向量b在向量a上的投影长度,具体如何使用我们马上就会看到

在理解了上面的概念以后,我们就可以实现沿视角方向平移的操作:我们知道,物体沿平面(XOZ)坐标系运动都可以分解为X方向上的运动分量和Z轴方向上的运动分量。首先获取视角的方向,以三维向量表示。接着我们需要以这个向量和X轴方向上的一个三维向量做点积运算,从而得到一个投影长度。这个长度即代表物体沿视角方向移动的水平x轴方向上的运动分量。同理,我们在计算与Z轴方向上的点积,又可以获得物体沿视角方向移动的z轴方向的运动分量。同时执行两个方向上的运动分量完成平移操作。

接下来,我们先通过实验观察是否能够获得这两个运动分量和投影长度。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>平移与碰撞</title>
  6. <script src="js/three.js"></script>
  7. <script src="js/jquery3.4.1.js"></script>
  8. </head>
  9. <body>
  10. <canvas id="mainCanvas"></canvas>
  11. </body>
  12. <script>
  13. let scene, camera, renderer, leftPress, cube, arrowFront, arrowFrontX, arrowFrontZ;
  14. let left, right, front, back;
  15. init();
  16. // helper();
  17. createBoxer();
  18. animate();
  19.  
  20. function init() {
  21. // 初始化场景
  22. scene = new THREE.Scene();
  23. scene.background = new THREE.Color(0xffffff);
  24.  
  25. // 创建渲染器
  26. renderer = new THREE.WebGLRenderer({
  27. canvas: document.getElementById("mainCanvas"),
  28. antialias: true, // 抗锯齿
  29. alpha: true
  30. });
  31. renderer.setSize(window.innerWidth, window.innerHeight);
  32.  
  33. // 创建透视相机
  34. camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  35. camera.position.set(0, 40, 30);
  36. camera.lookAt(0, 0, 0);
  37.  
  38. // 参数初始化
  39. mouse = new THREE.Vector2();
  40. raycaster = new THREE.Raycaster();
  41.  
  42. // 环境光
  43. var ambientLight = new THREE.AmbientLight(0x606060);
  44. scene.add(ambientLight);
  45. // 平行光
  46. var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
  47. directionalLight.position.set(1, 0.75, 0.5).normalize();
  48. scene.add(directionalLight);
  49. }
  50.  
  51. function helper() {
  52. var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
  53. grid.material.opacity = 0.1;
  54. grid.material.transparent = true;
  55. scene.add(grid);
  56.  
  57. var axesHelper = new THREE.AxesHelper(30);
  58. scene.add(axesHelper);
  59. }
  60.  
  61. function animate() {
  62. requestAnimationFrame(animate);
  63. renderer.render(scene, camera);
  64. arrowFront.setDirection(cube.getWorldDirection(new THREE.Vector3()).normalize());
  65. arrowFront.position.copy(cube.position);
  66.  
  67. let vect = cube.getWorldDirection(new THREE.Vector3());
  68. arrowFrontX.setDirection(new THREE.Vector3(1, 0, 0));
  69. arrowFrontX.setLength(vect.dot(new THREE.Vector3(15, 0, 0)));
  70. arrowFrontX.position.copy(cube.position);
  71.  
  72. arrowFrontZ.setDirection(new THREE.Vector3(0, 0, 1));
  73. arrowFrontZ.setLength(vect.dot(new THREE.Vector3(0, 0, 15)));
  74. arrowFrontZ.position.copy(cube.position);
  75. if (front) {
  76. // cube.translateZ(-1)
  77. cube.position.z -= 1;
  78. }
  79. if (back) {
  80. // cube.translateZ(1);
  81. cube.position.z += 1;
  82. }
  83. if (left) {
  84. // cube.translateX(-1);
  85. cube.position.x -= 1;
  86. }
  87. if (right) {
  88. // cube.translateX(1);
  89. cube.position.x += 1;
  90. }
  91. }
  92.  
  93. function createBoxer() {
  94. var geometry = new THREE.BoxGeometry(5, 5, 5);
  95. var mats = [];
  96. mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
  97. mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
  98. cube = new THREE.Mesh(geometry, mats);
  99. for (let j = 0; j < geometry.faces.length; j++) {
  100. if (j === 8 || j === 9) {
  101. geometry.faces[j].materialIndex = 1;
  102. } else {
  103. geometry.faces[j].materialIndex = 0;
  104. }
  105. }
  106. scene.add(cube);
  107. arrowFront = new THREE.ArrowHelper(cube.getWorldDirection(), cube.position, 15, 0xFF0000);
  108. scene.add(arrowFront);
  109.  
  110. let cubeDirec = cube.getWorldDirection(new THREE.Vector3());
  111. arrowFrontX = new THREE.ArrowHelper(cubeDirec.setY(0), cube.position, cubeDirec.dot(new THREE.Vector3(0, 0, 15)), 0x0000ff);
  112. scene.add(arrowFrontX);
  113.  
  114. arrowFrontZ = new THREE.ArrowHelper(cubeDirec.setY(0), cube.position, cubeDirec.dot(new THREE.Vector3(15, 0, 0)), 0xB5B5B5)
  115. scene.add(arrowFrontZ);
  116. }
  117.  
  118. $(window).mousemove(function (event) {
  119. event.preventDefault();
  120. if (leftPress) {
  121. cube.rotateOnWorldAxis(
  122. new THREE.Vector3(0, 1, 0),
  123. event.originalEvent.movementX / 500
  124. );
  125. cube.rotateOnAxis(
  126. new THREE.Vector3(1, 0, 0),
  127. event.originalEvent.movementY / 500
  128. );
  129. }
  130. });
  131.  
  132. $(window).mousedown(function (event) {
  133. event.preventDefault();
  134. leftPress = true;
  135.  
  136. });
  137.  
  138. $(window).mouseup(function (event) {
  139. event.preventDefault();
  140. leftPress = false;
  141. });
  142.  
  143. $(window).keydown(function (event) {
  144. switch (event.keyCode) {
  145. case 65: // a
  146. left = true;
  147. break;
  148. case 68: // d
  149. right = true;
  150. break;
  151. case 83: // s
  152. back = true;
  153. break;
  154. case 87: // w
  155. front = true;
  156. break;
  157. }
  158. });
  159.  
  160. $(window).keyup(function (event) {
  161. switch (event.keyCode) {
  162. case 65: // a
  163. left = false;
  164. break;
  165. case 68: // d
  166. right = false;
  167. break;
  168. case 83: // s
  169. back = false;
  170. break;
  171. case 87: // w
  172. front = false;
  173. break;
  174. }
  175. });
  176. </script>
  177. </html>

通过箭头的辅助,我们很容易获得以下图形:

红色箭头表示物体的朝向,蓝色表示物体沿x轴上的投影方向和长度。灰色表示沿z轴上的投影方向和长度。在确认方法可行以后,我们继续实现平移操作。完整代码如下,这个运算的方式很重要,读者应该仔细比较两段代码的差别。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>平移与碰撞</title>
  6. <script src="js/three.js"></script>
  7. <script src="js/jquery3.4.1.js"></script>
  8. </head>
  9. <body>
  10. <canvas id="mainCanvas"></canvas>
  11. </body>
  12. <script>
  13. let scene, camera, renderer, leftPress, cube, arrowFront, arrowFrontX, arrowFrontZ;
  14. let left, right, front, back;
  15. init();
  16. helper();
  17. createBoxer();
  18. animate();
  19.  
  20. function init() {
  21. // 初始化场景
  22. scene = new THREE.Scene();
  23. scene.background = new THREE.Color(0xffffff);
  24.  
  25. // 创建渲染器
  26. renderer = new THREE.WebGLRenderer({
  27. canvas: document.getElementById("mainCanvas"),
  28. antialias: true, // 抗锯齿
  29. alpha: true
  30. });
  31. renderer.setSize(window.innerWidth, window.innerHeight);
  32.  
  33. // 创建透视相机
  34. camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  35. camera.position.set(0, 40, 30);
  36. camera.lookAt(0, 0, 0);
  37.  
  38. // 参数初始化
  39. mouse = new THREE.Vector2();
  40. raycaster = new THREE.Raycaster();
  41.  
  42. // 环境光
  43. var ambientLight = new THREE.AmbientLight(0x606060);
  44. scene.add(ambientLight);
  45. // 平行光
  46. var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
  47. directionalLight.position.set(1, 0.75, 0.5).normalize();
  48. scene.add(directionalLight);
  49. }
  50.  
  51. function helper() {
  52. var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
  53. grid.material.opacity = 0.1;
  54. grid.material.transparent = true;
  55. scene.add(grid);
  56.  
  57. var axesHelper = new THREE.AxesHelper(30);
  58. scene.add(axesHelper);
  59. }
  60.  
  61. function animate() {
  62. requestAnimationFrame(animate);
  63. renderer.render(scene, camera);
  64. arrowFront.setDirection(cube.getWorldDirection(new THREE.Vector3()).normalize());
  65. arrowFront.position.copy(cube.position);
  66. let vect = cube.getWorldDirection(new THREE.Vector3());
  67. if (front) {
  68. cube.position.z += vect.dot(new THREE.Vector3(0, 0, 15)) * 0.01;
  69. cube.position.x += vect.dot(new THREE.Vector3(15, 0, 0)) * 0.01;
  70. }
  71. }
  72.  
  73. function createBoxer() {
  74. var geometry = new THREE.BoxGeometry(5, 5, 5);
  75. var mats = [];
  76. mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
  77. mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
  78. cube = new THREE.Mesh(geometry, mats);
  79. for (let j = 0; j < geometry.faces.length; j++) {
  80. if (j === 8 || j === 9) {
  81. geometry.faces[j].materialIndex = 1;
  82. } else {
  83. geometry.faces[j].materialIndex = 0;
  84. }
  85. }
  86. scene.add(cube);
  87. arrowFront = new THREE.ArrowHelper(cube.getWorldDirection(), cube.position, 15, 0xFF0000);
  88. scene.add(arrowFront);
  89. }
  90.  
  91. $(window).mousemove(function (event) {
  92. event.preventDefault();
  93. if (leftPress) {
  94. cube.rotateOnWorldAxis(
  95. new THREE.Vector3(0, 1, 0),
  96. event.originalEvent.movementX / 500
  97. );
  98. cube.rotateOnAxis(
  99. new THREE.Vector3(1, 0, 0),
  100. event.originalEvent.movementY / 500
  101. );
  102. }
  103. });
  104.  
  105. $(window).mousedown(function (event) {
  106. event.preventDefault();
  107. leftPress = true;
  108.  
  109. });
  110.  
  111. $(window).mouseup(function (event) {
  112. event.preventDefault();
  113. leftPress = false;
  114. });
  115.  
  116. $(window).keydown(function (event) {
  117. switch (event.keyCode) {
  118. case 65: // a
  119. left = true;
  120. break;
  121. case 68: // d
  122. right = true;
  123. break;
  124. case 83: // s
  125. back = true;
  126. break;
  127. case 87: // w
  128. front = true;
  129. break;
  130. }
  131. });
  132.  
  133. $(window).keyup(function (event) {
  134. switch (event.keyCode) {
  135. case 65: // a
  136. left = false;
  137. break;
  138. case 68: // d
  139. right = false;
  140. break;
  141. case 83: // s
  142. back = false;
  143. break;
  144. case 87: // w
  145. front = false;
  146. break;
  147. }
  148. });
  149. </script>
  150. </html>

向后和左右平移的操作留给大家自己实现。有了以上基础,如何控制Camera移动就很简单了。几乎就是将cube的操作替换成camera即可:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>第一人称视角移动</title>
  6. <script src="js/three.js"></script>
  7. <script src="js/jquery3.4.1.js"></script>
  8. </head>
  9. <body>
  10. <canvas id="mainCanvas"></canvas>
  11. </body>
  12. <script>
  13. let scene, camera, renderer, leftPress, cube, arrowFront, arrowFrontX, arrowFrontZ;
  14. let left, right, front, back;
  15. init();
  16. helper();
  17. animate();
  18.  
  19. function init() {
  20. // 初始化场景
  21. scene = new THREE.Scene();
  22. scene.background = new THREE.Color(0xffffff);
  23.  
  24. // 创建渲染器
  25. renderer = new THREE.WebGLRenderer({
  26. canvas: document.getElementById("mainCanvas"),
  27. antialias: true, // 抗锯齿
  28. alpha: true
  29. });
  30. renderer.setSize(window.innerWidth, window.innerHeight);
  31.  
  32. // 创建透视相机
  33. camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  34. camera.position.set(0, 10, 30);
  35.  
  36. // 参数初始化
  37. mouse = new THREE.Vector2();
  38. raycaster = new THREE.Raycaster();
  39.  
  40. // 环境光
  41. var ambientLight = new THREE.AmbientLight(0x606060);
  42. scene.add(ambientLight);
  43. // 平行光
  44. var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
  45. directionalLight.position.set(1, 0.75, 0.5).normalize();
  46. scene.add(directionalLight);
  47. }
  48.  
  49. function helper() {
  50. var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
  51. grid.material.opacity = 0.1;
  52. grid.material.transparent = true;
  53. scene.add(grid);
  54.  
  55. var axesHelper = new THREE.AxesHelper(30);
  56. scene.add(axesHelper);
  57. }
  58.  
  59. function animate() {
  60. requestAnimationFrame(animate);
  61. renderer.render(scene, camera);
  62. let vect = camera.getWorldDirection(new THREE.Vector3());
  63. if (front) {
  64. camera.position.z += vect.dot(new THREE.Vector3(0, 0, 15)) * 0.01;
  65. camera.position.x += vect.dot(new THREE.Vector3(15, 0, 0)) * 0.01;
  66. }
  67. }
  68.  
  69. $(window).mousemove(function (event) {
  70. event.preventDefault();
  71. if (leftPress) {
  72. camera.rotateOnWorldAxis(
  73. new THREE.Vector3(0, 1, 0),
  74. event.originalEvent.movementX / 500
  75. );
  76. camera.rotateOnAxis(
  77. new THREE.Vector3(1, 0, 0),
  78. event.originalEvent.movementY / 500
  79. );
  80. }
  81. });
  82.  
  83. $(window).mousedown(function (event) {
  84. event.preventDefault();
  85. leftPress = true;
  86.  
  87. });
  88.  
  89. $(window).mouseup(function (event) {
  90. event.preventDefault();
  91. leftPress = false;
  92. });
  93.  
  94. $(window).keydown(function (event) {
  95. switch (event.keyCode) {
  96. case 65: // a
  97. left = true;
  98. break;
  99. case 68: // d
  100. right = true;
  101. break;
  102. case 83: // s
  103. back = true;
  104. break;
  105. case 87: // w
  106. front = true;
  107. break;
  108. }
  109. });
  110.  
  111. $(window).keyup(function (event) {
  112. switch (event.keyCode) {
  113. case 65: // a
  114. left = false;
  115. break;
  116. case 68: // d
  117. right = false;
  118. break;
  119. case 83: // s
  120. back = false;
  121. break;
  122. case 87: // w
  123. front = false;
  124. break;
  125. }
  126. });
  127. </script>
  128. </html>

解决了平移操作以后,碰撞检测其实就不那么复杂了。我们可以沿着摄像机的位置向上下前后左右六个方向做光线投射(Raycaster),每次移动首先检测移动方向上的射线是否被阻挡,如果发生阻挡且距离小于安全距离,即停止该方向上的移动。后面的部分我打算放在下一篇博客中介绍,如果大家对这篇文章敢兴趣或有什么建议欢迎给我留言或加群讨论。

如何使用threejs实现第一人称视角的移动的更多相关文章

  1. D3D游戏编程系列(六):自己动手编写第一人称射击游戏之第一人称视角的构建

    说起第一人称射击游戏,不得不提第一人称视角啊,没有这个,那么这个第一就无从谈起啊,我作为一个观察者究竟如何在这个地图上顺利的移动和观察呢,那么,我们一起来研究下. 我们首先来看下CDXCamera类: ...

  2. ThingJS平台制作第一人称视角下的巡更效果

    今天想要做一个类似巡更的过程,就像是在学校保安巡更,小区保安巡更一样.巡更需要用到相应的场景,比如说:园区.学校.超市以及工厂等等,然后需要规划好路线也就是巡更的路线,并且视角要跟随路线来变换,从而达 ...

  3. unity中自制模拟第一人称视角

    public float sensitivityX = 5f; public float sensitivityY = 5f; public float sensitivetyKeyBoard = 0 ...

  4. unity3d学习笔记(一) 第一人称视角实现和倒计时实现

    unity3d学习笔记(一) 第一人称视角实现和倒计时实现 1. 第一人称视角 (1)让mainCamera和player(视角对象)同步在一起 因为我们的player是生成的,所以不能把mainCa ...

  5. unity中制作模拟第一人称视角下的指南针

    private int zRotation; public GameObject obj; public void Update() { //obj = GameObject.Find("C ...

  6. 让camera实现类似cs第一人称视角旋转和位移

    直接把这个脚本挂在摄像机上就可: using System.Collections; using System.Collections.Generic; using UnityEngine; /* * ...

  7. [UE4]第一人称与第三人称

    一.给Character添加一个SkeletalMesh,并设置为第三人称模型.并设置自己看不到该模型. 二.添加给骨骼的右手添加一个Socket插槽用来挂载武器 三.判断当前角色是否被本地Contr ...

  8. 微软Hyperlapse技术:让第一人称摄像稳定而流畅

    编者按:GoPro等第一人称摄像设备已经几乎成为了极限运动者的标配,但拍摄过程中的抖动常会让画面非常糟糕.微软Hyperlapse技术实现了将第一人称录像转化成稳定而流畅的视频.该成果的论文已发表在S ...

  9. Unity——第一人称控制器的实现

    Unity--第一人称控制器的实现 一.功能描述 在一个场景中实现人物的前后左右移动和跳跃功能:其中前后左右移动通过W.A.S.D方向键实现,跳跃功能通过空格键实现,并且考虑到重力作用,来调节跳跃功能 ...

随机推荐

  1. asp.net之大文件分段上传、断点续传

    ASP.NET上传文件用FileUpLoad就可以,但是对文件夹的操作却不能用FileUpLoad来实现. 下面这个示例便是使用ASP.NET来实现上传文件夹并对文件夹进行压缩以及解压. ASP.NE ...

  2. .net大文件分块上传断点续传demo

    IE的自带下载功能中没有断点续传功能,要实现断点续传功能,需要用到HTTP协议中鲜为人知的几个响应头和请求头. 一. 两个必要响应头Accept-Ranges.ETag 客户端每次提交下载请求时,服务 ...

  3. PHP安装mysql.so扩展及相关PHP.ini 配置参数说明

    在PHP中mysql_connect模块已经逐渐被弃用,我在搭建环境时也没有再安装mysql扩展,但是今天在维护一个老项目时,出现报错 Fatal error: Uncaught Error: Cal ...

  4. Mac 上 QuickTime Player 播放器以 1.1、1.2 倍速等更精确速度快进/快退播放的方法

    苹果的 QuickTime Player 播放器上点击双箭头按钮可以用 2.4.8 倍的速度快进/快退播放视频,但是 2 倍速太快了,如果我想以 1.1.1.2 倍速这种更精确的速度控制视频播放呢?按 ...

  5. 对okhttp参数的一些思考

    背景 项目中使用OkHttp访问三方服务 参数 创建okhttp客户端类的时候需要设置一些参数,有些可能是坑,仅供参考: client = new OkHttpClient.Builder() .di ...

  6. 【caffe Layer】代码中文注释

    src/caffe/proto/caffe.proto 中LayerParameter部分 // NOTE // Update the next available ID when you add a ...

  7. windows命令行操作

    一.打开方式 -  开始菜单  -->  运行 -->  输入cmd -->  回车 二.常用的指令 dir                             - 列出当前目录 ...

  8. ch341a编程器写操作超时失败

    当点击自动编写‘提示写操作超时失败’要怎么样才能解决,下面我给大家分享一下!   方法/步骤     首先我们点击操作   选择操作选项   看看箭头所指的几个地方是不是都没打上勾   我们把这几个地 ...

  9. 如将Myeclipse项目改成Eclipse项目

    由于项目需求,需要将原来Myeclipse项目转移到Eclipse中去.搞了半天才搞出来,分享给大家,希望对大家有用. 首先导入一个从Myeclipse导出的项目 然后无法进行tomcat发布. 但是 ...

  10. onenote架设在局域网服务器

    1.服务器端工作 1.1.在局域网服务器磁盘建个 文件夹,命名为 abc 1.2.右键共享,添加用户everyone 权限设置为可读写 不需要安装onenote 2.客户端工作 2.1.在客户端服务器 ...