如何使用threejs實現第一人稱視角的移動


在數據可視化領域利用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         // 創建透視相機
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     }
65 
66     function createBoxer() {
67         var geometry = new THREE.BoxGeometry(5, 5, 5);
68         var material = new THREE.MeshPhongMaterial({color: 0x00ff00});
69         cube = new THREE.Mesh(geometry, material);
70         scene.add(cube);
71     }
72 
73     $(window).mousemove(function (event) {
74         event.preventDefault();
75         if (leftPress) {
76             cube.rotateOnAxis(
77                 new THREE.Vector3(0, 1, 0),
78                 event.originalEvent.movementX / 500
79             );
80             cube.rotateOnAxis(
81                 new THREE.Vector3(1, 0, 0),
82                 event.originalEvent.movementY / 500
83             );
84         }
85     });
86 
87     $(window).mousedown(function (event) {
88         event.preventDefault();
89         leftPress = true;
90 
91     });
92 
93     $(window).mouseup(function (event) {
94         event.preventDefault();
95         leftPress = false;
96     });
97 </script>
98 </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         // 創建透視相機
 35         camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
 36         camera.position.set(0, 40, 30);
 37         camera.lookAt(0, 0, 0);
 38 
 39         // 參數初始化
 40         mouse = new THREE.Vector2();
 41         raycaster = new THREE.Raycaster();
 42 
 43         // 環境光
 44         var ambientLight = new THREE.AmbientLight(0x606060);
 45         scene.add(ambientLight);
 46         // 平行光
 47         var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
 48         directionalLight.position.set(1, 0.75, 0.5).normalize();
 49         scene.add(directionalLight);
 50     }
 51 
 52     function helper() {
 53         var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
 54         grid.material.opacity = 0.1;
 55         grid.material.transparent = true;
 56         scene.add(grid);
 57 
 58         var axesHelper = new THREE.AxesHelper(30);
 59         scene.add(axesHelper);
 60     }
 61 
 62     function animate() {
 63         requestAnimationFrame(animate);
 64         renderer.render(scene, camera);
 65         if (front) {
 66             cube.translateZ(-1)
 67         }
 68         if (back) {
 69             cube.translateZ(1);
 70         }
 71         if (left) {
 72             cube.translateX(-1);
 73         }
 74         if (right) {
 75             cube.translateX(1);
 76         }
 77     }
 78 
 79     function createBoxer() {
 80         var geometry = new THREE.BoxGeometry(5, 5, 5);
 81         var mats = [];
 82         mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
 83         mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
 84         cube = new THREE.Mesh(geometry, mats);
 85         for (let j = 0; j < geometry.faces.length; j++) {
 86             if (j === 8 || j === 9) {
 87                 geometry.faces[j].materialIndex = 1;
 88             } else {
 89                 geometry.faces[j].materialIndex = 0;
 90             }
 91         }
 92         scene.add(cube);
 93     }
 94 
 95     $(window).mousemove(function (event) {
 96         event.preventDefault();
 97         if (leftPress) {
 98             cube.rotateOnAxis(
 99                 new THREE.Vector3(0, 1, 0),
100                 event.originalEvent.movementX / 500
101             );
102             cube.rotateOnAxis(
103                 new THREE.Vector3(1, 0, 0),
104                 event.originalEvent.movementY / 500
105             );
106         }
107     });
108 
109     $(window).mousedown(function (event) {
110         event.preventDefault();
111         leftPress = true;
112 
113     });
114 
115     $(window).mouseup(function (event) {
116         event.preventDefault();
117         leftPress = false;
118     });
119 
120     $(window).keydown(function (event) {
121         switch (event.keyCode) {
122             case 65: // a
123                 left = true;
124                 break;
125             case 68: // d
126                 right = true;
127                 break;
128             case 83: // s
129                 back = true;
130                 break;
131             case 87: // w
132                 front = true;
133                 break;
134         }
135     });
136 
137     $(window).keyup(function (event) {
138         switch (event.keyCode) {
139             case 65: // a
140                 left = false;
141                 break;
142             case 68: // d
143                 right = false;
144                 break;
145             case 83: // s
146                 back = false;
147                 break;
148             case 87: // w
149                 front = false;
150                 break;
151         }
152     });
153 </script>
154 </html>

 

