6
头图

Hello everyone, I'm Qiufeng. In the last article on , I talked about the goals of the Three.js series and the Pokémon game, so today I'm going to talk about the perspective-following problem in the game through Three.js. I believe that my readers have played some games more or less, such as King of Glory, PUBG, Pokémon, Zelda, Genshin Impact and other games. So do you know what perspective games they are from? Do you know the difference between first-person and third-person perspectives? How can we achieve this effect through code? If you are curious about the above questions and cannot fully answer them. So please follow me down.

Perspective explanation

First, let's take a look at the concepts of first-person perspective and third-person perspective. In fact, the first person and the third person are very familiar to us. The first person tells something in one's own voice. For example, autobiographies are written in this form, and the third person is a bystander. For example, many novels, It's all based on him (xxx), and the audience looks at the whole story from the perspective of God. The corresponding first-person perspective and third-person perspective are also the same concept, but only visually. So what are the differences between them? The advantage of 's first-person perspective is that it can bring maximum immersion to the player. To observe the scene and the picture from the first-person perspective "I" can make the player feel the details in more detail. The most common one is similar to PUBG Mobile. , Need for Speed and the like.

The first-person perspective also has its limitations. The player's field of vision is limited and cannot see the wider field of view. Another is that the first-person perspective will bring players a "3D vertigo". When the reaction speed is not as fast as the camera speed, it will cause dizziness. What about third-person perspective? His advantage is freedom, wide field of vision, character movement and perspective are separated, one is used to control the character's forward direction, and the other is used to control the direction of vision.

Its disadvantage is that it cannot focus on local parts well, and it is easy to miss details. But in general, most games currently provide switching between two perspectives to meet different situations. For example, in Absolute Survival, I usually use the third-person perspective to follow the movement when walking, and generally use the first-person perspective when shooting. Well, so far, we have already known the concepts and differences between the first-person perspective and the third-person perspective. So let's take the third-person perspective as an example to analyze how we can achieve such an effect? (After the third person is written, it can be changed to the first person with a little modification, so take the more complicated third person as an example) How many steps does it take to put the elephant in the refrigerator? Three steps! Open the fridge, put the elephant in the fridge, close the fridge. Obviously putting an elephant in the freezer is difficult, but from a macro perspective, it's three steps. Therefore, we also divide the function of realizing the third-person perspective into three steps:

step split

The following steps will not contain any code, please use it with confidence: 1. How the characters move We all know that in the physical real world, we rely on our legs to move, and we move when we step. So what does this process look like from a broader perspective? In fact, from a farther point of view from outside the earth, our movement is more like a translational change. Likewise, when we represent motion in computers, we use translational changes. The details of translation changes are familiar to everyone before. If you are not familiar with them now, it doesn't matter. Look at the coordinate axes below. (The side length of the small square is 1)

The movement of the small square from the position A1 to the position A2 is a translation change, if expressed in mathematical expressions, it is

What does the above mean? That is, we add 2 to the x-values of all the small points in the small square, and the y-values remain the same. Let's take some random values to verify. For example, the small square in the position of A1, the lower left corner is (0, 0), through the above changes, it becomes (2, 0), let's look at the new position of the small square in A2 is (2, 0); then use the upper right corner Substitute (1,1) of , and the discovery becomes (3,1), which is the same as the position we actually moved to. So there is nothing wrong with the above formula. But later on, everyone felt that the above formula was a little bit less general. As for why it is not general enough, it will be explained in detail in the following series of articles, because other changes are also involved, such as rotation and scaling, they can all be described by a matrix, so if the translation can also be expressed in the form of a matrix , then the whole problem becomes simple, that is to say: motion change = matrix change Let's see what it looks like to turn the initial formula into a matrix:

You can briefly explain how the matrix on the right comes from

This part in the upper left corner is called the unit matrix, and the following 2 0 is the translation change we need. As for why it changed from 2 dimensions to 3 dimensions, it is because the concept of a homogeneous matrix is introduced. The same principle, analogy to 3-dimensional, we need to use a 4-dimensional matrix. So, through a series of examples, the final conclusion we want to get is that all movements are matrix changes .

