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

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

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>平移与碰撞</title>
<script src="js/three.js"></script>
<script src="js/jquery3.4.1.js"></script>
</head>
<body>
<canvas id="mainCanvas"></canvas>
</body>
<script>
let scene, camera, renderer, leftPress, cube;
init();
helper();
createBoxer();
animate(); function init() {
// 初始化场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff); // 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("mainCanvas"),
antialias: true, // 抗锯齿
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight); // 创建透视相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 40, 30);
camera.lookAt(0, 0, 0); // 参数初始化
mouse = new THREE.Vector2();
raycaster = new THREE.Raycaster(); // 环境光
var ambientLight = new THREE.AmbientLight(0x606060);
scene.add(ambientLight);
// 平行光
var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
directionalLight.position.set(1, 0.75, 0.5).normalize();
scene.add(directionalLight);
} function helper() {
var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
scene.add(grid); var axesHelper = new THREE.AxesHelper(30);
scene.add(axesHelper);
} function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
} function createBoxer() {
var geometry = new THREE.BoxGeometry(5, 5, 5);
var material = new THREE.MeshPhongMaterial({color: 0x00ff00});
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
} $(window).mousemove(function (event) {
event.preventDefault();
if (leftPress) {
cube.rotateOnAxis(
new THREE.Vector3(0, 1, 0),
event.originalEvent.movementX / 500
);
cube.rotateOnAxis(
new THREE.Vector3(1, 0, 0),
event.originalEvent.movementY / 500
);
}
}); $(window).mousedown(function (event) {
event.preventDefault();
leftPress = true; }); $(window).mouseup(function (event) {
event.preventDefault();
leftPress = false;
});
</script>
</html>

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

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

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>平移与碰撞</title>
<script src="js/three.js"></script>
<script src="js/jquery3.4.1.js"></script>
</head>
<body>
<canvas id="mainCanvas"></canvas>
</body>
<script>
let scene, camera, renderer, leftPress, cube;
let left, right, front, back;
init();
helper();
createBoxer();
animate(); function init() {
// 初始化场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff); // 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("mainCanvas"),
antialias: true, // 抗锯齿
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight); // 创建透视相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 40, 30);
camera.lookAt(0, 0, 0); // 参数初始化
mouse = new THREE.Vector2();
raycaster = new THREE.Raycaster(); // 环境光
var ambientLight = new THREE.AmbientLight(0x606060);
scene.add(ambientLight);
// 平行光
var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
directionalLight.position.set(1, 0.75, 0.5).normalize();
scene.add(directionalLight);
} function helper() {
var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
scene.add(grid); var axesHelper = new THREE.AxesHelper(30);
scene.add(axesHelper);
} function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
if (front) {
cube.translateZ(-1)
}
if (back) {
cube.translateZ(1);
}
if (left) {
cube.translateX(-1);
}
if (right) {
cube.translateX(1);
}
} function createBoxer() {
var geometry = new THREE.BoxGeometry(5, 5, 5);
var mats = [];
mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
cube = new THREE.Mesh(geometry, mats);
for (let j = 0; j < geometry.faces.length; j++) {
if (j === 8 || j === 9) {
geometry.faces[j].materialIndex = 1;
} else {
geometry.faces[j].materialIndex = 0;
}
}
scene.add(cube);
} $(window).mousemove(function (event) {
event.preventDefault();
if (leftPress) {
cube.rotateOnAxis(
new THREE.Vector3(0, 1, 0),
event.originalEvent.movementX / 500
);
cube.rotateOnAxis(
new THREE.Vector3(1, 0, 0),
event.originalEvent.movementY / 500
);
}
}); $(window).mousedown(function (event) {
event.preventDefault();
leftPress = true; }); $(window).mouseup(function (event) {
event.preventDefault();
leftPress = false;
}); $(window).keydown(function (event) {
switch (event.keyCode) {
case 65: // a
left = true;
break;
case 68: // d
right = true;
break;
case 83: // s
back = true;
break;
case 87: // w
front = true;
break;
}
}); $(window).keyup(function (event) {
switch (event.keyCode) {
case 65: // a
left = false;
break;
case 68: // d
right = false;
break;
case 83: // s
back = false;
break;
case 87: // w
front = false;
break;
}
});
</script>
</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
);
}
});

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

  function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
