首页
/ three.js 中的 Clock 时间类:getDelta 与 getElapsedTime 的实现原理、用法及 Timer 迁移指南

three.js 中的 Clock 时间类:getDelta 与 getElapsedTime 的实现原理、用法及 Timer 迁移指南

2026-09-06 14:15:08作者:段琳惟

本文以 three.js 官方 API 文档中的 Clock 参考文档 为主体,结合 src/core/Clock.js 的源码实现与 Clock 单元测试,完整讲解这个用于"记录时间"的核心类的构造参数、五个公开属性、四个方法的行为细节与边界情况,并说明为什么自 r183 起该模块已被标记弃用、以及如何平滑迁移到 Timer。读完本篇,你可以准确掌握在渲染循环中计算帧间隔(delta time)与累计时长(elapsed time)的正确方式,理解 autoStart/stop() 引发的常见"时钟不再走动"陷阱,并知道新代码应使用哪个替代类。

Clock 是做什么的:帧循环中的时间基准

在 3D 场景中,几乎所有"运动"——动画混合器推进、物体位移、相机控制器的阻尼——都需要一个时间量。three.js 的渲染惯例是 requestAnimationFrame 驱动的主循环:

renderer.setAnimationLoop( () => {
    const delta = clock.getDelta();      // 本帧与上一帧的间隔(秒)
    mixer.update( delta );               // 动画按帧间隔推进
    // ...
} );

Clock 的职责就是为这个循环提供时间基准:以 performance.now() 为时钟源(毫秒精度),对外输出以秒为单位的"帧间隔"(getDelta())和"累计运行时长"(getElapsedTime())。官方文档将其定义为一句话——"Class for keeping track of time"(用于跟踪时间的类),它是 Three.Core.js 中导出的核心模块之一(第 109 行 export { Clock } from './core/Clock.js')。

需要注意一个重要的版本事实:自 r183 起 Clock 已被标记为弃用(deprecated)Clock 类定义 上带有 @deprecated since r183 标注,且构造函数会直接调用 warn() 打印提示(Clock.js 第 61 行):

warn( 'Clock: This module has been deprecated. Please use THREE.Timer instead.' ); // @deprecated, r183

因此本篇的价值在于:一是完整读懂存量代码与文档中大量存在的 Clock 用法;二是理解其设计缺陷,为迁移到 Timer(官方文档 Timer 称其为 "an alternative to Clock with a different API design and behavior")提供依据。

构造函数与 autoStart 参数

官方文档给出的构造签名为:

new Clock( autoStart : boolean )
参数 说明 默认值
autoStart 是否允许时钟在首次调用 getDelta() 时自动启动 true

对照 构造函数源码,参数处理非常简单:

constructor( autoStart = true ) {
    this.autoStart = autoStart;
    this.startTime = 0;
    this.oldTime = 0;
    this.elapsedTime = 0;
    this.running = false;   // 注意:初始并不运行
    warn( 'Clock: This module has been deprecated. ...' );
}

源码中有两个值得注意的细节,文档只字未提:

  1. 初始状态 runningfalse:文档表格中 .running 的默认值写作 true,而实际代码初始化时是 false。之所以"默认自动启动"成立,是因为 autoStarttrue 时,首次 getDelta() 会触发隐式 start()(见下文)。也就是说"默认 true"描述的是等效行为,而非初始字段值。
  2. 构造即告警:实例化 Clock 会立即触发一条弃用警告,这在示例项目批量迁移时会很醒目。

单元测试 Clock.tests.js 验证了这两种实例化方式均合法:new Clock()new Clock( false ) 都可以成功创建对象。

五个公开属性逐项解析

官方文档列出了 5 个属性,以下结合 源码字段注释 逐一说明其语义与默认值:

