[Next.js #03] React Three Fiber(R3F)導入:Three.js を Next.js と正しく統合する

はじめに

next.js + React で、three.jsをやろうと意気込んで、いきなりぶち当たった壁。

R3F?え、なにそれ?😞

R3F(React Three Fiber)とは、一言で言うと「Three.jsをReactのコンポーネントとして扱えるようにするライブラリ」です。
WebGLを使った3D表現を、普段Reactで行うような宣言的な記述( “mesh /” や “boxGeometry /” など)で、簡単かつきれいに実装できる神ツールとして知られています。 (Gemini3 より😉)

R3Fを使わずに、開発しなれたヴァニラでいいじゃダメなのか?

next.jsはそもそも、サーバで動くことを前提で作られており何かと問題があり大人しくR3Fに従うしかない。

という事で、next.jsの3回目の記事は、R3Fの学習、備忘録メモ、恐らく大半のnext.jsユーザーに需要がないと思われます…。

前回の記事:

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 を奪わない → WebGLRenderTargetShaderMaterialgl.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 が必要とするもの:

  • window
  • document
  • <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” がクライアント側へ移すスイッチ
  • は R3F の renderer / scene / camera の起点
  • 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 では この“全詰め込み構造”を自然に分解できる。

では、どうやって?


🔥 設計の本質ルール(絶対に守れば破綻しない)

  1. Renderer / Scene / Camera は R3F に任せる(Canvas が持つ)
  2. ゲームオブジェクトはコンポーネント化する
  3. update は useFrame を“職能別”に分割する
  4. 低レイヤーは useThree で必要な時だけ触る
  5. グローバルな 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 へ移植する構造