Three.js 是一个强大的 JavaScript 3D 渲染库,它提供了丰富的功能和工具,用于创建交互式的 3D 场景和动画效果。其中一个常见的应用是实现第一人称视角漫游,使用户可以在虚拟场景中以第一人称的视角自由移动和探索。本文将介绍如何使用 Three.js 实现这样的第一人称视角漫游效果。
核心步骤如下:
创建 Three.js 场景
首先,需要创建一个 Three.js 场景,包括场景对象、相机、渲染器等基本组件。可以使用 Three.js 提供的 API 来创建这些对象,并将它们添加到页面中的 HTML 元素中。
导入模型和纹理
如果你希望在漫游中使用模型和纹理,可以使用 Three.js 的加载器来导入所需的 3D 模型文件和纹理图像。加载完成后,将它们添加到场景中。
控制相机
为了实现第一人称视角漫游,需要控制相机的位置和方向。可以使用 Three.js 提供的控制器库(如 OrbitControls 或 PointerLockControls)来实现相机的控制。这些控制器可以根据用户的输入(如鼠标移动、键盘按键等)来改变相机的位置和方向。
处理用户输入
为了让用户能够与场景进行交互,需要监听用户的输入事件,如鼠标移动、键盘按键等。根据用户的输入,更新相机的位置和方向,并重新渲染场景。
渲染场景
在每一帧中,使用渲染器将场景和相机中的内容渲染到屏幕上。通过调用渲染器的 render() 方法,可以在每一帧中更新场景的渲染结果。
循环更新
为了实现流畅的漫游效果,需要在每一帧中更新场景和相机的状态。可以使用 Three.js 提供的循环函数(如 requestAnimationFrame)来实现循环更新,并在每一帧中调用渲染器的 render() 方法。
控制器选择
这里要简单说明一下为什么选择PointerLockControls而不是FirstPersonControls控制器。
FirstPersonControls 和 PointerLockControls 都可以用于实现第一人称视角漫游,但在实现方式和功能上略有不同。FirstPersonControls 更适合简单的场景,使用鼠标和键盘进行控制;而 PointerLockControls 则提供了更自由和灵活的控制方式,通过鼠标锁定来实现更细致的相机控制。
完整示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style type="text/css">
html, body {
margin: 0;
height: 100%;
}
canvas {
display: block;
}
#blocker {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
#instructions {
width: 100%;
height: 100%;
display: -webkit-box;
display: -moz-box;
display: box;
-webkit-box-orient: horizontal;
-moz-box-orient: horizontal;
box-orient: horizontal;
-webkit-box-pack: center;
-moz-box-pack: center;
box-pack: center;
-webkit-box-align: center;
-moz-box-align: center;
box-align: center;
color: #ffffff;
text-align: center;
cursor: pointer;
}
</style>
</head>
<body onload="draw();">
<div id="blocker">
<div id="instructions">
<span style="font-size:40px">点击屏幕开始</span>
<br />
<br />
(W, A, S, D = 移动, SPACE = 跳跃, MOUSE = 移动视角)
</div>
</div>
</body>
<script src="/lib/three.js"></script>
<script src="/lib/js/loaders/OBJLoader.js"></script>
<script src="/lib/js/loaders/MTLLoader.js"></script>
<script src="/lib/libs/chroma.js"></script> <!--处理颜色的库-->
<script src="/lib/js/controls/PointerLockControls.js"></script>
<script src="/lib/js/libs/stats.min.js"></script>
<script src="/lib/js/libs/dat.gui.min.js"></script>
<script src="/lib/js/Detector.js"></script>
<script>
var renderer,camera,scene,gui,light,stats,controls;
var clock = new THREE.Clock();
//是否锁定页面的相关
var blocker = document.getElementById( 'blocker' );
var instructions = document.getElementById( 'instructions' );
//移动相关的变量
var controlsEnabled = false;
var moveForward = false;
var moveBackward = false;
var moveLeft = false;
var moveRight = false;
var canJump = false;
var spaceUp = true; //处理一直按着空格连续跳的问题
//声明射线
var upRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3( 0, 1, 0), 0, 10);
var horizontalRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(), 0, 10);
var downRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3( 0, -1, 0), 0, 10);
var velocity = new THREE.Vector3(); //移动速度变量
var direction = new THREE.Vector3(); //移动的方向变量
var rotation = new THREE.Vector3(); //当前的相机朝向
var speed = 500; //控制器移动速度
var upSpeed = 200; //控制跳起时的速度
//辅助箭头
var up,horizontal,down,group;
function initRender() {
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.sortObjects = false;
//告诉渲染器需要阴影效果
document.body.appendChild(renderer.domElement);
}
function initCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.1, 1000);
//camera.position.set(0, 0, 50);
}
function initScene() {
scene = new THREE.Scene();
}
//初始化dat.GUI简化试验流程
function initGui() {
//声明一个保存需求修改的相关数据的对象
//gui = {};
//var datGui = new dat.GUI();
//将设置属性添加到gui当中,gui.add(对象,属性,最小值,最大值)
}
function initLight() {
scene.add(new THREE.AmbientLight(0x444444));
light = new THREE.PointLight(0xffffff);
light.position.set(0,50,0);
//告诉平行光需要开启阴影投射
light.castShadow = true;
scene.add(light);
}
function initModel() {
//辅助工具
var helper = new THREE.AxesHelper(50);
//scene.add(helper);
var mtlLoader = new THREE.MTLLoader();
mtlLoader.setPath('/lib/assets/models/');
//加载mtl文件
mtlLoader.load('city.mtl', function (material) {
var objLoader = new THREE.OBJLoader();
//设置当前加载的纹理
objLoader.setMaterials(material);
objLoader.setPath('/lib/assets/models/');
objLoader.load('city.obj', function (object) {
//设置颜色的取值范围
var scale = chroma.scale(['yellow', '008ae5']);
//重新设置纹理颜色
setRandomColors(object, scale);
object.scale.set(5, 5, 5);
//将模型缩放并添加到场景当中
scene.add(object);
})
});
//添加辅助线
group = new THREE.Group();
up = new THREE.ArrowHelper(new THREE.Vector3(0, 1, 0), new THREE.Vector3(), 10, 0x00ff00);
horizontal = new THREE.ArrowHelper(new THREE.Vector3(1, 0, 0), new THREE.Vector3(), 10, 0x00ffff);
down = new THREE.ArrowHelper(new THREE.Vector3(0, -1, 0), new THREE.Vector3(), 10, 0xffff00);
group.add(up);
group.add(horizontal);
group.add(down);
//scene.add(group);
}
//添加纹理的方法
function setRandomColors(object, scale) {
//获取children数组
var children = object.children;
//如果当前模型有子元素,则遍历子元素
if (children && children.length > 0) {
children.forEach(function (e) {
setRandomColors(e, scale)
});
}
else {
if (object instanceof THREE.Mesh) {
//如果当前的模型是楼层,则设置固定的颜色,并且透明化
if(Array.isArray(object.material)){
for(var i = 0; i<object.material.length; i++){
var material = object.material[i];
var color = scale(Math.random()).hex();
if (material.name.indexOf("building") === 0) {
material.color = new THREE.Color(color);
material.transparent = true;
material.opacity = 0.7;
material.depthWrite = false;
}
}
}
// 如果不是场景组,则给当前mesh添加纹理
else{
//随机当前模型的颜色
object.material.color = new THREE.Color(scale(Math.random()).hex());
}
}
}
}
//初始化性能插件
function initStats() {
stats = new Stats();
document.body.appendChild(stats.dom);
}
function initControls() {
controls = new THREE.PointerLockControls( camera );
controls.getObject().position.y = 50;
controls.getObject().position.x = 100;
scene.add( controls.getObject() );
var onKeyDown = function ( event ) {
switch ( event.keyCode ) {
case 38: // up
case 87: // w
moveForward = true;
break;
case 37: // left
case 65: // a
moveLeft = true; break;
case 40: // down
case 83: // s
moveBackward = true;
break;
case 39: // right
case 68: // d
moveRight = true;
break;
case 32: // space
if ( canJump && spaceUp ) velocity.y += upSpeed;
canJump = false;
spaceUp = false;
break;
}
};
var onKeyUp = function ( event ) {
switch( event.keyCode ) {
case 38: // up
case 87: // w
moveForward = false;
break;
case 37: // left
case 65: // a
moveLeft = false;
break;
case 40: // down
case 83: // s
moveBackward = false;
break;
case 39: // right
case 68: // d
moveRight = false;
break;
case 32: // space
spaceUp = true;
break;
}
};
document.addEventListener( 'keydown', onKeyDown, false );
document.addEventListener( 'keyup', onKeyUp, false );
}
function initPointerLock() {
//实现鼠标锁定的教程地址 http://www.html5rocks.com/en/tutorials/pointerlock/intro/
var havePointerLock = 'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document;
if ( havePointerLock ) {
var element = document.body;
var pointerlockchange = function ( event ) {
if ( document.pointerLockElement === element || document.mozPointerLockElement === element || document.webkitPointerLockElement === element ) {
controlsEnabled = true;
controls.enabled = true;
blocker.style.display = 'none';
} else {
controls.enabled = false;
blocker.style.display = 'block';
instructions.style.display = '';
}
};
var pointerlockerror = function ( event ) {
instructions.style.display = '';
};
// 监听变动事件
document.addEventListener( 'pointerlockchange', pointerlockchange, false );
document.addEventListener( 'mozpointerlockchange', pointerlockchange, false );
document.addEventListener( 'webkitpointerlockchange', pointerlockchange, false );
document.addEventListener( 'pointerlockerror', pointerlockerror, false );
document.addEventListener( 'mozpointerlockerror', pointerlockerror, false );
document.addEventListener( 'webkitpointerlockerror', pointerlockerror, false );
instructions.addEventListener( 'click', function ( event ) {
instructions.style.display = 'none';
//全屏
launchFullScreen(renderer.domElement);
// 锁定鼠标光标
element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock;
element.requestPointerLock();
}, false );
}
else {
instructions.innerHTML = '你的浏览器不支持相关操作,请更换浏览器';
}
}
function render() {
if ( controlsEnabled === true ) {
//获取到控制器对象
var control = controls.getObject();
//获取刷新时间
var delta = clock.getDelta();
//velocity每次的速度,为了保证有过渡
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
velocity.y -= 9.8 * 100.0 * delta; // 默认下降的速度
//获取当前按键的方向并获取朝哪个方向移动
direction.z = Number( moveForward ) - Number( moveBackward );
direction.x = Number( moveLeft ) - Number( moveRight );
//将法向量的值归一化
direction.normalize();
group.position.set(control.position.x,control.position.y,control.position.z);
//判断是否接触到了模型
rotation.copy(control.getWorldDirection().multiply(new THREE.Vector3(-1, 0, -1)));
//判断鼠标按下的方向
var m = new THREE.Matrix4();
if(direction.z > 0){
if(direction.x > 0){
m.makeRotationY(Math.PI/4);
}
else if(direction.x < 0){
m.makeRotationY(-Math.PI/4);
}
else{
m.makeRotationY(0);
}
}
else if(direction.z < 0){
if(direction.x > 0){
m.makeRotationY(Math.PI/4*3);
}
else if(direction.x < 0){
m.makeRotationY(-Math.PI/4*3);
}
else{
m.makeRotationY(Math.PI);
}
}
else{
if(direction.x > 0){
m.makeRotationY(Math.PI/2);
}
else if(direction.x < 0){
m.makeRotationY(-Math.PI/2);
}
}
//给向量使用变换矩阵
rotation.applyMatrix4(m);
//horizontal.setDirection(rotation);
horizontalRaycaster.set( control.position , rotation );
var horizontalIntersections = horizontalRaycaster.intersectObjects( scene.children, true);
var horOnObject = horizontalIntersections.length > 0;
//判断移动方向修改速度方向
if(!horOnObject){
if ( moveForward || moveBackward ) velocity.z -= direction.z * speed * delta;
if ( moveLeft || moveRight ) velocity.x -= direction.x * speed * delta;
}
//复制相机的位置
downRaycaster.ray.origin.copy( control.position );
//获取相机靠下10的位置
downRaycaster.ray.origin.y -= 10;
//判断是否停留在了立方体上面
var intersections = downRaycaster.intersectObjects( scene.children, true);
var onObject = intersections.length > 0;
//判断是否停在了立方体上面
if ( onObject === true ) {
velocity.y = Math.max( 0, velocity.y );
canJump = true;
}
//根据速度值移动控制器
control.translateX( velocity.x * delta );
control.translateY( velocity.y * delta );
control.translateZ( velocity.z * delta );
//保证控制器的y轴在10以上
if ( control.position.y < 10 ) {
velocity.y = 0;
control.position.y = 10;
canJump = true;
}
}
}
//窗口变动触发的函数
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
render();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
//更新控制器
render();
//更新性能插件
stats.update();
renderer.render( scene, camera );
requestAnimationFrame(animate);
}
function draw() {
//兼容性判断
if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
initPointerLock();
initGui();
initRender();
initScene();
initCamera();
initLight();
initModel();
initControls();
initStats();
animate();
window.onresize = onWindowResize;
}
function launchFullScreen(element) {
/*if (element.requestFullscreen) {
element.requestFullscreen();
}
else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
}
else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
}
else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}*/
}
/*var v = new THREE.Vector3(1,0,1).normalize();
console.log(v);
var m = new THREE.Matrix4();
m.makeRotationY(Math.PI/4);
console.log(m);
console.log(v.applyMatrix4(m));
console.log(v.multiply(new THREE.Vector3(-1, 0, -1)));*/
</script>
</html>
参考:
1.第一人称控制器 threejs.org/docs/index.…
2.指针锁定控制器 threejs.org/docs/index.…
3.官方实例 threejs.org/examples/?q…
4.PointerLockControls www.wjceo.com/blog/threej…