我們添加了keydown()事件和keyup()事件用來捕獲鍵盤響應。我們還修改了createBoxer()方法,給朝向我們的那一面塗上紅色。你一定發現了BoxGeometry所代表的立方體雖然只有6個面,可是為了給“1個面”上色我們卻需要同時在“2個面”的材質上着色。這是因為在三維場景中,“面”的含義表示由空間中3個點所代表的區域,而一個矩形由兩個三角形拼接而成。完成以后的樣子如下:

隨意拖動幾下鼠標,我們可能會得到一個類似的狀態:

設想一下在第一人稱視角的游戲中,我們抬高視角觀察周圍后再降低視角,地平線是否依然處於水平狀態。換句話說,無論我們如何拖動鼠標,紅色的那面在朝向我們的時候都不應該傾斜。要解釋這個問題,我們首先需要搞清楚三維場景中的坐標系概念。在threejs的世界中存在兩套坐標體系:世界坐標系和自身坐標系。世界坐標系是整個場景的坐標系統,通過它可以定位場景中的物體。而自身坐標系就比較復雜,實際上一個物體的自身坐標系除了用來表示物體各個部分的相對關系以外主要用來表示物體的旋轉。想象一下月球的自轉和公轉,在地月坐標系中,月球圍繞地球公轉,同時也繞着自身的Y軸旋轉。在我們上面的場景中,立方體自身的坐標軸會隨着自身的旋轉而改變,當我們的鼠標自下而上滑動后,Y軸將不再垂直於地面。如果這時我們再橫向滑動鼠標讓立方體繞Y軸旋轉,自然整個面都會發生傾斜。如果你還不理解可以在自己的代碼中多嘗試幾次,理解世界坐標系和自身坐標系對於學習webgl尤其重要。很顯然,要模擬第一人稱的視角轉動我們需要讓視角上下移動的旋轉軸為自身坐標系的X軸,左右移動的旋轉軸固定為穿過自身中心的一條與世界坐標系Y軸保持平行的軸線。理解這個問題很不容易,可是解決它卻非常簡單。threejs為我們提供了方法,我們只需要修改mousemove()方法:

$(window).mousemove(function (event) {
        event.preventDefault();
        if (leftPress) {
            cube.rotateOnWorldAxis(
                new THREE.Vector3(0, 1, 0),
                event.originalEvent.movementX / 500
            );
            cube.rotateOnAxis(
                new THREE.Vector3(1, 0, 0),
                event.originalEvent.movementY / 500
            );
        }
    });

