three.js - カメラ追跡

この記事は主に、高度なまたは複雑なロジックやコードを使用せずに、公式 Web サイトのいくつかのケースをつなぎ合わせたものです。

カメラ トラッキングは、ゲームやパビリオンなどのシーンで非常に一般的です。一人称または三人称の場合があります。今回は三人称カメラ トラッキング方法を導入します。

モデルとアニメーションのソース

ポインタコントローラ

以下、一つずつ紹介していきます

効果は以下の通りです

2023-04-07 15.49.20.gif

WGIF 画像から、マウスを動かしてキャラクターの回転を制御し、キーを押し続けるとキャラクターがrun実行状態に切り替わり、Wキーを放すとキャラクターがidleアイドル状態に切り替わるという効果がわかります。アニメーションは glf モデルによって編集され、後で逆アセンブルされます。

それでは、一歩ずつ進んでいきましょう

初期シーンの作成

環境

基本シーンで使用される API はscene、シーン、PerspectiveCameraパースペクティブ カメラ、DirectionalLight平行ライト、HemisphereLight半球ライト、WebGLRendererレンダラです。ここではコードを繰り返しません。

接地

地面はtextureテクスチャを使用しています。まずPlaneGeometry平面を作成します。マテリアルはMeshLambertMaterialグリッド マテリアルを使用します。主な機能は光を感知し、その後の影の準備をすることです。このコードには影は含まれていません。興味のある学生は独学で学ぶことができます。

テクスチャは公式サイトから提供されておりgrid.pngTextureLoaderそれを読み込んでマテリアルに引き渡してレンダリングしますベースプレートのサイズを1000*1000に設定し、作成した平面のX軸はデフォルトでMath.PI(垂直)とします. ration を使用して、平面方向の X 軸を (水平-0.5*Math.PI) に変更します。コードは以下のように表示されます。

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ローダーメソッドを単純にカプセル化する

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

ロードされたファイル構造にはsceneシーンとアニメーション コレクションが含まれています。デフォルトのアニメーションは 7 つあり、後で3 つのアニメーションanimationsを使用します。idlerunwalk

画像.png

AnimationMixer アニメーターを登録する

遍历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. 正規化された座標に対応する距離を乗算しますmultiplyScalar

3. カメラをキャラクターの真後ろに設定し、距離を逆にしますnegate

4. カメラの高さを設定しますXBotSize.y

画像.png

上記と同様に、キャラクターの背後の座標を計算し、事前に正規化したベクトルをキャラクターのワールド座標に追加します。これにより、カメラの位置がキャラクターの位置に従って移動します。

以下は、render メソッドで呼び出される上記のロジックのコードです。

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 ゲームの作成、衝突検出のためにレイを介してレイを追加すること、弾薬物理エンジンの環境で実行することもでき、パラメータの位置を変更し、一人称に変更し、一人称を実行しますmultiplyScalar

倉庫の住所

歴史的な記事

# 楽しいクリック効果を作成するための Javascript の基本

#JavaScript ベースのマウスのドラッグ アンド ドロップ

# three.js 小さなゲームシーンを作成する (武器を拾う、タスクを受け取る、モンスターをスポーンする)

# threejs world.ipanda.com と同じ 3D ホームページを作成します

# three.js - 物理エンジン

# three.js - カメラ追跡

# threejs Note 03 - トラックコントローラー

おすすめ

転載: juejin.im/post/7220321558102392892