限制FPS/用CameraControls控制相机绕某点观察

Three 限制显示刷新率

import * as THREE from "three";
import Stats from "stats.js";

const stats = new Stats(); // 显示帧数插件
document.body.appendChild(stats.dom)
/* render() */
renderAtFrameCount(renderer,new THREE.Clock(),scene, camera, 50); // 限制最大帧数50

/**
 * @param {Renderer} renderer
 * @param {THREE.Clock} clock
 * @param {THREE.Scene} scene
 * @param {THREE.Camera} camera
 * @param {Number} FPS 限制最大帧数
 */
function renderAtFrameCount(renderer, clock, scene, camera, FPS) {
  let renderInterval = 1000 / FPS
  let timeS = 0

  const render = () => {
    if (!renderer) return;

    let t = clock.getDelta()
    timeS += t * 1000;
    if (timeS > renderInterval) {
      stats.update();
      renderer.render(scene, camera);
      timeS %= renderInterval; // 关键行,timeS收集rAF的时间片空余量用于判断渲染时机,若直接设为0则会丢失一部分时间片,造成实际帧率低于设定值。
    }
    requestAnimationFrame(render);
  }
  render();
}

读 CameraControls 源码

读 CameraControls 库源码重点看两个方面:

  1. 如何实现 camera 绕某点旋转
  2. 如何进行插值

绕点旋转

绕点旋转如果直接对 xyz 进行线性插值会造成路径平直,镜头中的投影结果就是忽大忽小。因此需要使 camera 绕目标旋转,但直接使用矩阵旋转会造成 camera.up 方向乱动,这个问题在自己的原生实现中没有解决。

CameraControls 库的解决方式是使用 球坐标,不改变 camera 到旋转点的欧几里得距离(r),只修改方位角 θ 和 φ 使其转动,最后将旋转分量提取到旋转矩阵中应用到 camera 上即可。

rotateTo( azimuthAngle: number, polarAngle: number, enableTransition: boolean = false ): Promise<void>:source

CameraControls 随时间更新动画函数

update( delta: number ): boolean:source

概括过程:

/**
 * Update camera position and directions.
 * This should be called in your tick loop every time, and returns true if re-rendering is needed.
 * @param delta
 * @returns updated
 * @category Methods
 */
update(delta: number): boolean {

  const deltaTheta = this._sphericalEnd.theta - this._spherical.theta;
  const deltaPhi = this._sphericalEnd.phi - this._spherical.phi;
  const deltaRadius = this._sphericalEnd.radius - this._spherical.radius;
  const deltaTarget = _deltaTarget.subVectors(this._targetEnd, this._target);
  const deltaOffset = _deltaOffset.subVectors(this._focalOffsetEnd, this._focalOffset);
  const deltaZoom = this._zoomEnd - this._zoom;

  // update theta
  ...
  // update phi
  ...
  // update distance
  ...
  // update target position
  ...
  // update focalOffset
  ...
  // update zoom
  ...
  // decompose spherical to the camera position
  this._spherical.makeSafe();
  this._camera.position.setFromSpherical(this._spherical).applyQuaternion(this._yAxisUpSpaceInverse).add(this._target);
  this._camera.lookAt(this._target);
  // set offset after the orbit movement
  ...
}

Ease-In-Out 插值

如何对 camera 进行缓动进出的插值?

update(delta) 函数中用到了缓动函数 smoothDampsmoothDampVec3 分别对数值差值和对三维向量插值。

camera-controls/src/utils/math-utils.ts - smoothDamp

// https://docs.unity3d.com/ScriptReference/Mathf.SmoothDamp.html
// https://github.com/Unity-Technologies/UnityCsReference/blob/a2bdfe9b3c4cd4476f44bf52f848063bfaf7b6b9/Runtime/Export/Math/Mathf.cs#L308
export function smoothDamp(
    current: number,
    target: number,
    currentVelocityRef: Ref,
    smoothTime: number,
    maxSpeed: number = Infinity,
    deltaTime: number,
): number {

    // Based on Game Programming Gems 4 Chapter 1.10
    smoothTime = Math.max( 0.0001, smoothTime );
    const omega = 2 / smoothTime;

    const x = omega * deltaTime;
    const exp = 1 / ( 1 + x + 0.48 * x * x + 0.235 * x * x * x );
    let change = current - target;
    const originalTo = target;

    // Clamp maximum speed
    const maxChange = maxSpeed * smoothTime;
    change = clamp( change, - maxChange, maxChange );
    target = current - change;

    const temp = ( currentVelocityRef.value + omega * change ) * deltaTime;
    currentVelocityRef.value = ( currentVelocityRef.value - omega * temp ) * exp;
    let output = target + ( change + temp ) * exp;

    // Prevent overshooting
    if ( originalTo - current > 0.0 === output > originalTo ) {

        output = originalTo;
        currentVelocityRef.value = ( output - originalTo ) / deltaTime;

    }

    return output;

}