有了控制視角的方式,接下來我們移動一下方塊。新的問題又出現了:盒子的運動方向也是沿着自身坐標系的。就和我們看着月亮行走並不會走到月亮上去的情形一樣,如果要模擬第一人稱視角的移動,視角的移動方向應該永遠和世界坐標系保持平行,那么我們是否可以通過世界坐標系來控制物體的移動呢:

 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         // 創建透視相機
 35         camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
 36         camera.position.set(0, 40, 30);
 37         camera.lookAt(0, 0, 0);
 38 
 39         // 參數初始化
 40         mouse = new THREE.Vector2();
 41         raycaster = new THREE.Raycaster();
 42 
 43         // 環境光
 44         var ambientLight = new THREE.AmbientLight(0x606060);
 45         scene.add(ambientLight);
 46         // 平行光
 47         var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
 48         directionalLight.position.set(1, 0.75, 0.5).normalize();
 49         scene.add(directionalLight);
 50     }
 51 
 52     function helper() {
 53         var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
 54         grid.material.opacity = 0.1;
 55         grid.material.transparent = true;
 56         scene.add(grid);
 57 
 58         var axesHelper = new THREE.AxesHelper(30);
 59         scene.add(axesHelper);
 60     }
 61 
 62     function animate() {
 63         requestAnimationFrame(animate);
 64         renderer.render(scene, camera);
 65         arrowFront.setDirection(cube.getWorldDirection(new THREE.Vector3()).normalize());
 66         arrowFront.position.copy(cube.position);
 67 
 68         let vect = cube.getWorldDirection(new THREE.Vector3());
 69         arrowFrontX.setDirection(new THREE.Vector3(1, 0, 0));
 70         arrowFrontX.setLength(vect.dot(new THREE.Vector3(15, 0, 0)));
 71         arrowFrontX.position.copy(cube.position);
 72 
 73         arrowFrontZ.setDirection(new THREE.Vector3(0, 0, 1));
 74         arrowFrontZ.setLength(vect.dot(new THREE.Vector3(0, 0, 15)));
 75         arrowFrontZ.position.copy(cube.position);
 76         if (front) {
 77             // cube.translateZ(-1)
 78             cube.position.z -= 1;
 79         }
 80         if (back) {
 81             // cube.translateZ(1);
 82             cube.position.z += 1;
 83         }
 84         if (left) {
 85             // cube.translateX(-1);
 86             cube.position.x -= 1;
 87         }
 88         if (right) {
 89             // cube.translateX(1);
 90             cube.position.x += 1;
 91         }
 92     }
 93 
 94     function createBoxer() {
 95         var geometry = new THREE.BoxGeometry(5, 5, 5);
 96         var mats = [];
 97         mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
 98         mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
 99         cube = new THREE.Mesh(geometry, mats);
100         for (let j = 0; j < geometry.faces.length; j++) {
101             if (j === 8 || j === 9) {
102                 geometry.faces[j].materialIndex = 1;
103             } else {
104                 geometry.faces[j].materialIndex = 0;
105             }
106         }
107         scene.add(cube);
108         arrowFront = new THREE.ArrowHelper(cube.getWorldDirection(), cube.position, 15, 0xFF0000);
109         scene.add(arrowFront);
110 
111         let cubeDirec = cube.getWorldDirection(new THREE.Vector3());
112         arrowFrontX = new THREE.ArrowHelper(cubeDirec.setY(0), cube.position, cubeDirec.dot(new THREE.Vector3(0, 0, 15)), 0x0000ff);
113         scene.add(arrowFrontX);
114 
115         arrowFrontZ = new THREE.ArrowHelper(cubeDirec.setY(0), cube.position, cubeDirec.dot(new THREE.Vector3(15, 0, 0)), 0xB5B5B5)
116         scene.add(arrowFrontZ);
117     }
118 
119     $(window).mousemove(function (event) {
120         event.preventDefault();
121         if (leftPress) {
122             cube.rotateOnWorldAxis(
123                 new THREE.Vector3(0, 1, 0),
124                 event.originalEvent.movementX / 500
125             );
126             cube.rotateOnAxis(
127                 new THREE.Vector3(1, 0, 0),
128                 event.originalEvent.movementY / 500
129             );
130         }
131     });
132 
133     $(window).mousedown(function (event) {
134         event.preventDefault();
135         leftPress = true;
136 
137     });
138 
139     $(window).mouseup(function (event) {
140         event.preventDefault();
141         leftPress = false;
142     });
143 
144     $(window).keydown(function (event) {
145         switch (event.keyCode) {
146             case 65: // a
147                 left = true;
148                 break;
149             case 68: // d
150                 right = true;
151                 break;
152             case 83: // s
153                 back = true;
154                 break;
155             case 87: // w
156                 front = true;
157                 break;
158         }
159     });
160 
161     $(window).keyup(function (event) {
162         switch (event.keyCode) {
163             case 65: // a
164                 left = false;
165                 break;
166             case 68: // d
167                 right = false;
168                 break;
169             case 83: // s
170                 back = false;
171                 break;
172             case 87: // w
173                 front = false;
174                 break;
175         }
176     });
177 </script>
178 </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         // 創建透視相機
 35         camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
 36         camera.position.set(0, 40, 30);
 37         camera.lookAt(0, 0, 0);
 38 
 39         // 參數初始化
 40         mouse = new THREE.Vector2();
 41         raycaster = new THREE.Raycaster();
 42 
 43         // 環境光
 44         var ambientLight = new THREE.AmbientLight(0x606060);
 45         scene.add(ambientLight);
 46         // 平行光
 47         var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
 48         directionalLight.position.set(1, 0.75, 0.5).normalize();
 49         scene.add(directionalLight);
 50     }
 51 
 52     function helper() {
 53         var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
 54         grid.material.opacity = 0.1;
 55         grid.material.transparent = true;
 56         scene.add(grid);
 57 
 58         var axesHelper = new THREE.AxesHelper(30);
 59         scene.add(axesHelper);
 60     }
 61 
 62     function animate() {
 63         requestAnimationFrame(animate);
 64         renderer.render(scene, camera);
 65         arrowFront.setDirection(cube.getWorldDirection(new THREE.Vector3()).normalize());
 66         arrowFront.position.copy(cube.position);
 67         let vect = cube.getWorldDirection(new THREE.Vector3());
 68         if (front) {
 69             cube.position.z += vect.dot(new THREE.Vector3(0, 0, 15)) * 0.01;
 70             cube.position.x += vect.dot(new THREE.Vector3(15, 0, 0)) * 0.01;
 71         }
 72     }
 73 
 74     function createBoxer() {
 75         var geometry = new THREE.BoxGeometry(5, 5, 5);
 76         var mats = [];
 77         mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
 78         mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
 79         cube = new THREE.Mesh(geometry, mats);
 80         for (let j = 0; j < geometry.faces.length; j++) {
 81             if (j === 8 || j === 9) {
 82                 geometry.faces[j].materialIndex = 1;
 83             } else {
 84                 geometry.faces[j].materialIndex = 0;
 85             }
 86         }
 87         scene.add(cube);
 88         arrowFront = new THREE.ArrowHelper(cube.getWorldDirection(), cube.position, 15, 0xFF0000);
 89         scene.add(arrowFront);
 90     }
 91 
 92     $(window).mousemove(function (event) {
 93         event.preventDefault();
 94         if (leftPress) {
 95             cube.rotateOnWorldAxis(
 96                 new THREE.Vector3(0, 1, 0),
 97                 event.originalEvent.movementX / 500
 98             );
 99             cube.rotateOnAxis(
100                 new THREE.Vector3(1, 0, 0),
101                 event.originalEvent.movementY / 500
102             );
103         }
104     });
105 
106     $(window).mousedown(function (event) {
107         event.preventDefault();
108         leftPress = true;
109 
110     });
111 
112     $(window).mouseup(function (event) {
113         event.preventDefault();
114         leftPress = false;
115     });
116 
117     $(window).keydown(function (event) {
118         switch (event.keyCode) {
119             case 65: // a
120                 left = true;
121                 break;
122             case 68: // d
123                 right = true;
124                 break;
125             case 83: // s
126                 back = true;
127                 break;
128             case 87: // w
129                 front = true;
130                 break;
131         }
132     });
133 
134     $(window).keyup(function (event) {
135         switch (event.keyCode) {
136             case 65: // a
137                 left = false;
138                 break;
139             case 68: // d
140                 right = false;
141                 break;
142             case 83: // s
143                 back = false;
144                 break;
145             case 87: // w
146                 front = false;
147                 break;
148         }
149     });
150 </script>
151 </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         // 創建透視相機
 34         camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
 35         camera.position.set(0, 10, 30);
 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         let vect = camera.getWorldDirection(new THREE.Vector3());
 64         if (front) {
 65             camera.position.z += vect.dot(new THREE.Vector3(0, 0, 15)) * 0.01;
 66             camera.position.x += vect.dot(new THREE.Vector3(15, 0, 0)) * 0.01;
 67         }
 68     }
 69     
 70     $(window).mousemove(function (event) {
 71         event.preventDefault();
 72         if (leftPress) {
 73             camera.rotateOnWorldAxis(
 74                 new THREE.Vector3(0, 1, 0),
 75                 event.originalEvent.movementX / 500
 76             );
 77             camera.rotateOnAxis(
 78                 new THREE.Vector3(1, 0, 0),
 79                 event.originalEvent.movementY / 500
 80             );
 81         }
 82     });
 83 
 84     $(window).mousedown(function (event) {
 85         event.preventDefault();
 86         leftPress = true;
 87 
 88     });
 89 
 90     $(window).mouseup(function (event) {
 91         event.preventDefault();
 92         leftPress = false;
 93     });
 94 
 95     $(window).keydown(function (event) {
 96         switch (event.keyCode) {
 97             case 65: // a
 98                 left = true;
 99                 break;
100             case 68: // d
101                 right = true;
102                 break;
103             case 83: // s
104                 back = true;
105                 break;
106             case 87: // w
107                 front = true;
108                 break;
109         }
110     });
111 
112     $(window).keyup(function (event) {
113         switch (event.keyCode) {
114             case 65: // a
115                 left = false;
116                 break;
117             case 68: // d
118                 right = false;
119                 break;
120             case 83: // s
121                 back = false;
122                 break;
123             case 87: // w
124                 front = false;
125                 break;
126         }
127     });
128 </script>
129 </html>

解決了平移操作以后,碰撞檢測其實就不那么復雜了。我們可以沿着攝像機的位置向上下前后左右六個方向做光線投射(Raycaster),每次移動首先檢測移動方向上的射線是否被阻擋,如果發生阻擋且距離小於安全距離,即停止該方向上的移動。后面的部分我打算放在下一篇博客中介紹,如果大家對這篇文章敢興趣或有什么建議歡迎給我留言或加群討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM