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 库源码重点看两个方面:
- 如何实现 camera 绕某点旋转
- 如何进行插值
绕点旋转
绕点旋转如果直接对 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)
函数中用到了缓动函数 smoothDamp
和 smoothDampVec3
分别对数值差值和对三维向量插值。
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