if (front) {
// cube.translateZ(-1)
cube.position.z -= 1;
}
if (back) {
// cube.translateZ(1);
cube.position.z += 1;
}
if (left) {
// cube.translateX(-1);
cube.position.x -= 1;
}
if (right) {
// cube.translateX(1);
cube.position.x += 1;
}
}

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

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

 let arrowFront, arrowBack;

 function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
arrowFront.setDirection(cube.getWorldDirection(new THREE.Vector3()).normalize());
arrowFront.position.copy(cube.position);
arrowBack.setDirection(cube.getWorldDirection(new THREE.Vector3()).negate().normalize());
arrowBack.position.copy(cube.position);
if (front) {
// cube.translateZ(-1)
cube.position.z -= 1;
}
if (back) {
// cube.translateZ(1);
cube.position.z += 1;
}
if (left) {
// cube.translateX(-1);
cube.position.x -= 1;
}
if (right) {
// cube.translateX(1);
cube.position.x += 1;
}
} function createBoxer() {
var geometry = new THREE.BoxGeometry(5, 5, 5);
var mats = [];
mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
cube = new THREE.Mesh(geometry, mats);
for (let j = 0; j < geometry.faces.length; j++) {
if (j === 8 || j === 9) {
geometry.faces[j].materialIndex = 1;
} else {
geometry.faces[j].materialIndex = 0;
}
}
scene.add(cube);
arrowFront = new THREE.ArrowHelper(cube.getWorldDirection(), cube.position, 15, 0xFF0000);
scene.add(arrowFront);
arrowBack = new THREE.ArrowHelper(cube.getWorldDirection().negate(), cube.position, 15, 0x00FF00);
scene.add(arrowBack);
}

