6
头图

大家好,我是秋风,在上一篇 中说到了 Three.js 系列的目标以及宝可梦游戏,那么今天就来通过 Three.js 来谈谈关于游戏中的视角跟随问题。相信我的读者都或多或少玩一些游戏,例如王者荣耀、绝地求生、宝可梦、塞尔达、原神之类的游戏。那么你知道他们分别是什么视角的游戏么?你知道第一人称视角和第三人称视角的差异么?通过代码我们怎么能实现这样的效果呢?如果你对以上问题好奇,并且不能完全回答。那么请跟随着我一起往下看吧。

视角讲解

首先我们先来看看第一人称视角、第三人称视角的概念。其实对于我们而言 第一人称 和 第三人称,是非常熟悉的,第一人称就是以自己的口吻讲述一件事,例如自传都是以这种形式抒写,第三人称则是以旁观者,例如很多小说,都是以他(xxx)来展开式将的,观众则是以上帝视角看着这整个故事。对应的第一人称视角、第三人称视角也是相同的概念,只不过是视觉上面。那么他们各自有上面区别呢?第一人称视角的有点是可以给玩家带来最大限度的沉浸感,从第一人称视角“我”去观察场景和画面,可以让玩家更加细致地感受到其中的细节,最常见的就是类似绝地求生、极品飞车之类的。

而第一人称视角也有他的局限性。玩家的视野受限,无法看到更广阔的的视野。另一个就是第一人称视角会给玩家带来“3D眩晕感”。当反应速度更不上镜头速度的时候会造成眩晕感。那么第三人称视角呢?他的优势就是自由,视野开阔,人物移动和视角是分开的,一个用来操作人物前进方向,另一个则是用来操控视野方向。

它的劣势就是无法很好的聚焦局部,容易错过细节。但是总的来说,目前大多数游戏都提供了两种视角的切换来满足不同的情形。例如绝对求生中平时走路用第三人称视角跟随移动,开枪的时候一般用第一人称视角。好了,到目前为主我们已经知道了第一人称视角、第三人称视角各自概念、区别。那么我们接下来以第三人称视角为例,展开分析我们该如何实现这样的一个效果呢?(第三人称的编写好后,稍加修改就可以变成第一人称,因此以更加复杂的第三人称为例)把大象放入冰箱需要几步?三步!打开冰箱,把大象放进冰箱,关上冰箱。显然如果真的要把大象放进冰箱是很难的事情,但是从宏观角度来看,就是三个步骤。因此我们也将实现第三人称视角这个功能分成三步:

步骤拆分

以下的步骤拆分不会包含任何代码,请放心使用:1.人物如何运动我们都知道在物理真实的世界中,我们运动起来是靠我们双腿,迈开就动起来了。那这个过程从更宏观的角度来看是怎么样的呢?其实如果从地球外,从一个更远的角度来看,我们做运动更像是一个个平移变化。相同地,我们在计算机中来表示运动也就是运用了平移变化。平移变化详细大家以前都比较熟悉,如果现在不熟悉了呢,也没有什么关系,先看下面的坐标轴。(小方块的边长是1)

小方块从A1位置移动到位置A2就是平移变化,如果用数学表达式来表示的话就是

上面是什么意思呢?就是说我们让小方块中所有的小点的 x 值都加2,而 y 的值不变。我们随意取一些值来验证一下。例如A1位置小方块,左下角是 (0,0), 通过以上变化,就变成了 (2, 0),我们来A2中看小方块新的位置就是 (2, 0);再用右上角的 (1,1) 代入,发现就变成了(3,1),和我们真实移动到的位置也是一样的。所以上面的式子没有什么问题。但是后来呢,大家觉得像上面那样的式子用来表示稍微有点不够通用。至于这里为什么说不够通用,在后面的系列文章中会详细讲解,因为还涉及到了其他变化,例如旋转、缩放,他们都可以用一个矩阵来进行描述,因此如果平移也能够用矩阵的方式来表达,那么整个问题就变得简单了,也就是说:运动变化 = 矩阵变化我们来看看把最开始的式子变成矩阵是什么样子的:

可以简单讲解一下右边这个矩阵是怎么来的

