欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Three.js 第一人称和第三人称漫游

最编程 2024-04-17 13:38:55
...

Threejs中文网www.webgl3d.cn/

第三人称漫游

工厂漫游.gif

跳跃.gif

第一人称漫游:展厅

8.1 键盘WASD按键状态记录

如果你玩过游戏,一般都知道,通过键盘的W、A、S、D按键可以控制玩家角色在3D场景中运动,比如控制一个人前后左右运动,比如控制一辆车前后左右运动。

image.png

键盘事件

如果你不熟悉HTML5前端鼠标、键盘事件,可以去学习下,下面主要说下思路,不在一行一行演示。

下面代码功能是当你按下随便一个键盘按键,就会触发参数2表示的函数执行,并log打印对应按键名字相关信息。

// 监听鼠标按下事件
document.addEventListener('keydown', (event) => {
    console.log('event.code',event.code);
})

执行上面代码后,你可以分别按下键盘W、A、S、D、空格键测试,你可以看到浏览器控制台打印输出对应键盘按键名字,也就是KeyW、KeyA、KeyS、KeyD、Space。

// 监听鼠标松开事件
document.addEventListener('keyup', (event) => {
    console.log('event.code',event.code);
})

记录键盘按键WASD状态

// 声明一个对象keyStates用来记录键盘事件状态
const keyStates = {
    // 使用W、A、S、D按键来控制前、后、左、右运动
    // false表示没有按下,true表示按下状态
    W: false,
    A: false,
    S: false,
    D: false,
};
// 当某个键盘按下设置对应属性设置为true
document.addEventListener('keydown', (event) => {
    if (event.code === 'KeyW') keyStates.W = true;
    if (event.code === 'KeyA') keyStates.A = true;
    if (event.code === 'KeyS') keyStates.S = true;
    if (event.code === 'KeyD') keyStates.D = true;
});
// 当某个键盘抬起设置对应属性设置为false
document.addEventListener('keyup', (event) => {
    if (event.code === 'KeyW') keyStates.W = false;
    if (event.code === 'KeyA') keyStates.A = false;
    if (event.code === 'KeyS') keyStates.S = false;
    if (event.code === 'KeyD') keyStates.D = false;
});

测试键盘状态

在循环执行的函数中查看键盘状态值。

// 循环执行的函数render
function render() {
    requestAnimationFrame(render);
}
render();

你可以执行下面代码,然后反复按下或松开W键,浏览器控制台查看keyStates.W的变化。

// 循环执行的函数中测试W键盘状态值
function render() {
    if(keyStates.W){
        console.log('W键按下');
    }else{
        console.log('W键松开');
    }
    requestAnimationFrame(render);
}
render();

其他写法(可以跳过)

批量记录所有键盘事件状态

// 声明一个对象keyStates用来记录键盘事件状态
const keyStates = {
    // // false表示没有按下,true表示按下状态
    // keyW:false,
    // keyA:false,
    // keyS:false,
    // keyD:false,
};

// 当某个键盘按下设置对应属性设置为true
document.addEventListener('keydown', (event) => {
    keyStates[event.code] = true;
});
// 当某个键盘抬起设置对应属性设置为false
document.addEventListener('keyup', (event) => {
    keyStates[event.code] = false;
});

8.2 W键控制角色模型运动

WASD的综合控制比较复杂,本节课,先给大家演示下,怎么通过W监控人物模型沿着Z轴运动。

当W键一直处于按下状态时,人物模型沿着Z轴向前运动,松开的时候,不在运动(你可以测试课件的源码)。

演示文件

演示文件里面给大家提供了一个基本的文件,加载了一个人物模型,设置一个简单网格线地面,关于人物模型的骨骼动画,你可以参考前面基础课程章节16讲解:16.4. 解析外部模型关键帧动画;

知识回顾

上节课给大家讲解过,怎么通过一个对象的属性,记录W、A、S、D四个按键的状态。