修改后的效果如下:

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

  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轴方向的运动分量。同时执行两个方向上的运动分量完成平移操作。

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

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>平移与碰撞</title>
<script src="js/three.js"></script>
<script src="js/jquery3.4.1.js"></script>
</head>
<body>
<canvas id="mainCanvas"></canvas>
</body>
<script>
let scene, camera, renderer, leftPress, cube, arrowFront, arrowFrontX, arrowFrontZ;
let left, right, front, back;
init();
// helper();
createBoxer();
animate(); function init() {
// 初始化场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff); // 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("mainCanvas"),
antialias: true, // 抗锯齿
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight); // 创建透视相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 40, 30);
camera.lookAt(0, 0, 0); // 参数初始化
mouse = new THREE.Vector2();
raycaster = new THREE.Raycaster(); // 环境光
var ambientLight = new THREE.AmbientLight(0x606060);
scene.add(ambientLight);
// 平行光
var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
directionalLight.position.set(1, 0.75, 0.5).normalize();
scene.add(directionalLight);
} function helper() {
var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
scene.add(grid); var axesHelper = new THREE.AxesHelper(30);
scene.add(axesHelper);
} function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
arrowFront.setDirection(cube.getWorldDirection(new THREE.Vector3()).normalize());
arrowFront.position.copy(cube.position); let vect = cube.getWorldDirection(new THREE.Vector3());
arrowFrontX.setDirection(new THREE.Vector3(1, 0, 0));
arrowFrontX.setLength(vect.dot(new THREE.Vector3(15, 0, 0)));
arrowFrontX.position.copy(cube.position); arrowFrontZ.setDirection(new THREE.Vector3(0, 0, 1));
arrowFrontZ.setLength(vect.dot(new THREE.Vector3(0, 0, 15)));
arrowFrontZ.position.copy(cube.position);
if (front) {
// cube.translateZ(-1)
cube.position.z -= 1;
}
if (back) {
// cube.translateZ(1);
cube.position.z += 1;
}
if (left) {
// cube.translateX(-1);
cube.position.x -= 1;
}
if (right) {
// cube.translateX(1);
cube.position.x += 1;
}
} function createBoxer() {
var geometry = new THREE.BoxGeometry(5, 5, 5);
var mats = [];
mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
cube = new THREE.Mesh(geometry, mats);
for (let j = 0; j < geometry.faces.length; j++) {
if (j === 8 || j === 9) {
geometry.faces[j].materialIndex = 1;
} else {
geometry.faces[j].materialIndex = 0;
}
}
scene.add(cube);
arrowFront = new THREE.ArrowHelper(cube.getWorldDirection(), cube.position, 15, 0xFF0000);
scene.add(arrowFront); let cubeDirec = cube.getWorldDirection(new THREE.Vector3());
arrowFrontX = new THREE.ArrowHelper(cubeDirec.setY(0), cube.position, cubeDirec.dot(new THREE.Vector3(0, 0, 15)), 0x0000ff);
scene.add(arrowFrontX); arrowFrontZ = new THREE.ArrowHelper(cubeDirec.setY(0), cube.position, cubeDirec.dot(new THREE.Vector3(15, 0, 0)), 0xB5B5B5)
scene.add(arrowFrontZ);
} $(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
);
}
}); $(window).mousedown(function (event) {
event.preventDefault();
leftPress = true; }); $(window).mouseup(function (event) {
event.preventDefault();
leftPress = false;
}); $(window).keydown(function (event) {
switch (event.keyCode) {
case 65: // a
left = true;
break;
case 68: // d
right = true;
break;
case 83: // s
back = true;
break;
case 87: // w
front = true;
break;
}
}); $(window).keyup(function (event) {
switch (event.keyCode) {
case 65: // a
left = false;
break;
case 68: // d
right = false;
break;
case 83: // s
back = false;
break;
case 87: // w
front = false;
break;
}
});
</script>
</html>

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

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

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>平移与碰撞</title>
<script src="js/three.js"></script>
<script src="js/jquery3.4.1.js"></script>
</head>
<body>
<canvas id="mainCanvas"></canvas>
</body>
<script>
let scene, camera, renderer, leftPress, cube, arrowFront, arrowFrontX, arrowFrontZ;
let left, right, front, back;
init();
helper();
createBoxer();
animate(); function init() {
// 初始化场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff); // 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("mainCanvas"),
antialias: true, // 抗锯齿
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight); // 创建透视相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 40, 30);
camera.lookAt(0, 0, 0); // 参数初始化
mouse = new THREE.Vector2();
raycaster = new THREE.Raycaster(); // 环境光
var ambientLight = new THREE.AmbientLight(0x606060);
scene.add(ambientLight);
// 平行光
var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
directionalLight.position.set(1, 0.75, 0.5).normalize();
scene.add(directionalLight);
} function helper() {
var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
scene.add(grid); var axesHelper = new THREE.AxesHelper(30);
scene.add(axesHelper);
} function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
arrowFront.setDirection(cube.getWorldDirection(new THREE.Vector3()).normalize());
arrowFront.position.copy(cube.position);
let vect = cube.getWorldDirection(new THREE.Vector3());
if (front) {
cube.position.z += vect.dot(new THREE.Vector3(0, 0, 15)) * 0.01;
cube.position.x += vect.dot(new THREE.Vector3(15, 0, 0)) * 0.01;
}
} function createBoxer() {
var geometry = new THREE.BoxGeometry(5, 5, 5);
var mats = [];
mats.push(new THREE.MeshPhongMaterial({color: 0x00ff00}));
mats.push(new THREE.MeshPhongMaterial({color: 0xff0000}));
cube = new THREE.Mesh(geometry, mats);
for (let j = 0; j < geometry.faces.length; j++) {
if (j === 8 || j === 9) {
geometry.faces[j].materialIndex = 1;
} else {
geometry.faces[j].materialIndex = 0;
}
}
scene.add(cube);
arrowFront = new THREE.ArrowHelper(cube.getWorldDirection(), cube.position, 15, 0xFF0000);
scene.add(arrowFront);
} $(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
);
}
}); $(window).mousedown(function (event) {
event.preventDefault();
leftPress = true; }); $(window).mouseup(function (event) {
event.preventDefault();
leftPress = false;
}); $(window).keydown(function (event) {
switch (event.keyCode) {
case 65: // a
left = true;
break;
case 68: // d
right = true;
break;
case 83: // s
back = true;
break;
case 87: // w
front = true;
break;
}
}); $(window).keyup(function (event) {
switch (event.keyCode) {
case 65: // a
left = false;
break;
case 68: // d
right = false;
break;
case 83: // s
back = false;
break;
case 87: // w
front = false;
break;
}
});
</script>
</html>

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

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>第一人称视角移动</title>
<script src="js/three.js"></script>
<script src="js/jquery3.4.1.js"></script>
</head>
<body>
<canvas id="mainCanvas"></canvas>
</body>
<script>
let scene, camera, renderer, leftPress, cube, arrowFront, arrowFrontX, arrowFrontZ;
let left, right, front, back;
init();
helper();
animate(); function init() {
// 初始化场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff); // 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("mainCanvas"),
antialias: true, // 抗锯齿
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight); // 创建透视相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 10, 30); // 参数初始化
mouse = new THREE.Vector2();
raycaster = new THREE.Raycaster(); // 环境光
var ambientLight = new THREE.AmbientLight(0x606060);
scene.add(ambientLight);
// 平行光
var directionalLight = new THREE.DirectionalLight(0xBCD2EE);
directionalLight.position.set(1, 0.75, 0.5).normalize();
scene.add(directionalLight);
} function helper() {
var grid = new THREE.GridHelper(100, 20, 0xFF0000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
scene.add(grid); var axesHelper = new THREE.AxesHelper(30);
scene.add(axesHelper);
} function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
let vect = camera.getWorldDirection(new THREE.Vector3());
if (front) {
camera.position.z += vect.dot(new THREE.Vector3(0, 0, 15)) * 0.01;
camera.position.x += vect.dot(new THREE.Vector3(15, 0, 0)) * 0.01;
}
} $(window).mousemove(function (event) {
event.preventDefault();
if (leftPress) {
camera.rotateOnWorldAxis(
new THREE.Vector3(0, 1, 0),
event.originalEvent.movementX / 500
);
camera.rotateOnAxis(
new THREE.Vector3(1, 0, 0),
event.originalEvent.movementY / 500
);
}
}); $(window).mousedown(function (event) {
event.preventDefault();
leftPress = true; }); $(window).mouseup(function (event) {
event.preventDefault();
leftPress = false;
}); $(window).keydown(function (event) {
switch (event.keyCode) {
case 65: // a
left = true;
break;
case 68: // d
right = true;
break;
case 83: // s
back = true;
break;
case 87: // w
front = true;
break;
}
}); $(window).keyup(function (event) {
switch (event.keyCode) {
case 65: // a
left = false;
break;
case 68: // d
right = false;
break;
case 83: // s
back = false;
break;
case 87: // w
front = false;
break;
}
});
</script>
</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. Linux系统性能10条命令

    概述 通过执行以下命令,可以在1分钟内对系统资源使用情况有个大致的了解. uptime dmesg | tail vmstat 1 mpstat -P ALL 1 pidstat 1 iostat - ...

  2. loj #10131

    抽离题意 求删除一条树边和一条非树边后将图分成不连通的两部分的方案数 对于一棵树,再加入一条边就会产生环.若只有一个环,说明只加入了一条非树边 (x, y),记 lca 为 l, 那么 对于任意一条 ...

  3. 二扩域(GF(2^m))中的逆矩阵

    通常的逆矩阵可以用高斯消去法计算.十分有效.还可以使用LU分解,QR分解等. 二扩域中的逆矩阵则不同.看似简单,其实有别:它的所有元素定义在GF(2^m)中.从理论来看,似乎也可以用高斯消去法,只是计 ...

  4. Python3文件

    open()方法 Python open()方法永于打开一个文件,并返回文件对象,并对文件进行处理过程中都需要用到这个方法,如果该文件无法被打开,则抛出OSError 注意:使用open()方法一定要 ...

  5. 数据结构实验之排序二:交换排序 (SDUT 3399)

    #include <iostream> #include <bits/stdc++.h> using namespace std; typedef long long ll; ...

  6. 数据结构实验之查找六:顺序查找(SDUT 3378)

    (不知道为啥开个数组就 TLE .QAQ) #include <stdio.h> #include <stdlib.h> #include <string.h> / ...

  7. JSP迭代标签

    1. 新建LoopTag类,代码如下: package bid.zhazhapan.fims.tag; import java.io.IOException; import java.util.Col ...

  8. php unset

    说明:unset ( mixed $var [, mixed $... ] ) : void unset() 销毁指定的变量. unset() 在函数中的行为会依赖于想要销毁的变量的类型而有所不同. ...

  9. PC 端常用软件

    WPS  金山文档  有道云笔记  格式工厂 运行精灵 UC 遨游 360 Firefox 浏览器 光影魔术手 美图秀秀 2345好压 火绒安全软件 有道云笔记 悟空游戏厅 微信 QQ 迅雷 百度网盘 ...

  10. Simplifying Failures

    # # Finish the delta debug function ddmin # import re def test(s): print s, len(s),repr(s) if re.sea ...