左上角的这个部分称为单位矩阵,后面的 2 0 则就是我们需要的平移变化,至于为什么从2维变成了3维,则是因为引入了一个齐次矩阵的概念。同样的原理,类比到 3维,我们就需要用到4维矩阵。所以说,我们通过一系列的例子,最终想要得到的一个结论就是,所有的运动都是矩阵变化

2.镜头朝向人物我们都知道,在现实世界中我们眼睛看出去的视野是有限的,在电脑中也是一样的。假设在电脑中我们的视野是  3 * 3 的方格,我们还是以之前坐标轴举例子,黄色区域是我们的视野可见区域:

现在我们让小块往右移动3个单位,再网上移动1个单位。

这个时候我们会发现,我们的视野内已经看不到这个小块了。试想一下,我们正在玩一个射击游戏,敌人在眼前移动,我们为了找到它会在怎么办?没错,我们会旋转我们的脑袋,从而使得敌人暴露在我们的视野内。就像这样:

这下就把敌人锁定住了,能够始终让人物出现在我们的视野内并且保持相对静止。3.镜头与人物同距光有镜头朝向人物还不够,我们还得让我们的镜头和人物同距。为什么这么说呢,首先还是我们坐标轴的例子,但是这次我们将扩充一个z轴:然后我们看看正常下的平面截图

截图:

现在我们将我们的小块往-Z 移动1个单位:

截图:

这个时候我们发现这个小方块变小了,并且随着小方块往 -z方向移动的越多,我们看到的小块会越来越小。这个时候我们明明没有改变我们的视角,但是还是无法很好的跟踪小块。因此我们需要移动为我们视角的位置,当我们看不清一个远处的路标的时候,我们会怎么办?没错,凑近点!

截图:

完美!现在我们通过三个方向的讲解,将如果实现一个第三人称视角的功能从理论上面实现了!

搞代码

接下来我们只需要按照我们的以上的理论,来实现代码就好了,代码无法就是我们用另一种语言的实现方式,知道了原理都是非常简单的。1.初始化画布场景

<canvas class="webgl"></canvas>  
...  
<script>  
// 创建场景  
const scene = new THREE.Scene()  
// 加入相机  
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height);  
camera.position.y = 6;  
camera.position.z = 18;  
const controls = new OrbitControls(camera, canvas)  
controls.enableDamping = true; // 设置阻尼,需要在 update 调用  
scene.add(camera);  
// 渲染  
const renderer = new THREE.WebGLRenderer({  
    canvas  
})  
renderer.setSize(sizes.width, sizes.height)  
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))  
renderer.render(scene, camera);  
</script>  

场景、相机、渲染器是一些比较固定的东西,这一节不主要进行讲解,可以理解为我们项目初始化的时候一些必备的语句。这个时候我们打开页面,是黑乎乎的一片,为了美观,我给整个场景加上一个地板。

// 设置地板  
const geometry = new THREE.PlaneGeometry(1000, 1000, 1, 1);  
// 地板贴图  
const floorTexture = new THREE.ImageUtils.loadTexture( '12.jpeg' );  
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;   
floorTexture.repeat.set( 10, 10 );  
// 地板材质  
const floorMaterial = new THREE.MeshBasicMaterial({   
    map: floorTexture,   
    side: THREE.DoubleSide   
});  
  
const floor = new THREE.Mesh(geometry, floorMaterial);  
// 设置地板位置  
floor.position.y = -1.5;  
floor.rotation.x = - Math.PI / 2;  
  
