Three.js第一、三人称漫游

Three.js中文网http://www.webgl3d.cn/

第三人称漫游

工厂漫游.gif

跳跃.gif

第一人称漫游:展厅

视频效果展示:https://www.ixigua.com/7307473448324989492?logTag=be55fee76297458c2f7e

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

知识点回顾

8.8小节给大家讲解过,执行player.getWorldDirection(front);,可以获取玩家角色模型,当前视线正前方方向,用于W键和S键的前后运动控制。

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

叉乘计算左右方向

你可以先回顾下,前面3.5节 向量叉乘的知识点。

两个向量a、b叉乘有一个特点,叉乘结果是一个同时垂直于a和b的向量。这就是说只要知道玩家角色模型人正前方方向和高度方向向量,就可以计算出来人的左右方向。

小技巧:叉乘获得垂直于向量up和front的向量,左右方向与叉乘顺序有关,左右方向,可以用右手螺旋定责判断,但是比较麻烦,如果你懒得思考,干脆不用右手螺旋定则判断,代码测试测试下最简单,先随便写一个叉乘顺序,如果不对,就把up和front叉乘顺序换下。

image.png

玩家角色的正前方

const front = new THREE.Vector3();
player.getWorldDirection(front);

玩家角色的高度方向(竖直方向)

const up = new THREE.Vector3(0, 1, 0);//y方向

A和D按键对应的方向计算代码。

if (keyStates.A) {//向左运动
    const front = new THREE.Vector3();
    player.getWorldDirection(front);
    const up = new THREE.Vector3(0, 1, 0);//y方向

    const left = up.clone().cross(front);
    v.add(left.multiplyScalar(a * deltaTime));
}
if (keyStates.D) {//向右运动
    const front = new THREE.Vector3();
    player.getWorldDirection(front);
    const up = new THREE.Vector3(0, 1, 0);//y方向
    //叉乘获得垂直于向量up和front的向量 左右与叉乘顺序有关,可以用右手螺旋定则判断,也可以代码测试结合3D场景观察验证
    const right = front.clone().cross(up);
    v.add(right.multiplyScalar(a * deltaTime));
}

8.11 鼠标滑动改变视角(指针锁定模式)

前面改变相机左右和上下视角,用的是鼠标拖动方式,本节课给大家演示一个游戏、元宇宙项目中,常用的不用拖动,鼠标直接滑动改变相机玩家角色或相机视角。

知识点回顾

你可以把原来鼠标左键拖动改变视角的代码if (leftButtonBool) {},if条件去掉,只剩下鼠标滑动改变视角,你会发现并不是很好控制,有个问题,鼠标离开网页后,无法在旋转玩家视角,那么怎么解决?请看下面关于指针锁定模式的介绍。

let leftButtonBool = false;//记录鼠标左键状态
document.addEventListener('mousedown', (event) => {
    leftButtonBool = true;
});
document.addEventListener('mouseup', () => {
    leftButtonBool = false;
});

document.addEventListener('mousemove', (event) => {
    if (leftButtonBool) {//根据左键按下拖动才起作用
        player.rotation.y -= event.movementX / 600;
        cameraGroup.rotation.x -= event.movementY / 600;

    }
});

体验案例源码

你可以打开本节课案例源码,然后鼠标点击页面随便一个位置,就会进入指针锁定模式,人话说,这时候,你发现鼠标箭头不见了,鼠标可以上下左右无限滑动,如果你按下键盘左上角Esc按键,鼠标指针箭头恢复到原来状态。

请求指针锁定requestPointerLock

鼠标滑动时候,会受到浏览器网页页面窗口范围限制,不能无限制移动,这时候可以通过执行requestPointerLock()请求指针锁定。

// 当鼠标左键按下后进入指针锁定模式(鼠标无限滑动)
addEventListener( 'mousedown', () => {
    document.body.requestPointerLock();//body页面指针锁定
});

你执行document.body.requestPointerLock();以后,意味着document.pointerLockElement属性,会拥有一个值document.body

if(document.pointerLockElement == document.body){
    // 指针锁定模式下,才能执行的代码
}

在指针锁定模式下,改变玩家人姿态角度

进入指针模式后,才能根据鼠标位置控制人旋转

通过document.pointerLockElement判断web页面是否进入指针锁定模式。

鼠标点击页面进入指针锁定模式的时候,点击位置默认鼠标的坐标为原点,左右方向是x坐标.movementX(单位像素),上下方面是y坐标.movementY

// 人和相机初始姿态正前方:沿着z轴正半轴方向
//鼠标左右移动,人绕y轴旋转
addEventListener('mousemove', (event) => {
    // 进入指针模式后,才能根据鼠标位置控制人旋转
    if (document.pointerLockElement == document.body) {
        // 鼠标左右滑动,让人左右转向(绕y轴旋转),相机会父对象人绕左右转向
        //加减法根据左右方向对应关系设置,缩放倍数根据,相应敏感度设置
        person.rotation.y -= event.movementX / 500;
    }
});

