Three.jsとは?WebGLを抽象化して3D表現を加速する実践フレームワーク入門
Three.jsでポストプロセスに手を出すと、最初は「Bloomを足すとそれっぽい」「SSAOで立体感が増える」といった即効性に目が行きます。ただ、実装が育つほど効いてくるのは個別エフェクトの知識よりも、パイプライン全体を「パスの連鎖」として扱えるかどうかです。入力と出力が鎖状に依存し、深度・色空間・解像度の前提が一つずつ増えていく構造は、放置すると調整も性能も運用も崩れやすくなります。
実務で事故が多いのは、効果が弱いからではなく「どの段階の画像に対して調整しているか」が揃っていない状態です。トーンマッピングの前後で閾値の意味が変わり、線形とsRGBが混ざり、UI合成の位置が曖昧なままAAだけ強くする、といった小さなズレが連鎖して、数値調整が収束しなくなります。連鎖の前提と順序を仕様として固定できると、同じ数値が同じ意味を持ち、比較が成立して調整が一気に速くなります。
また、ポストは「盛るほど重くなる」よりも「重くなる理由が見えないまま盛られる」ことが問題になりがちです。フルスクリーン回数、解像度、サンプル数、深度依存の増加がどう積み上がるかを言語化できると、対策が「全部弱める」一択になりません。内部解像度の段階化、パス別解像度、プリセットで落とす順序、フォールバックといった逃げ道が設計として持てるようになります。
本記事は、EffectComposerを「便利な箱」として使うのではなく、中間バッファの流れ・パスの契約・順序・解像度・深度前提・UI合成・運用監視までを一本の設計として整理します。狙いは、エフェクトを増やすことではなく、増えても破綻しない基準線を作ることです。結果として、追加も撤去も比較も短くなり、画づくりの試行回数そのものが増える状態を目指します。
1. Three.jsとは
Three.jsを捉えるときに大切なのは「WebGLを隠す便利ツール」ではなく「WebGLを現実の開発工程へ接続する翻訳層」だと理解することです。翻訳層には、数学や状態管理の吸収だけでなく、アセット読み込み、PBR、影、アニメーション、ポストプロセスといった周辺の仕組みも含まれます。つまりThree.jsは「3Dを描く」だけでなく「3Dを作り、配り、保守する」ための骨格を提供します。
Three.jsの全体像が掴めると、導入時に迷いやすい論点(WebGL直書きとの棲み分け、抽象化の限界、最適化の方向性)が整理されます。以降の章で扱うSceneやRendererの話も、単なる用語説明ではなく、責務分離と運用のための設計単位として読めるようになります。
1.1 Three.jsの役割
Three.jsの第一の役割は、3Dシーンを構築するための「共通言語」を提供することです。Sceneにオブジェクトを配置し、Cameraで切り取り、Rendererで描くという流れは、WebGLの低レベルな状態操作を、意図が伝わる構造へ変換します。これにより、チーム内で「何を変えたのか」「どこに影響が出るのか」を共有しやすくなり、属人化しやすい3D開発を組織開発へ寄せられます。
第二の役割は、周辺機能を含めた「実務の道具箱」を揃えることです。glTF(GLTFLoader)、PBR(StandardMaterial)、環境マップ(PMREM)、圧縮テクスチャ(KTX2)、アニメーション(AnimationMixer)、ポストプロセス(EffectComposer)、WebXRなど、Webの3Dで避けられない要素を同じ思想で扱えます。個別に寄せ集めると統合で破綻しやすい領域を、最初から統合されやすい形にしている点が、短期開発だけでなく長期運用でも効きます。
1.2 Three.jsとWebGLの関係
Three.jsはWebGLの上で動きます。SceneやMaterialといった高レベルの表現は、最終的にWebGLRendererによって、バッファ(VBO/IBO)、テクスチャ、シェーダプログラム、そしてドローコールへ変換されます。つまりThree.jsを使うほど、直接WebGLを書かない場面が増える一方で、性能や互換性の制約はWebGLの制約として残り続けます。この関係を理解しておくと、抽象化の外に出るべき領域(独自シェーダや特殊最適化)も判断しやすくなります。
WebGL直接実装とThree.jsの違いは「自由度」ではなく「責務の置き場」に表れます。WebGL直書きは最小限の薄いレイヤーで自由に組めますが、その自由は「設計の負担」を自分で背負うことでもあります。Three.jsは逆に、よくある責務を既定の構造へ寄せる代わりに、その構造の前提(レンダリング順序やマテリアルの概念)に合わせる必要が出ます。
| 観点 | WebGL直接実装 | Three.js |
|---|---|---|
| 開発速度 | 下層から積むため遅くなりやすい | 概念が揃っており速い |
| 設計負担 | 状態・リソース管理を自前で背負う | 多くをフレームワークへ寄せられる |
| 拡張の自由度 | 非常に高いが検証コストも高い | 拡張点があり、段階的に深掘りしやすい |
| チーム共有 | 設計がないと属人化しやすい | 共通言語ができ、レビューしやすい |
| 実務の落とし穴 | デバッグと運用まで含めて地獄になりやすい | 抽象化の前提を崩すと詰まりやすい |
1.3 なぜThree.jsが必要とされるのか
Three.jsが必要とされる理由は、WebGLの難しさが「描画の難しさ」よりも「維持の難しさ」に現れるからです。プロダクトでは、機能追加に伴いオブジェクト数が増え、アセット種類が増え、演出が増え、端末差が増えます。ここで状態管理やリソース寿命の設計が曖昧だと、変更のたびに別箇所が壊れ、性能が不安定になり、最終的に触れない領域が増えます。Three.jsはその劣化を抑えるための「構造」を提供します。
さらに、Three.jsはエコシステムの価値も大きいです。glTF、PBR、KTX2、WebXRといった標準や現実解が、サンプルや実装として共有されやすく、試行錯誤の再発明を減らせます。つまり、Three.jsは学習の近道であると同時に、実務での意思決定を「確率的に正しい方向」へ寄せる道具でもあります。
2. Three.js基本構成要素
Three.jsの基本構成要素は、Scene・Camera・Renderer・Meshの4つで整理できます。この分類は単なる学習用の枠ではなく、実務で問題を切り分ける枠としてそのまま使えます。描画が崩れたとき、重くなったとき、意図と違う見え方になったとき、どの層の責務かを分けられるだけで調査が速くなります。
ここでは表を使わずに文章で概念を固めます。用語の定義が曖昧なまま進むと、最適化の議論が「雰囲気」で流れやすく、運用の議論が「誰が直すか」で止まりやすいです。最初に責務の輪郭を揃えておくことが、後半の実務パートの理解を支えます。
2.1 Scene
Sceneは、描画対象となる世界のコンテナです。MeshやLight、Groupなどのオブジェクトを階層構造として保持し、親子関係による座標変換(トランスフォームの合成)を成立させます。描画はRendererが行いますが、Rendererが何を描くかはSceneに入っているものと可視性の判定に依存するため、Sceneは「入力データの集約点」として非常に重要です。
実務では、Sceneを「何でも入れる箱」にすると、後から破綻しやすくなります。デバッグ可視化、影専用の簡略オブジェクト、UIっぽい3Dオーバーレイなどが混ざると、レンダリング順序やレイヤーが絡み、意図しない副作用が増えます。複数Sceneを用途別に分ける、レイヤーを設計する、グルーピングで責務をまとめるといった整理を先に入れておくと、成長しても扱いやすい構造になります。
2.2 Camera
Cameraは、Sceneをどの視点からどう投影するかを決めます。Perspective(透視)かOrthographic(平行)かで見え方が根本的に変わり、ユーザー体験にも直結します。さらに、Cameraのnear/farは深度バッファの精度を左右し、シャドウやSSAOなど深度に依存する機能の品質に影響します。カメラは「見た目の中心」であると同時に「深度系の前提」でもあります。
実務では、カメラ設計が後回しになると、アセットや演出が固まった後に「深度が汚い」「影が破綻する」「酔う」といった形で戻りが発生します。FOV、距離、操作制限(OrbitControlsの範囲)を仕様として決め、体験が安定する範囲に収めると、後からの調整が短くなります。カメラは自由に動かせるほど良いわけではなく、意図した体験へ寄せるほど強くなります。
2.3 Renderer
Rendererは、SceneとCameraを入力として、GPUへ描画命令を発行する翻訳器です。WebGLRendererは、Materialからシェーダを準備し、ジオメトリのバッファをバインドし、テクスチャや状態を切り替え、ドローコールを積み上げます。この翻訳の量がそのままCPU負荷にもなるため、Rendererは性能設計の中心になります。Sceneが大きくなるほど、Rendererが行う「準備」が重くなります。
実務では、Renderer設定(DPR、色空間、トーンマッピング、シャドウ、ポストプロセス)が品質と負荷の両方を決めます。ここを固定値で持つと端末差で破綻しやすく、プリセット(Low/Med/High)や段階フォールバックへ落とすと運用が安定します。Rendererは「最後に描く関数」ではなく「運用戦略を実装する場所」として扱うと、後からの改善が現実的になります。
2.4 Mesh
Meshは、Geometry(形)とMaterial(見た目)を組み合わせた描画の主役です。Sceneに配置され、座標変換、可視性、レイヤーなどの情報も持ちます。3D開発の作業の多くは、Meshをどう生成し、どう配置し、どう更新するかに集中します。glTFから読み込む場合も、最終的にはMesh群としてSceneへ展開されます。
実務で詰まりやすいのは、Meshが増えるほどドローコールが増え、CPU側の準備が支配的になる点です。InstancedMesh、マテリアル共有、ジオメトリ結合、LOD、カリングといった最適化は、結局「Meshをどう扱うか」の設計へ収束します。Meshは見た目の単位であると同時に、性能の単位でもあるため、増え方を設計できるほど強くなります。
3. Three.js最小描画コード
最小の動くコードは、Three.jsの抽象化がどこに価値を持つかを体感する近道です。SceneにMeshを置き、Cameraで切り取り、Rendererで描くという流れは、Three.jsの中心思想であり、以降の機能(ライト、テクスチャ、アニメーション、ポスト)もこの流れに差し込まれていきます。まず骨格を自分の目で確認すると、後の章が「どこに何を足す話なのか」見えるようになります。
ここでは立方体を描画し、リサイズ同期とアニメーションループまで含めた最小構成を示します。実務ではビルド環境(Vite等)やアセット読み込みが入りますが、まずはレンダリングの最小単位が回ることを優先します。
import * as THREE from 'three'
const canvas = document.querySelector('#c')
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x101014)
const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100)
camera.position.set(0, 1.2, 3)
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({
color: 0x4aa3ff,
roughness: 0.45,
metalness: 0.1,
})
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
const light = new THREE.DirectionalLight(0xffffff, 2.0)
light.position.set(2, 3, 2)
scene.add(light)
function resize() {
const w = canvas.clientWidth
const h = canvas.clientHeight
const dpr = renderer.getPixelRatio()
const rw = Math.floor(w * dpr)
const rh = Math.floor(h * dpr)
if (canvas.width !== rw || canvas.height !== rh) {
renderer.setSize(w, h, false)
camera.aspect = w / h
camera.updateProjectionMatrix()
}
}
function tick(t) {
resize()
cube.rotation.y = t * 0.001
cube.rotation.x = t * 0.0007
renderer.render(scene, camera)
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
この最小コードでも、Three.jsの実務的な性質が見えます。描画は毎フレーム行われますが、負荷は「描画そのもの」だけでなく、リサイズ同期、DPR、ライト、マテリアルの選択などの前提によって大きく変わります。つまり、最小構成の段階から「設定がコスト構造を決める」ことが分かります。
また、ここで作った骨格は、後からそのまま拡張できます。glTFを読み込むならcubeの代わりにシーングラフを追加し、ポストを入れるならrenderer.renderをcomposer.renderへ置き換えます。Three.jsは「差し替えやすい骨格」を最初から持っているため、実験を積み重ねても構造が保たれやすい点が強みです。
4. Three.jsジオメトリとバッファ管理
Three.jsのGeometryは、WebGLの頂点バッファ操作を、実務の粒度へまとめたレイヤーです。プリミティブ(BoxGeometryなど)で素早く形を作ることもできますし、BufferGeometryを使って低レベルに制御することもできます。性能と表現が衝突する場面では、BufferGeometryの理解がそのまま判断材料になります。
ここでは、BufferGeometryの考え方、attribute構造の整理、カスタムGeometryでの実務上の注意を掘ります。特にattributeの整理は、シェーダ拡張や最適化と直結するため、曖昧なままだと後で詰まりやすくなります。
4.1 BufferGeometry
BufferGeometryは、頂点データをTypedArrayとして保持し、GPUへ効率的に転送するための形式です。positionやnormal、uvなどが属性として明示され、WebGLの考え方に近い構造になっています。Three.jsの多くの最適化手法(Instancing、カスタムシェーダ、LOD)はBufferGeometry前提で進みます。
実務で重要なのは、Geometryを「生成して捨てる」扱いにしないことです。毎フレーム新しいBufferGeometryを作ると、GC負荷とGPU転送が増え、性能が崩れやすくなります。更新が必要ならattributeの中身を更新する、更新頻度を落とす、遠距離はLODで止めるなど、更新を設計に含めると安定します。Geometryは形状資産であると同時に、転送コスト資産でもあります。
4.2 attribute構造
attributeは、頂点ごとに並ぶデータ列です。positionは座標、normalは法線、uvはテクスチャ座標、indexは頂点参照を表します。これらが揃うことで、ライティングやテクスチャが成立し、さらにPBRの見た目が安定します。indexがあると、同じ頂点を面ごとに再利用でき、転送量と頂点処理負荷を抑えられます。
attributeの整理が実務で効くのは、不要なものを持たない判断ができるからです。例えば、単色でライティング不要ならnormalやuvを省ける場合があります。逆にPBRで品質を出したいならnormalと環境の整合が必要になります。何を持つかは「表現の仕様」でもあり「性能の仕様」でもあります。
| attribute | 役割 | 典型用途 | 実務での注意 |
|---|---|---|---|
| position | 頂点座標 | 形状の基礎 | itemSizeの不整合で破綻しやすい |
| normal | 法線 | ライティング | 変形時は再計算が必要な場合がある |
| uv | テクスチャ座標 | 画像マッピング | シーム設計が悪いと継ぎ目が目立つ |
| index | 頂点参照 | 面構成 | 無いと頂点が増えやすく負荷が増える |
| color(任意) | 頂点色 | 色差分 | instancingと組み合わせると強い |
4.3 カスタムGeometry
カスタムGeometryは、必要な形状を自前で生成し、BufferGeometryへ流し込む設計です。データ可視化、手続き生成、独自メッシュ(道路、地形、線表現)など、既存プリミティブで足りない領域でよく出ます。Three.jsはattribute追加を自然にサポートしており、独自属性をShaderMaterialで読む構成も取りやすいです。
実務では、カスタムGeometryを作れることより、更新と再利用が設計できることが重要になります。毎フレーム作り直すとGCと転送がボトルネックになりやすく、結果として「動くが重い」になります。差分更新、更新頻度の制御、LOD、インスタンシングなどを併用して、コストの増え方を段階的に抑えると、カスタムの価値を保ったまま運用できます。
5. Three.jsマテリアルとシェーダー
Three.jsのマテリアルは「見た目」の抽象化ですが、内部的にはシェーダ生成とレンダリング状態の集合です。つまり、マテリアル選択は表現選択であると同時に、プログラム数や状態切替の増え方を決める性能選択でもあります。便利なStandardMaterialを多用すると品質は上がりますが、前提(環境・色空間・ライト)が揃わないと調整が迷走しやすく、逆にShaderMaterialを多用すると保守と運用が重くなります。
ここでは代表マテリアルの性格差を比較し、ShaderMaterialの使い所と、onBeforeCompileによる差分拡張を整理します。完全自作に飛びつく前に「既存を活かして足りない部分だけ足す」判断ができると、現場では強いです。
5.1 MeshBasicMaterial
MeshBasicMaterialはライティングの影響を受けず、色やテクスチャをそのまま出します。これにより、ライト設定や環境の影響を切り離して確認できるため、デバッグや基準確認に向きます。意図した形状やUVが正しいか、ロードしたテクスチャが合っているかを切り分ける場面で、Basicは非常に有用です。
実務では、Basicが「軽い」ことも重要です。ライティング計算を省けるため、UI的な3D表現や、背景の装飾など、立体感より読みやすさを優先する箇所で効きます。全てをPBRで統一するとコストが増え、逆に必要な箇所へコストを回せなくなるため、Basicを適材適所で使える設計は性能の余裕を作ります。
5.2 MeshStandardMaterial
MeshStandardMaterialはPBR(物理ベースレンダリング)を前提にした、実務で最も使われるマテリアルの一つです。roughness/metalnessを中心に、ライトと環境マップと整合した見た目が得られます。glTF標準とも相性が良く、アセット制作パイプライン(DCCツール)からWebへ持ち込むときに、意図が崩れにくい利点があります。
一方で、Standardは前提が多いです。環境マップが無い、露出が合っていない、色空間が混ざっている、ライトの方向が曖昧といった状態だと「地味」「暗い」「素材が嘘っぽい」などのズレが起きやすくなります。実務では、マテリアル単体で頑張るより、環境(IBL)と露出・トーンを先に固め、Standardが気持ちよく動く土台を作る方が調整が短くなります。
5.3 ShaderMaterial
ShaderMaterialは頂点・フラグメントシェーダを自前で記述でき、特殊表現や独自データを扱えます。例えば、データ可視化で独自属性を色へ反映する、手続きノイズで表面を揺らす、トゥーン表現や独自ライティングを実装するなど、Standardでは届かない領域で強いです。抽象化を部分的に外せるため、表現の自由度は最大になります。
ただし実務では、ShaderMaterialが増えるほど運用が重くなります。バリアント管理、uniform更新、デバッグ手順、端末差の扱い、そしてプログラム数の増加が積み上がるからです。ShaderMaterialを使う場合は「Standardでは足りない理由」を明確にし、シェーダを小さく保つ設計が効きます。自由度は、制御できる範囲に閉じて初めて価値になります。
| マテリアル | ライティング | 得意 | コスト感 | 実務の注意 |
|---|---|---|---|---|
| MeshBasicMaterial | 無し | UI/デバッグ/軽量表現 | 低 | 立体感は別要素で補う必要がある |
| MeshStandardMaterial | PBR | glTF/PBR/一般実務 | 中〜高 | 環境・露出・色空間が揃わないと破綻しやすい |
| ShaderMaterial | 自前 | 特殊表現/独自属性 | 幅広い | バリアントとデバッグが負債化しやすい |
| MeshPhongMaterial | 簡易 | 軽量な光沢 | 中 | PBRと混在すると質感が揃いにくい |
5.4 onBeforeCompile拡張
onBeforeCompileは、Three.jsが生成するシェーダへ差分を注入する拡張点です。StandardMaterialのPBRやライト計算を維持しつつ、一部の挙動だけ変更したい場面で特に強いです。完全にShaderMaterialへ移行すると、ライトや影、環境との整合を自前で背負う必要が出ますが、差分注入ならその負担を抑えられます。
実務では、差分注入は小さく局所的に保つと安定します。文字列置換の対象がThree.jsのバージョンで変わる可能性があり、大規模な差分はアップデート時に壊れやすいからです。注入箇所を限定し、必要なuniformだけ追加し、可視化やテストで回帰を検知できる状態を作ると、安全に拡張できます。
const mat = new THREE.MeshStandardMaterial({ color: 0xffffff })
mat.onBeforeCompile = (shader) => {
shader.uniforms.uTime = { value: 0 }
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
transformed.y += sin(uTime + position.x * 2.0) * 0.05;
`
)
mat.userData.shader = shader
}
// ループ内で更新する場合
// mat.userData.shader.uniforms.uTime.value = t * 0.001
6. Three.jsカメラ制御
カメラは見え方を決めるだけでなく、操作感、深度精度、演出の成立条件を同時に支配します。Three.jsではPerspectiveとOrthographicの両方が自然に扱え、Controlsやマルチカメラも揃っています。カメラが安定すると、アセットの見え方が揃い、深度系エフェクトも破綻しにくくなり、結果として運用が楽になります。
ここではカメラの種類の比較、OrbitControls、行列の捉え方、リサイズ同期、マルチカメラ設計を整理します。数値の選び方が「好み」ではなく「仕様」になっていく感覚が掴めると、プロダクトでは強いです。
6.1 PerspectiveCamera
PerspectiveCameraは遠近感が出る透視投影で、一般的な3D体験の中心です。FOVが広いとダイナミックですが歪みが増え、狭いと圧縮され落ち着きます。操作感や酔いにも影響するため、FOVはデザイン仕様と同等に扱うのが安全です。プロダクトビューアなら落ち着いたFOV、ゲーム的体験なら広めのFOV、といった形で目的と合わせます。
near/farは深度バッファ精度に直結し、シャドウやSSAOの品質を左右します。farを広げすぎる、nearを小さくしすぎると精度が落ち、深度系が汚れます。見せたい範囲を欲張るほど品質が落ちやすいので、見せる範囲を設計として絞り、必要ならシーン分割やLODで補う方が、品質と性能が両立しやすくなります。
6.2 OrthographicCamera
OrthographicCameraは距離による縮小が起きない平行投影で、建築の俯瞰やデータ可視化、UI的3Dに向きます。サイズが安定するため、情報の可読性を優先する場面で強く、Perspectiveで起きやすい歪みや誤読を抑えられます。見せたい情報が「形の正しさ」ならOrthographicは有力です。
一方で平行投影は奥行きの手がかりが弱くなるため、ライティングや影、色のコントラストで立体感を補う必要が出ます。さらに、表示範囲(left/right/top/bottom)をリサイズに合わせて更新する設計が必須です。ここが曖昧だと、ウィンドウサイズが変わった瞬間に見え方が崩れ、操作感が不安定になります。
| 観点 | Perspective | Orthographic |
|---|---|---|
| 見え方 | 遠近感が自然 | サイズが安定 |
| 得意 | 没入感・自然な3D | 俯瞰・可視化・UI |
| 調整レバー | FOV、near/far | 表示範囲、ズーム |
| 注意点 | 深度精度設計が重要 | リサイズ同期が必須 |
6.3 OrbitControls
OrbitControlsは、ターゲットを中心に回転・ズーム・パンできる操作で、ビューアや検証で非常に有効です。短時間で「触れる3D」を成立させられ、ユーザーにも直感的です。プロトタイプで価値を出しやすいのは、操作が整っているからです。
本番では、自由度をそのまま出すより制限を設計します。角度の範囲、距離の範囲、パンの可否を決めると、迷いにくく、UIやコンテンツと衝突しにくくなります。操作の自由は体験の自由と一致しないことが多く、意図した視点範囲へ寄せるほど体験は安定します。
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0.8, 0)
controls.enableDamping = true
controls.minDistance = 1.2
controls.maxDistance = 6.0
controls.maxPolarAngle = Math.PI * 0.48
6.4 カメラ行列構造
Three.jsは内部的に、モデル行列(オブジェクト)、ビュー行列(カメラ)、投影行列(レンズ)を合成してクリップ空間へ変換します。普段は意識せずとも動きますが、シェーダ拡張、深度系エフェクト、レイキャスト精度、カメラ同期などに踏み込むと、この構造が理解の土台になります。空間の混同は、原因不明の破綻を生みやすいからです。
実務で効くのは「どの値がどの空間にいるか」を揃える癖です。ワールド空間の位置をビュー空間として扱う、深度を線形と非線形で混同する、といったズレがあると、SSAOやDOFが壊れたり、デバッグが難しくなったりします。Three.jsの提供する行列を必要な場面だけ正しい意味で使えるようになると、拡張が安全になります。
6.5 アスペクト同期
リサイズは必ず起きます。PerspectiveCameraはaspectの更新とprojectionMatrixの再計算が必須で、renderer.setSizeも合わせて更新する必要があります。リサイズ処理が散らばると更新漏れが起きやすいため、テンプレ化して一箇所へ寄せると運用が楽になります。ポストプロセスやマルチビューが入るほど、更新漏れの影響が大きくなるためです。
DPRや内部解像度もリサイズと一緒に扱うと安定します。端末差が大きいWeb環境では、DPRを無制限に上げるとコストが跳ねます。上限を設け、内部解像度を段階化し、同じ方針でrendererとcomposer(導入する場合)を更新できるようにすると、性能と見た目が揺れにくくなります。
6.6 マルチカメラ設計
マルチカメラは、ミニマップ、分割ビュー、鏡面、UI専用投影などで使われます。Three.jsではviewportとscissorを使うことで、同一キャンバスに複数視点を描画できます。視点が増えるほど描画回数が増えるため、負荷は素直に増えます。つまり、マルチカメラは表現の幅とコストの幅を同時に広げます。
実務では、どのカメラが「正」で、どのカメラが補助かを決め、入力とUIの責務を分けると破綻しにくくなります。ポストプロセスを全体へかけるのか、カメラごとに別処理にするのかでも構成が変わります。マルチカメラを導入するなら、最初から責務分離と段階設計をセットで持つ方が安全です。
7. Three.jsライティング設計
ライティングは、PBRの成立、陰影の読みやすさ、性能コスト、シャドウ品質を同時に左右します。ライトを増やすほど豪華になるように見えますが、実務では調整が散り、コストが増え、見た目が眠くなることもあります。役割を絞り、主光源と補助のバランスを設計した方が、結果として説得力が出やすいです。
ここでは光源タイプの使い分けを表で整理し、影の有効化と解像度最適化を実務の観点で掘ります。ライト設計は「美しい」だけでなく「安定して同じに見える」ことが価値になります。
7.1 DirectionalLight
DirectionalLightは太陽光のような平行光で、屋外や広域照明に向きます。方向が陰影を決め、位置は見た目に直結しにくいです。シャドウも比較的扱いやすく、シーンの主光源として置くと陰影の読みやすさが一気に上がります。主光源があるだけで、マテリアルの質感が立ち上がりやすくなります。
実務では、DirectionalLightは一つに絞る設計が強いことが多いです。主光源が複数あると影の方向が散って形状の読みが弱くなり、調整も迷走します。主光源の方向を固定し、補助光は控えめに入れると、見た目が安定し、後からの演出追加も破綻しにくくなります。
7.2 PointLight
PointLightは点光源で、ランプや発光体の表現に向きます。距離減衰があるため、局所的な演出が作れます。空間にメリハリを作りたいときに強いですが、数が増えるとコストと調整が増えます。特に影を付けると重くなりやすいため、影は最小限に絞るのが現実的です。
実務では、PointLightは「演出用」として役割を限定すると扱いやすいです。例えば重要オブジェクトの強調、特定エリアの雰囲気作りなど、用途を絞ると調整が収束します。影を付けるライトと付けないライトを分け、影はDirectionalに寄せる、といった役割分担があると性能の予算配分がしやすくなります。
7.3 AmbientLight
AmbientLightは全体を均一に明るくするライトで、陰影を作りません。暗部が潰れるのを防ぐ用途に便利ですが、強くしすぎると陰影が消え、立体感が失われます。最低限の視認性を確保する補助として、控えめに使うのが安定します。
実務では、AmbientLightで暗部を救いすぎると、PBRの質感が平板になり、結果として別の演出を足して補うことになりがちです。環境マップやHemisphereLightで環境の色を整え、Ambientは下支えに留めると、陰影の読みやすさと質感が両立しやすくなります。
7.4 HemisphereLight
HemisphereLightは上方向と下方向で色が分かれるライトで、空と地面の反射を簡易的に表現できます。屋外の自然な雰囲気を作りやすく、Ambientより方向性があるため、陰影を完全には潰しません。全体の色温度を揃える役として使うと、マテリアルの馴染みが良くなります。
実務では、HemisphereLightは「環境色の固定」として価値があります。空色と地面色を適切に設定し、全体が同じ色のルールで照らされる状態を作ると、PBR素材が揃って見えやすくなります。色の土台が揃うほど、後からの演出(Bloomなど)も破綻しにくくなります。
7.5 SpotLight
SpotLightは円錐状に光を当てるスポットライトで、注目点を作る演出に向きます。舞台照明のような強い意図を出せる反面、自然さを保つには調整が必要で、影も含めるとコストが重くなりやすいです。扱いは難しいですが、効果が明確なため、必要な場面に絞ると価値が高いです。
実務では、SpotLightは「ここを見せる」という合意があるときに強いです。漫然と入れると嘘っぽくなりやすく、逆に意図がはっきりしていると、少ない手数で体験を変えられます。数を増やすより、1つを丁寧に整える方が、結果として品質も性能も安定します。
| 光源 | 得意 | コスト感 | 影 | 典型用途 |
|---|---|---|---|---|
| DirectionalLight | 広域・主光源 | 中 | 扱いやすい | 太陽光、屋外 |
| PointLight | 局所演出 | 中〜高 | 条件付き | ランプ、発光 |
| AmbientLight | 底上げ | 低 | 無し | 暗部救済 |
| HemisphereLight | 環境色 | 低〜中 | 無し | 空と地面の雰囲気 |
| SpotLight | 注目点演出 | 高 | 重い | 舞台照明 |
7.6 ShadowMap有効化
影は立体感に効きますが、コストと品質のトレードオフが厳しい領域です。Three.jsではrenderer.shadowMap.enabled、ライトのcastShadow、オブジェクトのcastShadow/receiveShadowを揃える必要があります。影が出ないときは、どこかのフラグが欠けていることが多いので、最初に「影の前提」をチェックできるようにしておくと切り分けが速くなります。
実務では、影を「全てに付ける」より「必要な箇所へ付ける」方が安定します。キャラクタだけ影を落とす、重要オブジェクトだけ影を落とすなど、対象を絞るとコストが読めます。影は視覚効果が強い分、乱用すると性能を支配しやすいため、運用上は段階設計が必須になりやすいです。
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
light.castShadow = true
cube.castShadow = true
plane.receiveShadow = true
7.7 シャドウ解像度最適化
影の品質はshadow.mapSizeとライトの影カメラ範囲で決まります。mapSizeを上げるほど綺麗になりますが、メモリとGPU負荷が増えます。実務では「影を見せたい範囲」を絞り、影カメラの範囲を狭めて密度を上げる方が、コスト効率が良いことが多いです。影が汚いからmapSizeを上げるのではなく、範囲を詰める方が先に効きます。
さらに、影が必要なライトを絞る、遠距離は影を切る、Lowプリセットでは影を落とすなど、段階設計が運用に効きます。影は品質を上げると一気に重くなるため、プリセットとフォールバックを用意しておくと「重い端末だけ壊れる」状態を避けやすくなります。
8. Three.jsテクスチャとPBR
テクスチャは情報量を増やしますが、メモリと帯域のコストを増やします。PBRでは複数マップが絡むため、色空間、圧縮、環境マップ、露出・トーンの設計が揃って初めて安定した見た目になります。Three.jsはPBRを扱いやすくしていますが、前提が揃わないと「なぜか地味」「質感が嘘っぽい」といった状態になりやすいです。
ここではTextureLoader、環境マップ、PBRマップの役割を整理します。マテリアル単体で頑張るより、環境と色空間を整える方が調整が短くなる点を意識すると、実務では強くなります。
8.1 TextureLoader
TextureLoaderは画像を読み込み、マテリアルへ割り当てます。実務では非同期ロードであり、ロード完了前のプレースホルダや、ロード完了後の更新(needsUpdate)が絡みます。ロード処理が散らばると状態管理が複雑になり、エラー時の扱いも難しくなるため、ローディング戦略(後の章)と一体で扱うと安定します。
テクスチャは「色」と「データ」で色空間の扱いが違います。アルベド(色)はsRGB、roughness/metalness/normalなどのデータは線形、という整理を守ると見え方が安定します。色空間が混ざると「なんとなく変」な状態になり、調整が収束しにくくなります。
const loader = new THREE.TextureLoader()
const albedo = loader.load('/textures/albedo.jpg')
albedo.colorSpace = THREE.SRGBColorSpace
material.map = albedo
material.needsUpdate = true
8.2 環境マップ
PBRで見た目が立ち上がるかは環境マップ(IBL)が大きく支配します。Three.jsではPMREMで環境マップを前処理し、反射のぼけを整合させます。環境がないとPBRは暗く単調に見えがちなので、最初に環境を整えるとマテリアル調整が短くなります。環境は「背景」ではなく「照明の一部」です。
実務では、環境マップは品質とコストの両方に効きます。高解像度HDRは綺麗ですが、ロードとメモリが増えます。プリセットで段階を作り、低性能端末は軽量環境へ落とす設計が現実的です。環境が段階化できると、PBRの価値を保ったまま配布の現実に合わせられます。
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
const pmrem = new THREE.PMREMGenerator(renderer)
new RGBELoader().load('/hdr/studio.hdr', (hdr) => {
const envMap = pmrem.fromEquirectangular(hdr).texture
scene.environment = envMap
hdr.dispose()
pmrem.dispose()
})
8.3 Roughness・Metalness
Roughnessは反射のぼけ、Metalnessは金属度で、PBRの核になるパラメータです。Roughnessが低いほど反射がシャープになり、環境マップの影響が強く出ます。Metalnessが高いほど反射に色が乗り、非金属とは挙動が変わります。ここを理解すると「なぜこの素材がプラスチックっぽいか」を説明しやすくなります。
実務では、Roughness/Metalnessマップはデータとして扱うため線形で扱います。圧縮やガンマ誤りがあると質感が崩れ、調整が迷走します。さらに、チャンネルパック(複数マップを1枚へまとめる)で転送量を抑えるなど、アセットパイプラインの設計とセットで進めると、品質と性能の両立がしやすくなります。
8.4 NormalMap
NormalMapは細かな凹凸を法線として表現し、ポリゴン数を増やさずに情報量を増やします。ライティング反応が変わるため、質感が大きく向上します。一方で、UV品質やタンジェント空間、強度の調整が絡み、破綻も起きやすいです。圧縮方式によってもアーティファクトが出るため、配布設計と切り離せません。
実務では、NormalMapを強くしすぎると嘘っぽくなり、逆に弱すぎると価値が出ません。素材のスケール(現実の大きさ)と合わせて強度を設計すると、自然に収束しやすくなります。NormalMapの価値は大きいですが、雑に扱うと破綻も大きいので、PBRの土台が揃った上で調整するのが安全です。
8.5 AOMap
AOMapは隙間や接地感の陰影を補強し、立体感を出します。SSAOのような画面空間AOより安定して見える一方、UV2が必要になるなどアセット側前提がある場合があります。動的シーンではAOMapだけで足りないこともありますが、静的資産には非常に効きます。
実務では、AOMapを強くしすぎると暗部が潰れ、情報が読めなくなることがあります。露出や環境光とのバランスが必要で、AOを増やすよりライトと環境で整える方が自然に収束します。AOは便利ですが、全体へ波及しやすい要素なので、値の管理をプリセットへ寄せると運用が安定します。
| マップ | 役割 | 色空間 | 実務の注意 |
|---|---|---|---|
| Roughness | 反射のぼけ | 線形 | 圧縮/ガンマ誤りで質感が崩れる |
| Metalness | 金属度 | 線形 | 値の意味が曖昧だと嘘っぽい |
| NormalMap | 凹凸表現 | 線形 | UV品質と圧縮の影響が大きい |
| AOMap | 接地感 | 線形 | 暗部潰れとUV2前提に注意 |
9. Three.jsアニメーション
アニメーションは体験価値を大きく押し上げますが、更新処理が増えるほどCPUとGPUの両方へ負荷が乗り、状態管理も複雑になります。Three.jsはrequestAnimationFrameのループから、AnimationMixer、glTFアニメーション、モーフ、スケルトンまで揃っており、用途別に選べます。実務では「何を動かすか」だけでなく「何を動かさないか」を設計することが重要です。
ここでは、フレームループの基本、AnimationMixer、glTFアニメ、モーフ、スケルトンを整理し、比較表で特性差を掴みます。動きが増えるほど段階設計(遠距離停止、Lowでは簡略)を持っておくと、本番での安定性が上がります。
9.1 requestAnimationFrame
requestAnimationFrameはブラウザの描画タイミングに合わせてループを回す仕組みです。Three.jsではループ内でcontrols.updateやmixer.updateを呼び、最後にrenderします。毎フレーム何を更新しているかが増殖すると、CPU負荷が増えてフレームが不安定になります。つまり、ループは最適化の入口でもあります。
dt(デルタタイム)で動かすと端末差に強くなります。固定ステップが必要ならdtの累積で制御しますが、まずは「時間に比例して動かす」基本を守るだけで、アニメの速度が端末で揺れにくくなります。ループを整理し、更新対象を制御できる構造があるほど、後からの演出追加が怖くなくなります。
const clock = new THREE.Clock()
function tick() {
const dt = clock.getDelta()
controls.update()
mixer?.update(dt)
renderer.render(scene, camera)
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
9.2 AnimationMixer
AnimationMixerは複数のAnimationClipを管理し、ブレンドやクロスフェードを扱います。glTFから読み込んだアニメーションを動かす場面では中心的な仕組みになります。複数キャラクタが出るほどMixerの数が増え、更新コストも増えるため、管理単位を設計する価値が上がります。
実務では、画面外のキャラクタは更新を止める、遠距離はLODで軽いアニメへ切り替えるなど、更新対象を段階化します。全てを毎フレーム更新すると、CPUが支配的になりやすく、描画が軽くても重い状態になります。アニメの価値を守るには、更新の制御が必要です。
9.3 GLTFアニメーション
glTFは実務で最も使われる3D形式で、アニメーションも同梱できます。GLTFLoaderで読み込んだanimationsをMixerへ渡すと、DCCツールで作った動きをそのまま再生できます。制作側の意図が保持されやすく、Webにおける3D制作パイプラインの標準になりやすい理由の一つです。
実務では、アニメーション数が増えるほど管理が難しくなります。不要なクリップを削る、再生するクリップを限定する、プリセットでアニメ品質を落とすなど、運用設計が必要です。アセットに入っているから全部使う、という設計は増殖しやすく、結果として保守が止まります。
9.4 モーフターゲット
モーフターゲットは頂点差分をブレンドして形状を変える方式で、表情や破壊表現などに強いです。スケルトンでは表現しにくい微細変形を実現でき、アーティストの意図を保持しやすい利点があります。見た目の効果が大きい一方で、頂点数×ターゲット数に比例してメモリと負荷が増えます。
実務では、モーフは「必要なものだけ」に絞るほど回りやすいです。表情だけに限定する、遠距離はモーフを止める、LODで頂点数を減らすなど、段階設計が効きます。モーフを増やすと価値も増えますが、コストも増えるため、増やす前に制御の仕組みを持つのが安全です。
9.5 スケルトンアニメーション
スケルトンはボーンでメッシュを変形させる方式で、キャラクタや可動物に向きます。glTF標準であり、Mixerとも自然に接続できます。ボーン数やウェイトの設計、メッシュ分割が品質と性能に影響し、特に低性能端末で効きやすいです。
実務では、ボーン数の上限、遠距離での更新停止、影の簡略化などをプリセットへ落とします。キャラクタが増えるほどアニメと影のコストが増えるため、フレーム時間の予算配分が必要になります。スケルトンは「動くほど魅力が出る」領域なので、段階的に落とせる設計が表現を守ります。
| 方式 | 得意 | コスト感 | 典型用途 | 実務の注意 |
|---|---|---|---|---|
| AnimationMixer | 再生管理・ブレンド | 中 | glTF再生 | 更新対象を制御しないと増殖しやすい |
| glTFアニメ | 制作ツール連携 | 中 | 製品・キャラ | サイズとクリップ数が運用負債になりやすい |
| モーフ | 微細変形 | 中〜高 | 表情 | ターゲット数増でメモリ・負荷が増える |
| スケルトン | 骨格変形 | 中〜高 | キャラクタ | ボーン数と影が支配的になりやすい |
| 手書き更新 | シンプル | 低〜中 | 物理っぽい動き | dt設計が曖昧だと揺れやすい |
10. Three.jsポストプロセス
ポストプロセスは画づくりを加速しますが、レンダリングを多段処理へ変えるため、設計の重要度が上がります。エフェクトを足すほどフルスクリーン処理が増え、DPRや内部解像度の影響が露骨に出ます。実務では、エフェクト単体の知識より、パスの順序と解像度戦略、撤去の容易さが結果を左右します。
ここではEffectComposerの基本構造を文章で固め、代表パスを比較表で整理します。ポストは「盛る」より「制御する」ほど長期運用で価値が残ります。
10.1 EffectComposer
EffectComposerは中間RenderTargetを使い、パスを鎖状につなぐ実行基盤です。Composerは単なる「パス管理」ではなく、バッファ受け渡し(ping-pong)を管理しており、ここを理解すると壊れたときの切り分けが速くなります。特定パスで真っ黒になる、結果が消える、といった現象は、入力と出力の連鎖が原因で起きることが多いからです。
実務では、Composer導入は責務分離の決定でもあります。色補正やAAをどこで行うか、UIは別レイヤーか、深度系はどう前提を揃えるかなど、運用に関わる判断が必要になります。Composerを増殖させないためには、設定を外出しし、プリセットとフラグで管理できる構造を先に作ると安定します。
10.2 RenderPass
RenderPassはSceneとCameraを描画し、パイプラインの土台を作るパスです。深度系を使う場合、DepthTextureの用意やnear/farの設計が前提になります。土台が揃っていないと、後段のSSAOやDOFが破綻し、調整が迷走します。RenderPassは「最初の描画」ではなく「後続の前提を作る工程」です。
実務では、透明物、レイヤー分離、影の扱いなどをRenderPassの土台として整理すると、後段のパスが安定します。例えば、透明物は深度に書かれずSSAOを壊しやすいため、対象を限定する、別パスに分けるなどの割り切りが必要になります。土台を整えるほど、ポストの調整時間が短くなります。
10.3 ShaderPass
ShaderPassは前段の結果(tDiffuse)を入力に、フルスクリーンで加工して出力する汎用パスです。FXAA、色補正、デバッグ可視化など、用途が広い分、増殖しやすいです。増殖するとコストが素直に増えるため、目的と順序を固定し、撤去できる形にすることが運用上重要になります。
実務では、中間結果を可視化するShaderPassを用意しておくと強いです。どの段階で壊れたかが見えるだけで、原因追跡が「推測」から「確認」へ変わります。ポストは壊れ方が複雑になりがちなので、可視化があるだけで改善速度が大きく上がります。
10.4 UnrealBloomPass
Bloomは雰囲気作りに効きますが、白飛びとにじみで破綻しやすいです。strengthを上げる前にthresholdと露出・トーンを整えると収束しやすくなります。低解像度で成立しやすいため、パス別解像度の対象としても扱いやすく、コストを抑えながら「それっぽさ」を出せます。
実務では、Bloomは土台で決まります。色空間やトーンの順序が混ざると、閾値の意味が変わって調整が迷走します。構成を固定し、プリセットで段階を作ると、表現を保ったまま性能を守りやすくなります。Bloomは強い表現ほど運用設計が必要になる代表例です。
10.5 OutlinePass
Outlineは選択状態や注目点の可視化に強く、UI価値が高いエフェクトです。一方で透明物やレンダリング順序と衝突しやすく、適用範囲が広いほど破綻が増えます。用途が明確な分、対象を限定し、レイヤーで分離する設計が効きます。
実務では、Outlineを常時全体へかけるより、特定オブジェクトのみへ適用する方が安定します。対象が限定されるほど、デバッグもしやすく、性能も読みやすくなります。UI機能としてのOutlineは、見た目だけでなく操作性に直結するため、破綻しない範囲へ閉じる設計が強いです。
10.6 SSAOPass
SSAOは接地感と立体感に効きますが、深度品質に支配され、ノイズとコストが増えやすいです。透明物、薄いジオメトリ、スケール差が大きいシーンで破綻しやすく、万能化するとサンプル数増で重くなり、結局オフになりがちです。適用対象を限定し、プリセットで段階を作ると価値が残りやすくなります。
実務では、SSAOを入れる前にnear/far、スケール、DepthTextureの更新、深度可視化を揃えると成功率が上がります。深度の前提が揃っていれば、少ないサンプルでも十分な見た目になることが多いです。逆に前提が揃っていない状態でサンプルを増やすと、重くなるだけで改善が弱くなります。
10.7 FXAAPass
FXAAは軽量で導入しやすいAAですが、文字やUIが滲みやすい弱点があります。3Dシーンだけなら有効でも、同一キャンバスでUIも描くと可読性が落ちることがあります。AAは見た目だけでなく体験の一部なので、適用範囲と合成方針が重要になります。
実務では、UIを別レイヤー合成にする、FXAAをプリセットで切る、あるいは別方式(SMAA/TAA)を検討するなど、状況に合わせて制御できる形にします。AAを固定値として持つより、制御可能な仕様として持つほど、運用が安定しやすくなります。
| エフェクト | 用途 | 負荷 | 視覚効果 | 実務の注意 |
|---|---|---|---|---|
| UnrealBloomPass | 発光・雰囲気 | 中〜高 | にじみ・輝度強調 | 閾値・露出・順序が揃わないと白飛びしやすい |
| OutlinePass | 選択可視化 | 中 | 輪郭強調 | 透明物・順序・深度で破綻しやすい |
| SSAOPass | 接地感 | 高 | 隙間AO | 深度品質とスケール設計が前提、ノイズ対策で重くなる |
| FXAA | AA | 低 | エッジ緩和 | UIが滲むことがある、合成方針が重要 |
| Tone/Color補正 | 画づくり土台 | 低〜中 | 全体統一 | 二重適用事故を避ける必要がある |
11. Three.jsパフォーマンス最適化
Three.jsの最適化は「設定を1つ変えて速くする」類のものではなく、コスト要因を分解し、支配要因に手を入れる作業です。ドローコールが支配的なのか、頂点処理が支配的なのか、テクスチャ帯域が支配的なのか、ポストが支配的なのかで対策は変わります。現場ではまず「何が支配しているか」を見立て、次に段階設計で運用へ落とします。
ここではInstancedMesh、LOD、FrustumCullingを表で比較し、他の実務手法も要点を押さえます。最適化は単発で終わらず、プロダクトが成長するほど再発するため、仕組みとして整えるほど強くなります。
11.1 InstancedMesh
InstancedMeshは同じジオメトリ・マテリアルを多数描くとき、ドローコールを大幅に減らす手段です。木や粒子、タイル配置など、同形状の繰り返しに強く、CPU側のオーバーヘッドを抑えられます。オブジェクト数が増えるほど効果が大きく、スケールするほど価値が出ます。
実務では、インスタンスごとの違いをどこまで許すかが設計になります。色やスケール程度なら属性やinstanceMatrixで吸収できますが、複雑な分岐を増やすほどシェーダが重くなります。まずは「同じものを大量に描く」純粋用途で導入し、必要に応じて段階的に拡張すると、保守と性能のバランスが取りやすくなります。
11.2 LOD
LODは距離に応じて詳細度を切り替える手法で、遠距離の無駄なコストを削減します。Three.jsにはLODクラスがあり、実装として扱いやすいです。遠景が多いシーンでは、LODがあるかどうかで大規模表現の成立が変わります。
実務では、切替時のポッピング(不自然な変化)をどう抑えるかが課題になります。閾値の調整、段階数、フェードなどの工夫が必要で、体験とコストのバランスを取ります。LODは「入れれば速い」より「入れても違和感が少ない」設計が重要で、違和感が少ないほど長期運用で残りやすくなります。
11.3 FrustumCulling
FrustumCullingは視錐台外のオブジェクトを描かない仕組みで、大規模シーンほど効きます。Three.jsは基本的に自動でカリングしますが、バウンディング情報が正しくない、シェーダ変形で実形状が変わるなどの理由で効かなくなることがあります。効いていないと、画面外のものまで描いてしまい、無駄に重くなります。
実務では、カリングが効いているかを可視化・計測で確認します。boundingSphereの更新、オブジェクト分割単位の見直しなど、設計で解決することが多いです。カリングは地味ですが、効いているだけで「同じ見た目で速い」状態が作れるため、早めに観測できる仕組みを持つと強いです。
| 手法 | 主に減るコスト | 得意シーン | 実務の注意 |
|---|---|---|---|
| InstancedMesh | ドローコール | 同形状大量 | 変化を増やすほど複雑化 |
| LOD | 頂点処理・帯域 | 遠景多数 | ポッピング対策が必要 |
| FrustumCulling | 無駄描画 | 大規模 | バウンディング整合が前提 |
| HLOD(概念) | ドローコール | 都市・群集 | 統合単位の設計が難しい |
11.4 draw call削減
ドローコールはCPU側の準備コストで、オブジェクト数が増えるほど支配的になります。同じマテリアルを共有する、Instancingを使う、ジオメトリを結合するなどで削減できます。GPUが余裕なのに重い場合は、ドローコールや状態切替が疑いどころになります。
実務では、ドローコール削減はアセット分割とセットです。細かい部品をバラバラに持ちすぎると結合や共有が難しくなります。制作段階で「何を独立に動かすか」「何をまとめるか」を決めると、後からの最適化が短くなります。
11.5 mergeBufferGeometries
mergeBufferGeometriesは複数のBufferGeometryを結合して描画回数を減らす手段です。静的背景や動かないオブジェクト群に対して効きます。Instancingが難しい(形状が違う)場合でも、結合でドローコールを抑えられます。
実務では、結合すると個別操作が難しくなる副作用があります。個別に消せない、当たり判定が取りにくいなど、機能要件と衝突することがあります。静的資産へ限定して使い、動的要素は別に保つと、表現と機能の両立がしやすくなります。
11.6 KTX2圧縮テクスチャ
KTX2はGPU向け圧縮テクスチャの配布を現実的にし、転送量とメモリを大きく削減できます。特にモバイルや帯域が厳しい環境で効果が大きく、初期表示速度にも効きます。Three.jsはKTX2Loaderで扱えるため、パイプラインが整えば本番での強力な武器になります。
実務では、圧縮形式は互換性と品質のトレードオフです。KTX2は複数フォーマットを内包でき、端末ごとに最適なものを選べますが、生成工程が必要になります。工程を自動化しないと属人化し、回帰が増えるため、アセットパイプラインとして固めることが重要です。
| 形式 | 強み | 弱み | 主な用途 |
|---|---|---|---|
| KTX2(Basis) | 配布効率・互換性 | 生成工程が必要 | 本番の3Dテクスチャ |
| PNG/JPEG | 手軽 | 重い・メモリ大 | 開発・小規模 |
| WebP/AVIF | 圧縮効率 | GPU圧縮ではない | 2D寄り・一部用途 |
| HDR(EXR/HDR) | 環境品質 | サイズ大 | 環境マップ(段階設計が必要) |
11.7 Shadow最適化
影は立体感に効きますがコストが重いです。影を落とすライトを絞る、mapSizeを抑える、影範囲を狭める、遠景は影を切るなど、段階的に落とす設計が効きます。影の品質を上げる前に、影が価値を持つ場面を見極めると、コスト効率が上がります。
実務では、影はプリセット管理が強いです。Lowでは影なし、Medでは主光源の影だけ、Highでは影の解像度を上げる、といった段階を作ると端末差に耐えやすくなります。影は「一度入れると戻しにくい」ため、落とせる設計を先に入れるほど運用が楽になります。
11.8 マテリアル共有
マテリアル共有は状態切替を減らし、ドローコール削減に寄与します。似た見た目を別マテリアルで作ると、内部では別プログラムになりやすく、初期コンパイルやキャッシュが膨らみます。色違い程度ならvertex colorやinstance colorで吸収し、マテリアルは共有する方が、性能と運用が安定します。
実務では、共有と表現の自由が衝突します。ここは設計で解決します。共有したい要素(シェーダとパイプライン)は維持し、変化する要素(色や強度)はuniformや属性で制御する、といった分離ができると、表現と性能が両立しやすくなります。
11.9 レンダラー設定最適化
renderer.setPixelRatioの上限、内部解像度、アンチエイリアス、トーンマッピング、ポストプロセスの有無など、Renderer設定はコスト構造を決めます。個別最適化より、まず「解像度戦略」を固める方が効きやすいです。DPRを無制限にするとフルスクリーンパスのコストが跳ね、重い端末が増えます。
実務では、設定はプリセットとして運用へ落とします。重い端末では段階的に落とし、軽い端末では表現を保つ、といった構造が現実的です。設定を運用可能な形にした時点で、最適化は「削る」から「制御する」に変わります。
12. Three.js実務応用例
Three.jsは用途が広く、用途によって設計の中心が変わります。ビューアは質感とロード、可視化は大量描画と更新設計、WebXRはフレームレート制約、広告は初期表示速度、といった形で「守るもの」が違います。同じ実装方針で押し切ると破綻しやすいため、用途別に論点を整理しておくと意思決定が速くなります。
ここでは代表用途を表でまとめます。用途が違うと、使うThree.js機能も、注意点も変わるため「何を先に決めるべきか」が見えやすくなります。
12.1〜12.8 用途別整理
| 用途 | 主な機能 | よく使うThree.js機能 | 注意点 |
|---|---|---|---|
| プロダクトビューア | 回転・PBR・環境 | OrbitControls、PMREM、glTF | 初期表示速度と端末差が支配的 |
| データ可視化 | 大量描画・選択 | InstancedMesh、Orthographic、Raycaster | 更新設計が性能を決める |
| ゲームUI | 演出・入力 | ポスト、合成、アニメ | UI可読性と遅延を守る必要がある |
| デジタルツイン | データ統合 | 更新制御、カリング、LOD | 監視・ログ・再現性が重要 |
| AR/VR(WebXR) | 没入体験 | WebXR、軽量レンダ | FPS制約が厳しく段階設計必須 |
| 建築ウォークスルー | 大規模資産 | 分割ロード、LOD、圧縮 | ロードとメモリがボトルネックになりやすい |
| インタラクティブ広告 | 即時性 | 軽量アセット、段階 | 盛り過ぎると離脱が増える |
| 医療可視化 | 正確性 | 透明・断面・強調 | 再現性と説明可能性が重要 |
用途別の整理ができると、導入時の設計が「何でもできる」から「何を守るか」へ変わります。Three.jsは万能ですが、万能さは設計判断を増やすことでもあるため、用途の前提を揃えるほど成功しやすくなります。
13. Three.jsとWebGLの内部関係
Three.jsを実務で使い込むほど、内部で何が起きているかの理解が効いてきます。抽象化の上で性能が崩れたとき、原因は「オブジェクト数」「マテリアルバリアント」「状態切替」「ポストの解像度」などに分解できます。内部の地図があると、最適化が推測ではなく手順になり、改善の優先順位も付けやすくなります。
ここでは、WebGLRendererの内部を「設計の見取り図」として扱い、何が増えると何が重くなるかを整理します。内部を追いすぎる必要はありませんが、支配要因を理解するだけで意思決定が速くなります。
13.1 WebGLRenderer内部構造
WebGLRendererはSceneを走査し、可視性・レイヤー・ソートを評価し、描画リストを作ります。その上で、Geometryのバッファ、Materialのプログラム、テクスチャ、ブレンドや深度などの状態を整え、WebGLへドローコールを発行します。つまり、レンダリングは「準備の仕事」が多く、準備が増えるほどCPU負荷が増えます。
実務では、Rendererが毎フレームやる仕事を減らすのが近道になります。Instancingやマテリアル共有が効くのは、Rendererの準備仕事を減らせるからです。内部を知ると、なぜ「同じ見た目でも実装によって重い/軽い」が起きるかを説明できるようになります。
13.2 マテリアル→GLSL変換
Three.jsはMaterial設定を元に、ライト数、影、環境、フォグなどを考慮してGLSLを生成します。MeshStandardMaterialが便利なのは、この生成が多くの前提を吸収しているからです。一方で、条件が増えるほどシェーダバリアントが増え、プログラム数が増えます。プログラム数が増えると初期コンパイルが重くなり、初期表示の体験にも影響します。
実務では、マテリアルの乱立がプログラムキャッシュの膨張に直結します。見た目が少し違うだけで別マテリアルを作ると、内部では別プログラムになりやすく、性能と運用が不安定になります。共有できるものは共有し、差分はuniformや属性で吸収する設計が、内部のコストを抑えます。
13.3 パイプライン管理
WebGLは状態機械で、ブレンド、深度テスト、カリング、テクスチャ、バッファなどの状態が多いです。Three.jsは状態切替を抽象化しますが、状態が増えるほど切替回数が増え、CPU側のgl呼び出しが増えます。透明物が増える、ブレンドモードが増える、ポストが増えるほど、状態管理が支配的になっていきます。
実務では、状態の多様性を減らす設計が効きます。透明の使い方を絞る、ブレンドモードを揃える、影の有無を段階化するなど、表現の設計がそのまま状態管理の設計になります。状態管理は見えにくいですが、CPUが支配的な現場では非常に効きます。
13.4 状態キャッシュ
Three.jsは同じ状態を何度も設定しないようにキャッシュを持ちます。キャッシュが効くほどCPU負荷が減り、フレームが安定します。逆に、毎回状態が変わる構成ではキャッシュが効きにくくなり、Rendererが忙しくなります。オブジェクト数が少なくても、状態が多いと重くなるのはこのためです。
実務では、キャッシュが効く方向へ設計するのが近道になります。マテリアル共有やテクスチャ共有は、見た目の都合だけでなくキャッシュを効かせる都合としても重要です。内部のキャッシュの存在を意識すると、最適化の狙いがぶれにくくなります。
13.5 WebGLState管理
WebGLStateは、ブレンドや深度テスト、カリングなどのGL状態を集約し、必要な切替を行う部品です。状態の組み合わせが増えるほど切替が増え、CPU負荷が増えます。GPUが暇でも重いときに疑うべき領域の一つです。
実務的には、状態の多様性を減らすほどWebGLStateの仕事が減ります。影の有無、透明の種類、マテリアルのブレンド設定などをプリセットで段階化すると、状態の増殖を抑えられます。表現の設計が内部状態の設計になっている点を押さえると、運用での改善がしやすくなります。
13.6 プログラムキャッシュ機構
Three.jsは生成したプログラムをキャッシュし、同条件なら再利用します。条件が増えるほどキャッシュキーが増え、プログラム数が膨らみます。onBeforeCompileの差分、ライト数、defines、影などが条件を分岐させます。プログラム数が増えるほど初期コンパイルが増え、初期表示が遅くなりやすいです。
実務では、プログラム数を抑える設計が初期体験を守ります。ライト数や影設定をプリセット化して段階を減らす、マテリアル種類を絞る、ShaderMaterialのバリアントを増やしすぎないなどの判断が効きます。キャッシュは便利ですが、条件が増えるとキャッシュが重荷になるため、条件の整理が重要になります。
| 内部要素 | 役割 | 増えると起きやすい問題 |
|---|---|---|
| WebGLState | GL状態の切替 | CPU負荷増、切替地獄 |
| ProgramCache | シェーダ再利用 | 初期コンパイル増、バリアント爆発 |
| Materialバリアント | 条件分岐 | キャッシュ肥大、回帰増 |
| RenderList | 描画リスト | ソートと準備の増加 |
13.7 DrawCall生成過程
DrawCallは「このジオメトリをこのマテリアルで描く」という単位です。Three.jsはScene走査→可視性判定→ソート→状態とプログラム準備→発行、という流れでDrawCallを作ります。オブジェクト数が増えるほどこの準備が増え、CPUが支配的になります。ドローコールが多い現場でGPUが余っているのは、準備で詰まっているサインです。
実務では、DrawCallを減らす手段(Instancing、マテリアル共有、結合、カリング、LOD)を「設計として」入れると強いです。単発の最適化ではなく、増えたときに自然に抑えられる構造があると、プロダクトが成長しても性能が破綻しにくくなります。内部の流れが分かるほど、最適化は狙い撃ちになります。
14. Three.js導入の実務設計と運用戦略
Three.jsを本番へ導入する段階では、描画の技術よりも「配布と運用」が中心になります。バンドルサイズ、ロード、アセット圧縮、監視、エラーハンドリング、SSR/SPA統合など、3D以外の要素が体験を決めます。PoCは動けば勝ちですが、本番は「速く開いて、壊れずに回って、直せる」ことが勝ちです。
ここでは導入時に詰まりやすい実務論点を整理します。運用戦略を先に設計しておくほど、後からの改善が現実的になり、表現の挑戦も続けやすくなります。
14.1 バンドルサイズ最適化
Three.jsはモジュールとして使えますが、examples由来の依存(ControlsやLoader)を増やすほどバンドルが膨らみます。初期表示が遅いと離脱が増え、3Dの価値が届く前に終わってしまいます。実務では「初期に必要な最小」と「後から必要」を分け、遅延ロードする設計が効きます。
また、Shaderやポストを増やすほど依存が増え、初期コンパイルも増えます。最初の体験に必要な要素を固定し、追加要素はユーザー操作や可視領域進入などの条件でロードすると、体験が安定します。バンドルサイズは性能だけでなく、プロダクトの入り口そのものを左右します。
14.2 モジュール分割戦略
モジュール分割は「コード整理」ではなく「ロード設計」です。ビューア本体、ポスト、WebXR、重いローダ、デバッグ機能などを分け、必要なときだけロードできる構造にすると、初期体験が軽くなります。分割しすぎると管理が複雑になるため、機能単位と運用単位(A/Bやフラグ)に合わせて境界を決めると回りやすいです。
実務では、分割は運用にも効きます。段階ロールアウトをする場合、機能フラグで分岐し、必要なモジュールだけロードできると、問題が起きたときに止血しやすくなります。分割は「本番で困らないための構造」でもあるため、最初から運用目線で設計する価値があります。
| 手法 | 効果 | 落とし穴 | 実務の使い所 |
|---|---|---|---|
| 動的import | 初期軽量化 | 分割し過ぎると管理が複雑 | WebXR/ポストの遅延ロード |
| 依存削減 | バンドル減 | 実装が冗長化する場合 | 使わないexamplesを入れない |
| 機能フラグ | 段階配布 | 分岐が散ると破綻 | カナリア・A/B |
14.3 TypeScript型安全設計
TypeScriptはThree.js導入で特に効きます。Sceneに入るオブジェクトの型、ロード結果の型、設定(プリセット)の型が揃うと、変更が安全になります。3Dは状態が多く、型がないと「何が入っているか」が曖昧になり、修正が怖くなります。怖さは変更頻度を下げ、結果として改善が止まります。
実務では、型は厳密さのためだけでなく境界のために使うと効果が出ます。レンダリング設定をConfigとして一箇所に集約し、プリセットをUnionで表現し、ログに出す情報を型で固定すると、監視と回帰防止が楽になります。型は短期速度を落とすのではなく、長期速度を上げる仕組みとして設計すると強いです。
14.4 ローディング戦略
3Dは初期ロードが重くなりやすく、ロード中の空白は離脱に直結します。LoadingManagerで進捗をまとめることも重要ですが、もっと重要なのは「何をいつロードするか」です。最初に必要な最小資産を決め、残りは遅延ロードに寄せると、体験が安定します。必要な瞬間までロードを遅らせる設計は、初期体験の勝率を上げます。
実務では、ロード失敗を前提にしたUXも必要です。再試行、軽量モード、説明、サポート用のログなどを用意すると、運用が現実的になります。ロードは成功前提で設計すると事故が大きくなるため、失敗時に「何が起きるか」を仕様として持つと強いです。
14.5 アセットパイプライン設計
アセットは、glTF最適化、圧縮、テクスチャ圧縮、LOD生成、検証の工程を通すほど本番に耐えます。手作業で都度最適化すると属人化し、回帰が増えます。Three.js側が便利な分、アセット側が雑だと性能が破綻しやすく、逆にパイプラインが整うと「同じ見た目で速い」状態が作りやすくなります。
実務では、ビルド時にアセットを自動最適化し、成果物のサイズ・ロード時間・品質を計測できる状態を作ると強いです。工程が見える化されるほど、試行回数を増やしても破綻しにくくなり、表現の改善が続けられます。パイプラインは地味ですが、長期的には最も投資対効果が高い領域の一つです。
| 工程 | 目的 | 代表手段 | 注意点 |
|---|---|---|---|
| glTF最適化 | サイズ削減 | Meshopt/Draco | デコード負荷と互換性 |
| テクスチャ圧縮 | 帯域・メモリ削減 | KTX2 | 生成工程の自動化が必須 |
| LOD生成 | 遠景軽量化 | 自動/手動 | ポッピング対策が必要 |
| 検証 | 回帰防止 | 画像差分/計測 | 許容範囲の定義が必要 |
| 配信 | キャッシュ効率 | CDN/ハッシュ | 更新戦略を揃える |
14.6 パフォーマンス監視設計
本番で重要なのは「遅い」を再現できることです。平均FPSだけでは原因が分からないため、フレーム時間、DPR、内部解像度、プリセット、有効パス、GPU/ブラウザ情報など、切り分けに必要な情報をログに出します。これがないと改善が止まり、最終的に表現の機能が削られていきます。
実務では、監視は止血にも使います。一定以上重ければ内部解像度を落とす、SSAOを切るなど、段階フォールバックを用意すると端末差に耐えやすくなります。監視とフォールバックはセットで設計すると、本番での意思決定が短くなり、表現の挑戦が続けやすくなります。
14.7 エラーハンドリング戦略
Webの3Dは失敗が起きます。WebGLコンテキスト取得失敗、テクスチャロード失敗、メモリ不足、GPU差の破綻など、成功前提だとユーザー体験が崩れます。エラー時の説明、軽量モード、再試行、サポート用ログを用意すると運用が現実的になります。エラーを例外扱いではなく分岐として扱うのが実務的です。
実務では、ユーザーが何をすれば良いかが分かるだけで離脱と不満が減ります。例えば「端末の省電力モードで重いので軽量モードへ切替」など、現実の状況に寄り添う導線を用意すると、サポートの負担も下がります。3Dは「動けば終わり」ではないため、失敗時の体験も設計に含める価値があります。
14.8 SSR/SPA統合設計
Three.jsは基本的にクライアント側で動くため、SSRと統合する場合は起動タイミングが設計になります。SSRでHTMLを先に出し、クライアントでThree.jsを起動する場合、キャンバスのレイアウト確定、ロード、ハイドレーションの整合を取らないとレイアウトシフトや二重初期化が起きます。ここは描画より運用の話になりやすいです。
実務では、3Dは遅延起動や条件起動へ寄せると安定します。可視領域に入ったら起動する、ユーザー操作後に起動する、軽量モードで先に出すなど、SPAの体験設計と合わせて決めます。SSR/SPA統合は「いつ動かすか」「どこまで先に見せるか」が中心になるため、前提を揃えるほど手戻りが減ります。
おわりに
定することです。線形/sRGB、トーンマッピングの位置、深度の扱い、UI合成の責務が曖昧なままだと、数値調整が効かない破綻に入りやすくなります。逆に、前提が固定されるだけで同じ設定の意味が一定になり、比較が可能になって調整が収束しやすくなります。
EffectComposerは、見た目を作る道具であると同時に「中間バッファの受け渡しを管理する実行基盤」です。ping-pongの流れが追えると、真っ黒になる、出力が消える、リサイズで崩れるといった事故が「どこで壊れたか」へ分解できるようになります。深度依存パスは特に、前提(near/far、透明物、解像度一致)を固めてから入れるほど成功率が上がり、ノイズ対策としてのサンプル増加に逃げにくくなります。
性能は、最終的には解像度設計が支配します。DPRの上限、内部解像度の段階、パス別解像度、ブラー系の低解像度化を「後から」足すのではなく、最初から逃げ道として持つと、品質を保ったまま軽くできます。重くなったときに落とす順序が決まっているほど、連鎖的な悪化(SSAO→サンプル増→内部解像度低下→AA強化→UI滲み)のような迷走を止めやすくなります。
最後に、長期運用で差が出るのはコード構造と運用設計です。パス生成を設定から分離し、依存関係を宣言し、デバッグ可視化と有効パス一覧のログを持つだけで、追加・撤去・回帰防止が現実的になります。プリセットとフォールバックがある構成は、本番で「止められる」安心感があり、結果として表現の挑戦も続けやすくなります。ポストを育てられる状態を作ることが、最終的に画づくりの速度そのものになります。
EN
JP
KR