本篇文章主要缝合了官网的几个案例,并无高深或复杂的逻辑和代码;

镜头跟踪在游戏或者展馆等场景下非常常见,可以是第一人称,也可以是第三人称,本次带来的是第三人称的镜头跟踪方法

模型与动画来源

指针控制器

下面会一一介绍

效果如下

2023-04-07 15.49.20.gif

从gif图中可以得知效果为,鼠标移动控制人物旋转,按住W键,人物切换run即跑步状态,释放W键人物切换为idle即空闲状态,动画是glf模型编辑好的,后续会进行拆解。

那么我们一步一步来

创建初始场景

环境

基础场景用到的API为scene场景,PerspectiveCamera透视相机,DirectionalLight平行光,HemisphereLight半球光,WebGLRenderer渲染器,代码不在这里赘述。

地面

地面使用到了texture贴图,首先创建一个PlaneGeometry平面,材质使用MeshLambertMaterial网格材质,主要作用是感光,可以为后续加阴影做准备,本次代码没含阴影部分,感兴趣的同学可以自行学习;

贴图选用官网提供的grid.png,再使用TextureLoader进行加载并交给材质渲染,设置底板尺寸1000*1000,默认创建出来的plane的X轴为Math.PI(纵向),使用ration修改plane方向X轴为-0.5*Math.PI(横向),代码如下:

typescript const geometry = new THREE.PlaneGeometry(PlaneSize, PlaneSize); const material = new THREE.MeshLambertMaterial({ color: 0xffffff, side: THREE.DoubleSide }); const plane = new THREE.Mesh(geometry, material); plane.receiveShadow = true; textureLoader.load('../src/assets/textures/grid.png', function (texture) { texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(1000, 1000); plane.material.map = texture; plane.material.needsUpdate = true; }); plane.rotation.set(-0.5 * Math.PI, 0, 0);

模型

加载模型

选用官网提供的Xbot.glb模型文件

简单的封装一个loadGltf加载器方法

typescript export function loadGltf(url: string) { return new Promise<Object>((resolve, reject) => { gltfLoader.load(url, function (gltf) { resolve(gltf) }); }) }

加载后的文件结构中带有scene场景、animations动画集合,默认动画有7种,后续我们会采用idlerun,walk三种动画,

image.png

注册AnimationMixer动画器

遍历animations收集到所有动画的名称和动画内容,

收集动画

```typescript const animations = xbot.animations

playerMixer = new THREE.AnimationMixer(XBot);

for (let i = 0; i < animations.length; i++) {

const clip = animations[i];

const action = playerMixer.clipAction(clip);

action.clampWhenFinished = true;

actions[clip.name] = action

createHandleButton(clip.name)

} ```

clampWhenFinished 字段的作用是在执行一个单次动画后恢复成上一个或者指定的某个动画,目前用不到,如果在运动过程中执行跳跃或者打招呼的动画可以使用这个字段

目前我们收集了所有的动画到actions变量内,actions需要设置成全局的变量,以供后续使用

修改动画

playerMixer = new THREE.AnimationMixer(XBot); 这行代码就是注册动画器,在render运行过程中用来更新动画的,

typescript const dt = playerClock.getDelta(); if (playerMixer) playerMixer.update(dt);

收集到所有动画后,将模型动画默认动画设置为idle

```typescript playerActiveAction = actions['idle'];

playerActiveAction.play(); ```

playerActiveAction字段是全局变量,用来存储模型当前执行的动画,切换模型动画时,将即将更新的动画previousAction和当前动画进行对比,如果属于不同的动画,退出当前动画,执行新的动画

```typescript /** * * @param name 下一个动画名称 * @param duration 过渡时间 */ function fadeToAction(name: string, duration = 0.5) {

previousAction = playerActiveAction; playerActiveAction = actions[name]; if (previousAction !== playerActiveAction) { previousAction.fadeOut(duration); }

playerActiveAction .reset() .setEffectiveTimeScale(1) .setEffectiveWeight(1) .fadeIn(duration) .play();

} ```

setEffectiveTimeScale 为动画执行缩放,超过1为加速播放,低于1为缓慢播放,fadeIn字段为执行过渡时间,与fadeOut作用相同,不过含义不同,fadeOut为取消动画时的过渡时间,下面看一下效果

动画成果展示

在播放动画时,添加一个角色的骨骼,这样更能直观的看到各个关节和部位的关系,对于three.js绘制骨骼动画也有帮助,three.js骨骼动画一般用于物理引擎下的两个物体链接,比如钟摆,竹蜻蜓等

const skeleton = new THREE.SkeletonHelper(XBot);
skeleton.visible = true;
helperGroup.add(skeleton);

2023-04-07 18.06.53.gif

指针控制器

指针锁定控制器 实现原理是采用js中的requestpointerlockAPI,在实例化PointerLockControls时,接受两个参数,源码中接受camera和HTMLElement(可选),在实验的过程中,发现第二个参数camera继承了Object3D,所以将mesh或者group作为第一个参数传入也没有关系,他们的基类都是Object3D(Camera extends Object3D);

