24
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

写真1枚から3DをWebで表示する方法【SAM 3D x React Three Fiber】

Last updated at Posted at 2025-12-02

はじめに

2025年11月19日(水)にMeta社から「SAM 3D」が公開され、写真1枚から3Dオブジェクトが生成できるようになりました。
これまでも近しいサービスはありましたが
・現状無料で利用できること
・特殊なケースを除き、商用利用可(SAM license)

・写真1枚から3Dオブジェクトが作成でき、映っていない部分の補完が優秀
などの理由から大注目のサービスです!

この記事では、SAM 3Dから3Dオブジェクトを作成し、Webブラウザで扱えるようになるまでの手順について書いていきます。

Videotogif.gif

この記事で扱う技術

技術 用途
SAM 3D 画像から3Dオブジェクト生成
SuperSplat 3Dファイルの編集・変換
React Three Fiber Webでの3D表示
spark.js Gaussian Splatファイルの読み込み

1. 画像から3Dオブジェクトを作成

1-1. デモサイトにアクセス

まずはデモサイトにアクセスして3D化したい画像をアップロードします。

image.png

1-2. 3Dオブジェクトを生成

選択範囲を決定して、Generate 3Dをクリックします。
image.png

3秒くらいでクリックした3Dの椅子が生成されました。すごい。。。
image.png

1-3. 3Dオブジェクトをダウンロード

デモサイトなのでこの3Dオブジェクトがダウンロードできないと思いきや、
Add effects → Share → 3D Modelタブ → Download 3D Model
で流行りのGaussian Splatting形式のPLYファイルがダウンロードできます!
image.png

ローカルでの実行について
ちなみにローカルでも利用可能なのですが、
一般家庭にはまずないだろうというスペックが求められるため、デモサイトから実行しています。
A NVIDIA GPU with at least 32 Gb of VRAM.
https://github.com/facebookresearch/sam-3d-objects

Gaussian Splattingとは?
2023年に登場した3D表現の新技術。従来のポリゴンや点群ではなく、
無数の「ぼかし(ガウス分布)」を重ね合わせて3Dを表現します。
リアルな質感を軽量・高速に再現できるため、急速に普及しています。

2. ダウンロードしたオブジェクトの編集

次にWebで扱えるようにするためにSuperSplatを使用していきます。

2-1. ファイルをインポート

サイトを開いたら、左上にある「ファイル」から「インポート」を選択して先ほどダウンロードした3Dオブジェクトを読み込みます。

見づらければ右のスプラットの表示をオフにしましょう
image.png

image.png

2-2. 回転を調整

左にあるトランスフォームの回転を変更します。

回転

X -90
Y 0
Z 180

椅子がひっくり返った感じになっていますが、Webで表示したときには正しく表示されますのでこの設定で大丈夫です。
image.png

2-3. エクスポート

「ファイル」→「エクスポート」→「Splat(.splat)」を選択してエクスポートします。

項目 変換前 変換後
拡張子 .ply .splat
サイズ 7,363KB 3,465KB

ファイルサイズが約半分になりました!

さらに容量を削減したい場合

以下のサイトでksplat形式にコンバートできます。

https://projects.markkellogg.org/threejs/demo_gaussian_splats_3d.php


3. Web表示

ここまでで表示するオブジェクトファイルの作成が完了したので、React Three Fiberを使用してWeb表示できるようにしましょう。

3-1. プロジェクトのセットアップ

# Vite + Reactプロジェクトの作成
npm create vite@latest my-app -- --template react-ts

次に依存関係の3D関連のライブラリをインストールします。

3-2. 依存関係のインストール

npm install three @react-three/fiber @react-three/drei @sparkjsdev/spark

各ライブラリの役割は以下の通りです。

ライブラリ 役割
three 3D表現のコアライブラリ
@react-three/fiber Three.jsをReactの宣言的な方法で扱う
@react-three/drei React Three Fiber用のヘルパーコンポーネント集
@sparkjsdev/spark .splat/.ksplatファイルの表示機能

3-3. CSSの修正

Viteのテンプレートに存在しているcssを書き換えます。
全画面でCanvasを表示するための修正になります。

src/index.css
 body {
   margin: 0;
-  display: flex;
-  place-items: center;
-  min-width: 320px;
-  min-height: 100vh;
+  padding: 0;
+  overflow: hidden;
 }

3-4. splat(ksplat)ファイルを配置

作成したsplat(またはksplat)ファイルをpublicフォルダに配置します。

3-5. App.tsxの書き換え