const keyStates = {
    W: false,
    A: false,
    S: false,
    D: false,
};
document.addEventListener('keydown', (event) => {
    if (event.code === 'KeyW') keyStates.W = true;
    if (event.code === 'KeyA') keyStates.A = true;
    if (event.code === 'KeyS') keyStates.S = true;
    if (event.code === 'KeyD') keyStates.D = true;
});
document.addEventListener('keyup', (event) => {
    if (event.code === 'KeyW') keyStates.W = false;
    if (event.code === 'KeyA') keyStates.A = false;
    if (event.code === 'KeyS') keyStates.S = false;
    if (event.code === 'KeyD') keyStates.D = false;
});

W键控制人物模型运动

你先回顾下,本课程章节2关于位移、速度、加速的讲解,更好理解接下来要讲解的内容。

// 用三维向量表示玩家角色(人)运动漫游速度
//按下W键对应的人运动速度
const v = new THREE.Vector3(0, 0, 3);

渲染循环里面,通过时间*速度,来更新人模型位置。

// 渲染循环
const clock = new THREE.Clock();
function render() {
    const deltaTime = clock.getDelta();
    if (keyStates.W) {
        // 在间隔deltaTime时间内,玩家角色位移变化计算(速度*时间)
        const deltaPos = v.clone().multiplyScalar(deltaTime);
        player.position.add(deltaPos);//更新玩家角色的位置
    }
    mixer.update(deltaTime);// 更新播放器相关的时间
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

8.3 加速度(按键给玩家加速)

上节课给大家讲解过,当你按下W键的时候,玩家角色模型,会运动,松开W键,人会停下来。但是这个运动效果是突然运动和突然停止,没有一个加速或减速的过程,本节课以W键为例,设置玩家加速过程,也就是当你按下W键以后,人的速度从0慢慢提升上来。

const v = new THREE.Vector3(0, 0, 3);
function render() {
    if (keyStates.W) {
        // 在间隔deltaTime时间内,玩家角色位移变化计算(速度*时间)
        const deltaPos = v.clone().multiplyScalar(deltaTime);
        player.position.add(deltaPos);//更新玩家角色的位置
    }
}

设置加速度

// 用三维向量表示玩家角色(人)运动漫游速度
const v = new THREE.Vector3(0, 0, 0);//初始速度设置为0
const a = 12;//加速度:调节按键加速快慢
// 渲染循环
const clock = new THREE.Clock();
function render() {
    const deltaTime = clock.getDelta();
    if (keyStates.W) {
        //先假设W键对应运动方向为z
        const front = new THREE.Vector3(0,0,1);
        // W键按下时候,速度随着时间增加
        v.add(front.multiplyScalar(a * deltaTime));
        // 在间隔deltaTime时间内,玩家角色位移变化计算(速度*时间)
        const deltaPos = v.clone().multiplyScalar(deltaTime);
        player.position.add(deltaPos);//更新玩家角色的位置
    }
    
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

限制最高速度

执行上面代码,当你按下W键的时候,v会一直加速,这时候可以通过v.length()计算速度v的值,当然速度小一个临界值的时候,才增加速度v的大小。

const vMax = 5;//限制玩家角色最大速度
...
if (v.length() < vMax) {//限制最高速度
    // W键按下时候,速度随着时间增加
    v.add(front.multiplyScalar(a * deltaTime));
}

8.4 阻尼(玩家角色逐渐减速停止)

继续上节课内容讲解,上节课以W键为例,给大家讲解了怎么通过WASD按键,给玩家角色模型加速,本节课给大家讲解,怎么设置阻尼,具体说,就是当没有WASD按键加速时候,玩家角色模型,会在阻尼作用下逐渐减速停止,就像地面上滚动的球逐渐停下来。

if (keyStates.W) {
    //先假设W键对应运动方向为z
    const front = new THREE.Vector3(0,0,1);
    // W键按下时候,速度随着时间增加
    v.add(front.multiplyScalar(a * deltaTime));
    // 在间隔deltaTime时间内,玩家角色位移变化计算(速度*时间)
    const deltaPos = v.clone().multiplyScalar(deltaTime);
    player.position.add(deltaPos);//更新玩家角色的位置
}

设置阻尼减速

大家可以思考下,当你没有按下WASD的时候,怎么给运动的物体减速。

其实很简单,可以在渲染循环中,重复执行速度v乘以一个小于1的数值,这样重复多次执行以后,速度就会逼近0。比如v* (1 - 0.04) = v * 0.96,多次循环乘以0.96(v*0.96*0.96*0.96...),v就会无限逼近于0。

const damping = -0.04;
function render() {
    if (keyStates.W) {
        ...
    }
    // v*(1 + damping) = v* (1 - 0.04) = v * 0.96
    // 多次循环乘以0.96(v*0.96*0.96*0.96...),v就会无限逼近于0。
    // v*(1 + damping) = v + v * damping
    v.addScaledVector(v, damping);//阻尼减速

    requestAnimationFrame(render);
}

验证阻尼是否生效

if (keyStates.W){}里面玩家角色位置更新的代码,挪到外面,你可以发现,当按键W松开,玩家角色会慢慢停下来,原因很简单,虽然一直在执行速度*时间更新玩家位置,但是在阻尼作用下,速度慢慢逼近0了,位移变化量自然逼近0。

// 用三维向量表示玩家角色(人)运动漫游速度
const v = new THREE.Vector3(0, 0, 0);//初始速度设置为0
const a = 12;//WASD按键的加速度:调节按键加速快慢
const damping = -0.04;//阻尼 当没有WASD加速的时候,人、车等玩家角色慢慢减速停下来
// 渲染循环
const clock = new THREE.Clock();
function render() {
    const deltaTime = clock.getDelta();
    if (keyStates.W) {
        //先假设W键对应运动方向为z
        const front = new THREE.Vector3(0, 0, 1);
        if (v.length() < 5) {//限制最高速度
            // W键按下时候,速度随着时间增加
            v.add(front.multiplyScalar(a * deltaTime));
        }
    }

    // 阻尼减速
    v.addScaledVector(v, damping);

    //更新玩家角色的位置  当v是0的时候,位置更新也不会变化
    const deltaPos = v.clone().multiplyScalar(deltaTime);
    player.position.add(deltaPos);

    mixer.update(deltaTime);
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

8.5 按键S退后运动

这些内容非常简单,算是一个练习题,你可以在前面几节课基础上,增加一个S按键,玩家角色模型的退后运动,学习视频之前,可以自己先动手写下。

S键退后

function render() {
    if (v.length() < vMax) {//限制最高速度
        if (keyStates.W) {
            //先假设W键对应运动方向为z
            const front = new THREE.Vector3(0, 0, 1);
            v.add(front.multiplyScalar(a * deltaTime));
        }
        if (keyStates.S) {
            // 与W按键相反方向
            const front = new THREE.Vector3(0, 0, -1);
            v.add(front.multiplyScalar(a * deltaTime));
        }
    }
    v.addScaledVector(v, damping);//阻尼减速
    //更新玩家角色的位置
    const deltaPos = v.clone().multiplyScalar(deltaTime);
    player.position.add(deltaPos);
}

下面是更多具体代码

const keyStates = {
    W: false,
    S: false,
};
document.addEventListener('keydown', (event) => {
    if (event.code === 'KeyW') keyStates.W = true;
    if (event.code === 'KeyA') keyStates.A = true;
});
document.addEventListener('keyup', (event) => {
    if (event.code === 'KeyW') keyStates.W = false;
    if (event.code === 'KeyA') keyStates.A = false;
});
// 用三维向量表示玩家角色(人)运动漫游速度
const v = new THREE.Vector3(0, 0, 0);//初始速度设置为0
const a = 12;//WASD按键的加速度:调节按键加速快慢
const damping = -0.04;//阻尼 当没有WASD加速的时候,人、车等玩家角色慢慢减速停下来
const vMax = 5;//限制玩家角色最大速度
// 渲染循环
const clock = new THREE.Clock();
function render() {
    const deltaTime = clock.getDelta();
    if (v.length() < vMax) {//限制最高速度
        if (keyStates.W) {
            const front = new THREE.Vector3(0, 0, 1);//先假设W键对应运动方向为z
            v.add(front.multiplyScalar(a * deltaTime));
        }
        if (keyStates.S) {
            // 与W按键相反方向
            const front = new THREE.Vector3(0, 0, -1);
            v.add(front.multiplyScalar(a * deltaTime));
        }
    }
    // v*(1 + damping) = v* (1 - 0.04) = v * 0.96
    // 多次循环乘以0.96(v*0.96*0.96*0.96...),v就会无限逼近于0。
    // v*(1 + damping) = v + v * damping
    v.addScaledVector(v, damping);//阻尼减速

    //更新玩家角色的位置   当v是0的时候,位置更新也不会变化
    const deltaPos = v.clone().multiplyScalar(deltaTime);
    player.position.add(deltaPos);

    mixer.update(deltaTime);
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

8.6 相机跟着玩家走(第三人称漫游)

前面案例,通过按键控制玩家角色模型运动的时候,并没有控制相机移动。下面给大家讲解怎么控制相机运动产生漫游的感觉。

const camera = new THREE.PerspectiveCamera();
camera.position.set(8, 10, 14);
camera.lookAt(0, 0, 0);

function render() {
    ...
    //更新玩家角色的位置
    player.position.add(deltaPos);
}
render();

层级模型知识点回顾

4.1 层级模型

4.3. 本地坐标和世界坐标

image.png

const mesh1 = new THREE.Mesh();
const group = new THREE.Group();
group.add(mesh1);
scene.add(group);

mesh1的父对象group移动,mesh1会跟着移动

group.position.y = 10;

mesh1.position表示mesh1局部坐标,也就是相对父对象group的位置。mesh1在三维场景scene中的实际位置(世界坐标)就是group.positionmesh1.position叠加。

mesh1.position.y = 10;

相机对象父类Object3D

相机对象Camera的父类和mesh、group一样,都是Object3D,这意味着,如果你把相机作为某个模型的子对象,相机的位置和姿态同样受到模型的影响。

const group = new THREE.Group();
group.add(camera);//相机作为group子对象
// 父对象group平移,相机跟着平移
group.position.y = 10;
// 父对象group旋转,相机跟着旋转
group.rotateY(Math.PI/6);

注释相机空间OrbitControls代码

注释相机空间OrbitControls代码,避免影响相机W、A、S、D对相机的控制,原来用OrbitControls只是为了方便观察测试3D场景。

const controls = new OrbitControls(camera, renderer.domElement);

相机作为玩家角色子对象

相机作为玩家角色子对象,可以实现相机对玩家角色模型的跟随运动,使相机运动,模拟人漫游3D场景的感觉。

// 把相机作为玩家角色的子对象,这样相机的位置和姿态就会跟着玩家角色改变
player.add(camera);//相机作为人的子对象,会跟着人运动

你可以根据需要放相机相对玩家角色的位置,比如我这里相机与人高度相近(你可以在blender中测量下人的高度),把相机放在人后脑勺,拉开一定距离,然后相机镜头对准人的后脑勺。

下面尺寸是以相机的父对象玩家角色模型的局部坐标系坐标原点为参照的。

camera.position.set(0, 1.6, -5.5);//玩家角色后面一点
camera.lookAt(0, 1.6, 0);//对着人身上某个点  视线大致沿着人的正前方

image.png

我这里是以一个人为例写的相机位置,如果你换成一个车,模拟人在驾驶位上的感觉,也可以把相机高度设置在车辆高度附近。

第三人称漫游

这里所谓的第三人称,你可以简单的理解为相机在运动漫游的过程中,你可以看到玩家角色模型,比如你能看到运动的人、车等角色模型,就是上面咱们写的代码,把相机放在玩家角色模型后面一点即可。

image.png

补充:相机视角参数fov影响相机位置设置!!!

const camera = new THREE.PerspectiveCamera(30,...);
player.add(camera);//相机作为人的子对象
//玩家角色后面一点  对应fov 30度
camera.position.set(0, 1.6, -5.5);
camera.lookAt(0, 1.6, 0);//对着人身上某个点

根据透视投影相机规律,fov变大,能够看到的视野范围角度更大。

const camera = new THREE.PerspectiveCamera(70,...);
//玩家角色后面一点  对应fov 70度
camera.position.set(0, 1.6, -2.3);

8.7 鼠标左右拖动改变玩家视角

本节课给大家讲解一个新功能,就是你按住鼠标左键左右拖动,改变玩家角色和相机的视角。

了解鼠标滑动事件规则

如果你不了解前端HTML5鼠标滑动事件的规则,可以跟着视频学习一遍,如果你非常熟悉,可以直接快进到下一步。

// 鼠标滑动期间,会不停地多次触发鼠滑动事件,直到不再滑动
document.addEventListener('mousemove', (event) => {
    console.log('触发1次');
});

鼠标持续滑动时候,会多次触发滑动事件。event.movementX表示本次触发事件相对上次,鼠标左右方向滑动的距离,单位是像素,往右滑动是正,往左滑动是负。

document.addEventListener('mousemove', (event) => {
    console.log('鼠标每次x方向移动距离', event.movementX);
});

鼠标控制玩家转向

通过鼠标左右滑动距离控制玩家角色模型player旋转

document.addEventListener('mousemove', (event) => {
    // 注意rotation.y += 与 -= 区别,左右旋转时候方向相反
    //event.movementX缩小一定倍数改变旋转控制的灵敏度
    player.rotation.y -= event.movementX / 600;
});

鼠标左键拖动时候,旋转玩家角色

鼠标左键拖动定义:鼠标左键按下,不松开,左右滑动

  1. 记录鼠标左键状态
let leftButtonBool = false;//记录鼠标左键状态
document.addEventListener('mousedown', () => {
    leftButtonBool = true;
});
document.addEventListener('mouseup', () => {
    leftButtonBool = false;
});
  1. 判断鼠标左键状态,决定是否旋转玩家角色
document.addEventListener('mousemove', (event) => {
    //鼠标左键按下时候,才旋转玩家角色
    if(leftButtonBool){
        player.rotation.y -= event.movementX / 600;
    } 
});

测试上节课代码:相机随着player旋转

上节课,给大家讲解过,相机对象是玩家角色模型的子对象,玩家角色player旋转的时候,子对象相机自然跟着同步旋转,你可以测试下执行player.add(camera)与不执行的区别。

//相机作为player子对象,会跟着player平移或旋转
player.add(camera);
camera.position.set(0, 1.6, -5.5);
camera.lookAt(0, 1.6, 0);

第三人称漫游相机位置转存失败,建议直接上传图片文件

image.png

8.8 获取玩家(相机)正前方方向

实际开发,玩家角色的视角或者说相机的视角,会随着鼠标左右移动变化的,不过前面几节课,为了降低学习难度,代码给的是固定方向。

function render() {
    if (keyStates.W) {
        //先假设W键对应运动方向为z
        const front = new THREE.Vector3(0, 0, 1);
        // 改变玩家速度
        v.add(front.multiplyScalar(a * deltaTime));
    }
}

.getWorldDirection()

Object3D类有一个获取模型局部z轴方向相关的方法.getWorldDirection()

obj.getWorldDirection()表示的获取obj对象自身z轴正方向在世界坐标空间中的方向。

模型没有任何旋转情况,.getWorldDirection()获取的结果(0,0,1)

const mesh = new THREE.Mesh();
const dir = new THREE.Vector3();
mesh.getWorldDirection(dir);
console.log('dir', dir);

模型绕y旋转90度情况,.getWorldDirection()获取的结果(1,0,0)

const mesh = new THREE.Mesh();
mesh.rotateY(Math.PI / 2);
const dir = new THREE.Vector3();
mesh.getWorldDirection(dir);
// 模型没有任何选择打印结果(1,0,0)
console.log('dir', dir);

.getWorldDirection()获取玩家角色正前方

注意:threejs加载的玩家角色gltf模型,自身.rotation没有任何旋转的情况下,注意玩家角色正前方方向最好和z轴方向一致,这样就可以直接用.getWorldDirection()获取的结果表示人的正前方。

// 按下W键,实时计算当前玩家角色的正前方向
if (keyStates.W) {
    const front = new THREE.Vector3();
    //获取玩家角色(相机)正前方
    player.getWorldDirection(front);
}

S键运动方向

注意S键运动方向与W的正前方相反,这时候很简单,可以计算方向的时候,把front取反,或者最简单加速度设置一个负号front.multiplyScalar(- a * deltaTime)

function render() {
    if (v.length() < vMax) {//限制最高速度
        if (keyStates.W) {
            const front = new THREE.Vector3();
            player.getWorldDirection(front);//获取玩家角色(相机)正前方
            v.add(front.multiplyScalar(a * deltaTime));
        }
        if (keyStates.S) {
            const front = new THREE.Vector3();
            player.getWorldDirection(front);
            // - a:与W按键反向相反
            v.add(front.multiplyScalar(- a * deltaTime));
        }
    }
}    

8.9 鼠标上下移动只改变相机视角

8.7小节给大家讲解了,通过鼠标左右移动,旋转玩家角色模型player,相机跟着player同步旋转。

player.add(camera);//相机作为player子对象

document.addEventListener('mousemove', (event) => {
    if(leftButtonBool){
        player.rotation.y -= event.movementX / 600;
    } 
});

本节课给大家讲解,鼠标上下移动后,只改变相机视角,但是不改变玩家角色模型的姿态角度,换句话说,就是玩家角色模型始终站在地面上不会倾斜。

有问题:通过player改变相机上下俯仰视角

event.movementY的值改变player.rotation.x的值,这样虽然可以通过player控制子对象相机视角上下俯仰,但是玩家角色模型也必须跟着旋转,这样会改变人与地面位置关系,你可以思考下,该怎么解决?

document.addEventListener('mousemove', (event) => {
    if(leftButtonBool){
        // 左右旋转
        player.rotation.y -= event.movementX / 600;
        // 玩家角色绕x轴旋转  视角上下俯仰
        player.rotation.x -= event.movementY / 600;
    } 
});

image.png

鼠标上下移动只改变相机视角,不改变player角度

可以在相机camera和玩家角色模型player之间,嵌入一个子节点cameraGroup,作为相机的父对象,作为玩家角色模型player的子对象。

// 层级关系:player  <—— camera
player.add(camera);//相机作为player子对象
// 层级关系:player <—— cameraGroup <—— camera
const cameraGroup = new THREE.Group();
cameraGroup.add(camera);
player.add(cameraGroup);

通过camera的父对象cameraGroup控制相机姿态角度变化。

document.addEventListener('mousemove', (event) => {
    if(leftButtonBool){
        // 左右旋转
        player.rotation.y -= event.movementX / 600;
        // 鼠标上下滑动,让相机视线上下转动
        // 相机父对象cameraGroup绕着x轴旋转,camera跟着转动
        cameraGroup.rotation.x -= event.movementY / 600;
    } 
});

限制视线上下浮动范围

你可以根据需要,约束上下浮动角度范围,比如我设置上下俯仰范围-15度~15度,共30度。

思路很简单,一旦判断.rotation.x小于-15度,就设置为-15度,大于15度,就设置为15度。

// 上下俯仰角度范围
const angleMin = THREE.MathUtils.degToRad(-15);//角度转弧度
const angleMax = THREE.MathUtils.degToRad(15);
document.addEventListener('mousemove', (event) => {
    if(leftButtonBool){
        // 左右旋转
        player.rotation.y -= event.movementX / 600;
        // 鼠标上下滑动,让相机视线上下转动
        // 相机父对象cameraGroup绕着x轴旋转,camera跟着转动
        cameraGroup.rotation.x -= event.movementY / 600;
        // 一旦判断.rotation.x小于-15,就设置为-15,大于15,就设置为15
        if (cameraGroup.rotation.x < angleMin) {
            cameraGroup.rotation.x = angleMin;
        }
        if (cameraGroup.rotation.x > angleMax) {
            cameraGroup.rotation.x = angleMax
        };
    } 
});

image.png


8.10 玩家角色左右运动(叉乘)

前面给大家讲解过,通过W和S按键控制玩家角色的前后运动,本节课给大家讲解通过A和D键控制玩家的左右运动。

image.png