ファイルからブラウザへ — この章のゴール

第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
Web 3Dの基本パイプライン。表示ライブラリは用途に応じて3つから選ぶ

表示の手段は大きく3つあります。 1タグで済ませる<model-viewer>、低レベルに全部書けるThree.js、そしてReactの書き方でThree.jsを扱えるReact Three Fiber(以下R3F)です。 まず2026年10月時点のバージョンを押さえておきます。3D系のライブラリは破壊的変更が多いので、ネットで拾ったコードを使う前に必ず版を確認してください。

2026年10月時点の主要パッケージ

パッケージ最新版押さえどころ
three0.186.1(r186、2026-09-24)リビジョン番号rNNNで呼ばれる。毎月のように更新
@react-three/fiber9.8.2peer依存がreact 19以上19.4未満。React 18ならv8系
@react-three/drei10.7.9R3F用の便利コンポーネント集。useGLTFなど
@google/model-viewer4.4.0peer依存がthree ^0.183.0。最新threeと同居させると衝突
@gltf-transform/cli4.5.1glTFの検査・最適化CLI。optimizeが入口
gltfpack1.3.0meshoptimizer付属の最適化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/three
import { 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"です。 端末が対応している方式のうち、先に書いたものから使われます。

モード動く環境中身
webxrAndroid Chromeブラウザ内でWebXRを使うAR
scene-viewerAndroidGoogleのScene Viewerアプリに渡す
quick-lookiOSAppleの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のものです。

設定推奨理由
FormatglTF 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=MetallicglTFのチャネル規約。Separate Colorノードで配線すると認識される
ノーマルマップNormal Mapノード経由、Non-ColorTangent 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.glb

optimizeは、複数の処理をまとめて実行するコマンドです。 主なオプションと既定値は次のとおりです(v4.5.1のoptimize --helpの出力より)。

オプション値既定
--compressdraco / meshopt / quantize / falsemeshopt
--texture-compressktx2 / webp / avif / auto / falseauto(元の形式のまま再圧縮)
--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でどう描くのかも、そこで見ていきます。

主な参考資料

理解度チェック

問題 0 / 50%
Q1

Three.jsでGLBを読み込んだところ、形は見えるがモデルがほぼ真っ黒に表示されました。最も可能性が高い原因はどれですか?

キーボード: 1〜4 で選択、Enter で回答