scene.add(floor);  
`

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa8cea3705594d10a7e01e7225283a78~tplv-k3u1fbpfcp-zoom-1.image)

这个时候画面还不错\~2.人物运动根据理论,我们需要加入一个人物,这里为了方便,也还是加入一个小方块为主:
// 小滑块  
const boxgeometry = new THREE.BoxGeometry(1, 1, 1);  
const boxMaterials = [];  
for (let i = 0; i < 6; i++) {  
    const boxMaterial = new THREE.MeshBasicMaterial({  
        color: Math.random() * 0xffffff,  
    });  
    boxMaterials.push(boxMaterial);  
}  
// 小块  
const box = new THREE.Mesh(boxgeometry, boxMaterials);  
box.position.y = 1;  
box.position.z = 8;  
  
scene.add(box);  
  

为了好看,我给小块加了六面不同的颜色。

虽然看起来还是有点简陋,但是俗话说高端的食材往往只需要最朴素的烹饪方式。小块虽小,但是五脏俱全。现在我们渲染出了小块后,要做的事情就是绑定快捷键。

对应的代码:

// 控制代码  
const keyboard = new THREEx.KeyboardState();  
const clock = new THREE.Clock();  
const tick = () => {  
    const delta = clock.getDelta();  
    const moveDistance = 5 * delta;  
    const rotateAngle = Math.PI / 2 * delta;  
      
    if (keyboard.pressed("down"))  
        box.translateZ(moveDistance);  
    if (keyboard.pressed("up"))  
        box.translateZ(-moveDistance);  
    if (keyboard.pressed("left"))  
        box.translateX(-moveDistance);  
    if (keyboard.pressed("right"))  
        box.translateX(moveDistance);  
  
    if (keyboard.pressed("w"))  
        box.rotateOnAxis( new THREE.Vector3(1,0,0), rotateAngle);  
    if (keyboard.pressed("s"))  
        box.rotateOnAxis( new THREE.Vector3(1,0,0), -rotateAngle);  
    if (keyboard.pressed("a"))  
        box.rotateOnAxis( new THREE.Vector3(0,1,0), rotateAngle);  
    if (keyboard.pressed("d"))  
        box.rotateOnAxis( new THREE.Vector3(0,1,0), -rotateAngle);  
          
    renderer.render(scene, camera)  
    window.requestAnimationFrame(tick)  
}  
tick();  

这里解释一下 translateZ、translateX,这俩函数就是字面意思,往 z 轴 和 x 轴移动,如果想要往前,就往 -z 轴移动,如果是往 左就是往 -x 轴移动。clock.getDelta () 是什么意思呢?简单说.getDelta ()方法的功能就是获得前后两次执行该方法的时间间隔。例如我们想要在1秒内往前移动5个单位,但是直接移动肯定比较生硬,因此我们想加入动画。我们知道为了实现流畅的动画,一般通过浏览器的APIrequestAnimationFrame实现,浏览器会控制渲染频率,一般性能理想的情况下,每秒s渲染60次左右,在实际的项目中,如果需要渲染的场景比较复杂,一般都会低于60,也就是渲染的两帧时间间隔大于16.67ms。因此为了移动这5个单位,我们将每一帧该移动的距离,拆分到了这 60次渲染中。最后来说说 rotateOnAxios,这个主要就是用来控制 小盒子的旋转。

.rotateOnWorldAxis ( axis : Vector3, angle : Float ) : this axis -- 一个在世界空间中的标准化向量。
angle -- 角度,以弧度来表示。

3.相机与人物同步回顾理论部分,我们最后一个步骤就是想要让相机(人眼)和物体保持相对静止的,也就是距离不变。

const tick = () => {  
  ...  
  const relativeCameraOffset = new THREE.Vector3(0, 5, 10);  
    
  const cameraOffset = relativeCameraOffset.applyMatrix4( box.matrixWorld );  
    
  camera.position.x = cameraOffset.x;  
  camera.position.y = cameraOffset.y;  
  camera.position.z = cameraOffset.z;  
  // 始终让相机看向物体  
  controls.target = box.position;  
  ...  
}  

这里有个比较核心的点就是 relativeCameraOffset.applyMatrix4( box.matrixWorld ); 其实这个我们在理论部分说过了,因为我们的物体移动的底层原理就是做矩阵变化,那么想要让相机(人眼)和物体的距离不变,我们只需要让相机(人眼)和物体做相同的变化。而在 Three.js 中物体所有的自身变化都记录在 .matrix 里面,只要外部的场景不发生变化,那么.matrixWorld 就等于  .matrix 。而applyMatrix4 的意思就是相乘的意思。

效果演示

这样我就最终实现了整个功能!我们下期见!

源码地址:https://github.com/hua1995116...

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号秋风的笔记,一个专注于前端面试、工程化、开源的前端公众号


程序员秋风
3k 声望3.9k 粉丝

JavaScript开发爱好者,全栈工程师。