83 Three.js 使用PointerLockControls控制相机实现射击游戏视角

简历

指针锁定API允许您在游戏界面中锁定鼠标或其他指针设备,以便您不用绝对定位光标就可以获得坐标变化值,从而准确地判断用户正在做什么,并且还可以防止用户意外地进入另一块屏幕或别的什么地方,从而导致误操作。
这一篇是我从官网上获得的相关控制器的,然后通过官网的案例进行一下修改扩展了一下功能。将案例实现了简单的碰撞检测。

案例实现

案例查看地址:http://www.wjceo.com/blog/threejs/2018-04-02/143.html

实现鼠标锁定

  • 首先,我们在body里面添加了dom标签,因为鼠标锁定只有通过用户才可以触发。
<div id="blocker">
    <div id="instructions">
        <span style="font-size:40px">点击屏幕开始</span>
        <br />
        <br />
        (W, A, S, D = 移动, SPACE = 跳跃, MOUSE = 移动视角)
    </div>
</div>
  • 然后,判断一下当前浏览器是否支持鼠标锁定。并绑定到了鼠标点击事件:
instructions.addEventListener( 'click', function ( event ) {
    instructions.style.display = 'none';
    // 锁定鼠标光标
    element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock;
    element.requestPointerLock();
}, false );

实现鼠标改变视角

  • 这里,就需要我们的PointerLockControls控件进行控制了,先引入PointerLockControls控件文件:
<script src="/lib/js/controls/PointerLockControls.js"></script>
  • 实例化控件,并把相机传入:
controls = new THREE.PointerLockControls( camera );
  • 通过实例化的对象可以通过getObject()获取到控制对象,可以设置它的位置来调整进入场景的位置。最后将对象放置到scene场景当中。
controls.getObject().position.y = 50;
controls.getObject().position.x = 100;
scene.add( controls.getObject() );
  • 最后,在render当中,我们让controls调用update函数实现更新
//获取到控制器对象
var control = controls.getObject();
//获取刷新时间
var delta = clock.getDelta();

//根据速度值移动控制器
control.translateX( velocity.x * delta );
control.translateY( velocity.y * delta );
control.translateZ( velocity.z * delta );

实现按键移动

我们要实现可以通过键盘案例进行控制器位置调整,首先就是要监听事件,所以我们监听的键盘按下事件和键盘抬起事件:

document.addEventListener( 'keydown', onKeyDown, false );
document.addEventListener( 'keyup', onKeyUp, false );

然后再监听回调里面判断当前的键盘按键,符合条件的,就将当前的方向设置为true:

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;
    }
};

最后,在render渲染中,进行判断当前的移动方向,实现的当前的移动,注意velocity向量,这是一个缓冲值,为了保证鼠标抬起后,场景不直接暂停,而是有一个简短的过渡效果:

if ( moveForward || moveBackward ) velocity.z -= direction.z * speed * delta;
if ( moveLeft || moveRight ) velocity.x -= direction.x * speed * delta;

//根据速度值移动控制器
control.translateX( velocity.x * delta );
control.translateY( velocity.y * delta );
control.translateZ( velocity.z * delta );

使用Raycaster实现简单的碰撞检测

这个部分是个难点,虽然以后会有更好的方法,但是,这个确实是一个好的练手方案。之前,我们使用Raycaster来进行窗口二维位置转three.js中的坐标位置。但是,Raycaster还有一个妙用,就是检测前方一定距离内是否有物体相撞。让我讲解一下实现原理。
- 首先,实例化Raycaster对象,然后,设置好Raycaster对象的,位置,方向,还有长度。Raycaster(起源:Vector3,方向:Vector3,近:浮动,远:浮动)

//声明射线
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);

虽然声明了三个,但是,其实用到了只用了两个,第三个向上的判断也没有写。

然后,将当前控制器的位置赋值给射线

//复制相机的位置
downRaycaster.ray.origin.copy( control.position );

判断当前射线的线朝向的方向是否有物体

//判断是否停留在了立方体上面
var intersections = downRaycaster.intersectObjects( scene.children, true);
var onObject = intersections.length > 0;
//判断是否停在了立方体上面
if ( onObject === true ) {
    velocity.y = Math.max( 0, velocity.y );
    canJump = true;
}

这是y轴的判断,如果脚下有物体,则不再下坠。实现了简单的碰撞检测。

案例代码

<!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>
  • 12
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
是的,three.js和tween.js可以一起使用实现复杂的动画效果,包括飞线动画。以下是一个简单的例子,演示了如何使用three.js和tween.js创建一条飞线动画: ```javascript // 创建three.js场景 var scene = new THREE.Scene(); // 创建相机 var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 5; // 创建渲染器 var renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 创建飞线路径 var curve = new THREE.CatmullRomCurve3([ new THREE.Vector3(-10, 0, 0), new THREE.Vector3(-5, 5, 0), new THREE.Vector3(0, 0, 0), new THREE.Vector3(5, -5, 0), new THREE.Vector3(10, 0, 0) ]); // 创建飞线材质 var material = new THREE.LineBasicMaterial({ color: 0xffffff }); // 将飞线路径转换为几何体 var geometry = new THREE.Geometry(); geometry.vertices = curve.getPoints(50); // 创建飞线网格 var line = new THREE.Line(geometry, material); scene.add(line); // 创建飞线动画 var tween = new TWEEN.Tween({t:0}) .to({t:1}, 5000) // 5秒钟 .onUpdate(function() { // 根据tween的进度计算飞线的位置 var position = curve.getPoint(this.t); // 更新飞线网格的位置 line.position.copy(position); }) .start(); // 渲染场景 function render() { requestAnimationFrame(render); TWEEN.update(); // 更新tween动画 renderer.render(scene, camera); } render(); ``` 在这段代码中,我们首先创建了一个three.js场景、相机和渲染器。然后,我们创建了一个CatmullRomCurve3曲线,用于定义飞线路径,并将其转换为three.js几何体。接下来,我们创建了一个TWEEN.Tween对象,将其起始值设置为0,结束值设置为1,表示飞线动画的进度。在Tween对象的 onUpdate 回调函数中,我们根据飞线路径计算当前进度对应的位置,并将飞线网格的位置更新为该位置。最后,我们创建了一个渲染函数,用于在每帧更新Tween动画和渲染场景。 希望这个例子可以帮助你了解如何使用three.js和tween.js创建飞线动画。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值