2. The camera is facing the character. We all know that in the real world, the field of vision of our eyes is limited, and it is the same in the computer. Assuming that our field of view is a 3 * 3 square in the computer, we still use the previous coordinate axis as an example, the yellow area is the visible area of our field of view:

Now we make the small block move 3 units to the right and 1 unit on the net.

At this time, we will find that this small block can no longer be seen in our field of vision. Imagine we are playing a shooting game and the enemy is moving in front of us, what are we going to do to find it? Yes, we rotate our heads so that the enemy is exposed to our view. like this:

This locks the enemy on, allowing the character to always appear in our field of vision and remain relatively still. 3. The lens is at the same distance as the character. It is not enough to have the lens facing the character. We also have to make our lens and the character at the same distance. Why do you say this? First of all, it is still an example of our coordinate axis, but this time we will expand a z-axis: then we will look at the normal plane screenshot

screenshot:

Now we move our small block 1 unit towards -Z:

screenshot:

At this time, we found that the small square became smaller, and as the small square moved more in the -z direction, the small block we saw became smaller and smaller. At this time, we obviously did not change our perspective, but we still couldn't track the small pieces well. So we need to move the position of our perspective, what do we do when we can't see a distant road sign? That's right, get closer!

screenshot:

Perfect! Now, through the explanation of three directions, we will realize the function of a third-person perspective theoretically!

engage in code

Next, we just need to implement the code according to our above theory. The code cannot be the way we implement it in another language. Knowing the principle is very simple. 1. Initialize the canvas scene

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

The scene, camera, and renderer are relatively fixed things. This section will not be mainly explained. It can be understood as some necessary statements when our project is initialized. At this time, when we opened the page, it was dark. For the sake of beauty, I added a floor to the whole scene.

// 设置地板  
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);  
  

To look good, I added six different colors to the small pieces.

Although it still looks a bit rudimentary, as the saying goes, high-end ingredients often only require the most simple cooking methods. The small pieces are small, but they have all the internal organs. Now that we have rendered the small block, all we have to do is bind the shortcut keys.

Corresponding code:

// 控制代码  
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();  

Explain here translateZ, translateX, these two functions are literally, move to the z-axis and x-axis, if you want to move forward, move to the -z axis, if it is to the left, move to the -x axis. What does clock.getDelta () mean? Simply put, the function of the .getDelta () method is to obtain the time interval between the two executions of the method before and after. For example, we want to move forward 5 units in 1 second, but direct movement is definitely more rigid, so we want to add animation. We know that in order to achieve smooth animation, it is generally implemented through the browser's API requestAnimationFrame , and the browser will control the rendering frequency. Generally, under ideal performance conditions, s rendered about 60 times per second. In actual projects, if the scene needs to be rendered It is more complicated, generally lower than 60, that is, the time interval between two frames of rendering is greater than 16.67ms. So in order to move these 5 units, we split the distance each frame should move into these 60 renders. Finally, let's talk about rotateOnAxios , which is mainly used to control the rotation of the small box.

.rotateOnWorldAxis ( axis : Vector3, angle : Float ) : this axis -- a normalized vector in world space.
angle – the angle, in radians.

3. Camera and character synchronization Looking back at the theoretical part, our last step is to keep the camera (human eye) and the object relatively still, that is, the distance remains unchanged.

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

A core point here is relativeCameraOffset.applyMatrix4( box.matrixWorld ); In fact, we said this in the theoretical part, because the underlying principle of our object movement is to make matrix changes, so if we want to keep the distance between the camera (human eye) and the object unchanged, we only The camera (human eye) and the object need to do the same changes. In Three.js, all the changes of the object itself are recorded in .matrix . As long as the external scene does not change, then .matrixWorld is equal to .matrix . And applyMatrix4 means multiply.

Effect demonstration

This way I ended up implementing the whole thing! See you next time!

Source address: https://github.com/hua1995116/Fly-Three.js

Epilogue

+ Like + Favorite + Comment + Forward , originality is not easy, encourage the author to create better articles

Pay attention to the notes of the public Qiufeng, a front-end public account focusing on front-end interviews, engineering, and open source


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

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