three.js - camera tracking

This article mainly stitches together several cases of the official website, without advanced or complicated logic and code;

Camera tracking is very common in scenes such as games or pavilions. It can be first-person or third-person. This time we bring a third-person camera tracking method

Model and Animation Sources

pointer controller

The following will introduce one by one

The effect is as follows

2023-04-07 15.49.20.gif

From the gif picture, we can know that the effect is that the mouse moves to control the rotation of the character, press and hold the Wkey, the character switches runto the running state, release Wthe key to switch the character to idlethe idle state, the animation is edited by the glf model, and will be disassembled later.

So let's go step by step

Create initial scene

environment

The APIs used in the basic scene are scenescene, PerspectiveCameraperspective camera, DirectionalLightparallel light, HemisphereLighthemispherical light, and WebGLRendererrenderer. The code will not be repeated here.

ground

The ground uses texturetextures. First, create a PlaneGeometryplane. The material uses MeshLambertMateriala grid material. Its main function is to sense light and prepare for subsequent shadowing. This code does not include shadows. Interested students can learn by themselves;

The texture is provided by the official website grid.png, and then used TextureLoaderto load and handed over to the material for rendering. Set the base plate size to 1000*1000, and the X axis of the created plane by default is Math.PI (vertical). Use ration to modify the X axis of the plane direction to (horizontal -0.5*Math.PI) ,code show as below:

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

Model

load model

Xbot.glbSelect the model file provided by the official website

Simply encapsulate a loadGltfloader method

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

The loaded file structure contains scenescenes and animationsanimation collections. There are 7 default animations, and we will use idle, run, walkthree animations later,

image.png

Register the AnimationMixer animator

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

收集动画

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运行过程中用来更新动画的,

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

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

playerActiveAction = actions['idle'];

playerActiveAction.play();

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

/**
 * 
 * @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在官网也可以查询到,这里不做赘述;

实现指针控制器

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

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

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

监听键盘按下

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

监听键盘抬起

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

镜头跟踪

计算摄像头位置

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

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

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

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

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

2. Multiply the normalized coordinates by the corresponding distance multiplyScalar;

3. Set the camera directly behind the character, and reverse the distance negate;

4. Set the camera height XBotSize.y;

image.png

As above, calculate the coordinates behind the character, and then add the previously normalized vector to the world coordinates of the character, so that the position of the camera will move according to the position of the character;

The following is the code for the above logic, which is called in the render method

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

}

positionModify the camera position, lookAtmodify the camera direction, and the camera direction looks at the character's head. This is the whole logic of lens tracking. Welcome to the comment area to communicate.

You can do many things in the follow-up, such as making a third-person RPG game, you can add rays through ray for collision detection, you can also run it in the environment of the ammo physics engine, you can also adjust the position of the parameters, change it to the first person, and do some first person multiplyScalar. Things to do

Warehouse Address

historical article

# Javascript basics to write a fun click effect

#Javascript based mouse drag and drop

# three.js Create a small game scene (pick up weapons, receive tasks, spawn monsters)

# threejs Create the same 3D homepage of world.ipanda.com

# three.js - physics engine

# three.js - camera tracking

# threejs Note 03 - Track Controller

Guess you like

Origin juejin.im/post/7220321558102392892