Modelコンポーネントでsplatファイルを読み込み、Canvas内に描画しています。
OrbitControls: マウス操作でカメラを動かし、オブジェクトを様々な角度から見られます。

src/App.tsx
import { Canvas } from "@react-three/fiber"
import { OrbitControls } from "@react-three/drei"
import { SplatMesh } from "@sparkjsdev/spark"
import { useEffect, useMemo } from "react"

const Model = ({ url }: { url: string }) => {
  // SplatMeshインスタンスを生成(urlが変わった時のみ再生成)
  const splat = useMemo(() => {
    return new SplatMesh({ url })
  }, [url])

  // クリーンアップ処理:コンポーネント破棄時にメモリを解放
  useEffect(() => {
    return () => {
      splat.dispose()
    }
  }, [splat])

  // Three.jsのオブジェクトをReactコンポーネントとして描画
  return (
    <primitive object={splat} />
  )
}

const App = () => {
  return (
    <div style={{
      width: "100%",
      height: "100vh",
      background: "linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 50%, #e1bee7 100%)"
    }}>
      <Canvas camera={{ position: [0, 1, 2], fov: 60 }}>
        {/* urlはpublic配下にいれたsplat,ksplatのファイル名を入れます */}
        <Model url="/yellow-chair.ksplat" />
        <OrbitControls />
      </Canvas>
    </div>
  )
}

export default App

3-6. 動作確認

npm run dev

ローカルを立ち上げて、椅子が表示できました!
image.png

ファイルローダーについて
メモリ管理やローディングを内部で処理してくれるため、最初はdreiのSplatコンポーネントの利用を検討しました。
しかし、画像の様に奥が透けたような見栄えになることがあるため、読み込みはsparkを採用しました。
image.png

4. 活用例

4-1. キービジュアル風

Webサイトやアプリのアクセントとして使えそうです。
ここで使用しているFloatはdreiのコンポーネントで、ラップした要素をゆるやかに揺らす事ができます。

src/App.tsx
import { Canvas } from "@react-three/fiber"
import { Float } from "@react-three/drei"
import { SplatMesh } from "@sparkjsdev/spark"
import { useEffect, useMemo } from "react"

type ModelProps = {
  url: string
  position?: [number, number, number]
  scale?: [number, number, number] | number
  rotation?: [number, number, number]
}

const Model = ({ url, position, scale, rotation }: ModelProps) => {
  // SplatMeshインスタンスを生成(urlが変わった時のみ再生成)
  const splat = useMemo(() => {
    return new SplatMesh({ url })
  }, [url])

  // クリーンアップ処理:コンポーネント破棄時にメモリを解放
  useEffect(() => {
    return () => {
      splat.dispose()
    }
  }, [splat])

  // Three.jsのオブジェクトをReactコンポーネントとして描画
  return (
    <primitive
      object={splat}
      position={position}
      scale={scale}
      rotation={rotation}
    />
  )
}

const App = () => {
  return (
    <div style={{
      width: "100%",
      height: "100vh",
      background: "linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 50%, #e1bee7 100%)"
    }}>
      {/* 3Dシーンのキャンバス:カメラ位置と視野角を設定 */}
      <Canvas camera={{ position: [0, 2, 5], fov: 60 }} onCreated={({ camera }) => camera.lookAt(0, -0.5, 0)}>
        <Float speed={2} rotationIntensity={0.5} floatIntensity={1}>
          <Model url="/yellow-chair.ksplat" position={[-0.8, 1, 0.5]} scale={1} rotation={[Math.PI / 8, 0, Math.PI / 8]} />
        </Float>
        <Float speed={2} rotationIntensity={0.5} floatIntensity={1}>
          <Model url="/blue-chair.ksplat" position={[1, 0, 0]} scale={1} rotation={[0, Math.PI / 4, 0]} />
        </Float>
        <Model url="/brown-sofa.ksplat" position={[0, -2, 0]} scale={3} rotation={[0, 0, 0]} />
      </Canvas>
    </div>
  )
}

export default App

4-2. クリックで情報パネルを表示

オブジェクトをクリックすると、詳細情報が表示されます。ギャラリー的な使い方が出来そうですね。

クリックしてオブジェクトの情報を表示する

src/App.tsx
import { Canvas } from "@react-three/fiber"
import { CameraControls, Html } from "@react-three/drei"
import { SplatMesh } from "@sparkjsdev/spark"
import { useEffect, useMemo, useState, useRef } from "react"
import type CameraControlsImpl from "camera-controls"