camera-controls/src/utils/math-utils.ts - smoothDampVec3

// https://docs.unity3d.com/ScriptReference/Vector3.SmoothDamp.html
// https://github.com/Unity-Technologies/UnityCsReference/blob/a2bdfe9b3c4cd4476f44bf52f848063bfaf7b6b9/Runtime/Export/Math/Vector3.cs#L97
export function smoothDampVec3(
    current: _THREE.Vector3,
    target: _THREE.Vector3,
    currentVelocityRef: _THREE.Vector3,
    smoothTime: number,
    maxSpeed: number = Infinity,
    deltaTime: number,
    out: _THREE.Vector3
) {

    // Based on Game Programming Gems 4 Chapter 1.10
    smoothTime = Math.max( 0.0001, smoothTime );
    const omega = 2 / smoothTime;

    const x = omega * deltaTime;
    const exp = 1 / ( 1 + x + 0.48 * x * x + 0.235 * x * x * x );

    let targetX = target.x;
    let targetY = target.y;
    let targetZ = target.z;

    let changeX = current.x - targetX;
    let changeY = current.y - targetY;
    let changeZ = current.z - targetZ;

    const originalToX = targetX;
    const originalToY = targetY;
    const originalToZ = targetZ;

    // Clamp maximum speed
    const maxChange = maxSpeed * smoothTime;

    const maxChangeSq = maxChange * maxChange;
    const magnitudeSq = changeX * changeX + changeY * changeY + changeZ * changeZ;

    if ( magnitudeSq > maxChangeSq ) {

        const magnitude = Math.sqrt( magnitudeSq );
        changeX = changeX / magnitude * maxChange;
        changeY = changeY / magnitude * maxChange;
        changeZ = changeZ / magnitude * maxChange;

    }

    targetX = current.x - changeX;
    targetY = current.y - changeY;
    targetZ = current.z - changeZ;

    const tempX = ( currentVelocityRef.x + omega * changeX ) * deltaTime;
    const tempY = ( currentVelocityRef.y + omega * changeY ) * deltaTime;
    const tempZ = ( currentVelocityRef.z + omega * changeZ ) * deltaTime;

    currentVelocityRef.x = ( currentVelocityRef.x - omega * tempX ) * exp;
    currentVelocityRef.y = ( currentVelocityRef.y - omega * tempY ) * exp;
    currentVelocityRef.z = ( currentVelocityRef.z - omega * tempZ ) * exp;

    out.x = targetX + ( changeX + tempX ) * exp;
    out.y = targetY + ( changeY + tempY ) * exp;
    out.z = targetZ + ( changeZ + tempZ ) * exp;

    // Prevent overshooting
    const origMinusCurrentX = originalToX - current.x;
    const origMinusCurrentY = originalToY - current.y;
    const origMinusCurrentZ = originalToZ - current.z;
    const outMinusOrigX = out.x - originalToX;
    const outMinusOrigY = out.y - originalToY;
    const outMinusOrigZ = out.z - originalToZ;

    if ( origMinusCurrentX * outMinusOrigX + origMinusCurrentY * outMinusOrigY + origMinusCurrentZ * outMinusOrigZ > 0 ) {

        out.x = originalToX;
        out.y = originalToY;
        out.z = originalToZ;

        currentVelocityRef.x = ( out.x - originalToX ) / deltaTime;
        currentVelocityRef.y = ( out.y - originalToY ) / deltaTime;
        currentVelocityRef.z = ( out.z - originalToZ ) / deltaTime;

    }

    return out;

}

注:源码中标注了 smoothDamp 的来源:Game Programming Gems 4 Chapter 1.10

对这本书的引用原文在这里:Game Programming Gems 4 : Andrew Kirmse : Free Download, Borrow, and Streaming : Internet Archive

Game Programming Gems 4 Edited by Andrew Kirmse Charles River Media, 2004 ISBN 1-58450-205-9