内部实现原理就是锁定指针,获取指针移动位置将指定的向量提供给moveRight(向右)、moveForward(向前)两个方法,从而修改第一个参数camera的矩阵,实现摄像头的旋转和移动,当然,过程中也可以监听控制器的锁定状态,来做一些事情,其API在官网也可以查询到,这里不做赘述;

实现指针控制器

加载到模型后,将模型传入到控制器中,我们最终实现的效果是利用指针控制角色,并实现镜头跟踪,所以只修改模型的矩阵即可

```typescript controls = new PointerLockControls(XBot, document.body);

controls.maxPolarAngle = Math.PI * 0.5
controls.minPolarAngle = Math.PI * 0.5

document.body.addEventListener('click', function () {

    controls.lock();

});

```

controls.lock(); 锁定指针,使指针控制器生效,controls.minPolarAngle = Math.PI * 0.5,设置控制器上仰和下俯的角度限制,这里设置的相当于禁止修改模型的上下转动,只左右转动

计算转动和移动的向量

在render函数中,计算一下每次更新的用时,使用performance.now() 毫秒级别,这样相对更精准,在render方法中调用updateControls方法

```typescript function updateControls() { const time = performance.now(); // 指针控制器锁定时计算方向和速度 if (controls.isLocked === true) {

const delta = (time - prevTime) / 1000;

    // 计算每次更新时的速度
    velocity.x -= velocity.x * 10.0 * delta;
    velocity.z -= velocity.z * 10.0 * delta;

    // 计算z轴方向数值 1向前,0站立,-1向后
    direction.z = Number(moveForward) - Number(moveBackward);

    // 将方向归一化
    direction.normalize(); // this ensures consistent movements in all directions

    // 如果按下向前或者向后,计算移动速度
    if (moveForward || moveBackward) velocity.z -= direction.z * 40.0 * delta;

    // 角色前后移动方向修改,z如果>0向前移动反之向后移动,如果为0站立
    controls.moveForward(velocity.z * delta);

}

prevTime = time;

} ```

定义一个全局变量prevTime(时间)、velocity(速度)、direction(方向),更新时根据moveForwardmoveBackward是否为true判断当前角色是否需要向前或者向后移动,具体过程可以看代码和备注。

键盘监听

监听键盘w键抬起或者按下修改moveForward字段

监听键盘按下

typescript const onKeyDown = function (event) { switch (event.code) { case 'ArrowUp': case 'KeyW': if (!moveForward&&controls.isLocked) { // 按下时移动角色,将角色动画状态改为run fadeToAction('run') } moveForward = true; // 按下的标记 break; } }

监听键盘抬起

typescript const onKeyUp = function (event) { switch (event.code) { case 'ArrowUp': case 'KeyW': if (moveForward&&controls.isLocked) { // 抬起时角色动画状态改为站立 fadeToAction('idle') } moveForward = false; break; } };

镜头跟踪

计算摄像头位置

首先计算出动画模型的高度,使用box3中的getSize,会获取到模型的具体尺寸,将得到一个三维向量,y字段就是模型的高度,将该字段存储起来给镜头使用。

```typescript const XBotSize = new THREE.Vector3(); const box = new THREE.Box3()

box.expandByObject(XBot) box.getSize(XBotSize) ```

之前的代码moveForward已经将角色的位置和方向进行了修改,所以在render函数中实时获取角色的世界坐标,和世界方向,再通过相应的计算,获取到摄像头的位置,大概如下图所示:

1、首先计算出角色的世界方向并归一化normalize获得方向;

2、将归一化的坐标乘以相应的距离 multiplyScalar ;

3、设置摄像头在角色正后方,将距离取反negate;

4、设置摄像头高度 XBotSize.y

image.png

如上,计算出角色后方的坐标,接下来,将之前归一化的向量加上角色的世界坐标,这样摄像头的位置就会根据角色的位置进行移动;

如下是上述逻辑的代码,在render方法中调用

```typescript if ( cone && XBot) { let xbotV3 = new THREE.Vector3();

XBot.getWorldPosition(xbotV3);

const playerDirection = new THREE.Vector3()
XBot.getWorldDirection(playerDirection);
playerDirection.normalize();
playerDirection.multiplyScalar(5)
// 2为高度的增量,可以让摄像头在角色的后上方,以俯视的角度观察角色
camera.position.copy(playerDirection.negate().setY(XBotSize.y + 2).add(xbotV3));
camera.lookAt(xbotV3.clone().setY(XBotSize.y))
camera.updateProjectionMatrix()

updateControls()

} ```

position修改相机位置,lookAt修改摄像头方向,摄像机方向看向角色头部,至此便是镜头跟踪的全部逻辑,欢迎评论区交流。

后续可以做很多事情,比如做一个第三人称RPG游戏,可以通过ray添加射线进行碰撞检测,也可以在ammo物理引擎环境下运行,也可以调整multiplyScalar参数的位置,改为第一人称,做一些第一人称可以做的事情

仓库地址

历史文章

# Javascript基础之写一个好玩的点击效果

# Javascript基础之鼠标拖拉拽

# three.js 打造游戏小场景(拾取武器、领取任务、刷怪)

# threejs 打造 world.ipanda.com 同款3D首页

# three.js——物理引擎

# three.js——镜头跟踪

# threejs 笔记 03 —— 轨道控制器

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