// モデルデータの型定義
type ModelData = {
  id: string
  url: string
  position: [number, number, number]
  scale: number
  rotation: [number, number, number]
  title: string
  description: string
}

// モデルのデータ
const models: ModelData[] = [
  {
    id: "yellow-chair",
    url: "/yellow-chair.ksplat",
    position: [-2, 0, 1],
    scale: 1.2,
    rotation: [0, Math.PI / 6, 0],
    title: "黄色い椅子",
    description: "明るい黄色の北欧風デザインチェア。軽量で持ち運びやすく、どんな部屋にも馴染みます。",
  },
  {
    id: "blue-chair",
    url: "/blue-chair.ksplat",
    position: [2, 0, 1],
    scale: 1.2,
    rotation: [0, -Math.PI / 6, 0],
    title: "青い椅子",
    description: "落ち着いた青色のモダンチェア。座り心地が良く、長時間の使用でも快適です。",
  },
  {
    id: "brown-sofa",
    url: "/brown-sofa.ksplat",
    position: [0, 0, -1],
    scale: 3,
    rotation: [0, 0, 0],
    title: "レザーソファ",
    description: "高級感のあるブラウンのレザーソファ。3人掛けで、リビングの主役になる一品です。",
  }
]

type ModelProps = {
  data: ModelData
  onClick: () => void
}

const Model = ({ data, onClick }: ModelProps) => {
  const { url, scale, rotation } = data

  const splat = useMemo(() => new SplatMesh({ url }), [url])

  useEffect(() => {
    return () => splat.dispose()
  }, [splat])

  return (
    <primitive
      object={splat}
      scale={scale}
      rotation={rotation}
      onClick={onClick}
    />
  )
}

type InfoPanelProps = {
  title: string
  description: string
}

const InfoPanel = ({ title, description }: InfoPanelProps) => (
  <Html position={[0, 1, 0]} center style={{ whiteSpace: "nowrap" }}>
    <div style={{
      background: "rgba(0, 0, 0, 0.6)",
      padding: "20px 24px",
      borderRadius: "16px",
      boxShadow: "0 10px 40px rgba(0,0,0,0.2)",
      width: "280px",
      backdropFilter: "blur(10px)",
      whiteSpace: "normal",
    }}>
      <h3 style={{
        margin: "0 0 12px 0",
        fontSize: "18px",
        fontWeight: "bold",
        color: "#fafafa",
        borderBottom: "2px solid #e0e0e0ff",
        paddingBottom: "8px"
      }}>
        {title}
      </h3>
      <p style={{ margin: 0, fontSize: "14px", color: "#fafafa", lineHeight: "1.6" }}>
        {description}
      </p>
    </div>
  </Html>
)

const App = () => {
  const [selectedId, setSelectedId] = useState<string | null>(null)
  const cameraControls = useRef<CameraControlsImpl | null>(null)

  const handleModelClick = (model: ModelData) => {
    setSelectedId(selectedId === model.id ? null : model.id)
    cameraControls.current?.setLookAt(
      model.position[0] + 2, model.position[1] + 1.5, model.position[2] + 3,
      model.position[0], model.position[1], model.position[2],
      true
    )
  }

  return (
    <div style={{ width: "100%", height: "100vh", background: "#fff" }}>
      <Canvas camera={{ position: [0, 3, 4], fov: 50 }}>
        <CameraControls ref={cameraControls} />
        <ambientLight intensity={0.6} />
        <directionalLight position={[5, 5, 5]} intensity={0.8} />

        {models.map((model) => (
          <group key={model.id} position={model.position}>
            <Model data={model} onClick={() => handleModelClick(model)} />
            {selectedId === model.id && (
              <InfoPanel title={model.title} description={model.description} />
            )}
          </group>
        ))}

        <gridHelper args={[20, 20, "#020202"]} position={[0, -0.5, 0]} />
      </Canvas>
    </div>
  )
}

export default App

まとめ

この記事では、Meta社の「SAM 3D」を使って写真1枚から3Dオブジェクトを生成し、Webブラウザで表示するまでの流れを解説しました。

ステップ 内容
1. 生成 SAM 3Dデモサイトで画像から3Dオブジェクトを生成
2. 変換 SuperSplatでPLY→splat形式に変換し軽量化
3. 表示 React Three Fiber + sparkでWebブラウザに表示

専門知識がなくても気軽に3Dオブジェクトを作成できることもあり、ゲームのオブジェクトとして利用してみたり、バーチャル展示会などの様々なケースで使えそうだと思いました。
ぜひ皆さんも試してみてください!
あと、R3Fの仕事あったら下さい!

24
6
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
24
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?