threejs-react-three-fiber について
React Three Fiber と drei で 3D シーンを構築するための Three.js スキルです。宣言的なシーン構成、フック、アセット読み込み、操作、React ベースの 3D プロジェクトの最適化を扱います。
主な機能
- 01JSX による Three.js シーンの構成
- 02フックによるレンダーループ制御
- 03GLTF モデルとテクスチャの読み込み
- 04レイキャストによるポインターイベント処理
- 05drei のコントロールとシーン用ヘルパー
- 06オンデマンド描画とインスタンス化
活用シーン
- 01React でインタラクティブな 3D 表示を構築
- 02GLTF アセットの読み込みと表示
- 03カメラ操作とシーン内ラベルの追加
- 04静的なシーンの描画処理を削減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cloudai-x/ threejs-skills --skill threejs-react-three-fiberインストール後、Claude Code で/threejs-react-three-fiberを入力して読み込まれたか確認します。
詳細
Three.js React Three Fiber
three.js r186 を対象としています。例では WebGLRenderer を使用し、WebGPU と TSL のセクションでは WebGPURenderer を使用します。
クイックスタート
@react-three/fiber 9.8、React 19、@react-three/drei 10.7 を対象としています。Canvas は WebGLRenderer、シーン、カメラを作成して管理します。three.js のオブジェクトは JSX 要素になります。Canvas は親要素いっぱいに広がるため、親要素に高さを指定してください。以下のコンポーネントのフラグメントは、このような Canvas 内でレンダリングされます。ライトを使うマテリアルは、その Canvas のライトを前提としています。
import { useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
function Box() {
const ref = useRef();
useFrame((_, delta) => {
ref.current.rotation.y += delta;
});
return (
<mesh ref={ref} position={[0, 0.5, 0]} castShadow>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#d9915b" />
</mesh>
);
}
export default function App() {
return (
<Canvas shadows="percentage" camera={{ position: [3, 2, 4], fov: 50 }}>
<color attach="background" args={["#111827"]} />
<ambientLight intensity={0.4} />
<directionalLight position={[3, 4, 2]} intensity={2} castShadow />
<Box />
<mesh rotation-x={-Math.PI / 2} receiveShadow>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial color="#20293a" />
</mesh>
<OrbitControls makeDefault />
</Canvas>
);
}
Canvas の Props
export function App() {
return (
<Canvas
shadows="variance"
dpr={[1, 2]}
frameloop="demand"
flat
camera={{ position: [0, 2, 6], fov: 45 }}
gl={{ powerPreference: "high-performance" }}
onCreated={(state) => state.gl.setClearColor("#111827")}
>
{/* scene contents */}
</Canvas>
);
}
camera:デフォルトカメラの props、またはカメラインスタンスを指定します。指定しない場合は、fov 75、near 0.1、far 1000、z 5 の位置から原点を見るPerspectiveCameraが使われます。fiber はリサイズのたびにアスペクト比(またはorthographicが指定されている場合は left/right/top/bottom)と投影行列を更新します。camera={{ manual: true }}を指定すると、投影の設定をすべて自分で行います。dpr:数値、またはwindow.devicePixelRatioを基準に制限される[min, max]を指定します。この範囲はディスプレイの変更に応じて調整されます。shadows:trueを指定するとシャドウマップが有効になります。文字列('basic'、'percentage'、'variance')でフィルターの種類を指定し、オブジェクトを指定するとレンダラーのshadowMapプロパティにマージされます。true、'soft'、デフォルトのfalseはすべて PCFSoftShadowMap を設定しますが、three はこれを PCFShadowMap に置き換え、コンソールに警告を表示します(WebGLRenderer ではシャドウのレンダリング時、WebGPURenderer では Canvas ごとに毎回)。'percentage'は警告なしで同じソフトフィルターを使用します。flatは NoToneMapping を設定します。指定しない場合、fiber は ACESFilmicToneMapping を適用します。通常の three.js と異なり、デフォルトでトーンマッピングが有効です。linearは出力を LinearSRGBColorSpace に切り替えます。デフォルトは sRGB です。legacyは three の ColorManagement を無効にします。gl:デフォルトのnew WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" })の props、レンダラーインスタンス、またはファクトリー関数(以下の WebGPU の例)を指定します。frameloop:'always'は毎フレーム描画し、'demand'は React の変更時またはinvalidate()の呼び出し後にのみ描画します。'never'はループを引き継ぎ、モジュールレベルのadvance(timestamp)エクスポートを秒単位の時刻で呼び出します。useFrameの delta は連続するタイムスタンプ間の差分なので、ミリ秒を指定すると 1000 倍大きくなります。eventsはイベントマネージャーを置き換えます(デフォルトでは DOM ポインターイベントをレイキャストにマッピングします)。eventSource(ポインターイベントを登録する要素。デフォルトは Canvas のラッパー)とeventPrefix(デフォルトは'offset'。ページのスクロール時や Canvas の変形時には'page'、'client'、'layer'、'screen')でポインターのマッピングを調整できます。onCreatedはレンダラーの作成後に一度実行され、onPointerMissedはクリックが何にもヒットしなかったときに呼び出されます。
JSX としてのシーングラフ
three 名前空間のすべてのエクスポートは要素になります。小文字のタグ名はクラス名を表します(<mesh> → THREE.Mesh、<meshStandardMaterial> → そのマテリアル)。props はインスタンスに適用されます。three に .set() がある場所では短縮形を使えます:position={[1, 2, 3]}、position={new THREE.Vector3(1, 2, 3)}、position={5}(setScalar)、color="#d9915b"。ハイフン付きの props は内部のプロパティにアクセスします(rotation-x、material-color、geometry-attributes-position)。attach="material-0" は配列のインデックスにアタッチします。
args はコンストラクター引数のリストです。値を変更すると、オブジェクトが最初から再構築されます。ジオメトリとマテリアルは親のメッシュに自動的にアタッチされるため、attach は不要です。
function Torus() {
return (
<mesh position={[0, 1, 0]}>
<torusKnotGeometry args={[0.7, 0.25, 128, 24]} />
<meshStandardMaterial
color="#42699c"
roughness={0.2}
metalness={0.8}
onUpdate={(self) => console.log(self.type)}
/>
</mesh>
);
}
すでに保持しているオブジェクトは <primitive object={obj} /> でマウントします。primitive ごとにオブジェクトは 1 つです(fiber はオブジェクトごとの状態をそのオブジェクト上に保持するため)。複製する場合は clone してください。fiber は primitive を破棄しません。オブジェクトのライフタイムは自分で管理します。任意の要素で dispose={null} を指定すると、アンマウント時の破棄を無効にできます。three 名前空間以外のクラスは extend で登録します。
import { extend } from "@react-three/fiber";
import { Water } from "three/addons/objects/Water.js";
extend({ Water });
// then render <water args={[geometry]} /> anywhere in the Canvas
TypeScript では line、audio、source、path のタグが DOM 型と競合します。threeLine、threeAudio、threeSource、threePath と記述してください(fiber がプレフィックスを取り除き、three のクラスを解決します)。
フック
useFrame(callback, renderPriority?) はレンダーループを購読します。シーンがレンダリングされる前に実行されます。
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
function Spinner({ radius = 2 }) {
const ref = useRef();
useFrame((state, delta) => {
ref.current.rotation.y += delta;
ref.current.position.x = Math.sin(state.clock.elapsedTime) * radius;
});
return (
<mesh ref={ref}>
<sphereGeometry args={[0.4, 32, 16]} />
<meshBasicMaterial color="#e8b4a0" />
</mesh>
);
}
正の renderPriority を指定した購読者は、レンダリングを自分で制御します。Canvas はレンダリングを停止するため、コールバック内で state.gl.render(state.scene, state.camera) を呼び出す必要があります。デフォルトの優先度では、自分で gl.render を呼び出すと二重にレンダリングされます。
useThree(selector?) は Canvas の状態をリアクティブに読み取ります。セレクターを指定しない場合は、状態全体(gl、camera、scene、raycaster、pointer、size、viewport、invalidate、setDpr、setFrameloop、controls、performance)を返します。
import { useThree } from "@react-three/fiber";
const camera = useThree((state) => state.camera);
const invalidate = useThree((state) => state.invalidate);
再レンダリングを伴わない同じ状態を使うには、useFrame 内で state 引数を使用します。state.clock は fiber 9.8 が作成する THREE.Clock であるため、すべての Canvas で three の Clock に関する非推奨警告がログに出ます。この警告はコードが原因ではありません。
useLoader(ClassOrInstance, url, extensions?, onProgress?) は、three のローダーが解決するまで処理を一時停止し、ローダーと URL をキーにキャッシュします。呼び出し元は Suspense 境界の下に配置する必要があります。extensions コールバックは、fiber がクラスごとに保持する単一のローダーインスタンスで実行されるため、そこで設定した内容(パスやデコーダーなど)は、そのクラスを使う後続のすべての useLoader 呼び出しに適用されます。useGraph(object) は、オブジェクトツリーから名前付きの検索マップ({ nodes, materials, meshes })を作成します。
import { useGraph, useLoader } from "@react-three/fiber";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
function Model({ url }) {
const gltf = useLoader(GLTFLoader, url, (loader) => loader.setPath("assets/"));
const { nodes } = useGraph(gltf.scene);
return <primitive object={gltf.scene} />;
}
useLoader.preload(Class, url) は React の外部でキャッシュを事前にウォームアップします。useLoader.clear(Class, url) はエントリーを削除します。
ポインターイベント
ハンドラーを持つオブジェクトは、ポインターイベントのたびにレイキャストされます(ポインターの移動では、move、over、out、enter、leave のいずれかのハンドラーを持つオブジェクトのみがテストされます)。ハンドラーは最も近いものから実行され、ヒットしたオブジェクトから親へとバブリングします。イベントには交差情報と、次のヘルパーが含まれます:object、eventObject(ハンドラーを登録したオブジェクト)、point、distance、face、instanceId、intersections、ray、camera、unprojectedPoint、delta(クリックに類するイベントで、pointerdown 以降に移動したピクセル数)、nativeEvent。ハンドラー:onClick、onDoubleClick、onContextMenu、onWheel、onPointerUp、onPointerDown、onPointerMove、onPointerOver、onPointerOut、onPointerEnter、onPointerLeave、onPointerMissed(onPointerCancel と onLostPointerCapture は型定義されていますが、fiber がオブジェクトに対して呼び出すことはありません)。
import { useState } from "react";
function Pickable() {
const [hovered, setHovered] = useState(false);
return (
<mesh
onClick={(event) => {
event.stopPropagation(); // underlying objects along the ray no longer receive it
console.log(event.object.name, event.distance, event.point, event.instanceId);
}}
onPointerOver={(event) => {
event.stopPropagation();
setHovered(true);
}}
onPointerOut={() => setHovered(false)}
onPointerDown={(event) => {
event.target.setPointerCapture(event.pointerId); // this pointer's events reach the mesh off its surface until release
}}
onPointerUp={(event) => {
event.target.releasePointerCapture(event.pointerId);
}}
onPointerMissed={() => console.log("clicked elsewhere")}
>
<sphereGeometry args={[0.75, 32, 16]} />
<meshStandardMaterial color={hovered ? "#e8b4a0" : "#42699c"} />
</mesh>
);
}
ポインターイベントは、どのレンダラーでも動作します。デフォルトのレイキャスターは intersectObject(object, true) で CPU ジオメトリをテストします。調整するには raycaster={{ params: { Line: { threshold: 0.1 } } }} を渡します。
アセットの読み込み
読み込みフックは処理を一時停止します。境界をフックより上に置くと、読み込み中はフォールバックが表示されます。
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import { useGLTF, useProgress, Environment } from "@react-three/drei";
function Model({ url }) {
const { scene, animations, nodes, materials } = useGLTF(url);
return <primitive object={scene} />;
}
function Hud() {
const { active, progress, errors } = useProgress(); // tracks three's DefaultLoadingManager
return <div>{active ? `${progress.toFixed(0)}%` : errors.length > 0 ? "failed" : "done"}</div>;
}
export function App() {
return (
<>
<Hud />
<Canvas camera={{ position: [3, 2, 4], fov: 50 }}>
<Suspense fallback={null}>
<Environment preset="city" />
<Model url="model.glb" />
</Suspense>
</Canvas>
</>
);
}
useGLTF(url)は、drei に同梱される GLTFLoader(three-stdlib)を使って読み込みます。Draco と meshopt はデフォルトで有効です。Draco デコーダーは固定された gstatic URL から取得されます(useGLTF.setDecoderPathで変更でき、useGLTF(url, false)で無効にできます)。useGLTF.preload(url)はキャッシュを事前にウォームアップします。戻り値にはscene、animations、camerasと、nodes/materials/meshesの検索マップが含まれます。スキン付きモデルを複数コピーする場合は、コンポーネントごとにuseMemo(() => SkeletonUtils.clone(scene), [scene])で一度クローンします(import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js")。それぞれのクローンを個別の primitive にマウントします。useTexture(input)は、単一の URL、配列(const [a, b] = useTexture([x, y]))、またはキー付きオブジェクトを読み込みます。fiber は props を通じて割り当てられた LDR(RGBA8)のカラーマップ(map、emissiveMap、envMapなど)のカラー空間を sRGB に設定するため、手動でcolorSpaceを指定しなくてもカラーマップが機能し、スカラーマップは NoColorSpace のままです。<Preload all />は、マウント時にすべてのマテリアルを一度コンパイルします(allを指定すると非表示オブジェクトのものも含みます)。また、テクスチャをアップロードするためにシーンをキューブターゲットにレンダリングします。初回表示時の引っかかりをなくす代わりに、最初に一度だけ停止が発生します。アセットの読み込み後に実行されるよう、Suspense 境界の内側に配置してください。そうしないと、読み込み前に実行されます。useProgressは通常のストアなので、上記の Hud は Canvas の外にある DOM から読み取れます。
drei ヘルパー
drei にはほかにも多くのヘルパーが用意されていますが、ここではこのサブセットのみを検証しており、すべてのヘルパーを @react-three/drei からインポートします。
コントロール:OrbitControls は、drei にバンドルされているコントロールクラスを生成し、イベントソース(デフォルトでは Canvas ラッパー)に接続します。ユーザーコールバックの前に毎フレーム更新し、変更時に invalidate() を呼び出すため、frameloop="demand" でも動作します。また、デフォルトでダンピングを有効にします。makeDefault はコントロールを state.controls に保存します(Bounds などのヘルパーがこれを利用します)。regress はドラッグ中に適応型パフォーマンスの値を下げます。Props はコントロールのインスタンスにそのまま渡されます。
import { OrbitControls } from "@react-three/drei";
<OrbitControls
makeDefault
enableDamping
dampingFactor={0.1}
target={[0, 1, 0]}
minDistance={2}
maxDistance={12}
maxPolarAngle={Math.PI / 2}
autoRotate
autoRotateSpeed={0.5}
/>
CameraControls は同じ方法で camera-controls ライブラリをラップします(fitToBox などの ref メソッドはそのライブラリから提供され、フレームのデルタ値を使って更新されます)。
シーンの装飾:
import {
ContactShadows, Environment, Lightformer, Text, Html, Center, Bounds,
} from "@react-three/drei";
export function Dressing({ overlay }) { // overlay: a ref to a DOM element laid over the Canvas
return (
<group>
<Environment resolution={256}>
<Lightformer form="rect" intensity={2} position={[0, 4, 3]} scale={[8, 2, 1]} />
</Environment>
<ContactShadows position={[0, -1, 0]} opacity={0.6} scale={12} blur={2.4} far={4} />
<Bounds fit clip observe margin={1.5}>
<mesh>
<sphereGeometry args={[1, 32, 16]} />
<meshStandardMaterial metalness={0.9} roughness={0.2} />
</mesh>
<Center top position={[2.5, -1, 0]}>
<mesh>
<boxGeometry args={[1, 0.6, 1]} />
<meshStandardMaterial />
</mesh>
</Center>
</Bounds>
<Text position={[0.75, 1.1, 0]} fontSize={0.4} anchorX="center" anchorY="bottom" color="#e8b4a0" outlineWidth={0.02}>
three.js
</Text>
<Html portal={overlay} position={[2.5, 0.2, 0]} center distanceFactor={8} occlude="raycast">
<div>dom overlay anchored to the scene</div>
</Html>
</group>
);
}
ContactShadowsはシーンを下から深度ターゲットにレンダリングし、ぼかします。影マップの設定なしで、床の近くにあるオブジェクトの下に柔らかな接触影の平面を作ります。静止シーンで一度だけ焼き付けるには、frames={1}を設定します。Lightformerは光源カードです。基本マテリアルを使ったメッシュで、formはcircle、ring、rect、boxのいずれかです。反射に焼き付ける<Environment>の子シーン内に配置することを想定しています。Textは SDF テキスト(troika)をレンダリングし、maxWidth、textAlign、lineHeight、letterSpacing、charactersなどのレイアウト用 Props を指定できます。フォントの読み込み中はサスペンドします。Htmlは DOM ノードをシーンに投影します。transformを指定すると、画面に正対するオーバーレイではなく、3D 回転するオーバーレイとしてレンダリングします。occlude(trueまたは"raycast")を指定すると、ジオメトリがアンカーへの光線を遮っている間は非表示になるため、メッシュ内にアンカーを配置しないでください。occlude="blending"は ShaderMaterial を使って深度に応じて非表示にします(WebGL のみ)。fiber 9.8、drei 10.7、React 19.2 では、デフォルトの(WebGLRenderer)Canvas と同じコミットでマウントされるHtmlは空のままになり、Canvas とともに再マウントされる場合も同様です。Dressing のように、Canvas 上に重ねた DOM 要素への ref をportalに渡すと、コンテナを固定できます。Canvas の後に、position: relativeのラッパー内で次のように配置します:<div ref={overlay} style={{ position: "absolute", inset: 0, pointerEvents: "none" }} />Centerは子要素を境界ボックスの中心に再配置します(top/bottom/left/right/front/backは端に揃えます。precise={false}はすべての頂点ではなくオブジェクトごとの境界を測定し、onCenteredはボックスを通知します)。頂点の位置を測定するため、ジオメトリが単位クワッドである drei のTextは誤った位置に配置されます。代わりにanchorX、anchorY、positionで Text を配置してください。Boundsは子要素に合わせてカメラを調整し、フレームに収めます(fit、clipは near/far を調整し、observeはリサイズ時に再調整します)。useBounds()は命令型 API(refresh、reset、moveTo、lookAt、fit、clip)を返します。
パフォーマンス
オンデマンドでレンダリングします。変更がなければ、何もレンダリングされません。
import { useEffect, useRef } from "react";
import { useThree } from "@react-three/fiber";
function OnDemand() {
const ref = useRef();
const invalidate = useThree((state) => state.invalidate);
// under <Canvas frameloop="demand"> imperative changes are invisible to React — request a frame for them
useEffect(() => {
const id = setInterval(() => {
ref.current.rotation.y += 0.5;
invalidate();
}, 100);
return () => clearInterval(id);
}, [invalidate]);
return (
<mesh ref={ref}>
<boxGeometry args={[1, 1, 1]} />
<meshNormalMaterial />
</mesh>
);
}
React の変更(props、state)は自動的に無効化されます。useFrame 内で setState を呼び出さないでください。Quick Start のように、ref を通じてオブジェクトを変更します。DOM をフレームごとの値に追従させる必要があるまれなケースでは、ストアまたは flushSync を使用します。
同じ形状のコピーを多数作成する場合、Instances を使うと 1 つのドローコールを共有できます。各 Instance は宣言的な変換(および省略可能な color)で、毎フレーム、インスタンス化された行列に合成されます。
import { Instance, Instances } from "@react-three/drei";
function Crowd({ count = 200 }) {
return (
<Instances limit={count} castShadow>
<capsuleGeometry args={[0.25, 0.8, 4, 12]} />
<meshStandardMaterial />
{Array.from({ length: count }, (_, i) => (
<Instance
key={i}
position={[(i % 15) - 7, 0.65, Math.floor(i / 15) - 7]}
color={`hsl(${(i * 37) % 360}, 60%, 55%)`}
/>
))}
</Instances>
);
}
Detailed はカメラからの距離(三つの LOD)に応じて子要素のいずれかを選択し、遠方のジオメトリを低コストで描画します。子要素 i は distances[i] の距離から表示され(最初の値は 0)、hysteresis はその距離に対する割合で、より詳細な表示へ戻る切り替えを遅らせます。
import { Detailed } from "@react-three/drei";
<Detailed distances={[0, 15, 40]} hysteresis={0.1}>
<mesh><sphereGeometry args={[1, 64, 32]} /><meshStandardMaterial /></mesh>
<mesh><sphereGeometry args={[1, 16, 8]} /><meshStandardMaterial /></mesh>
<mesh><sphereGeometry args={[1, 8, 4]} /><meshStandardMaterial /></mesh>
</Detailed>
解像度を適応させるには、regress を備えたコントロールで、ドラッグ中に performance.current を min(デフォルトは 0.5)まで下げ、200 ms 後に元に戻します。AdaptiveDpr は、Canvas が最初に解決したピクセル比にその値を掛けてピクセル比を設定するため、ドラッグが終わるたびに最初のピクセル比に戻ります。PerformanceMonitor は実際のフレームレートをサンプリングし、コールバックを通じて報告します。ピクセル比自体は変更しません。Canvas は再レンダリングのたびに dpr prop を再適用し、内部からの setDpr 呼び出しを元に戻すため、React の state を介してコールバックから Canvas の dpr prop を設定してください。また、AdaptiveDpr をその Canvas に含めないでください。次のドラッグで、下げた dpr よりも最初のピクセル比が優先されて復元されます。
import { useState } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, AdaptiveDpr, PerformanceMonitor } from "@react-three/drei";
// lower the resolution while the controls move
export function DragTunedApp() {
return (
<Canvas dpr={[1, 2]}>
<OrbitControls regress />
<AdaptiveDpr pixelated />
{/* scene contents */}
</Canvas>
);
}
// let the measured frame rate pick the resolution through the dpr prop
export function MonitoredApp() {
const [dpr, setDpr] = useState([1, 2]);
return (
<Canvas dpr={dpr}>
<OrbitControls />
<PerformanceMonitor
onDecline={() => setDpr(1)}
onIncline={() => setDpr([1, 2])}
flipflops={3}
onFallback={() => setDpr(1)}
/>
{/* scene contents */}
</Canvas>
);
}
fiber 固有のツール以外にも、dpr を制限し、KTX2 テクスチャを優先し、ジオメトリをマージまたはインスタンス化し、アンマウントするものを破棄してください。詳しくは threejs-performance をご覧ください。
破棄
Fiber はアンマウント時(アイドル時間に実行されるようスケジュールされます)に、自身が作成したものを破棄します。これには、すべての JSX 要素の背後にあるオブジェクト、JSX の子として宣言されたジオメトリ、マテリアル、テクスチャ、および gl props またはファクトリーから生成したレンダラーが含まれます。props 経由で渡されたオブジェクト(geometry={...}、map={...})とプリミティブは破棄されません。また、dispose={null} を指定すると、その要素と子要素が破棄の対象外になります。渡したレンダラーのインスタンス自体は破棄されませんが、Fiber はアンマウント時にそのコンテキストを強制的に失わせるため、再利用できません。useLoader、useGLTF、useTexture を通じて読み込んだアセットは再マウント時のためにキャッシュに保持されます。フックの clear はキャッシュエントリーを削除するだけなので、不要になったジオメトリ、マテリアル、テクスチャは自分で破棄してください。モジュールレベルの dispose(object) エクスポートは、オブジェクトと、dispose() を持つそのプロパティ(メッシュのジオメトリやマテリアルなど)を破棄します。子要素にはアクセスしないため、ツリーを自分でトラバースしてください。
WebGPU と TSL
gl プロパティには非同期ファクトリーを渡します。fiber から渡される props を使って WebGPURenderer を構築し、初期化を待ってから返します。fiber は Canvas 内でこの Promise の解決を待ち、解決後に子要素をレンダリングします(Canvas の周囲に Suspense のフォールバックを置いても、その間は表示されません)。Canvas はプレーンな three 名前空間(ノードマテリアルを含みません)をもとに要素カタログを作成するため、ノードマテリアルの要素を使う前に extend で three/webgpu 名前空間をマージします。three/tsl の TSL ノードも同じ方法で props に渡せますが、マテリアルがノードをコンパイルするのは一度だけです。ノードは一度だけ(モジュールスコープまたは useMemo で)構築し、uniform() の値を使ってアニメーションさせてください。後続のレンダーで新しいノードを渡しても、material.needsUpdate = true を設定するまでは再コンパイルされずに割り当てられるだけです。
import * as THREE from "three/webgpu";
import { color } from "three/tsl";
import { Canvas, extend, useFrame } from "@react-three/fiber";
import { useRef } from "react";
extend(THREE); // node materials and WebGPU classes join the element catalogue
function Knot() {
const ref = useRef();
useFrame((_, delta) => {
ref.current.rotation.y += delta;
});
return (
<mesh ref={ref}>
<torusKnotGeometry args={[0.8, 0.3, 128, 24]} />
<meshBasicNodeMaterial colorNode={color(0x42699c)} />
</mesh>
);
}
export default function App() {
return (
<Canvas
camera={{ position: [3, 2, 4], fov: 50 }}
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props);
await renderer.init();
return renderer;
}}
>
<color attach="background" args={["#111827"]} />
<Knot />
</Canvas>
);
}
fiber はファクトリーのレンダラーにもカラーのデフォルトを適用します。ACESFilmicToneMapping(flat の場合は NoToneMapping)と sRGB 出力です。WebGPURenderer は背景も含めてフレーム全体にトーンマッピングを適用するため、同じ <color attach="background"> でも、Canvas が flat でない限り WebGLRenderer より暗くレンダリングされます。
WebGPURenderer の WebGPU バックエンドおよび forceWebGL: true でレンダリングして確認済みです(fiber 9.8.2、drei 10.7.9)。両方のバックエンドで動作します。シーングラフ、フック、イベント(レイキャスターは CPU ジオメトリをテストするため、positionNode による変位は認識されません)、useGLTF(読み込まれたマテリアルはノードマテリアルに変換されます)、useTexture、Preload、Center、Bounds、Instances、Detailed、OrbitControls、CameraControls、occlude="blending" を指定しない Html、および preset、map、または .hdr ファイルを使う Environment です。useProgress、AdaptiveDpr、PerformanceMonitor はレンダラー API を呼び出しません(ソースで確認済み)。
WebGPU と互換性がないもの:Text はグリフではなく、テクスチャのない四角形を 1 つ描画します(troika が onBeforeCompile 経由で SDF シェーダーを挿入します)。ContactShadows(onBeforeCompile を使う深度マテリアルと、three が互換性なしと報告するブラー用の ShaderMaterial)、occlude="blending" を指定した Html、ground または .jpg/.webp のゲインマップ files を使う Environment(どちらも ShaderMaterial で描画されます。ソースで確認してください)、そして子要素を持つ Environment です。これはキューブカメラを使ってベイクするため、WebGPU バックエンドでは反射が表示されますが、WebGL 2 のフォールバックではエラーなしに黒く表示されます。TSL のマテリアルスロット(colorNode、positionNode)の動作は threejs-shaders とまったく同じです。GLSL ではなく Fn コードを書いてください。
よくある間違い
requestAnimationFrame(tick)→useFrame((state, delta) => { ... }):Canvas はすでにフレームループを実行しているため、別のループを動かすと競合し、その効果も迂回してしまいます。state.clock.getElapsedTime()→state.clock.elapsedTime:getElapsedTime() は内部で getDelta() を呼び出すため、次のフレームで fiber が計測する delta が短くなります。import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"→import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js":three/addons/はバンドラーと three.js がドキュメントで案内しているインポートマップの両方で解決されますが、examples/jsmのパスはそれらのインポートマップでは機能しません。useFrame(() => { ref.current.rotation.y += 0.01; })→useFrame((_, delta) => { ref.current.rotation.y += delta; }):第 2 引数はフレームの delta です。固定値を加算すると、リフレッシュレートによって速度が変わります。useFrame((_, delta) => setRotation((r) => r + delta))→useFrame((_, delta) => { ref.current.rotation.y += delta; }):フレームループ内で state を設定すると、毎フレーム React が再レンダリングされます。ref を使ってオブジェクトを変更してください。useFrame((state, delta) => { ref.current.rotation.y += delta; }, 1)→useFrame((_, delta) => { ref.current.rotation.y += delta; }):正のレンダリング優先度を指定すると Canvas のレンダリングが停止します。コールバック内でstate.gl.render(state.scene, state.camera)を呼び出さない限り、シーンは真っ黒になります。<mesh position={{ x: 1, y: 0, z: 0 }}>→<mesh position={[1, 0, 0]}>:positionは Vector3 を保持する読み取り専用プロパティです。そのため、プレーンオブジェクトを指定すると「Cannot assign to read only property」エラーが発生して Canvas が失敗します。一方、配列、数値、Vector3 は既存の Vector3 に書き込まれます(fromArray、setScalar、copy)。useEffect(() => { new GLTFLoader().load(url, (gltf) => setScene(gltf.scene)); }, [])→const gltf = useGLTF(url):useGLTF はサスペンドし、再マウント時や事前読み込み用に URL ごとに解析済みモデルをキャッシュし、Draco および meshopt のデコードを設定します。一方、effect を使う方法ではマウントのたびに再取得と再解析が行われ、Draco デコーダーもありません。useGLTF(url).scene.clone()→useMemo(() => SkeletonUtils.clone(scene), [scene]):Object3D.clone() は同じスケルトンを共有するため、コピーは元のボーンにバインドされたままになります。一方、SkeletonUtils はコピーごとに再バインドし、useMemo はレンダリングのたびではなくコンポーネントごとに 1 つのコピーを保持します。scene.add(gltf.scene)→<primitive object={gltf.scene} />:命令的に追加したオブジェクトは fiber のイベントプロパティを受け取らず、コンポーネントのアンマウント後もシーンに残ります。一方、primitive はイベントプロパティを受け取り、コンポーネントとともに追加・削除されます(破棄処理は引き続き自分で行います)。<water args={[geometry]} />→extend({ Water }); <water args={[geometry]} />:要素は three の名前空間を参照して解決されるため、アドオンのクラスは事前に登録する必要があります。<line>→<threeLine>:TypeScript ではタグ名が SVG の line 要素と衝突します。fiber は先頭のthreeを取り除き、THREE.Line を構築します。<Canvas shadows>→<Canvas shadows="percentage">:ブール値を指定すると PCFSoftShadowMap が選択されますが、r182 では非推奨です。three は警告をコンソールに表示して PCF にフォールバックします。文字列を指定すると、警告なしで種類を選択できます。<directionalLight intensity={2} />→<directionalLight castShadow intensity={2} />:メッシュの castShadow が影を描画するには、ライトにも castShadow が設定され、Canvas の shadows フラグも設定されている必要があります。ref.current.position.x += 1→ref.current.position.x += 1; invalidate();:frameloop="demand" では、変更だけでは再描画されません。命令的に変更した後はuseThree((state) => state.invalidate)からinvalidateを呼び出してください。React のプロパティ変更では自動的に無効化されます。gl={new THREE.WebGPURenderer()}→gl={async (props) => { const renderer = new THREE.WebGPURenderer(props); await renderer.init(); return renderer; }}:fiber の canvas を使わずに作成したレンダラーは、表示されない独自の canvas に描画し、init() が完了するまで render() が例外をスローします。一方、ファクトリーには Canvas の canvas が渡され、解決後にのみ fiber がレンダリングします。new OrbitControls(camera, renderer.domElement)→<OrbitControls makeDefault />:drei コンポーネントは、フレームごとの更新、無効化、破棄を設定します。生のクラスは、減衰が有効な状態では停止し、demand ループも無効化しません。<Environment><Lightformer /></Environment>→<Environment files="env.hdr" />:WebGPURenderer では、children 形式がキューブカメラを使ってベイクされますが、WebGL 2 のフォールバックバックエンドでは反射がエラーなしで真っ黒になります。一方、.hdrファイルとプリセットはテクスチャを割り当てるだけなので、どちらのバックエンドでも機能します。
関連項目
threejs-fundamentals- Canvas が管理するレンダラー、シーン、カメラの概念threejs-loaders- ローダーの設定、圧縮、破棄について詳しく解説しますthreejs-interaction- fiber のポインターイベント以外のレイキャスティングとコントロールthreejs-shaders- fiber 要素内で使用される TSL ノードマテリアルとスロットthreejs-performance- fiber の適応型ツール以外のプロファイリングと最適化threejs-asset-pipeline- gltfjsx コンポーネントと、useGLTF がモデルを読み込む前の圧縮