react-three-fiberはThree.js(3DグラフィックスのJSライブラリ)をReactの宣言的な書き方で使えるようにするレンダラーです。今日読むloop.tsは、画面を毎フレーム描き直す「アニメーションループ」の心臓部。無駄な再描画をしないよう、必要なときだけループを回す設計が詰まっているので、ファイル一覧の中からここを選びました。
コード
export function loop(timestamp: number): void {
frame = requestAnimationFrame(loop)
running = true
repeat = 0
// Run effects
flushGlobalEffects('before', timestamp)
// Render all roots
useFrameInProgress = true
for (const root of _roots.values()) {
state = root.store.getState()
// If the frameloop is invalidated, do not run another frame
if (
state.internal.active &&
(state.frameloop === 'always' || state.internal.frames > 0) &&
!state.gl.xr?.isPresenting
) {
repeat += update(timestamp, state)
}
}
useFrameInProgress = false
// Run after-effects
flushGlobalEffects('after', timestamp)
// Stop the loop if nothing invalidates it
if (repeat === 0) {
// Tail call effects, they are called when rendering stops
flushGlobalEffects('tail', timestamp)
// Flag end of operation
running = false
return cancelAnimationFrame(frame)
}
}
引用はリポジトリの実物と機械で照合しています。「// …略…」は省略した行です。
上から順に読む
export function loop(timestamp: number): void {関数宣言です。`timestamp: number` は引数timestampの型がnumberであるという注釈(TypeScriptの型アノテーション)。`: void` は「この関数は値を返さない」という戻り値の型宣言です。ブラウザのrequestAnimationFrame APIは、コールバック関数に『いつ呼ばれたか』を示すtimestampを渡してくれる約束になっているので、それを受け取る形になっています。
frame = requestAnimationFrame(loop)requestAnimationFrameはブラウザが用意している関数で、『次に画面を描き直すタイミングで、渡した関数を1回だけ呼んでね』とブラウザに予約するものです。ここでは自分自身(loop)を渡しています。つまりloopが呼ばれるたびに『次のloop呼び出し』を予約し直す、自己再帰的なスケジューリングです。戻り値(予約ID)を変数frameに保存しておくと、後で予約をキャンセルできます。
running = true
repeat = 0runningとrepeatはこのファイルの一番外側(関数の外)で定義されている変数です。関数の中だけで完結せず、ファイル全体で共有される『状態』として使われています。runningは『ループが今動いているか』のフラグ、repeatは『このフレームで実際に何か描画処理をした回数』を数えるカウンターです。関数が呼ばれるたびに0にリセットされます。
flushGlobalEffects('before', timestamp)'before'という文字列そのものを引数として渡しています。flushGlobalEffects関数の中はswitch文になっていて、'before'なら描画前に登録されたコールバック集合(addEffectで登録されたもの)をまとめて実行します。ライブラリ利用者が『毎フレーム、描画の前に何かしたい』と登録した処理をここでまとめて呼び出しているわけです。
useFrameInProgress = trueこれもファイル外側で定義された共有フラグです。ここから『useFrameというReactフックのコールバックを実行する区間に入るよ』という目印を立てています。後述するinvalidate関数がこのフラグを見て『今useFrameの中から呼ばれたのか、それとも外から呼ばれたのか』を判断し、動作を変えるために使われます。
for (const root of _roots.values()) {`for...of`は配列やMap、Setなど『順番に取り出せるもの(イテラブル)』を1つずつ処理する構文です。_rootsはMap(キーと値のペアを持つデータ構造)で、`.values()`はそのMapに入っている『値』だけを順番に取り出すためのメソッドです。react-three-fiberは1ページに複数の`<Canvas>`(3D描画領域)を置けるので、_rootsにはCanvasごとの管理データが入っており、それを1つずつ処理しています。
state = root.store.getState()各rootはstore(状態管理オブジェクト)を持っていて、`.getState()`を呼ぶと『今この瞬間の状態のスナップショット』が返ってきます。カメラの位置やレンダラーの設定など、そのCanvasに関するあらゆる情報がこのstate変数にまとまります。
state.internal.active &&
(state.frameloop === 'always' || state.internal.frames > 0) &&
!state.gl.xr?.isPresenting3つの条件を`&&`(AND、すべて真なら真)でつないでいます。1つ目はそのCanvasが有効か。2つ目は丸括弧でグループ化されていて`||`(OR、どちらか真なら真)で『常に描画するモードか』『まだ描画すべきフレームが残っているか』のどちらかを満たすか。3つ目の`state.gl.xr?.isPresenting`の`?.`はオプショナルチェイニングという構文で、`state.gl.xr`がnullやundefinedなら、そこでエラーにならず全体がundefinedになります(VRヘッドセット表示中でないことを確認しています)。先頭の`!`は否定(真偽を反転)です。
repeat += update(timestamp, state)`update`関数を呼び出し、その戻り値(このCanvasで実際に描画が必要だったフレーム数)をrepeatに足し込んでいます。`+=`は「右辺の値を左辺に足して代入し直す」という複合代入演算子です。複数のCanvasを合計して、最終的に『1つでも描画が必要なCanvasがあったか』を判定するための集計になっています。
useFrameInProgress = falseループの区間を抜けたのでフラグを元に戻しています。フラグを立てて処理をし、必ず戻す、という『区間を示すフラグ』のパターンは他の言語やコードでもよく使われる書き方です。
if (repeat === 0) {全Canvasを見終わってrepeatが0(=どこも描画する必要がなかった)なら、ループを止める処理に入ります。`===`は型も含めて厳密に等しいかを比較する演算子です。
flushGlobalEffects('tail', timestamp)
// Flag end of operation
running = falseまず終了時専用のコールバック('tail')を実行し、次にrunningフラグをfalseに戻します。これにより、後で誰かがinvalidateを呼んだときに『今ループが止まっているから、新しく起動し直す必要がある』と判断できるようになります。
return cancelAnimationFrame(frame)`return`の後に値を返さない関数呼び出しを書いていますが、これは「ここで関数を終える」という意図で、cancelAnimationFrameの戻り値(undefined)自体には意味がありません。冒頭で予約しておいた『次のloop呼び出し』をキャンセルします。repeatが0でなければ何もせず関数が終わり、冒頭で予約済みの次のrequestAnimationFrameが自然に発火してloopがまた呼ばれます。