はじめに
next.js + React で、three.jsをやろうと意気込んで、いきなりぶち当たった壁。
R3F?え、なにそれ?😞
R3F(React Three Fiber)とは、一言で言うと「Three.jsをReactのコンポーネントとして扱えるようにするライブラリ」です。
WebGLを使った3D表現を、普段Reactで行うような宣言的な記述( “mesh /” や “boxGeometry /” など)で、簡単かつきれいに実装できる神ツールとして知られています。 (Gemini3 より😉)
Introduction - React Three Fiber
React-three-fiber is a React renderer for three.js.
https://r3f.docs.pmnd.rs/getting-started/introductionR3Fを使わずに、開発しなれたヴァニラでいいじゃダメなのか?
next.jsはそもそも、サーバで動くことを前提で作られており何かと問題があり大人しくR3Fに従うしかない。
という事で、next.jsの3回目の記事は、R3Fの学習、備忘録メモ、恐らく大半のnext.jsユーザーに需要がないと思われます…。
前回の記事:
[Next.js #02] フレームワークの思想と哲学を理解する
Next.js を正しく使いこなすために欠かせない、React・Next.js・Tailwind の設計思想とその歴史的背景をまとめた記事。UI=状態の関数という概念、継承からCompositionへの転換、Server/Client 分離の思 …
https://humanxai.info/posts/nextjs-02-philosophy/R3F を理解するための地図
0) R3F は何をしていて、何をしていないか
していること(重要)
- Three.js の
Scene / Camera / Rendererを React のライフサイクルに乗せる - Three.js の render loop(XR なら
setAnimationLoop)を管理して、useFrame()を呼ぶ - JSX で
Mesh / Geometry / Materialを宣言できるようにする(React Reconciler)
していないこと(安心)
- Three.js の低レベル API を奪わない
→
WebGLRenderTargetもShaderMaterialもgl.render()も普通に使える (=“自前でやりたい人” を殺さない)
1) R3F のコアは 3つだけ
(A) Canvas
「renderer + scene + camera の起点」 ここが “Three.js の world を React に埋め込む穴”。
(B) useFrame((state, delta) => {})
「毎フレーム呼ばれる Update」
ヴァニラの requestAnimationFrame / XR の setAnimationLoop を自分で書かず、ここに差し込む。
(C) useThree()
「gl(renderer), scene, camera など低レベルへ降りる出口」 “ブラックボックス化”が怖いなら、ここが命綱。
2) drei(@react-three/drei)は何?
drei は 「よく使う三種の神器を部品化した棚」。
<OrbitControls />(Controls の定番)<Environment />(IBL 環境光)<Text />(文字)<Html />(3D上にDOM)<useGLTF />(glTFロード補助)
重要:drei は必須じゃない。 構造理解したい人は「最初は drei を最小限」でOK。
3) “コンポーネント化”は React の都合であって、R3Fの都合じゃない
l Next.js(App Router) の都合:
app/配下はデフォで Server Component- WebGL/DOM/window を触るのは Client Component
だから Three.js/R3F を書くファイルは先頭にこれが必要:
"use client";
これ、思想じゃなく 動作条件。
Next.js + R3F の最小構造(迷子にならない型)
1) app/page.tsx(サーバー側)
import ThreeCanvas from "@/components/ThreeCanvas";
export default function Page() {
return (
<main className="w-full h-screen">
<ThreeCanvas />
</main>
);
}
2) components/ThreeCanvas.tsx(クライアント側)
"use client";
import { Canvas } from "@react-three/fiber";
import Cube from "./Cube";
export default function ThreeCanvas() {
return (
<Canvas camera={{ position: [3, 3, 3] }}>
<ambientLight intensity={0.6} />
<directionalLight position={[5, 5, 5]} />
<Cube />
</Canvas>
);
}
3) components/Cube.tsx(クライアント側)に Update を書く
"use client";
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
export default function Cube() {
const ref = useRef<THREE.Mesh>(null);
useFrame((_state, delta) => {
if (!ref.current) return;
ref.current.rotation.x += delta * 0.4;
ref.current.rotation.y += delta * 0.6;
});
return (
<mesh ref={ref}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#4DA3FF" />
</mesh>
);
}
ここまでが「最低限の体系」。 これが頭に入ってれば、公式を見ても迷子にならない。
ルール1:useThree() を常に出口として持つ
「どうせ最後は renderer を触る」を前提にしておく。
const { gl, scene, camera } = useThree();- ここから先は ヴァニラと同じ
ルール2:レンダリングを “自分で握る” パターンも覚える
ミニマップみたいな RenderTarget + サブカメラは、R3Fでも書ける(君がさっき出したやつ)。
要点だけ言うと:
useFrame(..., 1)を使うと R3Fの自動renderを止めて自分でgl.render()できるgl.setRenderTarget(rt)もそのまま
「R3Fに移行しても、君のThree.js資産は死なない」ってことを、ここで保証できる。
1. なぜ R3F なのか(Next.js App Router と WebGL の相性問題)
Three.js を長く触ってきた開発者ほど、 「素の Three.js(ヴァニラ)でいいじゃないか」 と思う。 実際、通常の HTML / JS ならその通りで、R3F は不要だ。
しかし Next.js(特に App Router)と組み合わせた瞬間、 WebGL と SSR の前提が完全に衝突する。
この章では、その理由を明確にする。
1-1. WebGL(Three.js)は「ブラウザで動く」ことを前提にしている
Three.js が必要とするもの:
windowdocument<canvas>(GLコンテキスト)requestAnimationFrame- WebXR の
setAnimationLoop - GPU とのバインド(WebGLContext)
つまり “ブラウザ環境の存在が前提” になっている。
1-2. Next.js(App Router)は「まずサーバーで動く」が前提
App Router(app/)のデフォルトは Server Component。
Server Component の制限:
windowなしdocumentなし<canvas>作れない- ブラウザ API 不使用
- WebGL / WebXR の初期化不可
つまり Three.js をそのまま書くと落ちる。
new THREE.WebGLRenderer(); // ❌ サーバーでは canvas を作れない
App Router と WebGL は最初から成り立たない。
1-3. “use client” が必須になる理由
App Router では ブラウザ側(Client Component)だけが WebGL を扱える。
そのため、Three.js / R3F のコードは必ずこう始まる:
"use client";
これは思想ではなく、 WebGL を動かすための動作条件。
1-4. React の再レンダリングと Three.js の render loop が衝突する
React は:
- UI の更新のために再レンダリングを行う(仮想DOMの差分更新)
Three.js は:
requestAnimationFrameで毎フレーム 60 / 90 / 120Hz で描画する
この二つのループは性質が違いすぎる。
React のループ → UI
Three.js のループ → WebGL
そのため「1つのコンポーネントの中で両方動かす」と 同期が取れずに破綻しやすい。
1-5. ページ遷移で Three.js のリソースがリークする
Next.js はページ遷移すると:
- コンポーネントが unmount
- DOM が破棄
- 再レンダリング
- 状態がリセット
ヴァニラ Three.js を直接書くと、
- WebGLContext が残る
- Texture / Buffer / Shader が解放されない
- XR セッションが壊れる
- Canvas だけ残って2枚、3枚と増える
という “WebGL地獄” が起きる。
1-6. R3F は SSR × React × WebGL を“橋渡しするための適応層”
ここが最重要。
R3F は「Three.js の代わり」でもなく 「WebGL を抽象化するブラックボックス」でもない。
役割はひとつ:
Three.js の描画ループ・リソース管理・マウント/アンマウントを React/Next.js のライフサイクルと安全に接続する適応層(Adapter)
具体的には:
Canvas = Renderer + Scene + Camera の管理
useFrame = Three.js の render loop(内部は setAnimationLoop)
自動クリーンアップ(unmount 時に GLリソースを解放)
ページ遷移時の WebGLContext 漏れを防ぐ
XR モードへの自動対応
React の state と Three.js の同期
R3F の内部は Three.js の API を使っているだけ。
だから “学び直し” の必要はない。
1-7. まとめ:R3F が必要な理由は「Next.js と WebGL の前提が違いすぎるから」
- Three.js は「ブラウザ前提」
- Next.js(App Router) は「サーバー前提」
- React は「UIの再レンダリング前提」
- WebGL は「毎フレーム描画前提」
これらを「何もせず混ぜる」のは無理。
そこで R3F がちょうどその隙間を埋めてくれる。
R3F = “Three.js を Next.js / React に安全に統合するための最小レイヤー”
2. 最小構造:app/page.tsx は貼るだけ + components 側が “use client”
Next.js(App Router)は、 ファイルの場所によって“動く環境(サーバー or ブラウザ)が変わる” という特殊な仕様を持っている。
Three.js / R3F を扱うとき、 この仕組みを理解していないと 100% 動かなくなる。
2-1. app/page.tsx は「サーバー側で動く」= WebGL は使えない
Next.js の App Router(app/)は
デフォルトが Server Component。
Server Component の特徴:
windowなしdocumentなし- Canvas なし
- WebGL なし
- WebXR なし
- requestAnimationFrame なし
つまり Three.js の初期化が一切使えない。
これはこういうコードが禁止されるということ:
new THREE.WebGLRenderer(); // ❌ サーバーではエラー
renderer.setAnimationLoop(); // ❌
document.querySelector("canvas"); // ❌
App Router を使う以上、避けられない仕様。
2-2. Three.js や R3F を動かせるのは Client Component だけ
ブラウザ側(クライアント)に移したい場合は ファイルの先頭にこれを書く:
"use client";
これは “このファイルはブラウザ側で動かす” という Next.js への宣言。
- WebGL が動く
- window / document が使える
- XR が動く
- requestAnimationFrame が使える
- useFrame が動く
R3F のすべての処理は 必ず Client Component に置く。
2-3. 最小構造の「正しい書き方」
Next.js × R3F の正解構造はこれだけ。
app/page.tsx —(Server Component)
ここには 何も書かない。 ただ “R3Fコンポーネントを貼るだけ”。
import ThreeCanvas from "@/components/ThreeCanvas";
export default function Page() {
return (
<main className="w-full h-screen">
<ThreeCanvas />
</main>
);
}
なぜこれだけでいいのか?
Server Component は React の UI構造だけを返す場所。
WebGL 初期化はできないため、 Canvas の中身は全部クライアント側に任せる。
components/ThreeCanvas.tsx —(Client Component)
ここが R3F の本体。
"use client";
import { Canvas } from "@react-three/fiber";
import Cube from "./Cube";
export default function ThreeCanvas() {
return (
<Canvas camera={{ position: [3, 3, 3] }}>
<ambientLight intensity={0.6} />
<directionalLight position={[5, 5, 5]} />
<Cube />
</Canvas>
);
}
ココに Three.js / R3F を全部まとめる理由
- Canvas(renderer)はブラウザでしか生成できない
- R3F の render loop もブラウザでしか回せない
- VRM のロードもブラウザだけ
- WebXR session もブラウザだけ
だから “use client” ファイルに集約するのが必須。
2-4. Cube.tsx にも “use client” が必要なのか?
必要。
"use client";
理由:
- useRef / useFrame / Three.js API がクライアント側で必要
- Canvas の子要素は全部クライアントで動かないといけない (R3F の Reconciler がそう設計されている)
R3Fの子コンポーネントはすべて Client Component。
2-5. なぜ「app/page.tsx は貼るだけ」なのか?
これは Next.js の設計思想に基づいている。
- app/page.tsx → SSRとUIの骨格を作る
- Canvas(WebGL) → クライアントで生成する
- useFrame(アニメーション) → クライアントで動く
- Camera / Scene / Renderer → クライアントに固定
つまり Next.js の正しい使い方は:
“3Dは全部 components に隔離して、app/page.tsx は画面の入れ物だけにする”
これを理解すると Next.js × R3F × WebGL の事故率がゼロになる。
2-6. ミニマップ・サブカメラ・XR など高度な処理はどこに置く?
すべて components/ThreeCanvas.tsx or その配下。
理由は簡単:
- WebGL はクライアント側でしか動かない
- R3F は Canvas 内でのみ有効
- useFrame / useThree はクライアント必須
だから “全3Dロジック” を
components/three/ に固めるのが一番綺麗。
まとめ
- app/page.tsx はサーバー。WebGL は動かない
- Three.js/R3F は必ず Client Component 内に書く
- “use client” がクライアント側へ移すスイッチ
- Cube.tsx などの下層コンポーネントも Client Component
- 3Dロジックは components/three/ 以下に集約するのが最強
この土台を理解した時点で、 「Next.js × R3F の世界」で迷わなくなる。
3. R3F の3点セット:Canvas / useFrame / useThree
R3F の根幹を成すのは、この3つだけ。
- Canvas → renderer + scene + camera
- useFrame → animation / render loop
- useThree → Three.js の低レベル API へ降りるための出口
R3F のコードの 90% は、この3点で完結する。
3-1. Canvas:R3F の“3Dワールドの入口”
役割まとめ
- WebGLRenderer の生成
- Scene の生成
- Camera の生成
- render loop の開始
- ページ遷移時のクリーンアップ
- WebXR の有効化
- events(Raycaster)構築
- コンポーネント → Object3D に変換
- マウント / アンマウントの管理
つまり canvas = React の世界と Three.js の世界のブリッジ。
最小構造
<Canvas camera={{ position: [3, 3, 3] }}>
{/* Light / Mesh / Controls */}
</Canvas>
Three.js の何に対応している?
| R3F | Three.js |
|---|---|
<Canvas> |
new WebGLRenderer() + new Scene() + new PerspectiveCamera() |
つまり Canvas = Three.js 初期化の“全部”。
3-2. useFrame:アニメーションループ(内部は setAnimationLoop)
useFrame の正体
Three.js の render loop(requestAnimationFrame / setAnimationLoop)に “毎フレーム処理を差し込む” フック。
R3F が内部でやっている処理は:
renderer.setAnimationLoop((t) => {
// canvasに登録された useFrame を全部呼ぶ
});
だから XR でもそのまま動く。
最小構造
useFrame((state, delta) => {
meshRef.current.rotation.y += delta;
});
delta の正体
- 前フレームと今のフレームの差
- Three.js の
clock.getDelta()と同じ - XR なら 90Hz / 120Hz に最適化される
Three.js の何に対応している?
| R3F | Three.js |
|---|---|
useFrame(fn) |
renderer.setAnimationLoop(fn) |
useFrame(fn, 1) |
“手動レンダリング(自分で gl.render を書く)” |
Three.js の animate() を丸ごとイベント化しているだけ。
3-3. useThree:Three.js の素手 API に触れるための出口
役割
R3F の内部管理を抜けて 純粋な Three.js の低レイヤーへアクセスするための関数。
const { gl, scene, camera, size, viewport } = useThree();
ここでしか取得できないもの:
- gl (WebGLRenderer)
- scene(R3F が生成した root scene)
- camera(Canvas のデフォルトカメラ)
- pointer / viewport 情報
- invalidate(再描画要求)
Three.js の何に対応している?
| R3F | Three.js |
|---|---|
useThree().gl |
renderer |
useThree().scene |
scene |
useThree().camera |
camera |
完全に一致している。
だから R3F でもヴァニラ Three.js の実装ができる
例:ミニマップの RenderTarget
useFrame(() => {
gl.setRenderTarget(miniMapRT);
gl.render(scene, subCamera);
gl.setRenderTarget(null);
});
ヴァニラThree.jsと100%同じコードが動く。
ここが R3F が“学び直し不要”である理由。
3-4. この3点セットの関係性(図解)
┌──────────────────────────────┐
│ React │
│ (コンポーネント・再レンダリング) │
└───────────────┬───────────────┘
│
▼
<Canvas>(世界の入口)
┌─────────────────────────────┐
│ Scene Camera Renderer │
│ (Three.js の内部インスタンス) │
└──────┬──────────────┬────────┘
│ │
useFrame() useThree()
(Update差込) (内部APIへアクセス)
Canvas →(レンダラー構築)→ useFrame / useThree が動く仕組み。
3-5. この3点セットだけで R3F の 80% を理解できる
R3F 避けてきた人がよく言う問題:
- “ブラックボックスっぽい”
- “Three.js の構造が隠れて見えない”
- “何が自前で、何がR3F側の処理か区別できない”
➡ 3点セットの役割を理解すると その不安が全部消える。
まとめ
R3F の本体はたった3つ。
Canvas
Three.js の初期化(renderer / scene / camera)を担当する“入口”
useFrame
Three.js / WebXR のレンダーループに処理を差し込む
useThree
Three.js の low-level API へ降りる出口 (gl.render, WebGLRenderTarget, カメラ切替など全部ここ)
ここまで理解できれば R3F を使いながら Three.js の技術資産を 100% 再利用できる。
4. ヴァニラ対応表:scene/camera/renderer / requestAnimationFrame / controls
Three.js の主要コンポーネントを R3F ではどう書き換えるのか? これを全部 1:1 で見える化する。
全部 “Three.js の概念は消えていない” ことがわかるはず。
4-1. Scene(シーン)
ヴァニラ Three.js
const scene = new THREE.Scene();
R3F
<Canvas>
{/* ここに書いた要素全部が “scene.children” に入る */}
</Canvas>
対応
| ヴァニラ | R3F |
|---|---|
new THREE.Scene() |
<Canvas> が内部 scene を作る |
4-2. Camera(カメラ)
ヴァニラ Three.js
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.set(3, 3, 3);
R3F
<Canvas camera={{ position: [3, 3, 3], fov: 60 }}>
対応
| ヴァニラ | R3F |
|---|---|
new PerspectiveCamera() |
<Canvas camera={...}> |
camera.position.set() |
camera={{ position:[x,y,z] }} |
camera.fov = 60 |
camera={{ fov:60 }} |
4-3. Renderer(WebGLRenderer)
ヴァニラ
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
R3F
<Canvas gl={{ antialias: true }}>
対応
| ヴァニラ | R3F |
|---|---|
new WebGLRenderer() |
<Canvas> が内部で生成 |
renderer.setSize() |
自動 |
DOM.appendChild(renderer.domElement) |
React が管理 |
renderer.xr.enabled = true |
<Canvas vr> |
4-4. requestAnimationFrame(または setAnimationLoop)
ここが 最も重要。
ヴァニラ Three.js
function animate() {
requestAnimationFrame(animate);
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate); // XR
R3F(内部は setAnimationLoop)
useFrame((state, delta) => {
meshRef.current.rotation.y += delta;
});
対応
| ヴァニラ | R3F |
|---|---|
requestAnimationFrame() |
useFrame が内部ループに差し込まれる |
renderer.setAnimationLoop() |
R3F が Canvas 内で自動実行 |
update + render |
R3F が render を管理、update だけ書く |
Three.js の “ループ制御” を React のイベント(useFrame)に変換してるだけ。
4-5. Controls(OrbitControls など)
ヴァニラ Three.js
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
R3F(drei を利用)
<OrbitControls enableDamping={true} />
対応
| ヴァニラ | R3F |
|---|---|
new OrbitControls(camera, canvas) |
<OrbitControls /> |
.enableDamping = true |
enableDamping={true} |
R3F の OrbitControls も 中身は Three.js の OrbitControls そのまま。
4-6. Mesh / Geometry / Material
ヴァニラ
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(),
new THREE.MeshStandardMaterial({ color: 0xff0000 })
);
scene.add(mesh);
R3F
<mesh>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
対応
| ヴァニラ | R3F |
|---|---|
new Mesh() |
<mesh> |
new BoxGeometry() |
<boxGeometry> |
new MeshStandardMaterial() |
<meshStandardMaterial> |
scene.add(mesh) |
JSX 配置で自動で scene へ追加 |
4-7. RenderTarget(ミニマップや RTT)
ヴァニラ
renderer.setRenderTarget(rt);
renderer.render(scene, subCamera);
renderer.setRenderTarget(null);
R3F
const { gl, scene } = useThree();
useFrame(() => {
gl.setRenderTarget(rt);
gl.render(scene, subCamera);
gl.setRenderTarget(null);
});
対応
まったく同じ。 R3F は低レイヤーを一切隠さない。
4-8. XR(WebXR)
ヴァニラ
renderer.xr.enabled = true;
renderer.setAnimationLoop(onXRFrame);
R3F
<Canvas vr>
内部で:
- renderer.xr.enabled = true
- setAnimationLoop()
- HMD カメラ同期
- controller 更新
全部やってくれる。
対応
| ヴァニラ | R3F |
|---|---|
renderer.xr.enabled = true |
<Canvas vr> |
setAnimationLoop() |
内部に統合 |
| HMDカメラ同期 | Canvas が管理 |
全対応表まとめ(保存版)
| 要素 | ヴァニラ Three.js | R3F |
|---|---|---|
| Scene | new THREE.Scene() | <Canvas> 内部で自動生成 |
| Camera | new PerspectiveCamera | <Canvas camera={}> |
| Renderer | new WebGLRenderer | <Canvas gl={}> |
| requestAnimationFrame | 自分で書く | useFrame が内蔵 loop に差し込み |
| setAnimationLoop | 自分で管理 | Canvas が自動管理 |
| OrbitControls | new OrbitControls() | <OrbitControls /> |
| Mesh | new Mesh() | <mesh> |
| Geometry | new BoxGeometry | <boxGeometry> |
| Material | new MeshStandardMaterial | <meshStandardMaterial> |
| RenderTarget | renderer.setRenderTarget() | gl.setRenderTarget()(同じ) |
| XR | renderer.xr.enabled | <Canvas vr> |
| Scene.add() | 手動 | JSX の配置が自動で追加 |
結論:
R3F は Three.js を抽象化しない
Three.js の低レイヤーは全部そのまま使える
学ぶべきは「書き方の置き換え」だけ
ヴァニラ Three.js の資産は 100% 活きる
XR / マルチパス / RenderTarget も全部同じコードで動く
R3F を学んでも Three.js を忘れないどころか、 逆に Three.js の構造理解がより強固になるタイプのライブラリ。
5. 「自前エンジン」を R3F に移植する時の設計(config地獄から抜ける)
まずは結論から
config 1枚に全部詰める構造は R3F では逆効果。
“責務ごとにコンポーネント化”することで 自前エンジンを進化させる方向に移行する。
理由は 2つ。
R3F の Scene は「ツリー構造」で管理する設計
useFrame は「各責務ごとに分散」できるので巨大 update() が不要
今の君の構造は次の問題を抱えている:
- すべてが
configに集まり過ぎ - update が 1つの巨大関数に集約
- 機能追加するたびに
configが肥大化 - 小規模では便利だが、大規模サンドボックスでは破綻する
R3F では この“全詰め込み構造”を自然に分解できる。
では、どうやって?
🔥 設計の本質ルール(絶対に守れば破綻しない)
- Renderer / Scene / Camera は R3F に任せる(Canvas が持つ)
- ゲームオブジェクトはコンポーネント化する
- update は useFrame を“職能別”に分割する
- 低レイヤーは useThree で必要な時だけ触る
- グローバルな config は“限界まで減らす”
ここを守ると 君の Three.js エンジンを “R3F版へ自然移植” できる。
5-1. config を「役割ごとに分解」する
君の今の構造:
config.renderer
config.scene
config.camera
config.cameraSub
config.miniMapRT
config.miniMapPlane
config.player
config.gameState
config.input
...
➡ R3F 版はこうする:
ThreeCanvas.jsx
├─ MiniMap.jsx
├─ Player.jsx
├─ World.jsx
└─ GameLogic.jsx
役割分解例:
| 役割 | R3Fコンポーネント |
|---|---|
| プレイヤー | <Player /> |
| 地形 / ワールド | <World /> |
| ミニマップRT | <MiniMap /> |
| Input制御 | <InputSystem /> |
| ゲームステート | <GameStateProvider /> |
| Update統合 | 各コンポーネントが必要な useFrame を持つ |
config は「最低限のグローバル状態だけ」に縮小すべき
5-2. 巨大 updateRender() → useFrame の“分散”へ
君の updateRender() はこうだった:
if (renderer.xr.isPresenting) { ... }
config.miniMapPlane.visible = false;
renderer.setRenderTarget(config.miniMapRT);
renderer.render(scene, config.cameraSub);
renderer.setRenderTarget(null);
config.miniMapPlane.visible = true;
renderer.render(scene, config.camera);
R3Fではこれを1ファイルに“全部書かない”。
ルール
- ミニマップの描画 →
<MiniMap />の useFrame - XR 分岐 → Canvas が持つ
- メイン描画 → Canvas が持つ
つまり、分散する。
R3F 版はこう:
// MiniMap.jsx
useFrame(({ gl, scene }) => {
miniMapPlane.visible = false;
gl.setRenderTarget(miniMapRT);
gl.render(scene, subCamera);
gl.setRenderTarget(null);
miniMapPlane.visible = true;
});
Canvas が描くので「最後の renderer.render() は不要」。
結果:
updateRender() の巨大さが自然消滅する。
5-3. プレイヤー更新(キャラ制御)も分散できる
君の元コード:
updatePlayer();
updateGame();
updateCamera();
updateRender();
R3F ではこう:
<Player />
<GameCamera />
<MiniMap />
<World />
そして各ファイル内で:
useFrame(() => {
// Player position logic
});
コンポーネントごとに 職能別 update が並ぶだけで超読みやすくなる。
5-4. “低レイヤーの自由度”は保持(configが不要になる理由)
config 方式:
config.renderer
config.scene
config.camera
config.cameraSub
R3F:
const { gl, scene, camera } = useThree();
つまり config の大半が不要になる。
ミニマップやサブカメラは useMemo で作ればいい
const subCamera = useMemo(
() => new THREE.PerspectiveCamera(60, 1, 0.1, 100),
[]
);
RenderTarget も useMemo
const miniMapRT = useMemo(
() => new THREE.WebGLRenderTarget(256, 256),
[]
);
プレイヤーモデルもコンポーネントで保持
const meshRef = useRef();
5-5. まとめ:自前エンジン構造を R3F がどう変えるか
Before:
- config に全部詰める
- update() に全部集約
- renderer.render を手動管理
- シーン内オブジェクトを全部 config から参照
- WebGL の扱いが肥大化
After(R3F):
- Canvas / Scene / Camera / Render → R3F が持つ
- update は useFrame に分散
- 各コンポーネントが自分でロジックを持つ
- 低レイヤーは useThree で必要な時だけ触る
- config は「ゲームステートだけ」になる
これは 「自前エンジン」→「綺麗に分解された ECS 的構造」 への転換。
次の章(候補)
- RenderTarget / minimap を R3F で完全再現
- XR(WebXR)を R3F Canvas でどう動かすか
- VRM/MMD を R3F へ移植する構造
💬 コメント