退出指针锁定模式

执行document.exitPointerLock();可以退出指针锁定,或者键盘键盘Esc退出指针锁定模式,就像你玩游戏一样。


8.12快捷键切换第一、第三人称

你可以先复习下前面8.6.相机跟着玩家走(第三人称漫游)在学习本节课内容。

知识回顾:第三人称

image.png

注意一点透视投影相机fov视野的角度值会影响,相机与人距离的设置。

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

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

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

第一人称

第一人称,简单点说,就是看不到玩家角色的模型,相当于把相机放在人的前面。

// camera.position.set(0, 1.6, -2.3);//第三人称
// camera.lookAt(0, 1.6, 0);
camera.position.set(0, 1.6, 1);//第一人称
camera.lookAt(0, 1.6, 2);//目标观察点注意在相机位置前面一点

如果lookAt后面执行第一人称代码,不重新执行camera.lookAt,视线方向还是原来的。

// z距离人远近具体值,可以根据模型尺寸去测试调节
camera.position.set(0, 1.6, -2.3);//第三人称
camera.lookAt(0, 1.6, 0);
camera.position.set(0, 1.6, 1);//第一人称

image.png

第一、第三人称切换

第一、第三人称,快捷键v切换的完整代码。

let viewBool = true;//true表示第三人称,false表示第一人称
document.addEventListener('keydown', (event) => {
    if (event.code === 'KeyV') {
        if (viewBool) {
            // 切换到第一人称
            camera.position.z = 1;//相机在人前面一点 看不到人模型即可
        } else {
            // 切换到第三人称
            camera.position.z = -2.3;//相机在人后面一点
        }
        viewBool = !viewBool;
    }
});

8.13 骨骼动画与运动状态关联

学习本节课之前,首先确保你已经熟悉前面基础内容16. 关键帧动画,要不然大部分代码你也看不懂,也跟不上视频。

骨骼动画有休息、步行、跑步等动作,本节课大家简单讲解下,根据玩家角色的运动状态来执行对应的骨骼动画。

查看模型所有骨骼动画数据

const gltf = await loader.loadAsync("../../人.glb");
const player = gltf.scene;//玩家角色模型

//包含关键帧动画的模型作为参数创建一个播放器
const mixer = new THREE.AnimationMixer(player);
console.log('所有骨骼动画数据', gltf.animations);
// 骨骼动画动画名字和对应含义,名字是可以在bledner中随意命名的
//Idle  休息
//Run   跑步
//Walk  走路
//休息动作
const IdleAction = mixer.clipAction(gltf.animations[5]);
//步行动作
const WalkAction = mixer.clipAction(gltf.animations[13]);
//跑步动作
const RunAction = mixer.clipAction(gltf.animations[0]);

查看gltf.animations,你可以看到不同clip动画的名字等数据

console.log('所有骨骼动画数据', gltf.animations);
// 骨骼动画动画名字和对应含义,名字是可以在bledner中随意命名的
//Idle  休息
//Run   跑步
//Walk  走路

不同clip动画数据的索引值,可以在浏览器控制台打印gltf.animations查看

不同模型不同,你根据自己项目模型,灵活应对即可。

//休息动作
const IdleAction = mixer.clipAction(gltf.animations[5]);
//步行动作
const WalkAction = mixer.clipAction(gltf.animations[13]);
//跑步动作
const RunAction = mixer.clipAction(gltf.animations[0]);

站着休息和步行两个动作切换

具体思路:你参考前面基础课程11. 骨骼动画不同动作切换里面案例2即可。

//休息动作
const IdleAction = mixer.clipAction(gltf.animations[5]);
//步行动作
const WalkAction = mixer.clipAction(gltf.animations[13]);
IdleAction.play();
WalkAction.play();
IdleAction.weight = 1.0;//默认休息状态
WalkAction.weight = 0.0;

在站着休息和步行两个动作之间切换

function changeAction(name){
    if (name=='Idle'){
        IdleAction.weight = 1.0;//休息状态
        WalkAction.weight = 0.0;
    }else if (name == 'Walk') {
        IdleAction.weight = 0.0;
        WalkAction.weight = 1.0;//步行状态
    } 
}

根据玩家速度v的大小v.length(),控制使用休息和步行那个动作。

function playerUpdate(deltaTime) {
    const vL = v.length();
    if (vL < 0.2) {//速度小于0.2切换到站着休息状态
        changeAction('Idle');
    } else if (vL >= 0.2) {//步行状态
        changeAction('Walk');
    } 
}

阻尼和加速度调节

当不在使用WASD加速的时候,如果你希望玩家快速减速,可以适当提升阻尼。

const damping = -0.1;
v.addScaledVector(v, damping);//阻尼减速