.autoStart : boolean(默认 true

若为 true,当 getDelta() 被首次调用而时钟尚未运行时,类会自动替你调用 start()。置为 false 则完全交由你手动控制 start()/stop() 的时机。

.elapsedTime : number(默认 0

时钟累计运行总时长(秒)。它不是"从某一起点到现在"的墙钟时间,而是历次 getDelta() 差值的累加,因此时钟 stop() 期间不会增长。

.oldTime : number(默认 0

记录最近一次调用 start()getElapsedTime()getDelta() 时的时间戳(毫秒)。它是计算帧间隔的"上一次读数"。

.running : boolean

时钟当前是否处于运行状态。stop() 后置 falsestart() 后置 true

.startTime : number(默认 0

最近一次 start() 被调用时的时间戳(毫秒)。注意它只是"起点记录",并不参与 elapsedTime 的计算——elapsedTime 是靠差值累加维护的。

方法行为深挖:源码级调用链

.getDelta() : number

返回距上一次查询以来的时间差(秒)。这是渲染循环中最常用的方法。完整实现见 Clock.js 第 107–131 行

getDelta() {
    let diff = 0;

    if ( this.autoStart && ! this.running ) {
        this.start();
        return 0;          // 自动启动的这一帧,间隔返回 0
    }

    if ( this.running ) {
        const newTime = performance.now();
        diff = ( newTime - this.oldTime ) / 1000;   // 毫秒 → 秒
        this.oldTime = newTime;
        this.elapsedTime += diff;
    }

    return diff;           // 未运行时恒为 0
}

从源码结构看,这里有三个关键行为:

  • 自动启动帧返回 0autoStart 生效的那一次调用只做 start(),返回间隔为 0,避免第一帧出现一个巨大的或无意义的 delta;
  • 毫秒转秒performance.now() 返回毫秒,/ 1000 转为秒。这与文档"Returns the delta time in seconds"严格对应;
  • 停止时恒返回 0runningfalse 且无法自动启动时,diff 保持初始值 0,同时 oldTime/elapsedTime 都不再变化。

.getElapsedTime() : number

返回累计运行时长(秒)。实现只有一行核心逻辑(Clock.js 第 95–100 行):

getElapsedTime() {
    this.getDelta();        // 先推进一次内部状态
    return this.elapsedTime;
}

这里隐含一个容易被忽略的事实:每次调用 getElapsedTime() 都会先执行一次 getDelta(),即它会刷新 oldTime 并累加 elapsedTime。因此连续两次 getElapsedTime() 之间只要隔了一段时间,返回值必然不同——这与 Timer.getElapsed()"同一帧内多次查询值不变"的设计形成鲜明对比,是 Clock 被评价为存在"概念性缺陷"(Timer 类注释原文)的原因之一。

.start()

重新开始计时:记录新的 startTime,把 oldTime 同步为该时刻,elapsedTime 归零,runningtrueClock.js 第 69–77 行)。文档说明:当 autoStarttrue 时,该方法会在首次 getDelta() 时被类自动调用。

.stop()

停止时钟。实现(Clock.js 第 82–88 行):

stop() {
    this.getElapsedTime();   // 先结算到当前时刻
    this.running = false;
    this.autoStart = false;  // 关键:永久关闭自动启动
}

这里藏着 Clock 最著名的坑:stop() 会把 autoStart 永久改写为 false。之后即使你再次调用 getDelta(),时钟也不会自己醒来,除非你显式调用 start()。许多"暂停恢复后时间不动了"的 bug 都源于此——想恢复必须手动 clock.start(),而不是期待自动重启。

用单元测试验证行为

Clock 单元测试 通过 mock 掉 performance 对象(构造一个可控的假时钟 performance.next( delta ))来精确断言行为,值得作为"Clock 到底该怎么算时间"的标准答案:

const clock = new Clock( false );
clock.start();

performance.next( 123 );
assert.numEqual( clock.getElapsedTime(), 0.123, 'okay' );       // 123ms → 0.123s

performance.next( 100 );
assert.numEqual( clock.getElapsedTime(), 0.223, 'okay' );       // 累计 0.223s

clock.stop();
performance.next( 1000 );
assert.numEqual( clock.getElapsedTime(), 0.223, "don't update time if the clock was stopped" );

这段测试精确印证了三点:毫秒到秒的换算;elapsedTime累加量(0.123 + 0.1 = 0.223);stop() 之后即便真实时间流逝 1000ms,getElapsedTime() 也停留在 0.223——时钟停止期间不计入累计时长。

典型使用模式:在渲染循环中消费时间

在 r183 之前的代码(以及大量存量示例)中,Clock 的标准用法是:

import * as THREE from 'three';

const clock = new THREE.Clock();

renderer.setAnimationLoop( () => {
    const delta = clock.getDelta();          // 秒
    const elapsed = clock.getElapsedTime();  // 秒(累计)

    mixer.update( delta );
    // 例:让一个物体以恒定速度旋转,速度不依赖帧率
    mesh.rotation.y = elapsed * Math.PI * 2;
} );

两个实践要点(均由上述源码行为直接推出):

  • getDelta() 每帧只应调用一次:它每调用一次都会推进 oldTime,同一帧内多处重复调用会互相"偷走"时间;
  • 暂停后恢复要显式 start()stop() 已关闭 autoStart,恢复动画前先 clock.start()(注意 start() 会把 elapsedTime 归零,如果依赖累计时间需自行补偿)。

需要说明的是,当前仓库的 examples/ 目录中的示例已基本完成迁移。例如 webgl_loader_gltf.html 中使用的就是新类:

const timer = new THREE.Timer();
// ...
timer.update();                    // 每帧先更新内部状态
mixer.update( timer.getDelta() );

Clock 的设计缺陷与 Timer 迁移路径

Timer 的源码注释 开宗明义地指出了迁移动机:Clock 在长期演进中暴露出"概念性缺陷"(conceptual flaws),Timer 用不同的 API 设计规避了这些问题。对照 Timer 实现,核心改进有两点:

对比维度 Clock(r183 起弃用) Timer
时间状态更新方式 查询即更新:getDelta()/getElapsedTime() 每次调用都刷新内部状态,同一帧多次查询得到不同值 显式 update( timestamp ) 每帧推进一次,之后 getDelta()/getElapsed() 可多次调用且值稳定
页面不可见时的行为 切换标签页再回来会算出一个巨大 delta(可能让物理/动画"跳变") 可调用 timer.connect( document ) 接入 Page Visibility API:隐藏时 delta 记 0,恢复可见时 reset() 重置基准,避免时间突刺
附加能力 start()/stop()/autoStart 状态机 setTimescale( timescale ) 直接做时间缩放(慢放/加速)、reset()dispose()

Timer.update() 的实现(Timer.js 第 156–174 行)还能接收 requestAnimationFrame 回调传入的 timestamp 参数,省得自己调 performance.now()

renderer.setAnimationLoop( ( timestamp ) => {
    timer.update( timestamp );
    mixer.update( timer.getDelta() );
} );

迁移建议:

  1. 新代码一律用 Timernew THREE.Timer() + 每帧 timer.update() + timer.getDelta(),需要防切页时间突刺时加 timer.connect( document )
  2. 存量 Clock 代码不必立即重写:它在 r183 及以后仍会正常工作(只是构造时打印警告),可结合项目维护节奏渐进替换;
  3. 注意语义差异:Clock.start() 会清零 elapsedTime,而 Timer.reset() 只重置当前步的 delta 基准;Clock 的暂停/恢复对应关系是 Timerupdate()/不调用,行为模型完全不同,替换时逐场景核对暂停逻辑。

参考

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391