三维场景中如何实现第一人称漫游

340 阅读6分钟

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…