1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Three.js + React Three Fiber でラグジュアリー D2C 向け 3D プロダクトビジュアライザーを作る

1
Posted at

先日、ある時計ブランドのプロダクトチームから連絡をいただいた。「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% がカスタマイズ利用という報告もある。数字を並べると派手だが、要は「迷いを残したまま閉じるユーザーを減らせる」という話だ。

ラグジュアリー腕時計の 3D ビジュアライズイメージ

前提・環境

  • 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

同じモデルをデスクトップとモバイルで使い回すと、片方が必ず損をする。dreiDetailed を使うと距離別に 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 イエローゴールド / プラチナ / ローズゴールド」をユーザーがその場で切り替えられる。MeshStandardMaterialcolor / 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/xrARButton と 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 プロジェクトを担当している。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?