ファイルからブラウザへ — この章のゴール
第7章では、OBJ・FBX・glTF・USDを「オーサリング用か、ランタイム用か」で整理しました。 Webで3Dを見せるときに使うのは、ランタイム用の代表であるglTF、特にテクスチャまで1ファイルにまとめたバイナリ形式のGLBです。 この章では、そのGLBを実際にブラウザへ表示し、軽くして配信するまでを手を動かす前提で解説します。
対象はTypeScriptとReactに慣れていて、3DCGは初めてというWebエンジニアです。 第3章〜第6章で扱ったメッシュ・PBRマテリアル・レンダリングの知識が、コードのどこに現れるかを意識しながら読み進めてください。 全体の流れは次の図のとおりです。
graph LR A[Blenderで制作] --> B[GLBで書き出し] B --> C[gltf-transform<br/>で最適化] C --> D[validateで検証] D --> E[CDNで配信] E --> F[model-viewer] E --> G[Three.js] E --> H[React Three Fiber] style C fill:#8b5cf6,stroke:#6d28d9,color:#fff style G fill:#3b82f6,stroke:#1d4ed8,color:#fff
表示の手段は大きく3つあります。 1タグで済ませる<model-viewer>、低レベルに全部書けるThree.js、そしてReactの書き方でThree.jsを扱えるReact Three Fiber(以下R3F)です。 まず2026年10月時点のバージョンを押さえておきます。3D系のライブラリは破壊的変更が多いので、ネットで拾ったコードを使う前に必ず版を確認してください。
2026年10月時点の主要パッケージ
| パッケージ | 最新版 | 押さえどころ |
|---|---|---|
| three | 0.186.1(r186、2026-09-24) | リビジョン番号rNNNで呼ばれる。毎月のように更新 |
| @react-three/fiber | 9.8.2 | peer依存がreact 19以上19.4未満。React 18ならv8系 |
| @react-three/drei | 10.7.9 | R3F用の便利コンポーネント集。useGLTFなど |
| @google/model-viewer | 4.4.0 | peer依存がthree ^0.183.0。最新threeと同居させると衝突 |
| @gltf-transform/cli | 4.5.1 | glTFの検査・最適化CLI。optimizeが入口 |
| gltfpack | 1.3.0 | meshoptimizer付属の最適化CLI。npmでWASM版が入る |
Three.jsでGLBを表示する最小コード
インストールとimportの作法
公式マニュアルが推奨するのはnpmとViteの組み合わせです。 ビルドなしで試したい場合は、import mapでCDNから読み込む方法もあります。
npm install --save three
npm install --save-dev vite
npx vite<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/"
}
}
</script>OrbitControlsやGLTFLoaderといったアドオンは本体と同じパッケージに含まれますが、three/addons/...から個別にimportする必要があります。 公式マニュアルは、すべての依存を同じバージョン・同じ配信元から読み込むよう求めています。 バージョンが混ざると同じコードが二重に読み込まれ、動作がおかしくなる原因になります。 また、HTMLをfile://で直接開くと動かない機能があるので、npx serve .やViteでローカルサーバーを立てて確認してください。 TypeScriptの型はコミュニティ管理の@types/threeで入ります。
最小コードを読む
次のコードは、公式サンプルwebgl_loader_gltf.htmlの構成を最小限まで削ったものです。 KhronosのサンプルモデルDamagedHelmet.glbを読み込み、マウスで回して見られるようにします。
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
// 1. レンダラ: canvasを作り、GPUに描画命令を出す
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping; // 既定は NoToneMapping
document.body.appendChild(renderer.domElement);
// 2. シーン: 描画対象を入れる箱
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xbbbbbb);
// 3. 環境マップ: HDRIなしで使える手続き的な「部屋」。PBRの反射と照明に必須
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
// 4. カメラ: 画角45度、near 0.1m〜far 100m を描く
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 1, 3);
// 5. マウス操作で注視点の周りを回す
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 6. GLBを読み込んでシーンに追加
const gltf = await new GLTFLoader().loadAsync('/models/DamagedHelmet.glb');
scene.add(gltf.scene);
// 7. 毎フレーム描画
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});このコードには、第6章で見たラスタライズの話が具体的なAPIとして出てきます。 カメラのnearとfarは描画範囲の手前と奥の境界で、この外にある物体は描かれません。setAnimationLoopはブラウザの描画タイミングに合わせて毎フレームrenderを呼ぶ仕組みです。
初学者が最も見落としやすいのは手順3の環境マップです。 glTFのマテリアルはThree.jsではMeshStandardMaterialというPBRマテリアルになります。 第4章で見たように、PBRは「周囲から届く光」を前提に色を計算するので、光源も環境マップもないシーンではモデルが暗く、ほぼ真っ黒に見えます。RoomEnvironmentはHDR画像を用意しなくても使える室内風の環境で、PMREMGeneratorがそれを粗さごとの反射に使える形へ前処理します。
HDRIを使う場合はHDRLoader(r180で改名)
実写のHDR画像で照明したい場合、以前はRGBELoaderを使うのが定番でした。 r180でこのクラスは非推奨になり、HDRLoaderに名前が変わっています。 ソースには「RGBELoader has been deprecated. Please use HDRLoader instead.」という警告が入っているので、古い記事のコードを写すときは置き換えてください。
import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
const tex = await new HDRLoader().loadAsync('/hdr/studio_1k.hdr');
tex.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = tex; // 照明と反射に使う
scene.background = tex; // 背景にも出すならなお、公式のwebgl_loader_gltf.htmlは現在、より軽量な.hdr.jpg形式を読むUltraHDRLoaderを使っています。
圧縮されたGLBを読むためのデコーダ設定
後半で扱う最適化を施したGLBは、ジオメトリやテクスチャが圧縮されています。 GLTFLoaderにはデコーダを登録しておかないと読み込みに失敗します。
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
// examples/jsm/libs/draco/ と basis/ を自サイトにコピーして配信する
const draco = new DRACOLoader().setDecoderPath('/draco/');
const ktx2 = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
const loader = new GLTFLoader()
.setDRACOLoader(draco)
.setKTX2Loader(ktx2)
.setMeshoptDecoder(MeshoptDecoder);GLTFLoaderは、Draco・Meshopt・KTX2(KHR_texture_basisu)のほか、WebPやAVIFのテクスチャ、多数のKHR_materials系拡張、KHR_lights_punctualなどを標準で扱えます。 第9章で扱うKHR_gaussian_splattingは標準対応ではなく、別途登録するプラグインの扱いです。
WebGPURendererの現状
Three.jsには次世代APIのWebGPUで描画するWebGPURendererもあります。 importを'three'から'three/webgpu'に変え、new THREE.WebGPURenderer({ antialias: true })で作ります。 初期化が非同期なのでsetAnimationLoopを使うのが推奨で、自前でrequestAnimationFrameを回すなら先にawait renderer.init()が必要です。
WebGPUに対応していない環境では、WebGL 2のバックエンドへ自動でフォールバックします(forceWebGL: trueで強制も可能)。 ブラウザ側はChrome・Edgeが113から既定で有効になり、SafariとFirefoxも対応を進めていますが、プラットフォームによって穴が残っています。 本番ではフォールバックが働く前提で設計するのが安全です。
React Three Fiber + dreiで宣言的に書く
Reactに慣れたエンジニアには、R3Fが最もなじむ入口です。 R3FはThree.jsのオブジェクトをJSXで組み立てるReactレンダラで、dreiはその上にローダーやカメラ操作などの部品をそろえたライブラリです。 現行のR3F v9はReact 19が必須で、peer依存は「react 19以上19.4未満」です。React 18のプロジェクトではv8系を選ぶことになります。
npm i three @react-three/fiber @react-three/drei
npm i -D @types/threeimport { Canvas } from '@react-three/fiber';
import { OrbitControls, Environment, useGLTF } from '@react-three/drei';
import { Suspense } from 'react';
function Model({ url }: { url: string }) {
// Suspense対応。Draco/Meshoptのデコードは既定で有効
const { scene } = useGLTF(url);
return <primitive object={scene} />;
}
// 表示前に読み込みを始めておく
useGLTF.preload('/models/DamagedHelmet.glb');
export default function Viewer() {
return (
<Canvas camera={{ position: [0, 1, 3], fov: 45 }}>
<Suspense fallback={null}>
<Model url="/models/DamagedHelmet.glb" />
<Environment preset="city" />
</Suspense>
<OrbitControls makeDefault />
</Canvas>
);
}Three.jsの素書きで7段階あった処理が、Canvas(レンダラ・シーン・カメラ・描画ループ)、useGLTF(読み込み)、Environment(環境マップ)、OrbitControls(操作)に対応しているのが分かります。useGLTFのシグネチャはuseGLTF(path, useDraco = true, useMeshOpt = true, extendLoader?)で、戻り値には名前で引けるnodesとmaterialsも含まれます。 KTX2を使う場合はextendLoaderの中でloader.setKTX2Loader(...)を呼びます。
model-viewerで1タグ表示とAR
ECの商品ページのように「GLBを見せてARで置ければよい」用途なら、Googleが開発するWeb Component <model-viewer>が最短です。 カメラ操作、環境光、影、ARボタンまでが属性だけで設定できます。
<script type="module"
src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.4.0/model-viewer.min.js"></script>
<model-viewer
src="/models/chair.glb"
alt="椅子の3Dモデル"
camera-controls
auto-rotate
ar
ar-modes="webxr scene-viewer quick-look"
ar-scale="auto"
environment-image="neutral"
tone-mapping="neutral"
shadow-intensity="1"
style="width:100%;height:480px">
</model-viewer>srcに指定できるのはglTFかGLBだけです。environment-imageにはneutral・legacyのほか、.hdrや.jpgのURLを指定できます。tone-mappingの既定はneutral(KhronosのPBR Neutral)で、acesやagxも選べます。 model-viewerは既定で環境光を持っているので、Three.jsの素書きのように「環境マップを忘れて真っ黒」という事故は起きにくくなっています。
ar-modesとiOSのUSDZ
ar-modesは使いたいAR方式の優先順リストで、既定値は"webxr scene-viewer quick-look"です。 端末が対応している方式のうち、先に書いたものから使われます。
| モード | 動く環境 | 中身 |
|---|---|---|
| webxr | Android Chrome | ブラウザ内でWebXRを使うAR |
| scene-viewer | Android | GoogleのScene Viewerアプリに渡す |
| quick-look | iOS | AppleのAR Quick Lookに渡す(USDZが必要) |
iOSのAR Quick LookはglTFではなくUSDZを読みます。ios-srcを省略すると、model-viewerはARボタンが押された時点でGLBからUSDZを自動生成してQuick Lookを起動します。ios-src="/models/chair.usdz"を指定すると追加のダウンロードが発生しますが、変換結果の品質を自分で管理できます。 ほかに、ar-scale="fixed"でピンチによる拡大縮小を禁止でき、ar-placement="wall"で壁に配置できます(既定はfloor)。
BlenderからGLBを書き出す設定
表示側のコードが正しくても、書き出しの設定を誤ると表示は崩れます。 BlenderのglTFエクスポーター(File > Export > glTF 2.0)で確認すべき項目をまとめます。 現行マニュアルはBlender 5.2 LTSのものです。
| 設定 | 推奨 | 理由 |
|---|---|---|
| Format | glTF Binary (.glb) | データもテクスチャも1ファイルに入る。Separateは.gltf・.bin・画像をセットで配る必要がある |
| Transform > Y Up | オン(既定) | BlenderはZ-up、glTFは+Y-up。オフにするとWebで横倒しになる |
| Mesh > Apply Modifiers | モディファイアを使ったらオン | Subdivision・Mirrorなどを適用した後の形が出力される |
| マテリアル | Principled BSDFで作る | glTFに出せるのはPrincipled BSDF(Metal/Rough)とUnlitだけ |
| 金属・粗さテクスチャ | G=Roughness、B=Metallic | glTFのチャネル規約。Separate Colorノードで配線すると認識される |
| ノーマルマップ | Normal Mapノード経由、Non-Color | Tangent Spaceのまま接続。色空間をsRGBにすると陰影が狂う |
| 圧縮 | Draco / Meshoptを選べる | エクスポーターに内蔵。後段でgltf-transformを使うならここでは不要 |
| Remember Export Settings | オン | .blendに設定を保存し、毎回の設定漏れを防ぐ |
glTFの仕様は、長さの単位をメートル、座標系を右手系の+Y-upと定めています。 Blenderの既定(Unit Scale 1.0、Metric)では1単位が1mなので、そのまま整合します。 また、モデリングを終えたらObject > Apply > All Transforms(Ctrl+A)でスケールを1に戻しておくのが定石です。 負のスケール(ミラー)や非一様スケールが残ると、法線の反転やボーンの不具合につながります。 手続き的テクスチャなどPrincipled BSDF以外のノードは書き出されないので、画像テクスチャへベイクしてから書き出してください。
配信前の最適化 — gltf-transformとgltfpack
Blenderから書き出したGLBは、そのまま配信するには重いことがほとんどです。 4Kテクスチャが何枚も入っていたり、ポリゴンが細かすぎたりするためです。 Web向けの最適化ツールとして定番なのが、glTF Transformと、meshoptimizerに付属するgltfpackです。
gltf-transformのoptimize
# インストール(グローバル)
npm install --global @gltf-transform/cli
# 中身を調べる(ジオメトリ・テクスチャ・ドローコールのどれが重いか)
gltf-transform inspect input.glb
# 最適化(インストールせずnpxでも可)
npx @gltf-transform/cli optimize input.glb output.glb --compress meshopt --texture-compress webp
# 仕様に沿っているか検証
gltf-transform validate output.glboptimizeは、複数の処理をまとめて実行するコマンドです。 主なオプションと既定値は次のとおりです(v4.5.1のoptimize --helpの出力より)。
| オプション | 値 | 既定 |
|---|---|---|
| --compress | draco / meshopt / quantize / false | meshopt |
| --texture-compress | ktx2 / webp / avif / auto / false | auto(元の形式のまま再圧縮) |
| --texture-size | テクスチャの最大辺 | 2048 |
| --simplify | ポリゴン削減 | true(誤差0.0001) |
| --flatten / --join / --instance / --prune / --weld など | ノード統合・メッシュ結合・インスタンス化・不要データ削除・頂点溶接 | いずれもtrue |
--joinや--instanceは、第6章で触れたドローコールを減らすための処理です。 ポリゴン数だけでなく、メッシュとマテリアルの組み合わせの数も描画コストに影響します。
DamagedHelmetでの実測
KhronosのサンプルDamagedHelmet.glb(3.77MB)を実際に最適化した結果が次のグラフです。 gltf-transformでMeshopt圧縮とWebPテクスチャを組み合わせると1.44MBまで縮みました。 一方、gltfpackでジオメトリだけをMeshopt圧縮(-cc)した場合は3.35MBにとどまりました。 このモデルはファイルの大半をテクスチャが占めているため、ジオメトリを圧縮しても効果が小さいのです。 まずinspectで何が重いのかを確かめてから、効果のある手段を選ぶのが近道です。
DamagedHelmet.glbのファイルサイズ(MB)
Draco・Meshopt・KTX2の使い分け
| 技術 | 対象 | 特徴 |
|---|---|---|
| Draco | ジオメトリ | 圧縮率は高いがデコードが重め。WASMデコーダの配信が必要。アニメーションは対象外 |
| Meshopt | ジオメトリ・アニメーション・モーフ | デコードが速く軽い。gzipやbrotliとの相性が良い。gltf-transformの既定 |
| KTX2 + Basis | テクスチャ | GPU上でも圧縮されたまま保持されVRAMが減る。ETC1Sは小さく低画質、UASTCは大きく高画質(ノーマルマップ向き) |
| WebP / AVIF | テクスチャ | 転送サイズは減るが、GPU上では展開されるのでVRAMは減らない |
テクスチャの圧縮には「転送サイズを減らす」ものと「VRAMを減らす」ものがあり、この区別が重要です。 WebPやAVIFはダウンロードを軽くしますが、GPUに載る時点で非圧縮の画素に戻ります。 スマートフォンでテクスチャの多いシーンを表示してメモリ不足になるなら、KTX2を検討する価値があります。
gltfpack
gltfpackはmeshoptimizerの作者による最適化CLIで、npmでWASM版が配布されています。 Three.jsの公式サンプルにあるcoffeemat.glbも、gltfpack -i coffeemat/scene.gltf -o coffeemat.glb -cc -tcで作られたとコメントに書かれています。
npx gltfpack -i input.glb -o output.glb -cc # Meshoptで高圧縮
npx gltfpack -i input.glb -o output.glb -cc -tc # さらにテクスチャをKTX2(ETC1S)へ
# -tu: UASTC、-tw: WebP、-si 0.5: ポリゴンを50%に簡略化、-tl 1024: テクスチャ上限gltf-validatorで仕様違反を確かめる
最適化の後は、Khronos公式のglTF-Validatorで仕様に違反していないかを確かめます。 Web版にファイルをドラッグ&ドロップする方法と、CLI・ライブラリとして使う方法があり、npmのgltf-validatorは2.0.0-dev.3.10です。 手軽にはgltf-transform validateでも同じ系統のレポートが得られます。 Meshopt圧縮済みのファイルを検査するとUNSUPPORTED_EXTENSION: EXT_meshopt_compressionが表示されますが、これはINFO扱いでエラーではありません。
よくある失敗と対処
最後に、Webで3Dを扱い始めたエンジニアがほぼ必ず一度は踏む失敗をまとめます。 多くは第3章〜第7章で扱った座標系・単位・法線・色空間の話が、表示上の症状として現れたものです。
| 症状 | 原因 | 対処 |
|---|---|---|
| モデルが真っ黒 | PBRマテリアルに当たる光も環境マップもない | scene.environmentにRoomEnvironmentかHDRを設定。必要ならDirectionalLightも追加 |
| 横倒しで表示される | Blenderで書き出し時にY Upがオフ、またはZ-upのツールから出力 | エクスポーターで+Y Upを有効にする |
| 何も見えない(エラーなし) | 単位違いでモデルが巨大または極小になり、カメラやnear/farの範囲外にある | Box3().setFromObject()でサイズを測ってカメラ距離を調整、またはgltf-transform center |
| 面が欠ける・裏返る | 法線の反転、負のスケール、片面描画 | BlenderでRecalculate Outside(Shift+N)とApply Scale。暫定策はside = DoubleSide |
| 色が白っぽい・くすむ | 自前のTextureLoaderで読んだ色テクスチャのcolorSpaceが未設定 | 色テクスチャだけSRGBColorSpaceにする。法線・粗さなどは既定のまま。GLTFLoader経由のbaseColorは自動でsRGB |
| 色が明るすぎ・暗すぎ | 出力時の色変換が重複または欠落 | renderer.outputColorSpaceは既定のSRGBColorSpaceのまま。EffectComposerを使うなら最後にOutputPass |
| CORSエラーで読めない | S3やCDNなど別オリジンからGLBやテクスチャを取得している | 配信側でAccess-Control-Allow-Originを設定。file://直開きはやめてローカルサーバーで確認 |
| 初期表示が遅い | 4Kテクスチャの多用、ハイポリゴン | gltf-transform optimize(--texture-size 1024なども)。inspectで原因を特定 |
| 圧縮GLBが読めない | Draco・Meshopt・KTX2のデコーダ未設定 | setDRACOLoader・setMeshoptDecoder・setKTX2Loaderを登録 |
| KTX2圧縮が失敗する | KTX-Softwareのktxコマンドが未インストール | KTX-Softwareを導入するか、webp・avifを使う |
| 本番で環境マップがたまに出ない | dreiのEnvironment presetが外部CDNに依存 | HDRを自前で配信しfilesで指定 |
| Blenderの見た目が再現されない | Principled BSDF以外のノードはglTFに出力されない | 画像テクスチャへベイクする |
| WebGPURendererで既存シェーダが動かない | ShaderMaterial・onBeforeCompile・EffectComposerが非対応 | WebGLRendererに戻すか、TSLへ移植する |
色空間の行は特に誤解が多いので補足します。 第4章で見たように、glTFはbaseColorテクスチャをsRGBで保存すると定め、ライティングはリニア空間で計算します。 GLTFLoaderは読み込み時にbaseColorテクスチャへ自動でSRGBColorSpaceを設定するので、GLBをそのまま表示する限り何もしなくて構いません。 問題が起きるのは、TextureLoaderで別の画像を読んで差し替えたときです。 色テクスチャにだけtexture.colorSpace = THREE.SRGBColorSpaceを指定し、法線や粗さのようなデータ用テクスチャは既定のNoColorSpaceのまま使います。 ここを全部sRGBにしてしまうと、陰影や光沢が狂います。
どれから始めるか
商品を回して見せ、ARで置ければ十分なら<model-viewer>から始めるのがよいでしょう。 既存のReactアプリにインタラクティブな3Dを組み込むならR3Fとdrei、シェーダやレンダリングの細部まで制御したいならThree.jsの素書きが向いています。 どれを選んでも、最適化と検証のパイプラインはgltf-transformとvalidatorで共通化できます。
ここまでは、人がBlenderで作ったモデルを扱ってきました。 次の第9章では、現実の物体や空間を撮影して3Dにする技術を扱います。 フォトグラメトリ、NeRF、そして3D Gaussian Splattingです。 メッシュとはまったく違う表現をWebでどう描くのかも、そこで見ていきます。
主な参考資料
- three.js manual(Installation / WebGPURenderer / Color management)
- pmndrs/drei(useGLTF・Environment のドキュメント)
- model-viewer 公式サイト
- Blender Manual: glTF 2.0
- glTF Transform CLI
理解度チェック
Three.jsでGLBを読み込んだところ、形は見えるがモデルがほぼ真っ黒に表示されました。最も可能性が高い原因はどれですか?
キーボード: 1〜4 で選択、Enter で回答