先日、ある時計ブランドのプロダクトチームから連絡をいただいた。「EC ページの写真をいくら増やしても、最後の一押しが効かない」と。
本シリーズ「Production で動く AI システム実装記録」は、1 回目と 2 回目で AI バックエンド側の話を扱ってきた。前回は n8n + Claude + Notion で動くナレッジボットを 3-layer Gateway パターンで構築する話を書いた。最終回となる今回は、同じ "Production で動く" を別レイヤー — フロント描画 — で扱う。具体的には、ラグジュアリー D2C 向けの 3D プロダクトビジュアライザーを Three.js + React Three Fiber で実装した記録だ。
ラグジュアリー領域では、素材感や仕上げが価格訴求の核心になる。EC 写真をいくら積んでも、たとえば 18K ゴールドの艶やプラチナの冷たさは伝わりきらない。業界事例として、衣料 D2C で 3D 表現を導入した結果コンバージョン 42% 増・返品 30% 減という公開データがあり、ジュエリー 3D コンフィギュレーターでは 3 か月で売上 55% 増・注文の 60% がカスタマイズ利用という報告もある。数字を並べると派手だが、要は「迷いを残したまま閉じるユーザーを減らせる」という話だ。

前提・環境
Next.js 14 (App Router)
React 18 / TypeScript
three.js 0.169
@react-three/fiber 9
@react-three/drei 9.x
@react-three/xr 6.x (WebXR)
gltf-transform CLI (アセット最適化)
実機検証は iPhone 15 Pro (iOS 17.5 Safari) / Pixel 8 (Android 14 Chrome) / Desktop Chrome の 3 機種で行った。数値は最後にまとめて出す。
実装ステップ
1. glTF アセット最適化 — Draco + KTX2
最初の壁はファイルサイズだった。クライアントから受け取った時計モデルの初期 glTF は 24MB. モバイル 4G で 8 秒前後の初回ロードがかかり、これでは EC のセッション内に 3D を見てもらう前に離脱される。
解はパイプラインを整えること。メッシュは Draco 圧縮で 90~95%、テクスチャは KTX2 (Basis Universal) で GPU 上の常駐サイズを 1/10 にできる。CLI はワンライナーで済む。
npx @gltf-transform/cli optimize input.glb output.glb --texture-compress ktx2 --compress draco細かい話だが、テクスチャは用途で codec を分けると画質が出る。ノーマルマップは UASTC (高品質)、ディフューズや AO は ETC1S (軽量) を選ぶ。gltf-transform の各サブコマンドで個別指定できる。
結果: 24MB → 2.1MB. モバイル 4G で 8 秒が 0.7 秒。10 倍弱に見えるかもしれないが、ファーストビューで動かない 3D を待ってくれるユーザーはほぼいない。この 7 秒の差が CV の "最後の一押し" を左右する。
2. Canvas と Lighting
R3F の基本セットアップ。ラグジュアリー商材では Lighting が 8 割を決めると言っていい。HDRI 環境マップを Studio プリセットで張り、加えてキー光を 1 灯。フィルライトは弱めにする。
import { Canvas } from '@react-three/fiber'
import { Environment, OrbitControls } from '@react-three/drei'
export function Stage({ children }: { children: React.ReactNode }) {
return (
<Canvas
shadows
dpr={[1, 2]}
camera={{ position: [0, 0.4, 1.2], fov: 35 }}
gl={{ antialias: true, powerPreference: 'high-performance' }}
>
<ambientLight intensity={0.25} />
<directionalLight position={[3, 4, 2]} intensity={1.2} castShadow />
<Environment preset="studio" />
{children}
<OrbitControls enablePan={false} minDistance={0.6} maxDistance={2.5} />
</Canvas>
)
}dpr={[1, 2]} は端末 DPR の上限を 2 に抑えて、Retina 端末で fillrate を浪費しすぎないための保険。OrbitControls はパンを切る — ラグジュアリー商材では「ユーザーが見たい角度に回す」だけが基本動線で、パンできると逆に迷子になる。
3. GLTF 読み込みと Auto LOD
同じモデルをデスクトップとモバイルで使い回すと、片方が必ず損をする。drei の Detailed を使うと距離別に LOD を自動切替できる。当初は 3 段にしていたが、結局 2 段に減らした (理由は後述)。
import { useGLTF, Detailed } from '@react-three/drei'
import { Suspense } from 'react'
useGLTF.preload('/models/watch-high.glb')
useGLTF.preload('/models/watch-low.glb')
export function Watch() {
const high = useGLTF('/models/watch-high.glb')
const low = useGLTF('/models/watch-low.glb')
return (
<Suspense fallback={null}>
<Detailed distances={[0, 1.8]}>
<primitive object={high.scene} />
<primitive object={low.scene} />
</Detailed>
</Suspense>
)
}事前に gltfjsx --transform で JSX 化してアセットをまとめると、Draco / WebP / binary 化が一括で走る。手動でやるより楽だ。
4. 素材バリアントの切替
ラグジュアリー時計やジュエリーで最も価値が出るのがここだ。「18K イエローゴールド / プラチナ / ローズゴールド」をユーザーがその場で切り替えられる。MeshStandardMaterial の color / metalness / roughness を state で切り替えるだけ。簡単に見えるが、ラグジュアリーらしさは roughness 0.12 か 0.18 かの差で決まる。
import { useEffect, useRef } from 'react'
import { Color, Mesh, MeshStandardMaterial } from 'three'
import type { GLTF } from 'three-stdlib'
type Variant = 'yellowGold' | 'platinum' | 'roseGold'
const variantConfig: Record<Variant, { color: string; metalness: number; roughness: number }> = {
yellowGold: { color: '#d4af37', metalness: 1.0, roughness: 0.15 },
platinum: { color: '#e5e4e2', metalness: 1.0, roughness: 0.12 },
roseGold: { color: '#b76e79', metalness: 1.0, roughness: 0.18 },
}
export function WatchBody({ variant, gltf }: { variant: Variant; gltf: GLTF }) {
const ref = useRef<Mesh>(null)
useEffect(() => {
if (!ref.current) return
const cfg = variantConfig[variant]
const mat = ref.current.material as MeshStandardMaterial
mat.color = new Color(cfg.color)
mat.metalness = cfg.metalness
mat.roughness = cfg.roughness
mat.needsUpdate = true
}, [variant])
return <primitive ref={ref} object={gltf.scene} />
}余談として、選んだ素材に応じて「この仕上げが映える生活シーン」を AI で生成・配信する拡張も入れられる。前回構築した Multi-LLM Gateway のような Gateway を挟むと、フロントから直接 LLM を叩かずに済むので、レート制限・キャッシュ・モデル切替を一箇所に閉じ込められて運用が静かになる。
5. AR モード — WebXR + Hit-Test
「腕に乗せて見たい」というのは時計ブランドの本懐だ。@react-three/xr の ARButton と Hit-Test API を組み合わせれば、専用アプリ不要で iOS Safari 17+ と Android Chrome の両方で動く。
import { Canvas } from '@react-three/fiber'
import { XR, ARButton, useHitTest } from '@react-three/xr'
import { useRef } from 'react'
import { Mesh } from 'three'
function Reticle() {
const ref = useRef<Mesh>(null)
useHitTest((hit) => {
if (!ref.current) return
hit.decompose(ref.current.position, ref.current.quaternion, ref.current.scale)
})
return (
<mesh ref={ref} rotation-x={-Math.PI / 2}>
<ringGeometry args={[0.04, 0.05, 32]} />
<meshBasicMaterial color="white" />
</mesh>
)
}
export function ARScene({ children }: { children: React.ReactNode }) {
return (
<>
<ARButton sessionInit={{ requiredFeatures: ['hit-test'] }} />
<Canvas>
<XR>
<ambientLight intensity={0.5} />
<Reticle />
{children}
</XR>
</Canvas>
</>
)
}iOS Safari 17 から WebXR の hit-test が安定してきた。Vision Pro / Quest 系でも同じコードがそのまま走る。WebXR は仕様が揺れがちだが、2026 年の今、ジュエリー・時計用途であれば実用域に入っていると判断していい。
6. ハマりポイント — LOD 3 段から 2 段へ
最初は SkinnedMesh + morphTarget でストラップの曲がりまで表現しようとした。デスクトップでは綺麗に動いた。だが、AR セッションを iPhone で起動した瞬間、数十秒で GPU メモリリークが起きてセッションが死ぬ。Safari の WebXR 実装が SkinnedMesh の morphTarget を毎フレーム再アロケートしている疑いがあった (厳密には確証はない)。
結局のところ、ストラップは静的な 2 段 LOD に切り替え、morphTarget は捨てた。ここで 30 分悩んだ。だが、AR モードでは「ユーザーは細部のしなりまで見ない」という前提が正しかった。失った表現より、セッションが落ちないほうが何倍も価値がある。
動作確認
3 機種で実機計測した結果がこちら。アセット最適化と LOD で大きく稼げた。
iPhone 15 Pro (iOS 17.5, Safari) — 初回ロード 0.7 秒 / 60 FPS 安定 / AR セッション 安定
Pixel 8 (Android 14, Chrome) — 初回ロード 0.9 秒 / 45-55 FPS / AR セッション 安定
Desktop Chrome (M2 Pro) — 初回ロード 0.4 秒 / 60 FPS 上限張り付き
「45-55 FPS」と書くと不安に見えるかもしれない。だがラグジュアリー商材の閲覧体験では 45 FPS でも体感ヌルさは出ない。プロダクトを回す速度自体が遅いからだ。FPS が効くのは AR でカメラを動かす瞬間の追従だけで、ここは Pixel 8 でも問題なかった。
応用・発展
時計だけでなく、メガネ・イヤリングなど「身に着ける」系全般に同じ AR 構造が流用できる
家具 EC: 部屋に置いて見られる WebAR は IKEA 以降標準化したが、ラグジュアリー家具 (1 点 100 万超のソファ等) は ROI が高い
化粧品パッケージ: ボトルの質感・ホログラム箔・グラデーションを 3D で見せる D2C は意外と未開拓
自動車外装: ボディカラーの実車光下シミュレーター. 屋外 HDRI を切り替える UI とセットで設計
アパレル: 生地ドレープを布シミュで動的に表現する重い実装より、まずは静的ポーズの素材バリアント切替から入るほうが ROI が早い
まとめ
3D Web は「派手だが重い」というイメージが長くついて回ったが、Draco + KTX2 + LOD + WebXR の組み合わせで 2026 年現在は実用域に入った。要は写真を増やすのと同じ感覚で 3D を載せられる時代になっている。ラグジュアリー D2C のような「素材感が価格を正当化する」領域では、とくに効きが早い。
本シリーズ「Production で動く AI システム実装記録」はこれで全 3 回完結となる。1, 2 回目で AI バックエンド (Multi-LLM Gateway / ナレッジボット) を扱い、最終回の本記事で 3D フロントエンドへ切り替えた構成だった。シリーズを通じて筆者が大事にしたのは「ProductionGrade に必要な手間を省かない」という一点に尽きる。
筆者は 5years+ で韓国・日本の D2C / 製造業向け Web ・ AI プロジェクトを担当している。