WASD按键加速的同时,也会通过阻尼减速,所以阻尼的存在会限制最大速度。如果达不到自己想要的最大速度,可以把加速度a的值提升。

const a = 30;//WASD按键的加速度:调节按键加速快慢
const damping = -0.1;
const vMax = 10;//限制玩家角色最大速度

const vL = v.length();
console.log('vL', vL);//浏览器控制查看速度大小

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

练习题:休息、步行、跑步三个动作

//休息动作
const IdleAction = mixer.clipAction(gltf.animations[5]);
//步行动作
const WalkAction = mixer.clipAction(gltf.animations[13]);
//跑步动作
const RunAction = mixer.clipAction(gltf.animations[0]);
IdleAction.play();
WalkAction.play();
RunAction.play();
IdleAction.weight = 1.0;//默认休息状态
WalkAction.weight = 0.0;
RunAction.weight = 0.0;


function changeAction(name) {
    if (name == 'Idle') {
        IdleAction.weight = 1.0;
        WalkAction.weight = 0.0;
        RunAction.weight = 0.0;
    } else if (name == 'Walk') {
        IdleAction.weight = 0.0;
        WalkAction.weight = 1.0;
        RunAction.weight = 0.0;
    } else if (name == 'Run') {
        IdleAction.weight = 0.0;
        WalkAction.weight = 0.0;
        RunAction.weight = 1.0;
    }
}

不同速度设置三个动画状态,测试上面骨骼动画效果

function playerUpdate(deltaTime) {
    const vL = v.length();
    if (vL < 0.2) {//速度小于0.2切换到站着休息状态
        // 注释如果当前就是Idle状态,不要再次执行changeAction
        changeAction('Idle');
    } else if (vL > 0.2 && vL < 4) {//步行状态
        changeAction('Walk');
    } else if (vL >= 4) {//跑步状态
        changeAction('Run');
    }
}    

作业练习:批量解析全部骨骼动画动作

如果动作多了你会发现一个一个写,比较麻烦,其实你也可以通过for循环批量解析所有骨骼动画动作。

const clipArr = gltf.animations;//所有骨骼动画
const actionObj = {};//包含所有动作action
for (let i = 0; i < clipArr.length; i++) {
    const clip = aniArr[i];//休息、步行、跑步等动画的clip数据
    const action = mixer.clipAction(clip);//clip生成action
    action.name = clip.name;//action命名name
    // 批量设置所有动画动作的权重
    if (action.name === 'Idle') {
        action.weight = 1.0;//这样默认播放Idle对应的休息动画
    } else {
        action.weight = 0.0;
    }
    action.play();
    // action动画动作名字作为actionObj的属性
    actionObj[action.name] = action;
}

动作切换函数

let currentAction = actionObj['Idle'];//记录当前播放的动作
// 切换不同动作
function changeAction(actionName) {
    currentAction.weight = 0.0;//原来动作权重为0,不播放
    const action = actionObj[actionName];//新的需要播放的动作
    action.weight = 1.0;//将要播放的动作权重为1
    currentAction = action;//替换记录的动作
}

根据玩家角色速度v设置休息和步行动作。

function playerUpdate(deltaTime) {
    const vL = v.length();
    if (vL < 0.2) {//速度小于0.2切换到站着休息状态
        // 如果当前就是Idle状态,不需要再次执行changeAction
        if (currentAction.name != 'Idle') changeAction('Idle');
    } else {//步行状态
        if (currentAction.name != 'Walk') changeAction('Walk');
    }
}
  • 22
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
要实现第一人称视角漫游,可以使用 three.js 中的摄像机和控制器。以下是一个简单的示例: ```javascript // 创建场景 var scene = new THREE.Scene(); // 创建相机 var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); // 创建渲染器 var renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 创建控制器 var controls = new THREE.PointerLockControls(camera); scene.add(controls.getObject()); // 添加一个立方体作为墙壁 var geometry = new THREE.BoxGeometry(10, 10, 10); var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); var cube = new THREE.Mesh(geometry, material); scene.add(cube); // 监听鼠标点击事件 document.addEventListener('mousedown', function () { controls.lock(); }); // 监听键盘事件 var onKeyDown = function (event) { switch (event.keyCode) { case 38: // up arrow case 87: // W key controls.moveForward(1); break; case 37: // left arrow case 65: // A key controls.moveLeft(1); break; case 40: // down arrow case 83: // S key controls.moveForward(-1); break; case 39: // right arrow case 68: // D key controls.moveRight(1); break; } }; document.addEventListener('keydown', onKeyDown); // 渲染场景 function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); ``` 这个示例中,我们创建了一个场景、一个相机和一个渲染器。然后,我们创建了一个控制器,并将相机添加到场景中。我们还添加了一个立方体作为墙壁,并监听鼠标点击事件和键盘事件,以控制相机的移动。最后,我们使用 `requestAnimationFrame` 函数来渲染场景。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Threejs可视化

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值