Open44

Blender、Three.js学習チュートリアル集、成果物、Tipsなど

ピン留めされたアイテム
はる@フルスタックチャンネルはる@フルスタックチャンネル

目標 Become a 3D Web FullStack Engineer

  1. Blenderでオリジナルキャラクターを作成する
    1.VRM形式で出力する
    2. オリジナルキャラクターをUnity、VRChatで動かす
    3. オリジナルキャラクターをVTuver用に動かす
    4. フルスタックチャンネルのYouTubeにオリジナルキャラクターを登場させる
  2. インタラクティブなWebサイトを作成する
    1. Blenderで作成したシーンを、Next.jsとThree.jsに取り込む
    2. @pixiv/three-vrmで動かす
    3. フルスタックチャンネルのQAサイトに3DでAIアバターとして登場させる
  3. 3Dブラウザゲームを作成する
    1. シェーダーを自由に扱えるようになる
    2. Three.jsを使用して、ゲームを作成
    3. モデルはBlenderで作成

参考

https://next.junni.co.jp/

https://zenn.dev/junni/articles/bb71f44c89cb77

はる@フルスタックチャンネルはる@フルスタックチャンネル

成果物

01 ショートケーキ

ゆきさんのモデリングチュートリアル
初めてのモデリング

02 白銀圭

mmCGチャンネルさんの「白銀圭」チュートリアル
初めてのキャラクターモデリング
モデリングからアニメーションまで一通り学習

03 リアルタイムCGキャラクター制作入門

書籍でVRChatで動かすとこまで学習

04 ととたん

ふさこさんの「ととたん」チュートリアル
VRMを動かすところまで完走できて、とてもわかりやすいチュートリアル
Substance Painterにも入門
何回も見直す

05 ととたんWeb

Three.jsを使用して、Webでととたんを表示
GLB形式でアニメーションと一緒にエクスポート

06 Fall Guys

wawa senseiチュートリアル
Three.jsで作成
オリジナルキャラクターでゲームが作れるようになると楽しい

07 シューターゲーム

wawa sebseiチュートリアル
マルチプレイヤーで対戦できる

08 ブック

wawa senseiチュートリアル
本の表現方法を学習

09 水槽

wawa senseiチュートリアル
魚の動きを学習
ポストプロセッシングの使い方も学習

10 言語学習アプリ

Next.jsを使用してThree.jsを扱えるようになった
ChatGPTとの組み合わせができるようになったので、幅が広がった

11 リゾルブシェーディング

シェーディングの学習
three-custom-shader-materialでオリジナルのシェーダーを作ることが可能
シェーディングを極めれば色々な表現が可能

12 カーゲーム

車を選択して、レースゲームの作り方を学習

13 モデルとアニメーション

Mixamoアニメーションとモデルの組み合わせてブラウザで表示
https://poly.pizza/u/Polygonal Mind

14 ゲームマネージャー

ゲームマネージャーでゲームのロジックを組んで、3Dゲームの作り方を学習

15 3Dテキスト

3Dにテキストを表示
ECサイトにも使用できる

16 トランジション

シェーダーを使用したトランジションの学習

17 Mesh Portal Material

MeshPortalMaterialで別の空間を表示させて、CameraControlsでカメラの移動を学習

18 Mesh Explosion Effect

useScrollを使用して、スクロールすると徐々にmeshのpositionを移動

19 モデルカスタマイズ

ブラウザからモデルのカスタマイズする方法を学習
これぐらいは簡単にできる

20 シェーダー

シェーダーで色々と実験、奥が深い

21 スモーク

シェーダーの学習
シェーダーでスモークが作れるようになった

22 アニメーション

react-springでアニメーションの学習
@react-spring/threeでuseSpringを使用すると、アニメーションが作れる
https://www.react-spring.dev/docs/guides/react-three-fiber

23 スクロール連動飛行機サイト

laminaライブラリでグラデーション、カーブとラインの生成、スクロール、カメラと飛行機の位置合わせ、テキストセクション / 雲の配置など、スクロールと連動して飛行機をカーブに沿って移動

24 かなゲーム

キャラクターコントロールとゲームの作り方、メニューなど学習

25 3Dポートフォリオ

Blenderでライトベイクしたものを表示して、スクロールによるアニメーションを学習
参考にオリジナルの3Dポートフォリオを作る

26 Sims

Socket.ioでマルチプレイヤーを実装
パスファインディングは、ゲームでも使える
https://github.com/qiao/PathFinding.js/
チャットも追加して、ポートフォリオに組み込みたい

27 3Dアバタービルダー

複数のモデルを用意して着せ替えすることができる
この仕組みは応用できるので、組み合わせていきたい

28 シューズビルダー

シューズの各パーツの色が変更できる

29 Tシャツビルダー

Tシャツの色を変更

30 スケートボードビルダー

3Dモデルを使用したECサイトの作り方を学習

Build a 3D Skateboard Website and Customizer App with Next.js 15, GSAP, Three.js and Prismic - 2025

31 ライトセイバービルダー

はる@フルスタックチャンネルはる@フルスタックチャンネル

YouTubeチュートリアル

mmCGチャンネル animetic

神神モデラー
まずは白銀圭チュートリアル

https://www.youtube.com/@mmcganimetic/videos

ふさこ / 3D自習室

初心者向けチュートリアル
ととたんチュートリアルはまずやるべき
VRMでキャラクターを動かすところまでできます

https://www.youtube.com/@fusako3d/videos

夏森轄(なつもり かつ)

初心者向きのわかりやすいチュートリアル

https://www.youtube.com/@user-sy9do4tr4h/videos

Blender Guru

神チュートリアル
まずはドーナッツから

https://www.youtube.com/@blenderguru

Shonzo

神モデラー

https://www.youtube.com/@Shonzo/videos

3D Bibi

初心者向きのわかりやすいチュートリアル

https://www.youtube.com/@3DBibi/videos

Yuki's blender school

初心者向きのわかりやすいチュートリアル

https://www.youtube.com/@yuki_blender/videos

Polygon Runway

色々なモデリングの学習ができる

https://www.youtube.com/@polygonrunway/videos
https://polygonrunway.com/

The Goose Tavern

2D x 3D アニメーション

https://www.youtube.com/@goosetavern/videos

むえん

初心者向けチュートリアル
顔をモデリングするときに参考

https://www.youtube.com/@むえん-t7z/videos

TomCAT - Characters, Art and Tutorials

キャラクターモデリング

https://www.youtube.com/@TomCAT_Character_Art/videos

M design

初心者向けチュートリアル

https://www.youtube.com/@Mdesign_blender/videos

Ksenia Starkova

リアルタイム動画
かわいいキャラクターを作りたいときに参考

https://www.youtube.com/@KseniaStarkova/videos

Benyamin Bayati

キャラクターモデリングタイムラプス

https://www.youtube.com/@3dbenart/videos

Fullsworld

かわいいキャラクターモデリング

https://www.youtube.com/@fullsworld/videos

Bakuan Guen

キャラクターモデリング

https://www.youtube.com/@bakuanguen/videos

ZTOONE 3D

キャラクターモデリング

https://www.youtube.com/@Ztoone3d/videos

Blender Default Cube

キャラクターモデリング

https://www.youtube.com/@blenderdefaultcube/videos

lacruzo

ローポリモデルの参考

https://www.youtube.com/@lacruzo/videos

はる@フルスタックチャンネルはる@フルスタックチャンネル

Blenderプラグイン

Blenderのバージョンを変えるアプリ
Blender Lancher:https://github.com/Victor-IX/Blender-Launcher-V2

モデリング

AutoMirror:標準
LoopTools:標準
Copy Attributes Menu:標準
Edge Flow:https://github.com/BenjaminSauder/EdgeFlow
Hide Only Vertex:https://bookyakuno.gumroad.com/l/hide_only_vertex

リギング

VRM Import Export:https://vrm-addon-for-blender.info/en/

UV展開

TexTools:https://github.com/franMarz/TexTools-Blender
Magic UV:標準

テクスチャ

Auto Reload:https://github.com/samytichadou/Auto_Reload_Blender_addon

シェイプキー

SKkeeper:https://github.com/smokejohn/SKkeeper
Mio3 ShapeKey:https://github.com/mio3io/Mio3Shapekey
ShapeKeysUtil:https://booth.pm/ja/items/1224307

Unity

unity-overwriter:https://github.com/ina-amagami/unity-overwriter
VRMSpringBoneTool & VRMSkirtTool:https://nalulululuna.booth.pm/items/4649509
CopyComponentsByRegex:https://github.com/Taremin/CopyComponentsByRegex

はる@フルスタックチャンネルはる@フルスタックチャンネル

Three.js

Three.js公式

https://threejs.org/

React Three Fiber

https://r3f.docs.pmnd.rs/getting-started/introduction

Three.js Jorney

https://threejs-journey.com/

wawa sensei

https://wawasensei.dev/ja

Dan Greenheck

https://www.youtube.com/@dangreenheck/videos

Prismic

3D高品質ウェブサイトチュートリアル
https://www.youtube.com/@Prismic/videos

three-vrm

Three.jsでVRMを表示させる
https://github.com/pixiv/three-vrm

Grabient

グラデーション生成
https://www.grabient.com/

Poly Haven

HDR画像ダウンロード
https://polyhaven.com/

Skybox AI

AIでHDR画像を生成
https://skybox.blockadelabs.com/

Squoosh

画像圧縮
https://squoosh.app/

Shadow Mapping In Three.js

https://mofu-dev.com/en/blog/threejs-shadow-map/

はる@フルスタックチャンネルはる@フルスタックチャンネル

VRMモデル

3tene

表情を変えたり、モーションの確認が可能
背景を緑に設定できるので、OBSと組み合わせてVTuberになることも可能

https://3tene.com/free/

VMagicMirror

VRMをキーボードとマウスだけで動かせるWindows向けソフト
OBSと組み合わせてVTuberになることも可能

https://booth.pm/ja/items/1272298

ニコニ立体ちゃん

https://3d.nicovideo.jp/works/td32797

VRM Live Viewer

https://booth.pm/ja/items/1783082

はる@フルスタックチャンネルはる@フルスタックチャンネル

3Dモデル

Poly Pizza

ロイヤリティフリーの低ポリゴン3Dモデルのコレクション
https://poly.pizza/

Pmndrs Market

ロイヤリティフリー3Dアセットのコレクション
https://market.pmnd.rs/

Sketchfab

3Dモデルを共有およびダウンロードするためのプラットフォーム
https://sketchfab.com/

Unity Asset Store

Unity用ですが、プロジェクトに使用できる多くの3Dモデルパックがある
https://assetstore.unity.com/ja-JP

Quaternius

無料の3Dモデルパックを共有
https://quaternius.com/

Kenney

多くの高品質な無料3Dモデルパックを共有

https://www.kenney.nl/assets/category:3D

ithappy studio

高品質なローポリモデル
https://ithappystudios.com/characters/

はる@フルスタックチャンネルはる@フルスタックチャンネル

コードTips

remap

シェーダーでよく使う
指定した範囲に変更する

    float inverseLerp(float value, float minValue, float maxValue){
      return (value - minValue) / (maxValue - minValue);
    }

    float remap(float value, float inMin, float inMax, float outMin, float outMax){
      float t = inverseLerp(value, inMin, inMax);
      return mix(outMin, outMax, t);
    }

    float smoothenProgression = remap(uProgression, 0.0, 1.0, -uSmoothness / 2.0, 1.0 + uSmoothness / 2.0);

gltfjsx

Blenderで作成したモデルを、Three.jsで表示するときによく使う
-kはKeep original nameで名前を使用するときに設定

npx gltfjsx public/models/test.glb -o src/components/Test.jsx -r public -k

https://github.com/pmndrs/gltfjsx

はる@フルスタックチャンネルはる@フルスタックチャンネル

スクロールマネージャー

全体像

export const ScrollManager = (props) => {
  const { section, onSectionChange } = props

  // 1) useScroll()でスクロール情報を取得
  const data = useScroll()
  const lastScroll = useRef(0)
  const isAnimating = useRef(false)

  // 2) スタイル調整
  data.fill.classList.add("top-0")
  data.fill.classList.add("absolute")

  // 3) 受け取った section が変化したら、そのセクションへ GSAP で自動スクロール
  useEffect(() => {
    gsap.to(data.el, {
      duration: 1,
      scrollTop: section * data.el.clientHeight,
      onStart: () => {
        isAnimating.current = true
      },
      onComplete: () => {
        isAnimating.current = false
      },
    })
  }, [section])

  // 4) 毎フレームチェックし、スクロール方向でセクションを切り替える
  useFrame(() => {
    if (isAnimating.current) {
      lastScroll.current = data.scroll.current
      return
    }

    // 現在のスクロール位置を pages との関係でセクション計算
    const curSection = Math.floor(data.scroll.current * data.pages)

    // 下方向へスクロールしていて、まだセクション0なら sectionを1に変える
    if (data.scroll.current > lastScroll.current && curSection === 0) {
      onSectionChange(1)
    }
    // 上方向へスクロールしていて、スクロール位置が 1/(pages - 1) 未満なら sectionを0に戻す
    if (
      data.scroll.current < lastScroll.current &&
      data.scroll.current < 1 / (data.pages - 1)
    ) {
      onSectionChange(0)
    }

    lastScroll.current = data.scroll.current
  })

  return null
}

詳細解説

1. useScroll() によるスクロール管理

const data = useScroll()
  • @react-three/dreiuseScroll() は、WebGL コンテンツと HTML ページのスクロールを連動させる仕組みを提供します。
  • data.el はスクロール要素(<div>)への参照、data.scroll.current は 0~1 の範囲で現在のスクロール割合、data.pages は総ページ数などの情報が入っています。
  • data.fill はスクロールバー自体 (fill 要素) を指し、そこにクラスを追加してスタイル調整しています。

2. スタイル調整 (data.fill.classList.add("..."))

data.fill.classList.add("top-0")
data.fill.classList.add("absolute")
  • スクロールバー(data.fill)が固定されるように、CSS クラスを追加。
  • ここでは "top-0", "absolute" といったクラス名が使われています(Tailwind CSS などを想定?)
  • これでスクロールバーの見た目・位置を微調整しているだけです。

3. section の変更時に自動スクロール (useEffect + GSAP)

useEffect(() => {
  gsap.to(data.el, {
    duration: 1,
    scrollTop: section * data.el.clientHeight,
    onStart: () => {
      isAnimating.current = true
    },
    onComplete: () => {
      isAnimating.current = false
    },
  })
}, [section])
  • 親コンポーネントから section が渡される と、gsap.to(...)data.el.scrollTop をアニメーションし、特定セクション までスクロールさせます。
  • data.el.clientHeight は 1 ページ分の高さとみなし、section * clientHeight で “セクション番号 × 1 ページの高さ” に移動します。
  • onStart / onCompleteisAnimating.current を切り替えて、アニメーション中は他のスクロール処理を無視 しています。

4. useFrame で毎フレーム監視 → スクロール量に応じてセクション切り替え

useFrame(() => {
  if (isAnimating.current) {
    lastScroll.current = data.scroll.current
    return
  }

  const curSection = Math.floor(data.scroll.current * data.pages)

  // 下方向にスクロールかつセクション0 => セクション1へ
  if (data.scroll.current > lastScroll.current && curSection === 0) {
    onSectionChange(1)
  }
  // 上方向へスクロール && スクロール位置が 1/(data.pages - 1) より小さい => セクション0へ
  if (
    data.scroll.current < lastScroll.current &&
    data.scroll.current < 1 / (data.pages - 1)
  ) {
    onSectionChange(0)
  }
  lastScroll.current = data.scroll.current
})
  • isAnimating.currenttrue の場合は、GSAP アニメ中なので ユーザー操作を無視 し、lastScroll.current だけ更新して早期 return
  • そうでなければ、現在のスクロール量 (data.scroll.current) を見てセクションを判定
  • curSection = Math.floor(data.scroll.current * data.pages) で、今のページを整数でざっくり算出(例: pages = 2 なら 0~1ペ ージを想定)。
  • 条件分岐で「下方向スクロールしている && curSection === 0onSectionChange(1)」といったシンプルなロジックを使っています。
  • onSectionChange() は親から受け取ったコールバックで、**「現在セクションを X にして!」**という指示を行う → 結果、section の state が変わり、先ほどの useEffect によってスクロール位置が変わる……というループです。

動作イメージ

  1. セクション 0 → 1

    • ユーザーが下方向へスクロールすると、data.scroll.current が上昇。
    • if (scroll.current > lastScroll.current && curSection === 0) { onSectionChange(1) } が発火。
    • 親は section を 1 に更新 → useEffect の GSAP scrollTop アニメ → 実際に 1 ページ分下へ移動。
  2. セクション 1 → 0

    • ユーザーが上方向へスクロールし、 scroll.current < lastScroll.current となる。
    • さらに data.scroll.current < 1 / (data.pages - 1) ならまだ「上のセクションに戻りたい」状態とみなす。
    • onSectionChange(0) でセクションを戻し、再度 GSAP でスクロール位置を 0 に戻す。
  3. アニメーション中は無視

    • GSAP がスクロールをアニメ中 (isAnimating.current = true) はユーザー操作を上書きしない。
    • アニメ完了後に isAnimating.current = false となって、次の操作を受け付ける。

まとめ

  • useScroll() でスクロール要素と状態を取得し、gsap.to(..., {scrollTop: ...})セクション単位の自動スクロール を行う。
  • isAnimating フラグを設けて、アニメ中はユーザー操作をキャンセルして競合を防ぐ。
  • useFrame で毎フレームスクロール量を監視し、条件に応じて onSectionChange(...) コールバックを呼び出す。
  • つまり 「セクション 0/1」 の 2ページ切り替え をシンプルに制御する仕組み となっています。

このように、JS 側でスクロール位置を完全管理したいときに便利な設計です。ユーザーがスクロールするたびに「セクションが変わったか?」を判定し、自動でスクロール位置を補正(スナップ) するという流れです。

はる@フルスタックチャンネルはる@フルスタックチャンネル

PathFinding.js

https://github.com/qiao/PathFinding.js/

PathFinding.js を使ったパスファインディングの基礎的なサンプルコードを示しつつ、ポイントを解説していきます。

サンプル全体の流れ

  1. サーバーのセットアップ (socket.io)

    • 接続/切断を監視し、接続してきたクライアントを管理します。
    • キャラクターの情報や、マップにあるアイテムなどをクライアントへ送信します。
  2. マップとオブジェクトの定義

    • マップの大きさや区画数(gridDivision)、アイテム(オブジェクト)の配置情報を定義します。
    • アイテムの位置・回転・サイズから、通行可能かどうかを計算し、歩けるセル(タイル)を設定します。
  3. パスファインディングの初期化

    • new pathfinding.Grid(...) でマップに対応するグリッドを生成します。
    • new pathfinding.AStarFinder(...) で A* アルゴリズムを使えるように準備します。
  4. 歩行可能セルの更新 (updateGrid)

    • マップ全体を「歩行可(true)」で初期化し、家具や壁となるアイテムのセルは「歩行不可(false)」に書き換えます。
    • 回転によって幅(width)と高さ(height)が変わるアイテムは、その分のセルを歩行不可にします。
  5. パスの探索 (findPath)

    • クライアントから「(from) から (to) へ移動したい」というリクエストを受け取ったら、 findPath(from, to) を使って最短ルートを計算します。
    • 返ってきた path (座標リスト)をキャラクターの経路としてクライアントに送信します。
  6. キャラクターの更新やイベントの処理

    • キャラクターアバター、ダンスイベントなどをやり取りするために socket.io のイベントを定義しています。

コードのポイント解説

1. PathFinding.js のインポートと初期化

import pathfinding from "pathfinding"

const grid = new pathfinding.Grid(
  map.size[0] * map.gridDivision,
  map.size[1] * map.gridDivision
)

const finder = new pathfinding.AStarFinder({
  allowDiagonal: true,
  dontCrossCorners: true,
})
  • pathfinding.Grid
    マップを「升目(Grid)」として管理します。ここで map.size[0] * map.gridDivision は横方向のセル数、 map.size[1] * map.gridDivision は縦方向のセル数を示します。

    • 例: map.size = [10, 10]gridDivision = 2 の場合、横 20 マス、縦 20 マスのグリッドを生成します。
  • pathfinding.AStarFinder
    A* アルゴリズムを使った最短経路探索ができる Finder を生成します。 allowDiagonal: true は斜め移動を許可する設定で、dontCrossCorners: true は「角を対角線でショートカットしない」ようにする設定です。

2. 歩行不可セルの設定 (updateGrid)

const updateGrid = () => {
  // RESET: いったん全部「歩行可(true)」に初期化
  for (let x = 0; x < map.size[0] * map.gridDivision; x++) {
    for (let y = 0; y < map.size[1] * map.gridDivision; y++) {
      grid.setWalkableAt(x, y, true)
    }
  }

  // アイテムごとに歩行不可を設定
  map.items.forEach((item) => {
    if (item.walkable || item.wall) {
      return
    }
    // 回転によって width, height をスワップ
    const width  = (item.rotation === 1 || item.rotation === 3) ? item.size[1] : item.size[0]
    const height = (item.rotation === 1 || item.rotation === 3) ? item.size[0] : item.size[1]

    for (let x = 0; x < width; x++) {
      for (let y = 0; y < height; y++) {
        grid.setWalkableAt(
          item.gridPosition[0] + x,
          item.gridPosition[1] + y,
          false
        )
      }
    }
  })
}
  • 家具や壁などの位置(複数セル分)をまとめて歩行不可にする部分です。
  • item.walkabletrue ならカーペットなど、歩けるオブジェクトなのでスキップしています。

3. パス検索関数 (findPath)

const findPath = (start, end) => {
  // clone しておかないと毎回の検索でグリッドの状態が書き換わってしまう
  const gridClone = grid.clone()

  // A* アルゴリズムでパスを探索
  const path = finder.findPath(
    start[0], start[1],
    end[0], end[1],
    gridClone
  )
  return path
}
  • grid.clone() でグリッドをコピーし、 finder.findPath(...) で (startX, startY) から (endX, endY) までの最短パスを取得しています。
  • 戻り値 path は「 [x, y] 」の配列になっており、移動するための座標一覧が並びます。

4. socket.io イベント

socket.on("move", (from, to) => {
  const character = characters.find((character) => character.id === socket.id)
  const path = findPath(from, to)
  if (!path) {
    return
  }
  character.position = from
  character.path = path
  io.emit("playerMove", character)
})
  • クライアントから「(from) → (to) へ移動してほしい」というリクエストを受けたら findPath(...) を呼び出します。
  • 見つかった path をキャラクターに登録して、全クライアントに playerMove イベントを送っています。
  • character.position = from は「移動元を記録し直してから、クライアント側が path のとおり移動アニメーションをする」ような使い方ができます。

最小構成のシンプルサンプル

PathFinding.js の学習用に、サーバー機能を省いた単純なサンプルを示します。

// 1. pathfinding の読み込み (ブラウザなら <script src="pathfinding.min.js"> など)
import PF from 'pathfinding'

// 2. Grid を作成 (5 x 5 の例)
const gridWidth  = 5
const gridHeight = 5
const grid = new PF.Grid(gridWidth, gridHeight)

// 3. 一部を歩行不可に (2,2) のセルを壁にする
grid.setWalkableAt(2, 2, false)

// 4. AStarFinder でパス検索
const finder = new PF.AStarFinder()

// 5. スタート(0,0) から ゴール(4,4) までの経路を検索
//   - clone() しない場合は同じ grid を再利用できなくなる可能性がある点に注意
const path = finder.findPath(
  0, 0,
  4, 4,
  grid.clone()
)

// 6. 結果を出力
console.log('Path:', path)
// 例: Path: [ [0,0], [1,0], [2,0], [3,0], [4,0], ... [4,4] ]

このように Grid 上で壁を setWalkableAt(x, y, false) として設定し、 AStarFinder によって最短経路が得られる仕組みになっています。
より複雑なマップでは、今回の例のように壁セルや障害物をたくさん配置したり、対角移動を有効にしたりといった設定で使います。

まとめ

  • PathFinding.js は最短経路探索のライブラリとして有名で、GridFinder(AStarFinder など) を使ってパスの計算を行います。
  • 壁(または歩行不可セル)の設定 がパスファインディングでは最も重要な部分です。家具や障害物のサイズ、回転を考慮して正しくグリッドに書き込む必要があります。
  • socket.io と組み合わせると、リアルタイムでのマルチプレイヤー移動システムを容易に実装できます。
はる@フルスタックチャンネルはる@フルスタックチャンネル

React Three Fiber

1. カメラ

1.1 PerspectiveCamera

  • 遠近感(パースペクティブ)を表現できるカメラです。近くのオブジェクトは大きく、遠くのオブジェクトは小さく見えます。
  • three.js でいう new THREE.PerspectiveCamera() を React Three Fiber では <PerspectiveCamera /><canvas camera={{...}}> のプロパティで指定する場合があります。

1.2 OrthographicCamera

  • 平行投影をするカメラです。奥行きによるサイズの変化が起こらないのが特徴で、UI 的なシーンや 2D の表現を行う際によく使われます。

1.3 drei の PerspectiveCamera

  • @react-three/drei には、ラッパーとして <PerspectiveCamera /> が用意されています。R3F の <Canvas> 配下で扱いやすくするためのものです。makeDefault を指定すると自動で Canvas のカメラ設定を上書きしてくれます。

2. Canvas (React Three Fiber)

React Three Fiber の <Canvas> コンポーネントは、three.js のエントリーポイントとなる場所を React コンポーネントとして扱えるようにするものです。

  • 公式ドキュメント
  • <Canvas> の中にカメラやライト、メッシュなどを配置します。
  • Suspense や ErrorBoundary との併用ができ、非同期ローディングやエラーを扱いやすいのが特徴です。

3. デバッグのためのヘルパー

3.1 AxesHelper, GridHelper

  • three.js での座標軸やグリッドを表示するためのヘルパー。
  • R3F で使うなら <primitive object={new THREE.AxesHelper(10)} /> のように指定したり、drei の <Grid /> などを使用する方法もあります。

3.2 leva の useControls

  • leva は、UI 上でパラメータを調整できるライブラリです。
  • useControls() フックを使って、カメラやマテリアルのパラメーターをリアルタイムに変更してデバッグできます。

3.3 r3f-perf

  • FPS やメモリ使用量、draw call 数などのパフォーマンスを簡単にモニタリングできるツールです。

4. カスタムジオメトリ (BufferGeometry)

  • three.js の BufferGeometry を R3F で扱うには <bufferGeometry><bufferAttribute> を組み合わせて記述します。
  • <bufferAttribute attach="attributes-position" count="" itemSize={3} array={positionArray} />
  • positionArray は、JavaScript の Float32Array や数値配列で頂点座標を指定します。
  • これによって自由度の高いジオメトリを作ることができます。

5. GLTF / GLB モデルの読み込み

5.1 useLoader(GLTFLoader, "パス")

  • three.js でいう GLTFLoader を React Hooks で包んだフック。model = useLoader(GLTFLoader, "./model/dog.glb") などと書くと、モデルデータを非同期で読み込めます。

5.2 <primitive object={model.scene}>

  • 読み込んだ GLTF (GLB) のシーンをそのまま Three.js のオブジェクトとして配置する方法です。
  • モデルによってはライトが含まれていないため、シーンに別途ライトを設置する必要があります。

5.3 drei の useGLTF / gltfjsx

  • @react-three/dreiuseGLTF を使うと、モデルの nodes, materials が分解されて取得できるので、部分的にマテリアルを差し替えるなどカスタマイズが簡単になります。
  • gltfjsx という CLI ツールを使うと、GLB/GLTF ファイルから自動的に JSX コンポーネントを生成してくれます。

5.4 useAnimation(animations, scene)

  • GLTF ファイルに含まれるアニメーションを再生するときに使用します。複数のアニメーションを制御できる場合もあります。

5.5 Suspense でローディング表示

  • GLTF モデルの読み込みには時間がかかるので、React の <Suspense fallback={<Loading />}> を活用することで、モデルが準備完了するまでローダーやスピナーを表示することができます。

6. パーティクル

  • <mesh> の代わりに <points> 要素を使うと、頂点ごとに点を描画するパーティクルシステムが作れます。
  • drei の SparklesStars を使うと、星空やキラキラしたエフェクトなどを簡単に実装できます。

7. ライト

7.1 環境光・平行光

  • ambientLight は全体を均一に照らす環境光。
  • directionalLight は一方向からの光を表現。position で向きを変更できます。

7.2 useHelper

  • drei の useHelper(ref, THREE.DirectionalLightHelper, 1) などで、ライトの向きや位置を可視化できます。

7.3 影の設定

  • 影を使いたい場合、castShadow (影を落とす側) と receiveShadow (影を受ける側) の両方を設定し、renderer.shadowMap.enabled = true などの設定も必要になります。

7.4 drei の Enviroment, Sky, Cloud など

  • HDRI を読み込みたい場合は <Environment files="....hdr" /> で簡単に設定できます。
  • <Sky /><Cloud /><Lightformer /> を使ってリアルな空や雲、演出用のライティングを行うことも可能です。

8. イベント

<mesh
  onClick={(e) => console.log('click')}
  onContextMenu={(e) => console.log('context menu')}
  onDoubleClick={(e) => console.log('double click')}
  onWheel={(e) => console.log('wheel spins')}
  onPointerUp={(e) => console.log('up')}
  onPointerDown={(e) => console.log('down')}
  onPointerOver={(e) => console.log('over')}
  onPointerOut={(e) => console.log('out')}
  onPointerEnter={(e) => console.log('enter')}
  onPointerLeave={(e) => console.log('leave')}
  onPointerMove={(e) => console.log('move')}
  onPointerMissed={() => console.log('missed')}
  onUpdate={(self) => console.log('props have been updated')}
/>
  • 上記のように R3F では、マウス・ポインタ・クリックイベントなどを mesh に対して付与できます。
  • e.stopPropagation() を呼べば、レイキャスト(クリック判定)がその要素や子要素で止まります。

9. カメラ操作

9.1 OrbitControls

  • @react-three/drei<OrbitControls /> を使うと、ユーザーがマウスでドラッグやズームなどを自由に行えるカメラが簡単に作れます。
  • たとえば、maxAzimuthAngle={Math.PI / 2} など角度を制限し、maxPolarAngle={Math.PI / 4} でカメラの上下可動域を制限することも可能です。
<OrbitControls
  enablePan={false}
  maxPolarAngle={Math.PI / 2}
  minAzimuthAngle={-Math.PI / 2}
  maxAzimuthAngle={Math.PI / 2}
  minDistance={3}
  maxDistance={10}
/>

9.2 CameraControls

  • @react-three/drei に含まれない、別途インストールが必要な場合のあるコントロール(camera-controls というライブラリがベース)。
  • 角度回転 (rotate)、左右上下移動 (truck)、ズーム (zoom)、そして setLookAt メソッドなどが用意されており、プログラム的にカメラの動きを制御できます。
  • smoothTime プロパティや easing を組み合わせると、ゆるやかなカメラの動きを実現できます。

9.3 PresentationControls

  • ユーザーがドラッグすると少しカメラが回転し、ドラッグを離すともとの位置へスナップバックするような演出が可能。
  • @react-three/drei<PresentationControls /> を使います。

9.4 ScrollControls / useScroll

  • ページのスクロールに応じて、カメラの位置やオブジェクトの配置をアニメーションさせることができます。
  • ScrollControls コンポーネントを使って包み、Scroll コンポーネント内に <Image><primitive> を置くと、スクロールするごとに配置した要素がスライド表示されるなど、インタラクティブなページを作れます。

10. テキスト

10.1 <Text>, <Text3D>

  • @react-three/drei<Text> は、2D 的なテキストを 3D 上に配置できます。
  • <Text3D> は 3D モデルとしてテキストを表示するため、フォントの JSON データ(typeface.json など)が必要です。

10.2 <Html>

  • R3F の <Html> は、3D 空間内に DOM を埋め込む仕組みです。
  • wrapperClassdistanceFactor, occlude などを使って、モデルに隠れた場合に DOM も隠す、といった演出が可能になります。

11. 音声

  • @react-three/drei<PositionalAudio> で、オブジェクトから距離が離れるほど音量が下がる 3D サウンドを実装できます。

12. マテリアルいろいろ

12.1 meshReflectorMaterial

  • @react-three/drei のリフレクション表現用マテリアル。
  • 水面や鏡面などを手軽に演出できます。

12.2 useTexture

  • テクスチャを簡単に読み込むフックです。
  • const texture = useTexture("/path/to/image.png") のように書くと、自動的に THREE.Texture オブジェクトを取得できます。

12.3 MeshPortalMaterial

  • drei に含まれる、ポータル状の効果を作るためのマテリアル。
  • たとえば、枠の中だけ異世界が広がるような演出を作ることができます。

13. アニメーション

13.1 useFrame

  • R3F でアニメーションを行うときの基本フック。useFrame((state, delta) => { ... }) の中に、フレームごとに更新したい処理を記述します。
  • 例えば、物体の回転や移動をリアルタイムで更新したい場合に使います。

13.2 maath の easing.damp

  • [maath](https://github.com/pmndrs/maath) ライブラリには、イージング計算を簡単にするための関数が多数用意されています。
  • easing.damp(対象, プロパティ, 目標値, ダンピング係数, delta) のように書くと、スムーズなトランジションが実現できます。

13.3 @react-spring/three

  • React Spring の three.js / R3F 向け拡張。
  • animated.meshuseSpring, useSprings などを使用して物体やカメラ、マテリアルのアニメーションを “宣言的” に書けます。
  • 例: useSpring({ from: { x: 0 }, to: { x: 5 }, ... }) のように書き、<a.mesh position-x={spring.x}>...</a.mesh> と連動させる。
import { a, useSpring, useSpringRef } from "@react-spring/three";

const Scene = () => {
  const springRef = useSpringRef();

  const spring = useSpring({
    ref: springRef,
    from: { x: -2 },
  });

  const clickHandler = () => {
    springRef.start({
      to: { x: 2 },
      config: { duration: 5000 },
    });
  };

  const pointerOverHandler = () => {
    springRef.pause();
  };

  const pointerOutHandler = () => {
    springRef.resume();
  };

  return (
    <a.mesh
      position-x={spring.x}
      onClick={clickHandler}
      onPointerOver={pointerOverHandler}
      onPointerOut={pointerOutHandler}
    >
      <boxGeometry />
      <meshBasicMaterial color="orange" />
    </a.mesh>
  );
};

export default Scene;
  • さらに useSprings を使えば、複数オブジェクトを同時に動かすことなども簡単に実装できます。
はる@フルスタックチャンネルはる@フルスタックチャンネル

1. React Three Rapier とは

  • Rapier は Rust で書かれた物理シミュレーションエンジンで、WebAssembly を通じて JavaScript/TypeScript から高速に利用できます。
  • @react-three/rapier は、その Rapier を React Three Fiber (R3F) 上で手軽に使えるようにしたラッパーです。
  • たとえば 重力剛体(RigidBody) の設定、衝突判定(colliders) などが簡単に扱えます。
import { Physics, RigidBody } from "@react-three/rapier";

const PhysicsScene = () => {
  return (
    <Physics gravity={[0, -9.81, 0]}>
      {/* 動く物体 */}
      <RigidBody>
        <mesh castShadow position={[0, 1.5, 0]}>
          <boxGeometry />
          <meshStandardMaterial color="#CC3941" />
        </mesh>
      </RigidBody>

      {/* 固定された床(type="fixed") */}
      <RigidBody type="fixed">
        <mesh position-y={-1} rotation-x={-Math.PI * 0.5} receiveShadow>
          <boxGeometry args={[8, 8, 0.35]} />
          <meshStandardMaterial color="#C7CAC7" />
        </mesh>
      </RigidBody>
    </Physics>
  );
};

export default PhysicsScene;
  • <Physics> コンポーネントでシーン全体に物理演算を適用します。
  • 各オブジェクトを <RigidBody> で包むことで、Rapier に管理される剛体オブジェクトになります。
  • type="fixed" にすることで、その剛体は重力などを受けずに固定されます(床や壁などに適しています)。

2. コリジョン(衝突判定)と Debug 表示

2.1 Debug

  • @react-three/rapier<Debug> を使うと、Collider(コリジョン形状)が線で可視化され、どのように物理判定がなされているかを視覚的に把握できます。
import { Physics, RigidBody, Debug } from "@react-three/rapier";

const PhysicsScene = () => {
  return (
    <Physics gravity={[0, -9.81, 0]}>
      <Debug />
      {/* ここに RigidBody などを配置 */}
    </Physics>
  );
};

2.2 colliders

  • RigidBodycolliders プロパティで、どのような形状を衝突判定に使うか指定できます。
  • ball(球), hull(凸包), trimesh(ポリゴンメッシュそのもの)などがあります。
    • trimesh は形状を忠実にトレースする分、計算負荷が大きめ。
    • ball, cuboid, capsule などのプリミティブ形状のほうが計算コストが軽い。
  • colliders={false} を指定して Collider を自分でカスタムするときは、<CuboidCollider>, <CapsuleCollider> などを使用します。
<RigidBody colliders="trimesh">
  <mesh>
    <torusKnotGeometry args={[0.5, 0.15, 100, 100]} />
    <meshStandardMaterial color="orange" />
  </mesh>
</RigidBody>
<RigidBody colliders={false}>
  <CuboidCollider args={[0.5, 0.5, 0.5]} />
  <mesh>
    <boxGeometry />
    <meshStandardMaterial color="#CC3941" />
  </mesh>
</RigidBody>

3. 剛体への力の加え方

import { Physics, RigidBody } from "@react-three/rapier";

const PhysicsScene = () => {
  const cubeRef = useRef();

  const cubeClickHandler = () => {
    // applyImpulse は一瞬の力
    cubeRef.current.applyImpulse({ x: 1, y: 0, z: 0 });
    
    // addForce はフレームごとに作用し続ける力(使い分け可)
    // cubeRef.current.addForce({ x: 10, y: 0, z: 0 });
  };

  return (
    <Physics>
      <RigidBody ref={cubeRef}>
        <mesh onClick={cubeClickHandler}>
          <boxGeometry />
          <meshStandardMaterial color="#CC3941" />
        </mesh>
      </RigidBody>
    </Physics>
  );
};
  • applyImpulse({x, y, z}) は一瞬の衝撃を与えます。
  • addForce({x, y, z}) は継続的に力が働き続けます。
  • addTorque, applyTorqueImpulse を使うと回転力を与えることも可能です。

4. コリジョンイベント

RigidBodyonCollisionEnter / onCollisionExit / onSleep / onWake を使用すれば、他の剛体との衝突やスリープ状態に入ったことなどのコールバックを受け取れます。

<RigidBody
  onCollisionEnter={() => console.log("Collision Enter")}
  onCollisionExit={() => console.log("Collision Exit")}
  onSleep={() => console.log("Sleeping")}
  onWake={() => console.log("Wake")}
  restitution={0.5} // 反発係数
  friction={0.5}    // 摩擦係数
>
  <mesh>
    <boxGeometry />
    <meshStandardMaterial color="#CC3941" />
  </mesh>
</RigidBody>
  • restitution(反発力): バウンドするかどうかを調整。
  • friction(摩擦力): 表面の滑りやすさを調整。

5. RigidBody の種類 (type)

  • dynamic: 重力や衝突の影響を受ける一般的な剛体。
  • fixed: 位置や角度が固定された剛体。床や壁、動かないオブジェクトに使用。
  • kinematicPosition: 自身には物理的反応はないが、プログラムで position を動かせる(衝突相手には影響を与える)。
    • 例: “回転する床” とか “移動する壁” など。
    • setNextKinematicTranslation(), setNextKinematicRotation() で動かします。
  • kinematicVelocity: 速度ベースで動かせる kinematicBody。
<RigidBody ref={spinner} type="kinematicPosition">
  <mesh>
    <boxGeometry args={[2, 0.5, 10]} />
    <meshStandardMaterial color="orange" />
  </mesh>
</RigidBody>

useFrame で経過時間に応じて setNextKinematicTranslation などを更新してやれば、周期的に動くオブジェクトを実現できます。


6. センサー (sensor)

  • コリジョン検知はするが、衝突反応(弾かれるなど)をしない“センサー”モードが利用できます。
  • sensor プロパティを true にした Collider では、onIntersectionEnter, onIntersectionExit が呼ばれ、衝突したオブジェクトを検知できます。
<RigidBody type="fixed" position={[0, 0, -5]}>
  <CuboidCollider
    args={[1, 1, 1]}
    sensor
    onIntersectionEnter={() => console.log("Enter Sensor")}
    onIntersectionExit={() => console.log("Exit Sensor")}
  />
</RigidBody>
  • 例えば、“ゴールエリアに入ったらスコア加算” などの判定に使えます。

7. Instanced Mesh (大量配置)

  • 大量のオブジェクト(数百~数千)を配置したい場合、通常の <RigidBody> で 1 つずつ作るとパフォーマンス上の問題が出やすいです。
  • <InstancedRigidBodies><instancedMesh> を組み合わせると、一度の描画呼び出し(draw call)で多数のオブジェクトを描画できます。
import { Physics, InstancedRigidBodies } from "@react-three/rapier";

const PhysicsScene = () => {
  const count = 300;
  // positions, rotations, scales を useMemo で配列生成

  return (
    <Physics>
      <InstancedRigidBodies
        positions={cubesTransformations.positions}
        rotations={cubesTransformations.rotations}
        scales={cubesTransformations.scales}
      >
        <instancedMesh args={[null, null, count]}>
          <boxGeometry />
          <meshStandardMaterial color="#CC3941" />
        </instancedMesh>
      </InstancedRigidBodies>
    </Physics>
  );
};
  • これにより、物理計算もインスタンス単位でまとめて扱われ、描画やシミュレーションの負荷を大きく下げられます。

8. キーボード操作との連動

  • @react-three/drei<KeyboardControls> と組み合わせて、簡単に “WASD” や “スペースキーでジャンプ” などの操作を実装できます。
  • useKeyboardControls() フックで押されているキーを取得し、剛体に applyImpulse で移動力を加えます。
  • onCollisionEnter で地面に設置したらジャンプフラグ ON、ジャンプキーが押されたら上方向に力を加える… といった流れ。
const allKeys = useKeyboardControls((keys) => keys);
useFrame(() => {
  if (allKeys.forward) {
    cubeRef.current.applyImpulse({ x: 0, y: 0, z: -0.3 });
  }
  // ...
});
はる@フルスタックチャンネルはる@フルスタックチャンネル

1. Post Processing とは

  • 3D 描画の最終段階で画像を加工する技術。
  • 例: ピクセル化 (Pixelation)ビネット効果 (Vignette)ゴッドレイ (God Rays)コントラスト調整 (BrightnessContrast) など。
  • @react-three/postprocessingpostprocessing ライブラリを React Three Fiber 環境で簡単に使えるようにしたラッパーです。

2. 基本的な使い方

import { EffectComposer, Vignette, Pixelation } from "@react-three/postprocessing";

const Effects = () => {
  return (
    <EffectComposer>
      <Pixelation granularity={10} />
      <Vignette eskil={false} offset={0.2} darkness={1.2} />
      {/* 他のエフェクトを続けて追加可能 */}
    </EffectComposer>
  );
};

export default Effects;
  • ポストプロセッシングを行うためには、通常、R3F の <Canvas> 内に <EffectComposer> を配置します。
  • <EffectComposer> の中に各種エフェクトコンポーネントを並べていくことで、複数の効果を連携させることができます。

3. 主なエフェクト例

@react-three/postprocessing には多種多様なエフェクトが用意されています。ここではよく使われるものをいくつかピックアップします。

  1. Pixelation
    • ピクセルアート風の見た目にする効果。
    • granularity プロパティでピクセルのサイズを調整。
  2. Vignette
    • 画面の周辺を暗くするビネット効果。
    • offsetdarkness を調整することで効果範囲や暗さを変えられる。
  3. BrightnessContrast
    • 画面全体の明るさ (brightness) やコントラスト (contrast) を調整。
  4. ChromaticAberration
    • いわゆる色収差をシミュレート。画面端に赤・青のずれが生じる。
  5. Scanline
    • 古いテレビの走査線を再現。density などを指定。
  6. Grid, DotScreen
    • 複数の点・線のパターンによるレトロ表示や特殊エフェクト。
  7. Noise
    • 粒状感(フィルムグレイン)の演出。
  8. Glitch
    • 一時的に画面が乱れるような表現。delay, duration, strength などを設定。
  9. GodRays
    • オブジェクトから光が差し込むような表現。
    • sun プロパティで参照先の ref を指定し、そこから光が広がるイメージを作る。 samples, density などで調整。

4. GodRays の使い方

import { EffectComposer, GodRays } from "@react-three/postprocessing";
import { useRef, forwardRef } from "react";

const Effect = forwardRef((props, ref) => {
  return (
    <EffectComposer>
      {/* ref.current に指定したオブジェクトから God Rays (後光) を生成 */}
      {ref.current && <GodRays sun={ref.current} samples={60} density={0.45} />}
    </EffectComposer>
  );
});

export default Effect;
  • GodRays は “光源” に見立てるオブジェクトを指定する必要があります (例えば太陽や光を放つエンティティ)。
  • samples (サンプリング回数) や density (密度) を調整して見え方を変化できます。
  • ref.current が存在するかどうかで、GodRays の描画可否を判定する実装もよく行われます。

5. Leva と組み合わせる

ポストプロセス効果のパラメータは、実際に値をいじって試行錯誤することが多いです。
そこで LevauseControls を使うと便利です。

import { useControls } from "leva";
import { BrightnessContrast } from "@react-three/postprocessing";

const Effects = () => {
  const { brightness, contrast } = useControls({
    brightness: { value: 0, min: -1, max: 1, step: 0.01 },
    contrast: { value: 0, min: -1, max: 1, step: 0.01 },
  });

  return (
    <EffectComposer>
      <BrightnessContrast brightness={brightness} contrast={contrast} />
    </EffectComposer>
  );
};
  • Leva パネル上でリアルタイムにパラメータを操作できるので、どの数値が最適かを素早く探せます。

6. forwardRef で親コンポーネントの参照を受け取る

// Effect.js
import { EffectComposer, GodRays } from "@react-three/postprocessing";
import { forwardRef } from "react";

const Effect = forwardRef((props, ref) => {
  return (
    <EffectComposer>
      {ref.current && <GodRays sun={ref.current} samples={60} density={0.45} />}
    </EffectComposer>
  );
});

export default Effect;
// Scene.js
import { useRef } from "react";
import Effect from "./Effect";

const Scene = () => {
  const circleRef = useRef();

  return (
    <>
      {/* エフェクトのコンポーネントに ref を渡し、  */}
      {/* circleRef の指すオブジェクトを GodRays の光源とする */}
      <Effect ref={circleRef} />

      {/* 光源として扱う対象に同じ ref を指定 */}
      <mesh ref={circleRef} position-z={-12}>
        <circleGeometry args={[7, 64]} />
        <meshBasicMaterial color="orange" />
      </mesh>
    </>
  );
};

export default Scene;
  • forwardRef を使うことで、呼び出し元コンポーネント (Scene) から渡された ref を受け取れます。
  • ここでは <mesh> を GodRays の “光源” として使用しています。つまり ref.current<mesh> オブジェクトを指し、そこから光線が伸びる演出が生まれます。
はる@フルスタックチャンネルはる@フルスタックチャンネル

pmnd.rs で紹介されているライブラリの特徴と主な使いどころをまとめています。3D 表示やアニメーション、状態管理など、React を用いた開発を充実させる便利なツールが揃っています。


1. React Three Fiber

React Three Fiber は React のレンダラーとして three.js を扱うためのライブラリです。

  • 概要: three.js の API を直接扱うよりも、React のコンポーネントとして 3D シーンを記述できるので、宣言的かつ可読性の高いコードが書けます。
  • 特徴:
    • React のライフサイクルや Hooks と親和性が高い
    • ステートやコンポーネントの仕組みで 3D シーンを管理できる
    • 非同期ローディングやイベントなど React 的な書き方で簡単に実装
  • 公式ドキュメント
  • GitHub

2. React Spring

React Spring は React 向けのスプリングベースのアニメーションライブラリです。

  • 概要: 要素の動きを物理演算に基づくスプリング(バネ)挙動で実現できるため、自然で滑らかなアニメーションが作りやすいのが特徴です。
  • 特徴:
    • 数値や色、SVG、三次元など、さまざまなプロパティをアニメーション
    • 宣言的にアニメーションを定義するため理解しやすく、保守しやすい
    • @react-spring/three で Three.js/R3F 上のメッシュやカメラもアニメーション可能
  • 公式ドキュメント
  • GitHub

3. Drei

Drei は React Three Fiber のための便利なヘルパーコンポーネント集です。

  • 概要: R3F でよく使われるパターンをまとめた拡張パッケージ。カメラコントロール、テキスト表示、環境光設定、スタープリミティブなど、多岐にわたるコンポーネントが用意されています。
  • 特徴:
    • <OrbitControls>, <Environment>, <Text> など「よく使う機能」を簡単に導入
    • 三次元空間内で UI, 物理、キャラクターコントロールなどを扱うときの時短になる
    • R3F の周辺ツールとも互換性が高い
  • 公式ドキュメント
  • GitHub

4. Zustand

Zustand はフックベースのシンプルでスケーラブルな状態管理ライブラリです。

  • 概要: Redux のような概念をシンプルにしたフック形式の API でありながら、軽量・高速。
  • 特徴:
    • “Bearbones” な実装で、わずかなコード量でグローバルステートを管理可能
    • Immer や middleware との連携で柔軟に拡張できる
    • R3F と組み合わせて、カメラやオブジェクトの状態を Store で一元管理するケースが多い
  • 公式ドキュメント
  • GitHub

5. Jotai

Jotai はシンプルかつ柔軟な React 用状態管理ライブラリです。

  • 概要: “原子 (Atom)” の概念を用い、最小単位のステートを組み合わせることでグローバルステートを構築します。
  • 特徴:
    • アトミックデザインのような「小さな単位」のステート管理を重視
    • Redux, Context API, Recoil などと似た感覚で使えるが、シンプルで拡張性が高い
    • 非同期データや派生ステートも簡単に管理
  • 公式ドキュメント
  • GitHub

6. Valtio

Valtio は proxy ベースの状態管理ライブラリです。

  • 概要: JavaScript の Proxy を使い、オブジェクトをそのまま状態として扱い、変更が生じると自動でレンダリングをトリガーします。
  • 特徴:
    • “mutable なオブジェクトを直接扱える” というシンプルさ
    • Immer のようにイミュータブル変換をしなくても良い
    • 複雑なネスト構造のステートでも簡単に変更追跡
  • 公式ドキュメント
  • GitHub

7. A11y

@react-three/a11y は WebGL シーンへのアクセシビリティを提供するライブラリです。

  • 概要: React Three Fiber 上で 3D オブジェクトにフォーカスやスクリーンリーダーによる読み上げ対応など、アクセシビリティ機能を付与できる。
  • 特徴:
    • <A11y> コンポーネントを使い、フォーカスやタブ操作をサポート
    • 3D コンテンツでもアクセシブルなエクスペリエンスを実現
    • WebGL とアクセシビリティの相性を良くするための必須ツール
  • 公式ドキュメント
  • GitHub

8. React Postprocessing

React Postprocessing は、React Three Fiber でポストプロセス効果を扱うためのラッパーです。

  • 概要: postprocessing ライブラリを R3F 環境で簡単に導入可能。
  • 特徴:
    • ブルーム効果やゴッドレイ、グリッチなどの視覚効果を手軽に適用
    • <EffectComposer> を利用して複数のエフェクトをチェーンさせられる
    • Drei との相性も良く、見た目を華やかにしたい場合に重宝
  • 公式ドキュメント
  • GitHub

9. uikit

uikit は React Three Fiber 上で使える UI コンポーネント集です。

  • 概要: 3D シーンや WebGL のコンテキストの中で、手軽に UI 要素を配置できるようにするライブラリ。
  • 特徴:
    • 3D 上にボタンやスライダーを直接置くなど、UI の実装を手助け
    • 背景との一貫性を保ちつつ、エンドユーザーが操作しやすい UI を提供
    • WebGL と DOM UI との橋渡しをシンプルにする
  • 公式ドキュメント
  • GitHub

10. xr

@react-three/xr は R3F で VR/AR を扱うためのライブラリです。

  • 概要: WebXR API を React Three Fiber に統合し、VR/AR デバイスでの描画・操作を簡単に実装できる。
  • 特徴:
    • <VRCanvas><ARCanvas> を使ったり、コントローラーイベントをフックできる
    • Oculus などのヘッドセットで動く 3D アプリを React で構築
    • Hand tracking や gesture などの拡張機能も活用可能
  • 公式ドキュメント
  • GitHub
はる@フルスタックチャンネルはる@フルスタックチャンネル

カードアニメーション

タロットカードの画像をドラッグ操作でスワイプし、スワイプ速度が一定を超えると画面外に飛ばすアニメーションを @react-spring/webreact-use-gesture を用いて実装しています。

App.js (メインのコンポーネント)

import React, { useState } from 'react'
import { useSprings, animated, to as interpolate } from '@react-spring/web'
import { useDrag } from 'react-use-gesture'

import styles from './styles.module.css'

/**
 * 表示するタロットカードの画像 URL リスト。
 * 実際には6枚分のURLが配列に含まれている。
 */
const cards = [
  'https://upload.wikimedia.org/wikipedia/commons/f/f5/RWS_Tarot_08_Strength.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/5/53/RWS_Tarot_16_Tower.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/9/9b/RWS_Tarot_07_Chariot.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/d/db/RWS_Tarot_06_Lovers.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/RWS_Tarot_02_High_Priestess.jpg/690px-RWS_Tarot_02_High_Priestess.jpg',
  'https://upload.wikimedia.org/wikipedia/commons/d/de/RWS_Tarot_01_Magician.jpg',
]

/**
 * カードを配置する際の最終状態(to)を決める関数
 * @param i カードのインデックス
 */
const to = (i) => ({
  x: 0,
  y: i * -4,        // カードが少しずつズレて重なるように設定
  scale: 1,
  rot: -10 + Math.random() * 20, // -10〜+10度の間でランダムな回転
  delay: i * 100,                // 各カードが少しずつ遅れてアニメーション開始
})

/**
 * カードの初期位置(from)を決める関数
 * @param _i インデックス (ここでは使っていない)
 */
const from = (_i) => ({
  x: 0,
  rot: 0,
  scale: 1.5,
  y: -1000, // 画面上部よりかなり上から飛んでくる
})

/**
 * カードの回転角度(rot)と拡大率(scale)からCSS用 transform文字列を生成する関数
 * r: 回転角度, s: スケール(拡大率)
 */
const trans = (r, s) =>
  `perspective(1500px) rotateX(30deg) rotateY(${r / 10}deg) rotateZ(${r}deg) scale(${s})`

/**
 * Deckコンポーネント: カードの束(デッキ)を表示し、ドラッグ操作に連動したアニメーションを実装
 */
function Deck() {
  // 飛んでいったカードのインデックスを格納するSet
  const [gone] = useState(() => new Set())

  // useSprings: 複数カードのアニメーション状態をまとめて管理
  // props: カードごとのアニメーション値
  // api: 値を後から更新するためのAPI
  const [props, api] = useSprings(cards.length, (i) => ({
    ...to(i),
    from: from(i),
  }))

  /**
   * useDrag:
   * args: [index] -> バインドする際にカードのインデックスを渡す
   * down: マウス・タッチが押下中かどうか
   * movement: [mx, my] -> ドラッグ量(ここではmxのみ使う)
   * direction: [xDir, yDir] -> ドラッグ方向(ここではxDirのみ使う)
   * velocity -> ドラッグの速度
   */
  const bind = useDrag(({ args: [index], down, movement: [mx], direction: [xDir], velocity }) => {
    const trigger = velocity > 0.2        // スワイプ速度が0.2を超えるならカードを飛ばす
    const dir = xDir < 0 ? -1 : 1         // ドラッグが左なら-1, 右なら+1
    if (!down && trigger) gone.add(index) // ボタン離した&速度しきい値を超えたらgoneに登録

    api.start((i) => {
      // ドラッグしているカード以外は変更しない
      if (index !== i) return

      const isGone = gone.has(index)
      // カードが飛んでいったら画面外まで移動、そうでなければドラッグ量に応じて追従
      const x = isGone
        ? (200 + window.innerWidth) * dir
        : down
        ? mx
        : 0

      // 回転量: ドラッグ量を割り算、さらに飛んでいくカードは速度に応じた追加回転
      const rot = mx / 100 + (isGone ? dir * 10 * velocity : 0)

      // ドラッグ中は少し拡大(1.1)して持ち上げたように見せる
      const scale = down ? 1.1 : 1

      return {
        x,
        rot,
        scale,
        delay: undefined,
        config: {
          friction: 50,
          tension: down ? 800 : isGone ? 200 : 500,
        },
      }
    })

    // もしすべてのカードが飛んだら、0.6秒後にリセットして再度カードを元に戻す
    if (!down && gone.size === cards.length) {
      setTimeout(() => {
        gone.clear()
        api.start((i) => to(i))
      }, 600)
    }
  })

  // 実際の描画: 各カードに対してアニメーション値を割り当てる
  return (
    <>
      {props.map(({ x, y, rot, scale }, i) => (
        // スタイルの x, y でカード位置を決定
        <animated.div className={styles.deck} key={i} style={{ x, y }}>
          {/*
            カード本体。bind(i)を渡し、ドラッグ可能に。
            transformにはrotとscaleを組み合わせた3D変換を指定し、背景画像をcards[i]に設定
          */}
          <animated.div
            {...bind(i)}
            style={{
              transform: interpolate([rot, scale], trans),
              backgroundImage: `url(${cards[i]})`,
            }}
          />
        </animated.div>
      ))}
    </>
  )
}

/**
 * Appコンポーネント: 画面中央にDeckコンポーネントを配置
 */
export default function App() {
  return (
    <div className={styles.container}>
      <Deck />
    </div>
  )
}

JavaScript コードのポイント

  1. カードリスト: cards 配列に 6 つの画像 URL を格納しています。
  2. アニメーションパラメータ: to(i) で “最終状態”、from(i) で “初期位置” を定義。
  3. useSprings: 複数要素 (カード) の座標や回転などを一括で管理し、配列で props (各カードごとの状態) を取得します。
  4. useDrag: ドラッグ操作を取得し、速度(velocity) や方向(direction) に応じてカードの行き先を決定。
    • 一定速度を超えると gone に追加し、カードを画面外に飛ばす。
    • 全カードが飛んだら少し時間を置いて元の位置に戻すロジックを実装しています。
  5. interpolate + trans 関数: 角度 (rot) とスケール (scale) をまとめて CSS の transform プロパティに変換。3D パースを付けることで奥行きを演出しています。

styles.module.css

.container {
  background: lightblue; /* 背景色を淡い水色に設定 */
  cursor: url('https://uploads.codesandbox.io/uploads/user/b3e56831-8b98-4fee-b941-0e27f39883ab/Ad1_-cursor.png') 39 39,
    auto; /* カーソル画像を独自に設定 */
  display: flex;
  align-items: center; /* 子要素を縦方向に中央寄せ */
  height: 100%;        /* 画面全体の高さを占める */
  justify-content: center; /* 子要素を横方向に中央寄せ */
}

.deck {
  position: absolute; /* カードを重ねるために絶対配置 */
  width: 300px;       /* カードを配置する領域の横幅 */
  height: 200px;      /* カードを配置する領域の縦幅 */
  will-change: transform;  /* CSS最適化のための指定 */
  display: flex;       /* 子要素(実際のカード)を中央寄せにする */
  align-items: center;
  justify-content: center;
  touch-action: none;  /* タッチイベントを通常の画面スクロールなどに取られないようにする */
}

.deck > div {
  background-color: white;
  background-size: auto 85%;
  background-repeat: no-repeat;
  background-position: center center;
  width: 45vh; /* 高さに連動して横幅を可変にした例 (最大150px) */
  max-width: 150px;
  height: 85vh; /* 画面の縦方向に合わせて高さを可変に */
  max-height: 285px;
  will-change: transform;
  border-radius: 10px; /* 角を少し丸める */
  box-shadow: 0 12.5px 100px -10px rgba(50, 50, 73, 0.4),
              0 10px 10px -10px rgba(50, 50, 73, 0.3); /* カードに影を付けて浮いているように見せる */
}

スタイルのポイント

  • .container で画面全体を中央寄せにし、カーソル画像を独自設定して遊び心を出しています。
  • .deck は、絶対配置にしてカードを重ねられるようにしつつ、サイズを指定し、カード本体を中央に配置しています。
  • .deck > div は、実際にカード画像を表示する要素。
    • background-size: auto 85% で画像を縦方向優先で 85% まで拡大・表示しています。
    • will-change: transform を指定し、GPU によるハードウェアアクセラレーションが効きやすくしています。
    • box-shadow でカードに陰影を付け、フラットになり過ぎないよう調整。

動作

  • ページを開くと 6 枚のタロットカードが重なって表示されます。
  • カードをドラッグすると、ドラッグ量に応じて少し持ち上がったように回転 (rot) & 拡大 (scale) しながら動きます。
  • スワイプ速度が一定以上の場合はカードが画面の左右どちらかへ勢いよく飛んでいき、速度が遅ければ元の位置に戻ります。
  • すべて飛ばすと一旦カードが消え、0.6 秒後にまた元のスタック位置から復活します。
はる@フルスタックチャンネルはる@フルスタックチャンネル

同期アニメーション

@react-spring/three を使用して Three.js (React Three Fiber) 上のメッシュをスプリングアニメーションさせています。
また、onChange イベントを使ってスプリング値(ここでは position)を外部へ同期 (sync) する仕組みを解説しています。


1. 概要

  • 問題背景
    通常、useSpring 等で管理しているアニメーションの値は、React Hooks 内で閉じているため「アニメーション中の値」を外部から取得するのが難しくなりがちです。
    しかし、ほかのコンポーネントや外部の store が「現在の座標(あるいはスプリングの値)を参照したい」といったケースでは、その値を随時取得したくなることがあります。

  • 解決方法
    @react-spring/core (あるいは @react-spring/three) には、onChange というコールバックが用意されています。これを使うことで、「スプリング値が更新されるたびに外部へ通知する」仕組みを実装可能です。

  • 今回の例

    1. Blob(球体メッシュ) があり、その位置やスケール、カラーを useSpring でアニメーション管理。
    2. onChange のなかで position のスプリング値が変わるたびに THREE.Vector2 を更新。
    3. 親コンポーネントforwardRef + useImperativeHandle を介して、子コンポーネント (MyScene) から現在の座標を取得する。
    4. サンプルでは 2 秒おきにコンソールに position を出力して “同期が取れている” ことを示しています。

2. コード全体の流れ

2.1 MyScene コンポーネント

const MyScene = forwardRef(({}, ref) => {
  // -- 1) 各種RefsやStateの宣言 --
  const isOver = useRef(false)                 // ポインタがcanvas領域上にあるかどうかを示す
  const [vector2] = useState(() => new Vector2())  // onChangeで常に最新の座標を格納するためのVector2

  const { width, height } = useThree(state => state.size) // Canvasの幅・高さ

  // -- 2) useSpringの宣言 --
  const [springs, api] = useSpring(() => ({
    scale: 1,
    position: [0, 0],
    color: '#ff6d6d',
    // スプリング値が更新されるたびに呼ばれるコールバック
    onChange: ({ value }) => {
      // value.position[0], value.position[1] を vector2 に代入
      vector2.set(value.position[0], value.position[1])
    },
    // keyごとのconfig設定
    config: key => {
      switch (key) {
        case 'scale':
          return { mass: 4, friction: 10 }
        case 'position':
          return { mass: 4, friction: 220 }
        default:
          return {}
      }
    },
  }))

  // -- 3) 親コンポーネントから呼び出されるハンドル (useImperativeHandle) --
  useImperativeHandle(ref, () => ({
    getCurrentPosition: () => vector2,
  }))

  // 以下、各イベントハンドラを定義:

  // (A) クリックで color を切り替え
  const handleClick = useCallback(() => {
    let clicked = false
    return () => {
      clicked = !clicked
      api.start({ color: clicked ? '#569AFF' : '#ff6d6d' })
    }
  }, [])

  // (B) マウスオーバー/アウトで scale を変更
  const handlePointerEnter = () => {
    api.start({ scale: 1.5 })
  }
  const handlePointerLeave = () => {
    api.start({ scale: 1 })
  }

  // (C) Window上のpointerイベントで Blob を移動
  const handleWindowPointerOver = useCallback(() => {
    isOver.current = true
  }, [])
  const handleWindowPointerOut = useCallback(() => {
    isOver.current = false
    api.start({ position: [0, 0] })
  }, [])
  const handlePointerMove = useCallback(
    e => {
      if (isOver.current) {
        const x = (e.offsetX / width) * 2 - 1
        const y = (e.offsetY / height) * -2 + 1
        api.start({ position: [x * 5, y * 2] })
      }
    },
    [api, width, height]
  )

  // -- 4) Windowのポインタイベントを登録/解除
  useEffect(() => {
    window.addEventListener('pointerover', handleWindowPointerOver)
    window.addEventListener('pointerout', handleWindowPointerOut)
    window.addEventListener('pointermove', handlePointerMove)
    return () => {
      window.removeEventListener('pointerover', handleWindowPointerOver)
      window.removeEventListener('pointerout', handleWindowPointerOut)
      window.removeEventListener('pointermove', handlePointerMove)
    }
  }, [handleWindowPointerOver, handleWindowPointerOut, handlePointerMove])

  // -- 5) レンダリング --
  return (
    <animated.mesh
      onPointerEnter={handlePointerEnter}
      onPointerLeave={handlePointerLeave}
      onClick={handleClick()}
      scale={springs.scale}
      position={springs.position.to((x, y) => [x, y, 0])} // Vector3([x, y, 0])に変換
    >
      <sphereGeometry args={[1.5, 64, 32]} />
      <AnimatedMeshDistortMaterial
        speed={5}
        distort={0.5}
        color={springs.color}
      />
    </animated.mesh>
  )
})

ポイント解説

  1. onChange の利用

    onChange: ({ value }) => {
      vector2.set(value.position[0], value.position[1])
    }
    
    • スプリングが更新されるたびに呼ばれるコールバックです。value.position に現在の (x, y) が入ります。
    • ここで THREE.Vector2 (vector2) を書き換えておけば、常に「最新の座標」を保持できます。
  2. useImperativeHandleforwardRef

    useImperativeHandle(ref, () => ({
      getCurrentPosition: () => vector2,
    }))
    
    • 親コンポーネントが ref.current.getCurrentPosition() で、この vector2 を取得可能。
    • これにより「スプリング値を親が随時読み取れる」仕組みになっています。
  3. Window 上のポインタイベント

    • pointerover / pointerout / pointermove をグローバルに監視し、Canvas から外れても座標を追いかけられるようにしています。
    • isOver.current = true/false のフラグを用いて、Canvas 内にいる間だけ座標を更新。
  4. マウス・ポインタでメッシュを操作

    • handlePointerEnter/Leave ではスケールを変更して大きさを変える演出を行い、
    • handleClick では色 (color) を切り替えるなどのアニメーションを api.start で制御。

2.2 親コンポーネント (MyComponent)

export default function MyComponent() {
  const blobApi = useRef(null)

  useEffect(() => {
    const interval = setInterval(() => {
      if (blobApi.current) {
        // useImperativeHandle で定義した getCurrentPosition() を呼び出す
        const { x, y } = blobApi.current.getCurrentPosition()
        console.log('the blob is at position', { x, y })
      }
    }, 2000)

    return () => clearInterval(interval)
  }, [])

  return (
    <Canvas>
      <ambientLight intensity={0.8} />
      <pointLight intensity={1} position={[0, 6, 0]} />
      <MyScene ref={blobApi} />
    </Canvas>
  )
}
  • blobApi.currentMyScene コンポーネントで forwardRef により受け取った ref
  • 2 秒おきに getCurrentPosition() を呼び出し、コンソールに座標を表示しています。
  • これにより、“スプリングで動いている値” を外部で随時取得できていることが確認できます。

3. なぜ「onChange」で同期を取る必要があるのか?

  • スプリングの値 (springs.position など) は、SpringValue と呼ばれるクラスインスタンスで管理されており、単に外部 store に入れるだけだと “値” ではなく “オブジェクト” を入れてしまうことになります。
  • さらに、アニメーション中は値が連続で変化するため、必要なタイミングで「実際の数値」を取り出す仕組みがないと外部と同期が取りにくい。
  • onChange コールバックを活用すれば、「スプリング値が更新された瞬間」に「数値だけ」を受け取り、外部の変数へ反映できます。

4. まとめ

  1. React Spring の onChange

    • スプリング内部の値変化をフックし、最新の数値を取り出して必要な場所に転送できる。
    • 今回のように Three.js の Vector2 / Vector3 にセットし、外部のコンポーネントやロジックに受け渡す用途で使える。
  2. forwardRef + useImperativeHandle

    • 「子コンポーネントの内部状態(ここでは Blob の座標)を親が取り出す」構造が簡単に作れる。
    • Redux や Zustand などのグローバルストアを介するよりも、単純かつ直接的に値を取得できる手段となる。
  3. 外部ストアとの連携

    • 本例では単に Vector2 に値を保存しているだけですが、同様の手法で「外部の状態管理ストア (e.g. Redux, Zustand) に値を反映する」「サーバーに送信する」といったことも可能。
    • onChange で得た値を dispatchsetState でミドルウェアに渡すことで、UI アニメーションと外部ロジックの整合を保てる。
はる@フルスタックチャンネルはる@フルスタックチャンネル

@react-three/rapier

React Three Fiber (R3F) 上で Rapier という物理エンジンを扱うためのライブラリ @react-three/rapier(略称: r3/rapier)の基本的な使い方と主要な機能を、チュートリアル形式でわかりやすくまとめています。

1. イントロダクション

@react-three/rapier は、Rust 製の高速・高性能な物理エンジン Rapier を WebAssembly (WASM) を通じて JavaScript/TypeScript から利用できるようにし、さらに React Three Fiber とシームレスに連携するためのラッパーライブラリです。

  • 主な特長
    1. 最小限のコードで物理挙動を導入
    2. React Hooks で直感的に剛体(RigidBody)やコライダー(Collider)を定義
    3. 衝突・イベント・ジョイント(関節)など豊富な物理演算機能をサポート
    4. デバッグ描画 でコライダーの形状をリアルタイム可視化

2. 基本の使い方

2.1 セットアップ

npm install @react-three/rapier

react-three-fiber と合わせてインストールしておきます。
続いて、シーン (<Canvas>) の中に <Physics> コンポーネントを配置します。

import { Canvas } from "@react-three/fiber"
import { Physics } from "@react-three/rapier"
import { Suspense } from "react"

function App() {
  return (
    <Canvas>
      <Suspense fallback={null}>
        <Physics>
          {/* ここに剛体 (RigidBody) やコライダー (Collider) を配置 */}
        </Physics>
      </Suspense>
    </Canvas>
  )
}

2.2 RigidBody (剛体)

R3F の <mesh><RigidBody> でラップすると、自動的に物理演算の対象となります。

import { Physics, RigidBody } from "@react-three/rapier";
import { Box } from "@react-three/drei";

const Scene = () => {
  return (
    <Physics>
      {/* RigidBodyで包んだBox。自動的にコライダーも生成される */}
      <RigidBody>
        <Box />
      </RigidBody>
    </Physics>
  )
}
  • colliders
    <RigidBody> を包んだメッシュの形状から、コライダー(衝突判定形状)を自動的に生成します。
    • "cuboid", "ball", "trimesh", "hull", false などの指定が可能。
    • 何も指定しない場合のデフォルトは "hull" になる場合が多い(バージョンにより異なります)。
<RigidBody colliders="ball">
  <Sphere />
</RigidBody>

3. コライダーの設定

3.1 自動コライダー (Automatic Colliders)

  • globally: <Physics>colliders プロパティで全体デフォルトを設定
  • locally: <RigidBody>colliders プロパティで個別に上書き
<Physics colliders="hull">
  {/* global default = "hull" */}
  <RigidBody>
    <Box />
  </RigidBody>
  <RigidBody>
    <Sphere />
  </RigidBody>
</Physics>
<Physics colliders={false}>
  {/* 自動生成を無効化しつつ、個別には "cuboid" などを指定 */}
  <RigidBody colliders="cuboid">
    <Box />
  </RigidBody>
  <RigidBody colliders="ball">
    <Sphere />
  </RigidBody>
</Physics>

3.2 手動コライダー (Collider Components)

  • <BallCollider>, <CuboidCollider>, <CapsuleCollider>, <MeshCollider>, etc…
  • 同じ <RigidBody> の中に複数配置すれば複合コライダーが作れます。
<RigidBody>
  <Box />
  {/* Box用のカスタム collider */}
  <CuboidCollider args={[0.5, 0.5, 0.5]} />
</RigidBody>

ポイント: 目に見えないコライダーだけを置きたい場合でも、ダミーの <mesh> を置くか、<RigidBody> の中に <BoxCollider> などを挿入するだけでOKです。


4. デバッグ表示

物理エンジン上のコライダー形状を可視化するには、<Physics>debug プロパティを付けるだけ。

<Physics debug>
  <RigidBody>
    <Box />
  </RigidBody>
</Physics>

これでワイヤーフレーム状にコライダーが描画され、どのように衝突判定が生成されているかが確認できます。


5. 剛体の動かし方・力の加え方

5.1 剛体へのアクセス

<RigidBody>ref を取得すると、実際の物理剛体 (RapierRigidBody) を操作できます。

import { RigidBody, RapierRigidBody } from "@react-three/rapier";
const MyBox = () => {
  const boxRef = useRef<RapierRigidBody>(null);

  useEffect(() => {
    // 例えばフレーム開始時に力を加える
    if (boxRef.current) {
      boxRef.current.applyImpulse({ x: 0, y: 5, z: 0 }, true);
    }
  }, []);

  return (
    <RigidBody ref={boxRef}>
      <mesh>
        <boxGeometry />
        <meshStandardMaterial />
      </mesh>
    </RigidBody>
  );
};

5.2 力とトルク

  • applyImpulse({ x, y, z }, wake = true): 一瞬の衝撃
  • addForce({ x, y, z }, wake = true): 継続的な力
  • applyTorqueImpulse({ x, y, z }, wake = true): 一瞬の回転衝撃
  • addTorque({ x, y, z }, wake = true): 継続的な回転力
rigidBody.current.applyImpulse({ x: 0, y: 10, z: 0 }, true)

6. 衝突イベント / Collision

6.1 衝突コールバック

  • onCollisionEnter, onCollisionExit, onSleep, onWake, onContactForce などを <RigidBody> や個別 Collider に記述。
  • イベントハンドラの引数には衝突相手の情報や接触点のマニフォールドが含まれます。
<RigidBody
  onCollisionEnter={({ manifold, target, other }) => {
    console.log("衝突した", target.rigidBodyObject.name, "と", other.rigidBodyObject?.name);
  }}
  onSleep={() => console.log("Zzzz...")}
/>

6.2 センサー

コライダーを sensor にすると衝突判定はするが実際の物理干渉(反発)はしなくなります。
onIntersectionEnter, onIntersectionExit を使って “通過” を検知し、トリガーゾーンに利用可能です。

<RigidBody>
  <CuboidCollider
    args={[5, 5, 1]}
    sensor
    onIntersectionEnter={() => console.log("Goal!")}
  />
</RigidBody>

7. インスタンシング (Instanced Meshes)

多数のオブジェクトを同じジオメトリで描画する場合、<InstancedRigidBodies> を使うとパフォーマンスを向上できます。

import { InstancedRigidBodies, RapierRigidBody } from "@react-three/rapier";

const COUNT = 1000;
function MyScene() {
  // 剛体を保存しておく配列参照
  const rigidBodies = useRef<RapierRigidBody[]>(null);

  const instances = useMemo(() => {
    const arr = [];
    for (let i = 0; i < COUNT; i++) {
      arr.push({
        key: `instance_${i}`,
        position: [Math.random()*10, Math.random()*10, Math.random()*10],
        rotation: [Math.random(), Math.random(), Math.random()],
      });
    }
    return arr;
  }, []);

  useEffect(() => {
    // 例) 特定のインスタンスに衝撃を与える
    rigidBodies.current?.[40].applyImpulse({ x: 0, y: 10, z: 0 }, true);
  }, []);

  return (
    <InstancedRigidBodies ref={rigidBodies} instances={instances} colliders="ball">
      {/* 1つのInstancedMeshを描画。count=COUNT */}
      <instancedMesh args={[null, null, COUNT]} />
    </InstancedRigidBodies>
  );
}

8. 時間の進み方(timeStep)

物理シミュレーションの時間刻みを変更できます。

<Physics timeStep={1 / 30}> {/* 1フレームにつき1/30秒刻みで進める */}
  ...
</Physics>

// あるいは可変フレームレートを追従させる
<Physics timeStep="vary">
  ...
</Physics>

ただし、可変フレーム ("vary") は再現性(デターミニスム)が失われがちなので注意が必要です。


9. ジョイント (Joints)

2つの剛体を “接合” する機能。固定・回転・スライドなどさまざまな種類があり、useXxxJoint フックを通じて定義します。

  • FixedJoint:2つの剛体を完全に固定 (相対変位なし)
  • SphericalJoint:ボールジョイント (任意角度に回転、位置は固定)
  • RevoluteJoint:回転軸を1つに限定 (扉や車輪など)
  • PrismaticJoint:スライド(直線移動)のみ許可
  • RopeJoint:2つの剛体の最大距離を制限 (紐のような振る舞い)
  • SpringJoint:ばねによる弾性を付与 (質点バネ系)
const [bodyARef, bodyBRef] = [useRef<RapierRigidBody>(null), useRef<RapierRigidBody>(null)];
const joint = useRevoluteJoint(bodyARef, bodyBRef, [
  [0, 0, 0], // bodyAのローカル空間におけるジョイント位置
  [0, 0, 0], // bodyBのローカル空間におけるジョイント位置
  [0, 1, 0], // 回転軸
]);

10. 高度な使い方

  • useRapier: 物理ワールドやラッパーのインスタンスに直接アクセスできるフック。
  • useBeforePhysicsStep, useAfterPhysicsStep: 物理ステップ前後に特定の処理を差し込むことが可能。
  • Manual stepping: 自分で world.step() を呼び出して制御する方法。
  • On-demand rendering: updateLoop="independent" を設定し、フレームレンダリングと物理ステップを分離するなどの最適化が可能。
  • Snapshots: 物理ワールドをシリアライズ・デシリアライズして、セーブ/ロードのような仕組みを実現。

まとめ

  1. <Physics> コンポーネント
    • 物理ワールドを生成するルート。gravity などのグローバル設定が可能。
  2. <RigidBody>
    • メッシュを包むだけで自動的に物理オブジェクトにできる。
    • colliders プロパティで自動生成されるコライダータイプを切り替え可能。
  3. Collider Components
    • <BoxCollider>, <BallCollider> などを手動で付与し、複雑な衝突形状を構築できる。
  4. 衝突イベント
    • onCollisionEnter / onCollisionExit, onSleep, onIntersectionEnter(センサー)など、多彩なフックで反応を取得。
  5. ジョイント
    • useFixedJoint, useRevoluteJoint などで剛体同士を結びつけ、多様な物理表現 (ドア、車輪、チェーンなど) が実現。
はる@フルスタックチャンネルはる@フルスタックチャンネル

React Three Fiber でシーンにリアルな影を付ける方法はいくつかありますが、それぞれメリット・デメリットや目的が異なります。ここでは AccumulativeShadowsRandomizedLight を使ったアプローチ、そして ContactShadows との違い・使い分けを中心に解説します。

1. 影の全体像と基本的な手法

1.1 デフォルトのリアルタイムシャドウ

  • Canvas に shadows を付与
    <Canvas shadows>
      ...
    </Canvas>
    
  • ライトに castShadow
    <directionalLight castShadow ... />
    
  • 影を落とすオブジェクトに castShadow、影を受けるオブジェクトに receiveShadow
    <mesh castShadow receiveShadow />
    

この方法は「リアルタイムシャドウマップ」と呼ばれ、動的にオブジェクトが移動したりライトの角度が変わっても影が追従します。

メリット:

  • すべてが動的に変化するので、オブジェクトを動かしたりしても自然に影が変化
  • Three.js に標準で実装されているシャドウマップ機能をそのまま利用

デメリット:

  • シャドウマップを高解像度にするとパフォーマンスに影響
  • 大きなシーンだと、カメラの調整(shadow-camera-far/left/right など)も必要
  • 影のエッジが硬くなりやすく、ソフトにしようとすると設定が少し複雑

1.2 Drei の ContactShadows

  • ContactShadows は、ライトを使わず オブジェクト形状 + 仮想平面 から“接地面の影”を計算し、そこにだけ影を表示するものです。
  • ContactShadows コンポーネントを一つ置くだけでシーンの地面上に簡易的な影を表示できます。
<ContactShadows
  position={[0, -0.5, 0]} // 影を落とす平面の位置
  scale={10}              // シャドウが投影される範囲
  blur={2}                // ぼかし具合
  opacity={0.5}           // 影の不透明度
/>

メリット:

  • セットアップがとてもシンプル
  • ライトの castShadow 設定が不要
  • 軽量かつ、接地面の影をさっと付けたい場合に便利

デメリット:

  • 接地面以外への影は作れない(壁や他のオブジェクトへの投影は不可)
  • オブジェクトの高さや姿勢によっては「不自然な影」になりやすい
  • シンプルな演出向きで、高度な影には不向き

2. AccumulativeShadows と RandomizedLight

2.1 AccumulativeShadows とは

  • AccumulativeShadows は “累積” という名前のとおり、「複数フレームにわたって影を合成」しながらソフトシャドウを実現する仕組みです。
  • 1 フレームごとに少しずつ影を計算し、だんだんと滑らかな影にしていくイメージ。
<AccumulativeShadows
  temporal                 // 時間(フレーム)をかけて影を累積させる
  frames={35}             // 何フレームで累積を完了するか
  alphaTest={0.85}
  scale={5}
  position={[0, -0.49, 0]}
  color="#EFBD4E"
>
  {/* ここにライトを指定 */}
</AccumulativeShadows>
  • temporaltrue にすると、レンダリング開始後しばらく影が徐々に濃く・滑らかになっていきます(蓄積型)。
  • frames は何フレームかけて累積計算を終えるかの指定。累積が完了すると性能負荷が下がり、静止画のように非常に綺麗な影が得られます。

2.2 RandomizedLight とは

  • RandomizedLight は、内部的に複数のライトをランダムに配置し、いろいろな角度 からの影を AccumulativeShadows 内で合成する手法。
  • これによって「アンビエントオクルージョンのような複数方向から差し込む柔らかい影」をつくることができます。
<AccumulativeShadows ...>
  <RandomizedLight
    amount={4}          // ライトの数
    radius={9}          // ライトが散らばる半径
    intensity={0.55}
    ambient={0.25}
    position={[5, 5, -10]}
  />
  <RandomizedLight
    amount={4}
    radius={5}
    intensity={0.25}
    ambient={0.55}
    position={[-5, 5, -9]}
  />
</AccumulativeShadows>
  • amount:何本の光源をランダムに配置するか(増やすほどソフトかつリッチになるが計算量が上がる)
  • radius:光源がばらける範囲
  • intensity, ambient:それぞれのライトの強度や環境光成分

2.3 組み合わせのメリット

  • 超ソフトな影
    AccumulativeShadows + RandomizedLight の組み合わせで、様々な角度から影を重ね合わせるため、非常に柔らかい陰影が得られます。
  • 静的オブジェクト向け
    基本的に何フレームかけて影を蓄積するため、「動くオブジェクトがあるシーン」だと、影がすぐにリアルタイムで追従しない(残像のような影が溜まってしまう)という問題があります。ただし軽微な動きなら対応可能な設定もできます。
  • パフォーマンス
    初期累積が終われば、動きのない部分の影は大きく負荷がかからず、高品質と高速描画のバランスが良いです。

3. ContactShadows vs. AccumulativeShadows

両者は似たように「地面の上に柔らかい影を落とす」機能を持ちますが、主に以下の違いがあります。

  1. ライトを意識するか

    • ContactShadows は「ライト不要」でオブジェクトとの接地形状を元に地面に影を描画。ほぼ 2D 的なアプローチ。
    • AccumulativeShadows は「実際のライトを元に影を投影」するので、光源配置を調整でき、より現実的な光の当たり方が再現できる。
  2. 投影範囲

    • ContactShadows は主に接触面を中心とした影で、地面の“すぐ下”に限定。オブジェクトが浮いたりすると影が伸びるわけではなく、あくまで 2D 的にぼかした接地影。
    • AccumulativeShadows はオブジェクト全体の形状を使い、光源からの投影を計算する。高さや角度に応じて影がしっかり伸びる。
  3. 動的対応

    • ContactShadows はオブジェクトが動いても「接地部分」だけがスクリーン上で計算されるため、比較的軽量かつ動的に対応しやすい。
    • AccumulativeShadows(+ RandomizedLight)はフレームをかけて影を蓄積する仕組みなので、オブジェクトが激しく動くと影が追いつかず、意図しない残像やノイズが出る可能性がある。
  4. 見た目のリアリティ

    • 単に床へ落ちる接地影だけなら ContactShadows がシンプルで高速。
    • 床だけでなく、もう少し本格的なライティング効果やアンビエントオクルージョン風の演出がほしいなら AccumulativeShadows + RandomizedLight が強力。

4. 使い分けのポイント

  • 静的なシーン(オブジェクトやライトがほぼ動かない)
    • AccumulativeShadows + RandomizedLight … リッチでソフトな影を実現。フレーム経過後は高品質。
  • 軽量&単純な床影だけ使いたい
    • ContactShadows … 設定が簡単でパフォーマンスも良好。
  • 完全なリアルタイムかつすべてのオブジェクトに影が落ちる必要がある
    • 従来の castShadow + receiveShadow + (SoftShadows or standard shadowMap) … いわゆる通常の three.js シャドウ。
  • 動くオブジェクトだけリアルタイムに影を落としたいが、背景の大部分は静的
    • 静止部分に AccumulativeShadows かベイクした影を使い、動くオブジェクトには本来のリアルタイムシャドウか ContactShadows を組み合わせる。

5. まとめ

  1. リアルタイムシャドウ
    • <Canvas shadows> + ライトに castShadow + オブジェクトに castShadow/receiveShadow
    • 汎用的だが、調整やパフォーマンス面で苦労することも。
  2. ContactShadows
    • 設定がとても簡単。地面への接地影だけを軽量に表現したいときに最適。
    • 光源に依存しないので簡易的かつ高速。
  3. AccumulativeShadows + RandomizedLight
    • フレームをかけてソフトシャドウを累積し、多方向から当たる柔らかい影を再現する手法。
    • 静的なシーンや、動きが少ない状況に向いており、非常に美しい影が得られる。
  4. ベイク (Baking)
    • モデリングツール(Blender など)で影をテクスチャとして事前に焼き付ける方法。軽くて見た目が良いが、動的シーンには不向き。

どれか一つが万能というわけではなく、シーンの動的要素の有無・表現したい影の柔らかさ・パフォーマンス要件などを踏まえて選択すると良いでしょう。シンプルに床だけ影を落としたいなら ContactShadows、オブジェクトが静的でフォトリアリスティックな雰囲気を出したいなら AccumulativeShadows + RandomizedLight、といった使い分けが典型例です。

以上が AccumulativeShadows & RandomizedLightContactShadows の違いや使い分けの目安です。

はる@フルスタックチャンネルはる@フルスタックチャンネル

アニメーション

React で Three.js を使った 3D アニメーションを実装するとき、選択肢として GSAP, Framer Motion, React Spring などのライブラリがあります。それぞれに長所・短所や使い勝手の違いがあるため、本記事では React Three Fiber (以下 R3F) を前提にした概要とサンプルコードを交えて解説します。

1. GSAP

概要

  • JS アニメーションライブラリの老舗 で、アニメーション制御に強力な API とタイムライン機能がある
  • Three.js / React Three Fiber 向けに最適化されているわけではなく、DOM や任意の JS オブジェクトのプロパティを「数値やカラー値」として操作する
  • タイムライン (gsap.timeline()) で複数のアニメーションを順序付けたり、シーケンス制御が得意

メリット

  • 豊富な機能・柔軟性: 連続的なアニメーション、トリガー、タイムライン、イージングなどが強力
  • Web業界の定番 でリファレンス・サンプルが多い
  • スクロール連動ScrollTrigger 等を組み合わせれば、スクロールアニメもやりやすい

デメリット

  • R3F のオブジェクト (mesh.position, mesh.rotation など) を直接補間できない場合、プロパティをラップ して自分で数値を同期する必要がある
  • DOM アニメーションと一緒に使うのは簡単だが、Three.js の THREE.Color のような独自クラスを扱うには注意点が必要

サンプルコード

import { useRef, useEffect } from "react"
import { useFrame } from "@react-three/fiber"
import { gsap } from "gsap"
import * as THREE from "three"

export default function GsapExample() {
  const boxRef = useRef()
  const timelineRef = useRef(null)

  // “背景色” などをアニメーションしたい時に用いるラッパー (例)
  const colorState = useRef({ color: "#ff0000" })

  useEffect(() => {
    // タイムライン作成
    timelineRef.current = gsap.timeline({ paused: true })
      .to(colorState.current, {
        duration: 1,
        color: "#00ff00", // 1秒かけて緑へ
      })
      .to(colorState.current, {
        duration: 1,
        color: "#0000ff", // さらに青へ
      })
  }, [])

  useFrame(() => {
    // timeline の進捗を 0~1 で制御する例:
    // timelineRef.current.progress(someScrollOffset)
    
    // colorState.current.color が "#112233" の形式で変化している → Three.Color に反映
    if (boxRef.current) {
      boxRef.current.material.color.set(colorState.current.color)
    }
  })

  return (
    <mesh ref={boxRef}>
      <boxGeometry />
      <meshStandardMaterial />
    </mesh>
  )
}
  • GSAP は「数値やカラー値」を補間するのは得意ですが、THREE.ColorTHREE.Vector3 をそのまま扱えない場合があり、上記のようにラップ用のオブジェクトを使って手動で同期することが多いです。
  • ポジション・回転なども { x: 0, y: 0, z: 0 } のようなオブジェクトを作り、そこをアニメーションしつつ useFramemesh.position.set(x,y,z) するパターンが定番です。

2. Framer Motion

概要

  • React 向けのアニメーションライブラリ で、DOM アニメーションに強い
  • フックmotion コンポーネント を利用して宣言的にアニメーションを書く
  • スプリング + キーフレーム の両方のいいとこ取り
  • framer-motion-3d を使えば R3F の <mesh><group> などを motion.mesh として書ける

メリット

  • 宣言的記法: <motion.mesh animate={{x: 1, rotateY: 0.5}} /> のように Props で指定
  • Variants, Transitions, LayoutAnimations などの高機能が利用可能
  • スプリング & キーフレーム の設定がわかりやすい
  • 公式ドキュメントとコミュニティが充実

デメリット

  • R3F 用には framer-motion-3d が必須 (別パッケージ)
  • 複雑な 3D アニメーション (Three の独自要素) を扱うには工夫が必要
  • 大規模なアニメ制御 には若干のコード量が増える

サンプルコード

import { motion } from "framer-motion-3d"
import { useState } from "react"

export default function FramerMotionExample() {
  const [isOpen, setOpen] = useState(false)

  // クリックで「開く/閉じる」状態をトグル
  const toggle = () => setOpen((prev) => !prev)

  return (
    <group onClick={toggle}>
      {/* position-y や rotation などを Props で宣言的に制御 */}
      <motion.mesh
        animate={ isOpen ? "open" : "closed" }
        variants={{
          closed: { x: 0, y: 0, rotateY: 0 },
          open:   { x: 1, y: 1, rotateY: Math.PI }
        }}
        transition={{ type: "spring", stiffness: 200 }}
      >
        <boxGeometry />
        <motion.meshStandardMaterial
          variants={{
            closed: { color: "#ffffff" },
            open:   { color: "#ff0000" },
          }}
        />
      </motion.mesh>
    </group>
  )
}
  • motion.meshmotion.meshStandardMaterialvariants を使うと、状態 (open/closed) 毎にまとめたアニメーションが書けます。
  • 非常に宣言的かつコンポーネント指向でわかりやすい点が Framer Motion の強みです。

3. React Spring

概要

  • スプリングベースのアニメーションライブラリ
  • DOM 向けが @react-spring/web、Three.js 向けが @react-spring/three として提供
  • 物理演算に基づく「バネっぽい」自然な動きが特徴。いわゆる「イージング曲線」ではなく、バネのパラメータ (stiffness, dampingなど) を調整する

メリット

  • スプリング挙動 による自然なアニメーション (アニメ開始・終了時にふわっとする)
  • useSpring / useSprings などのフックを使い、細かい制御が可能
  • R3F コンポーネントを animated.mesh のように書ける

デメリット

  • キーフレーム的なアニメ制御 (このタイミングでこうなる…) は苦手
  • アニメーションを途中で止めたり、厳密な時間制御をする際はやや工夫が要る
  • 最新版と R3F (あるいは Drei) のバージョン相性による警告が出る場合もあり、注意が必要

サンプルコード

import { useSpring, animated } from "@react-spring/three"
import { useState } from "react"

export default function ReactSpringExample() {
  const [toggle, setToggle] = useState(false)

  // スプリングアニメーション設定
  const props = useSpring({
    // トグルで位置 & 回転を変化させる
    position: toggle ? [1, 1, 0] : [-1, -1, 0],
    rotation: toggle ? [Math.PI, 0, 0] : [0, 0, 0],
    config: { mass: 1, friction: 20, tension: 200 },
  })

  return (
    <animated.mesh
      onClick={() => setToggle(!toggle)}
      position={props.position}
      rotation={props.rotation}
    >
      <boxGeometry />
      <meshStandardMaterial color="orange" />
    </animated.mesh>
  )
}
  • 位置や回転をスプリングで補間し、自然な動きが実現できます。
  • 時間指定 (duration) というよりは「スプリング定数 (tension / friction)」でアニメ速度を制御するという考え方になります。

4. 比較まとめ

GSAP Framer Motion React Spring
特徴 汎用JSライブラリ。DOM/JSオブジェクト/数値等に強力 React向け宣言的ライブラリ + 3D対応(framer-motion-3d) React向けスプリングベースのライブラリ
得意分野 タイムライン、複雑な連続アニメ、スクロールトリガー等 Hooks/Variants/Propsでの宣言的アニメ バネのパラメータで自然な動き
R3Fへの統合 ラップ用のオブジェクトを作って手動で同期 motion.mesh などを使い宣言的に書ける animated.mesh など、スプリングを簡単に適用
メリット 高機能・長年の実績・豊富なイージング・カスタム性 シンプルなAPI、高度なUIトランジション機能もあり 物理演算による自然なアニメが簡単
デメリット Three.jsのColor/Vector 等を扱うには一手間 framer-motion-3d インストール必須 時間指定が苦手、バージョン相性に注意
適したケース すでにGSAPを使い慣れている、複雑なシーケンス制御 React的な宣言的アプローチで3Dアニメしたい 「バネっぽい動き」を簡単に実装したい

5. どれが一番やりやすいか?

  • GSAP:

    • もともと Web アニメーションの定番で、強力なタイムライン機能を持ちます。スクロール連動や複雑なシーケンスに強い反面、R3F と組み合わせるには「数値 or 独自オブジェクトの管理」が必要です。
    • “すでに GSAP に慣れている”、あるいは “多彩なタイムライン/スクロールアニメを活用したい” 場合におすすめ。
  • Framer Motion:

    • React ドリブンの宣言的アニメーション が使いやすい方にピッタリ。motion.mesh 等で 3D オブジェクトをスムーズに制御できます。
    • スプリング+キーフレームのバランスもよく、UI のアニメ or 3D のアニメを一元的に扱いたい 場合に向いています。
  • React Spring:

    • スプリングアニメーション” が主目的なら最適。自然な挙動やバウンス感を簡単に表現でき、animated.mesh で R3F に統合も容易。
    • 時間軸よりも物理挙動(バネ)を使うため、自然な動き連続的に変化するアニメ を作りたいときに最適。

まとめ

  • GSAP は Web アニメ全般の王道ライブラリ。複雑なアニメ制御やタイムライン管理、スクロールトリガーに強い。
  • Framer Motion は React 的な宣言的 API が魅力で、framer-motion-3d を使えば 3D メッシュもシンプルにアニメ可能。UI/UX 系アニメと併用するなら特に便利。
  • React Spring は スプリング物理演算 をベースにした自然なアニメーションが特徴。R3F 用の @react-spring/three で簡単に 3D オブジェクトをバネ挙動で動かせる。

結局どれが「一番やりやすい」かは、アニメーションの目的や作りたい演出スタイル、既存の知識によって変わります。

  • 直感的な「バネ感」が欲しければ React Spring
  • 宣言的・UIベース・Variants や LayoutAnimation を活用したいなら Framer Motion
  • シーケンスアニメやタイムラインで複雑な演出を作り込むなら GSAP
はる@フルスタックチャンネルはる@フルスタックチャンネル

物理演算ゲーム

React Three Fiber + @react-three/rapier を使った物理演算ゲームの作り方

1. シーンにマップ (Playground) を配置

マップ (Playground) に RigidBody を付与

import { RigidBody } from "@react-three/rapier"

export function Playground(props) {
  return (
    <group {...props} dispose={null}>
      {/* GLTF / 3D モデル */}
      <RigidBody type="fixed" name="ground" colliders="trimesh">
        {/* ここにまとめてマップのメッシュが入る */}
      </RigidBody>
    </group>
  )
}
  • type="fixed": 動かない地形を表す剛体。
  • colliders="trimesh": 複雑なモデル形状に合わせて「トライメッシュ」コライダーを自動生成。

ポイント: trimesh は細かい衝突判定をする一方、パフォーマンスコストが高いので、可能ならばシンプルなコライダー (cuboid など) で代用するか、一部のみ trimesh を使用するとよい。


2. プレイヤー (RigidBody) + カメラ追従

プレイヤーの RigidBody

import { RigidBody } from "@react-three/rapier"
import { PerspectiveCamera } from "@react-three/drei"
import { useFrame } from "@react-three/fiber"
import { useRef } from "react"
import { Vector3 } from "three"
import { vec3 } from "@react-three/rapier" // Rapier -> three.js 変換用

export const Player = () => {
  const rb = useRef()                      // RigidBody 参照
  const camera = useRef()                  // カメラ参照
  const cameraTarget = useRef(new Vector3(0, 0, 0))

  useFrame(() => {
    // プレイヤーの位置
    const playerPos = vec3(rb.current.translation()) // Rapier Vector -> three Vector

    // カメラがプレイヤーを滑らかに追従
    cameraTarget.current.lerp(playerPos, 0.5)
    camera.current.lookAt(cameraTarget.current)
  })

  return (
    <RigidBody ref={rb} name="player" /* ...他にもprops */>
      {/* カメラをプレイヤーに追従させるため、RigidBodyの子に置く */}
      <PerspectiveCamera makeDefault position={[0, 5, 8]} ref={camera} />

      {/* プレイヤーの見た目 (mesh) */}
      <mesh castShadow>
        <boxGeometry />
        <meshStandardMaterial color="orange" />
      </mesh>
    </RigidBody>
  )
}

ポイント:

  1. RigidBody の子<PerspectiveCamera> を配置すると、「カメラを剛体と一緒に移動・回転」できる。
  2. カメラがプレイヤーを自然に見るように useFrame 内で lookAt()lerp() を使って、視点が常に滑らかに追尾。

3. プレイヤーの操作 (回転と前進後退)

import { Controls, get } from "./store" // キー入力管理 (例)
import { euler, quat, vec3 } from "@react-three/rapier"

const MOVEMENT_SPEED = 5
const ROTATION_SPEED = 5
const JUMP_FORCE = 5

export const Player = () => {
  // ... (上と同じ一部省略)

  const inTheAir = useRef(false)
  const vel = new Vector3()

  useFrame(() => {
    // 入力に応じて「回転速度 (angVel)」「移動速度 (linVel)」を更新
    let rotVel = { x: 0, y: 0, z: 0 }
    vel.set(0, 0, 0)

    if (get()[Controls.forward]) vel.z -= MOVEMENT_SPEED
    if (get()[Controls.back])    vel.z += MOVEMENT_SPEED
    if (get()[Controls.left])    rotVel.y += ROTATION_SPEED
    if (get()[Controls.right])   rotVel.y -= ROTATION_SPEED

    // 回転を RigidBody にセット
    rb.current.setAngvel(rotVel, true)

    // 現在の回転角度を読み取り、移動方向へオイラー適用
    const eulerRot = euler().setFromQuaternion(quat(rb.current.rotation()))
    vel.applyEuler(eulerRot)

    // ジャンプ処理
    const curVel = rb.current.linvel()
    if (get()[Controls.jump] && !inTheAir.current) {
      vel.y += JUMP_FORCE
      inTheAir.current = true
    } else {
      vel.y = curVel.y // y軸速度はそのまま保持
    }

    // 最終的に RigidBody に linVel をセット
    rb.current.setLinvel(vel, true)
  })

  return (
    <RigidBody
      // ジャンプ着地判定
      onCollisionEnter={({ other }) => {
        if (other.rigidBodyObject.name === "ground") {
          inTheAir.current = false
        }
      }}
    >
      ...
    </RigidBody>
  )
}

ポイント:

  1. 回転setAngvel (角速度) を使い、左右矢印で Y 軸回転だけ行う。
  2. 移動方向は「現在の剛体の回転」を euler().setFromQuaternion(...) で取得 → vel.applyEuler(eulerRot) でローカル前方方向に変換。
  3. ジャンプは inTheAir フラグで多段ジャンプを防ぐ。地面と onCollisionEnter で接触したらフラグを false に。

4. リスポーン (落下したら戻す)

<RigidBody
  name="player"
  onIntersectionEnter={({ other }) => {
    if (other.rigidBodyObject.name === "space") {
      // 落下ゾーンに触れたらリスポーン
      rb.current.setTranslation({ x: 0, y: 5, z: 0 })
    }
  }}
>
  {/* ... */}
</RigidBody>

// Experience.jsx (落下ゾーン)
<RigidBody type="fixed" colliders={false} sensor name="space" position-y={-5}>
  <CuboidCollider args={[50, 0.5, 50]} />
</RigidBody>
  • センサー となる RigidBody (sensor プロパティ or colliders={false} + <...Collider sensor> など) を設置し、落下ゾーンにプレイヤーが触れたら setTranslation でプレイヤーを初期位置に戻す。

5. Swiper (動く障害物)

<RigidBody
  ref={swiper}
  type="kinematicVelocity"  // 外力を受けず、自分で回転/速度を設定
  colliders="trimesh"
  restitution={3}
  name="swiper"
>
  {/* モデル */}
</RigidBody>
// 角速度を設定 (一度だけ or useFrame)
useEffect(() => {
  swiper.current.setAngvel({ x: 0, y: 3, z: 0 }, true)
}, [])
  • type="kinematicVelocity" は「プレイヤーには影響を与えるが、自分自身は外力で動かない」特殊剛体。
  • setAngvel で継続的に回転し、プレイヤーが当たると弾き飛ばされる。
  • パンチされた効果 を出すには、一瞬だけ setLinvel を無効化するなど工夫。

6. ゲート (ゴール / テレポート)

// ゲートを sensor に
<RigidBody
  type="fixed"
  name="gateIn"
  sensor
  colliders={false}
  position={[-20.325, -0.249, -28.42]}
>
  <mesh /* メッシュ */ />
  <CuboidCollider position={[-1, 0, 0]} args={[0.5, 2, 1.5]} />
</RigidBody>
// Player 側で衝突検知 → テレポート
<RigidBody
  onIntersectionEnter={({ other }) => {
    if (other.rigidBodyObject.name === "gateIn") {
      // シーン内の gateOut を取得してそこへ移動
      const gateOut = scene.getObjectByName("gateLargeWide_teamYellow")
      rb.current.setTranslation(gateOut.position)
    }
  }}
>
  ...
</RigidBody>
  • ゲートの入り口 をセンサーにし、衝突したら “出口” のメッシュ位置を取得して setTranslation
  • さらにエフェクトなどを加えると「ゲームクリア」感が演出できる。

7. シャドウを大きなシーンに対応させる

<directionalLight
  position={[-50, 50, 25]}
  intensity={0.4}
  castShadow
  shadow-mapSize-width={1024}
  shadow-mapSize-height={1024}
>
  {/* shadow-camera を PerspectiveCamera で手動調整 */}
  <PerspectiveCamera
    ref={shadowCameraRef}
    attach="shadow-camera"
    near={55}
    far={86}
    fov={80}
  />
</directionalLight>
  • 物理演算ゲームは大きなマップになることが多いため、シャドウカメラの near/farfov を手動調整して、オブジェクトが影にちゃんと収まるようにする。
  • Drei の useHelper(shadowCameraRef, THREE.CameraHelper) で可視化しながら最適な位置を探すのが便利。

8. 総まとめ (ポイント一覧)

  1. マップ (Playground)
    • type="fixed" + colliders="trimesh" で地形を物理衝突判定にする
  2. プレイヤー
    • RigidBody + カメラを子に配置 → カメラ追従
    • 進行方向 + 回転操作をラジアンに変換 (euler) し、setLinvel + setAngvel
    • 落下判定はセンサー (space) で respawn()
  3. キネマティックな障害物 (Swiper)
    • type="kinematicVelocity"setAngvel → 常に回転
    • 当たったプレイヤーが吹っ飛ぶ演出などは setLinvel を一時的に抑制して実装
  4. ゲート (ゴール)
    • sensor の衝突判定で テレポート (出現位置に setTranslation)
  5. シャドウ
    • 大規模シーンでは shadow-camera の調整が必須
    • useHelper などで可視化しながら near, far, fov を最適化

応用・発展

  • タイマー・スコア: ゴールまでの時間やリトライ回数など
  • 複数レベル: Blender で複数マップを用意
  • 敵 NPC: kinematic RigidBody で自動移動させる / AI
  • アニメーション: GSAP などで要素を動かしたり、UI と連携
  • マルチプレイヤー: WebSockets + 状態同期
はる@フルスタックチャンネルはる@フルスタックチャンネル

ステージ

React Three Fiber (R3F) でシーンを魅力的に仕上げるためのステージング手法を、コード例とともにわかりやすくまとめます。「ただ 3D モデルを表示する」のではなく、照明・背景・反射・環境 を整え、よりプロフェッショナルな見た目にするコツを押さえていきましょう。

1. Drei の <Stage> コンポーネント

@react-three/drei が提供する <Stage> は、「スタジオ撮影用」のようなセットアップを手軽に用意できるコンポーネントです。

  • 複数のライトを配置し、ほどよい影・反射を付けてくれます。
  • モデルを自動的に中心に収めたり、地面を作ったりする機能もあり、シンプルにシーンを美しく見せられます。
import { Canvas } from "@react-three/fiber"
import { OrbitControls, Stage } from "@react-three/drei"
import { TeslaModel3 } from "./TeslaModel3"

function Experience() {
  return (
    <Canvas>
      <OrbitControls autoRotate autoRotateSpeed={0.72} />
      {/* シンプルに <Stage> でラップするだけ */}
      <Stage intensity={0.4} preset="upfront" environment="studio">
        <TeslaModel3 scale={0.012} position-z={0.6} />
      </Stage>
    </Canvas>
  )
}
export default Experience

主なポイント

  • intensity: ライトの強度 (デフォルト 0.5)
  • preset: 照明プリセット (例: "rembrandt", "portrait", "upfront", "soft"など)
  • environment: 背景と反射に使う環境プリセット ("city", "studio", "sunset", ...)

「とりあえず高品質なライティング」をパッと出したい時に便利ですが、細かい制御が必要な場合は、後述のように自分でライトや Environment を組む方法が有効です。


2. 背景色とフォグ

2.1 背景を Canvas に指定

<Canvas camera={{ position: [0, 1.5, 12], fov: 30 }}>
  {/* 三つの引数: 背景の color と near/far */}
  <color attach="background" args={["#171720"]} />
  <fog attach="fog" args={["#171720", 20, 30]} />

  {/* ...シーン内容... */}
</Canvas>
  • <color attach="background" />: シーンの背景色を指定する (透明背景にせず、シンプルに色を塗る)。
  • <fog attach="fog" args={["色", near, far]} />: カメラからの距離に応じて霧をかける。遠くほど霧が強くなることで奥行きを演出。

2.2 背景とフォグの効果

export function Experience() {
  return (
    <Canvas>
      <color attach="background" args={["#171720"]} />
      <fog attach="fog" args={["#171720", 20, 30]} />
      {/* ... */}
    </Canvas>
  )
}
  • 背景を暗めに設定して、幻想的・落ち着いた雰囲気を出せる
  • 霧があると、手前がはっきり・遠方がぼんやり、自然な奥行きが得られる

3. 反射する床: <MeshReflectorMaterial>

import { MeshReflectorMaterial } from "@react-three/drei"

function Experience() {
  return (
    <Canvas>
      {/* ... */}
      <mesh rotation-x={-Math.PI / 2} position={[0, -1.18, 0]}>
        <planeGeometry args={[100, 100]} />
        <MeshReflectorMaterial
          color="#171720"
          resolution={1024}
          mixStrength={3}
          roughness={0.6}
        />
      </mesh>
    </Canvas>
  )
}
  • 反射床 を作りたい時に便利
  • resolution: 反射の解像度 (上げるほど見た目がきれいだが、負荷が高まる)
  • mixStrength: 反射の強さ
  • roughness: 床の粗さ (0 に近いほど鏡面反射)

4. 環境光 (HDRI など) - <Environment>

4.1 プリセットを使う

Drei には複数の環境プリセットが用意されており、簡単に HDRI 風のライティングと反射を追加できます。

import { Environment } from "@react-three/drei"

function Experience() {
  return (
    <Canvas>
      {/* ... */}
      <Environment preset="city" background={false} />
    </Canvas>
  )
}
  • preset: "city", "sunset", "dawn", "night", "studio", "forest", etc.
  • background={true} にすると、シーンの背景として 360° の HDRI イメージが表示される。
  • 注意: preset は外部CDNを利用するため、本番では files プロパティを使ってローカル HDRI を指定するのが望ましい。

4.2 カスタム HDRI ファイルを使う

<Environment files="textures/umhlanga_sunrise_1k.hdr" />
  • たとえば Poly Haven などでダウンロードした HDRI ファイルを public/textures/xxx.hdr に置き、files にパスを指定すると、その HDRI を照明および反射のソースとして利用できる。
  • backgroundtrue にすると、その HDRI を背景として 360° 表示。

5. カスタム環境 (3Dオブジェクトを使う)

<Environment> に自分のオブジェクトをラップすると、そのオブジェクトをもとに環境マップを生成します。たとえば大きな球の内側にテクスチャを貼って、独自の空間を作るなど。

import { Environment } from "@react-three/drei"
import * as THREE from "three"

function Background() {
  // 球の裏面に画像を貼る例
  const map = useTexture("/textures/my_skybox.jpg")
  return (
    <mesh>
      <sphereGeometry args={[5, 64, 64]} />
      <meshBasicMaterial map={map} side={THREE.BackSide} toneMapped={false} />
    </mesh>
  )
}

function Experience() {
  return (
    <Canvas>
      <Environment resolution={512} frames={Infinity} background={true}>
        <Background />
      </Environment>
    </Canvas>
  )
}
  • frames={Infinity} を指定すると、Float アニメーションなど動く要素もリアルタイムに反映される。
  • 解像度 (resolution) を上げれば反射の精度が高まるが、パフォーマンス負荷も上がる。

6. Lightformer で独自ライティング

<Lightformer> は、環境マップ内で光を発しているように見える長方形/リング/円 を作れます。シーンにユニークな照明効果や色付けを与えるために使います。

import { Lightformer, Environment } from "@react-three/drei"

function Lights() {
  return (
    <>
      <Lightformer
        form="ring"           // "rect" | "circle" | "ring"
        intensity={2}
        position={[-3, 3, -2]}
        scale={[3, 3, 1]}
        color="red"
        target={[0, 0, 0]}
      />
      {/* 他にも複数配置 */}
    </>
  )
}

function Experience() {
  return (
    <Canvas>
      <Environment frames={Infinity} resolution={512} blur={0.5}>
        <Lights />
      </Environment>
    </Canvas>
  )
}
  • form: "rect", "circle", "ring" の形状
  • intensity: 光の強度
  • position, scale, color などで形状や色を調整
  • Float コンポーネントでラップし、動きのあるライティングを作るのも面白い

まとめとポイント

  1. Stage:
    • すぐにスタジオっぽい照明と地面を作りたい場合に便利 (一瞬で見栄えUP)。
  2. 背景色 / Fog:
    • Canvas 内で <color attach="background"><fog attach="fog"> を使う
    • シーンを統一感ある雰囲気に仕上げたり、遠近を強調する際に有用
  3. Reflective Floor:
    • <MeshReflectorMaterial> でモデルの足元に反射を加え、フォトリアル感をアップ
  4. Environment (HDRI):
    • preset="city" などの簡易プリセット、または files="myHDRI.hdr" で本格的なライティング
    • background を有効にすると、画面全体に 360° の HDRI が表示される
  5. カスタム環境 (自作オブジェクトや Lightformer):
    • <Environment> の子要素として 3D オブジェクトを配置し、それを環境マップに使う
    • <Lightformer> で独自ライトを置く / <Float> で動かして個性的な演出
はる@フルスタックチャンネルはる@フルスタックチャンネル

View

React Three Fiber (R3F) の Canvas を 1 つだけ用意しつつ、複数の HTML セクション にそれぞれ別々の 3D シーンを埋め込めるようにするテクニックとして、<View> コンポーネントの使い方をコード例とともに解説します。「ページ内の各セクションに異なる 3D コンテンツを表示したい」「しかし Canvas を大量に増やすのはパフォーマンス的に避けたい」という場合に便利です。

1. <View> の概要

<View> は、@react-three/drei が提供するコンポーネントで、「1 つの WebGL コンテキスト (Canvas) で複数のビューをレンダリング」する仕組みを提供します。

  • メリット: それぞれの <View> が独立した「ミニ・シーン」のように機能し、スクロール/リサイズ/イベントは、対応する HTML コンテナに合った形で処理される。
  • デメリット: すべての <View> が同じ Canvas を共有するため、カメラやイベント制御を複数個使う際に工夫が必要。また、高速スクロール時のズレが発生するケースもある。

2. 基本構造の例

2.1 HTML セクションの用意

まず、HTML の各セクション(ここでは Home, Services, Team, Portfolio など)に 3D を表示するための「コンテナ要素」を設け、その要素への ref を取得します。

// HomePage.jsx
import { useRef } from "react"

export function HomePage() {
  // 各セクションのコンテナ参照
  const heroContainer = useRef(null)
  const servicesContainer = useRef(null)
  const teamContainer = useRef(null)
  const portfolioContainer = useRef(null)

  return (
    <main>
      {/* Hero セクション */}
      <section className="hero" ref={heroContainer}>
        <h1>Welcome to our Agency</h1>
        {/* ... */}
      </section>

      {/* Services セクション */}
      <section className="services">
        <h2>Our Services</h2>
        <div ref={servicesContainer} className="services__3d" />
      </section>

      {/* Team セクション */}
      <section className="team">
        <h2>Meet the Team</h2>
        <div ref={teamContainer} className="team__3d" />
      </section>

      {/* Portfolio セクション */}
      <section className="portfolio">
        <h2>Our Works</h2>
        <div ref={portfolioContainer} className="portfolio__3d" />
      </section>
    </main>
  )
}
  • それぞれの ref は、<View> コンポーネントの track プロパティに使用され、「この HTML 要素をターゲットにした 3D 表示」を実現。

2.2 1つの <Canvas> + 複数の <View>

import { Canvas } from "@react-three/fiber"
import { View } from "@react-three/drei"
import { Hero3D } from "./Hero3D"
import { Services3D } from "./Services3D"
import { Team3D } from "./Team3D"
import { Portfolio3D } from "./Portfolio3D"

export function HomePage() {
  const container = useRef(null)
  const heroContainer = useRef(null)
  const servicesContainer = useRef(null)
  const teamContainer = useRef(null)
  const portfolioContainer = useRef(null)

  return (
    <main ref={container}>
      <Canvas
        className="canvas"
        // View 内のイベントを適切に処理するため、eventSource を指定
        eventSource={container}
        camera={{ position: [0, 0, 2], fov: 35 }}
      >
        {/* Hero 用の View */}
        <View track={heroContainer}>
          <Hero3D />
        </View>

        {/* Services 用の View */}
        <View track={servicesContainer}>
          <Services3D />
        </View>

        {/* Team 用の View */}
        <View track={teamContainer}>
          <Team3D />
        </View>

        {/* Portfolio 用の View */}
        <View track={portfolioContainer}>
          <Portfolio3D />
        </View>
      </Canvas>

      {/* 以下、HTML コンテンツ (セクション) */}
      <section className="hero" ref={heroContainer}>
        <h1>Welcome to our Agency</h1>
      </section>

      <section className="services">
        <h2>Our Services</h2>
        <div ref={servicesContainer} className="services__3d" />
      </section>

      <section className="team">
        <h2>Meet the Team</h2>
        <div ref={teamContainer} className="team__3d" />
      </section>

      <section className="portfolio">
        <h2>Our Works</h2>
        <div ref={portfolioContainer} className="portfolio__3d" />
      </section>
    </main>
  )
}

ポイント解説

  1. eventSource={container}:

    • <Canvas>eventSource を指定し、さらに <main ref={container}> にすることで、マウスやタッチイベントが適切に分配される。
    • これをしないと、Canvas が最前面を占有してしまい、HTML 要素をクリックできなくなるなどの不具合が起こる場合がある。
  2. <View track={someRef}>:

    • someRef.current が指す DOM 要素の上に「その 3D シーンを表示」する。
    • スクロールやリサイズで要素が動く場合でも、WebGL レイヤが追従して位置を調整。
    • 1つの Canvas 内で複数の <View> を定義することで、独立した 3D モジュールを複数配置できる。
  3. それぞれの <View> の中に 3D コンポーネント (例: Hero3D, Team3D) を置いている。

    • こうすることで、「Hero セクション用の 3D 表示」「Team セクション用の 3D 表示」を分離したコードとして管理できる。

3. デザイン上の注意点

3.1 z-index と背景

  • Canvas はデフォルトで最前面に描画されることが多く、HTML が見えなくなる ことがある。
  • もし「HTML テキストを上に重ねたい」なら、HTML 要素に position: relative; z-index: 1; を指定するか、逆に 3D シーンを背景扱いにするときは Canvas を下げるなどの工夫をする。
.hero {
  position: relative;
  z-index: 1; /* テキストを Canvas より前面に */
}

3.2 スクロール時の同期

  • <View> はスクロールに追従してコンテナを追いかけてくれますが、高速スクロール時に若干のズレ が出る場合があります。
  • 完璧に同期させたいときは <View> より別々の <Canvas> を用意したほうが安定することも (パフォーマンスは落ちるが同期ズレは解決される)。

3.3 カメラコントロール

  • 1つの Canvas 内で複数のビューポートを使う場合、カメラが共通 になってしまうと意図せず連動して動くことがある。
  • 解決策: <View> 内に専用カメラを用意して makeDefault するか、OrbitControls 等をコンポーネントごとに使い分ける。
  • 大規模なシーンや多彩なコントロールが必要な場合は、複数 Canvas で分けるか、カメラの管理を工夫する。

4. まとめ・使いどころ

  • <View> は「単一 Canvas に複数のミニ 3D シーン」を配置 する便利な手段。
  • パフォーマンス: Canvas が 1つだけなので、複数の WebGL コンテキストを生成するよりも軽量。
  • 構造: 各ビューポートが HTML 上の異なる要素と同期され、スクロールやリサイズで適切に位置が変化。
  • 注意:
    • 一部のケーズで微妙なズレやイベント問題が生じる → eventSourcez-index を調整
    • カメラやコントロールが共通になるため、カメラの独立性 を確保したい場合は工夫が必要
はる@フルスタックチャンネルはる@フルスタックチャンネル

カメラコントロール

React Three Fiber でカメラを自由にコントロール・トランジションする方法を、@react-three/drei<CameraControls> コンポーネントを中心にまとめます。<CameraControls> は、camera-controls という強力なライブラリをラップしたものです。これにより、カメラの位置・向きの移動を簡単にアニメーションでき、ユーザーインタラクション用の API も豊富に揃っています。

1. セットアップと基本使い方

import { Canvas } from "@react-three/fiber"
import { CameraControls } from "@react-three/drei"
import { useRef } from "react"

function Experience() {
  // controlsへの参照 (メソッドを呼び出すのに使用)
  const controls = useRef(null)

  return (
    <Canvas>
      {/* カメラ制御コンポーネント */}
      <CameraControls ref={controls} />

      {/* 以下に3Dオブジェクト、ライトなど */}
      <mesh>
        <boxGeometry />
        <meshStandardMaterial color="orange" />
      </mesh>
    </Canvas>
  )
}
  1. ref={controls} で、カメラコントロール にアクセスできます。たとえば controls.current で各種メソッド (dolly, truck, rotate など) を呼べます。
  2. 初期カメラ位置 (Canvascamera={{...}}) もしくは <CameraControls> 自体にパラメータを指定して、最初の位置を決められます。

2. カメラをプログラム制御する主なメソッド

2.1 Dolly (前後移動)

controls.current.dolly(distance, enableTransition)
  • distance: 正数で前進、負数で後退 (デフォルト単位は 1)
  • enableTransition: true ならアニメーション付き、false なら即座に移動
  • 例: controls.current.dolly(1, true) → カメラを 1m 手前に近づける
// Leva を使った例
useControls("dolly", {
  in: button(() => controls.current.dolly(1, true)),   // 前進
  out: button(() => controls.current.dolly(-1, true)), // 後退
})

2.2 Truck (左右・上下移動)

controls.current.truck(x, y, enableTransition)
  • x: 水平の移動量
  • y: 垂直の移動量
  • 例: controls.current.truck(0.5, 0, true) → カメラを右へ 0.5 移動
useControls("truck", {
  up:    button(() => controls.current.truck(0, -0.5, true)),
  down:  button(() => controls.current.truck(0, 0.5, true)),
  left:  button(() => controls.current.truck(-0.5, 0, true)),
  right: button(() => controls.current.truck(0.5, 0, true)),
})

2.3 Rotate (中心を基準に回転)

controls.current.rotate(azimuthAngle, polarAngle, enableTransition)
  • azimuthAngle: 水平方向 (Y 軸回り) の回転角度 (ラジアン)
  • polarAngle: 垂直方向 (X 軸まわり) の回転角度 (ラジアン)
  • 例: controls.current.rotate(0.5, -0.3, true) → 水平に 0.5 rad、垂直に -0.3 rad 回転しながらカメラをアニメーション移動

3. トランジションの設定

3.1 smoothTime プロパティ

controls.current.smoothTime = 0.5
  • カメラ移動の「アニメーション時間」を指定するパラメータ。
  • 大きいほどゆっくり、値を小さくすると急激な移動に。
  • enableTransitiontrue にしたときの速度が影響を受ける。

3.2 イントロアニメーション (例)

import { useEffect, useRef } from "react"
import { CameraControls } from "@react-three/drei"
import { degToRad } from "three/src/math/MathUtils"

function Experience() {
  const controls = useRef(null)

  useEffect(() => {
    const intro = async () => {
      // カメラを (0,0,5) から (0,0,0) を見る位置に (アニメなし)
      controls.current.setLookAt(0, 0, 5, 0, 0, 0, false)

      // Dolly でカメラを近づける (アニメ付き)
      await controls.current.dolly(3, true)
      // 45度, 25度だけ回転
      await controls.current.rotate(degToRad(45), degToRad(25), true)

      // ここでイントロ終了
    }
    intro()
  }, [])

  return <CameraControls ref={controls} />
}
  • setLookAt(px, py, pz, tx, ty, tz, enableTransition): カメラ位置を (px,py,pz) にし、ターゲットを (tx,ty,tz) に向ける
  • await で順番に実行 → スムーズな 3ステップアニメーションを構築できる。

4. シーンに合わせる (fitToBox / fitToSphere)

  • 3D オブジェクトをカメラがちょうど収まるように自動位置合わせする便利メソッド:
    • fitToBox(object3d, enableTransition, options?)
    • fitToSphere(object3d, enableTransition, options?)

例: フィットするバウンディングボックス・スフィア

const boxRef = useRef()
const sphereRef = useRef()

useControls("fit", {
  fitToBox: button(() => {
    controls.current.fitToBox(boxRef.current, true)
  }),
  fitToSphere: button(() => {
    controls.current.fitToSphere(sphereRef.current, true)
  }),
})

// Scene:
<mesh ref={boxRef}>
  <boxGeometry args={[0.5, 1, 0.2]} />
  <meshBasicMaterial wireframe />
</mesh>

<mesh ref={sphereRef}>
  <sphereGeometry args={[0.3, 64, 64]} />
  <meshBasicMaterial wireframe />
</mesh>
  • fitToBoxオブジェクトのバウンディングボックス を全部収まるようにカメラ位置・ターゲットを自動調整。
  • fitToSphereオブジェクトのバウンディングスフィア を基準に。
  • シンプルにオブジェクト全体を映したい時に便利。

5. JSON シリアライズ / ロード

// カメラの状態を取得
const jsonState = controls.current.toJSON()
console.log(jsonState)

// カメラを再現
controls.current.fromJSON(jsonState, true)
  • toJSON() でカメラコントロールの内部状態 (position, target, minDistance 等) を取得し、設定の保存/読み込みを行える。
  • fromJSON(jsonObjectOrString, enableTransition) で復元。

6. マウス・タッチ操作の無効化

<CameraControls
  ref={controls}
  // すべてのマウスボタンを無効化
  mouseButtons={{ left: 0, middle: 0, right: 0, wheel: 0 }}
  // すべてのタッチジェスチャを無効化
  touches={{ one: 0, two: 0, three: 0 }}
/>
  • mouseButtons および touches プロパティを 0 (None) にすることで、ユーザーによるドラッグやホイールズームを禁止し、プログラム制御だけにすることが可能。

7. 応用: 複雑なトランジション

1. カメラ位置+ターゲット座標を事前に定義 (セクションごとに視点を切り替えるなど):

const cameraPositions = {
  home:    [x1, y1, z1,  tx1, ty1, tz1],
  details: [x2, y2, z2,  tx2, ty2, tz2],
  // ...
}

// カメラを切り替える関数
function goTo(sectionKey) {
  controls.current.setLookAt(...cameraPositions[sectionKey], true)
}

2. getPosition() / getTarget() で現在座標を取得 し、調整が容易。たとえば、デベロッパーツールに出力した値を cameraPositions にコピペしておけば、簡単に再利用できる。


まとめ

  • <CameraControls> は、OrbitControls を拡張したようなもの。プログラムでカメラを移動/回転/ズームさせる API が充実。
  • 主なメソッド: dolly(), truck(), rotate(), setLookAt(), fitToBox(), fitToSphere(), toJSON(), fromJSON().
  • smoothTime / enableTransition でアニメーション速度を制御し、自然なカメラ移動が作れる。
  • マウス・タッチ操作 を無効化したり、逆に有効化したり自由。
  • 複雑なトランジションawait で順番にメソッドを呼べばシーケンス的に実行可能。
はる@フルスタックチャンネルはる@フルスタックチャンネル

シェーダー

React Three Fiber 環境で GLSL シェーダー を扱う際の基本的な知識と、実際にカスタムシェーダーを使うまでの流れをまとめています。シェーダーの世界は奥が深く、慣れるまで時間がかかるかもしれませんが、一歩ずつ理解を深めていきましょう。

1. シェーダー (Shaders) とは

GPU(グラフィックス処理装置) で実行される小さなプログラムで、C に似た言語の GLSL (OpenGL Shading Language) を用いて書かれます。

  • 頂点シェーダー (Vertex Shader): 頂点を 3D 空間から 2D スクリーンへ変換(投影)し、頂点ごとの位置や値を計算
  • フラグメントシェーダー (Fragment Shader): 各ピクセル(フラグメント)の色を計算し最終出力する

three.js の MeshStandardMaterialMeshBasicMaterial なども、内部的にはこうしたシェーダーコードを使っていますが、ユーザーが直接 GLSL コードを書く必要はありません。しかし、より高度なエフェクト を実装するにはカスタムシェーダーが必須になります。


2. React Three Fiber でのカスタムシェーダー

2.1 shaderMaterial (drei)

@react-three/drei が提供する shaderMaterial() を使えば、頂点/フラグメントシェーダー のコードをまとめて手軽に custom material を作成できます。

import { shaderMaterial } from "@react-three/drei"
import { extend } from "@react-three/fiber"
import { Color } from "three"

const MyShaderMaterial = shaderMaterial(
  // 1) uniforms (ここで shader に渡す変数を定義)
  {
    uColor: new Color("pink"), // 例: カラー用のuniform
  },
  // 2) vertexShader
  /* glsl */ `
    void main() {
      // 頂点の位置をスクリーン空間へ変換
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  // 3) fragmentShader
  /* glsl */ `
    uniform vec3 uColor; // さきほどのuniformと対応

    void main() {
      // ピクセルの色を uColor に
      gl_FragColor = vec4(uColor, 1.0);
    }
  `
)

// R3F で <myShaderMaterial> を使えるようにする
extend({ MyShaderMaterial })

ポイント:

  • 第一引数: uniforms のオブジェクト。JavaScript から値を更新したいものを入れます。
  • 第二,第三引数: それぞれ頂点/フラグメントシェーダの GLSL コード。

2.2 <myShaderMaterial> の使用

function ShaderPlane() {
  return (
    <mesh>
      <planeGeometry args={[1, 1]} />
      {/* ↓ カスタムmaterialを適用 */}
      <myShaderMaterial uColor="lightblue" />
    </mesh>
  )
}
  • uColor="lightblue" のようにプロップスとして渡せば、shaderMaterial 内で定義した uColor が更新されます。
  • これだけで、シンプルなフラグメントシェーダーを使ったオリジナルのマテリアルが完成します。

3. 頂点シェーダー & フラグメントシェーダーの基本

3.1 頂点シェーダー (vertexShader)

void main() {
  // 頂点シェーダで最終的に gl_Position に頂点のスクリーン座標を代入する
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
  • position: 自動的に提供される頂点の位置 (attribute)
  • modelViewMatrix, projectionMatrix: 3D シーン内での変換行列を含むuniform
  • gl_Position: シェーダが計算した「最終頂点位置」を GPU に渡すビルトイン変数

3.2 フラグメントシェーダー (fragmentShader)

void main() {
  // 最終ピクセルの色を gl_FragColor に設定
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
  • gl_FragColor というビルトイン変数に RGBA 値を代入すると、そのピクセルの色が決まる。

4. Uniforms と Attributes

4.1 Uniform (全頂点・全フラグメントに一定)

  • JavaScript (CPU 側) からシェーダー (GPU 側) に一貫した値として渡されます。
  • 例: 時間 (uTime), 敵数, ライトの色, カメラ位置、など全フレーム or 全頂点で共通の値。
// JavaScript 側
const MyShaderMaterial = shaderMaterial(
  {
    uTime: 0,
  },
  ` ...vertex shader... `,
  ` 
    uniform float uTime;
    void main() { gl_FragColor = vec4(uTime, 0., 0., 1.); }
  `
)

// 画面の経過時間を更新
materialRef.current.uTime = clock.getElapsedTime()

4.2 Attribute (頂点ごとに異なる)

  • 各頂点に固有の値を持たせる場合に使います。position, normal, uv はすでに three.js が自動用意。
  • カスタム属性を使いたい場合、bufferAttribute で geometry に定義しておく → 頂点シェーダーに attribute float aSpeed; のように記述 → 頂点ごとに違う挙動を与えたりできる。
<planeGeometry args={[1,1, rows, cols]}>
  <bufferAttribute
    attach="attributes-aSpeed"
    itemSize={1}
    array={new Float32Array(numVertices).map(() => Math.random() * 5)}
  />
</planeGeometry>

5. Varying (頂点 → フラグメント)

頂点シェーダーから計算結果をフラグメントシェーダーに渡す際は varying を使います。GPU が自動的に補間してくれます。

頂点シェーダー:

attribute float aSpeed;
varying float vSpeed;

void main() {
  vSpeed = aSpeed;
  ...
}

フラグメントシェーダー:

varying float vSpeed;

void main() {
  // aSpeed が頂点からフラグメントへ補間される
  gl_FragColor = vec4(vSpeed, 0.0, 0.0, 1.0);
}

6. シェーダーコード管理

6.1 インライン (JavaScript 文字列内に GLSL 書く)

const MyShaderMaterial = shaderMaterial(
  {},
  /* glsl */ `
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }`,
  /* glsl */ `
  void main() {
    gl_FragColor = vec4(1., 0., 0., 1.);
  }
  `
)
  • シンプルだがコード量が増えると読みにくい。
  • VSCode などのエディタで glsl 用のシンタックスハイライト拡張を使うと便利。

6.2 外部 .glsl ファイルをインポート

  • vite-plugin-glsl 等を使い、.vertex.glsl.fragment.glsl ファイルから読み込む。
  • shaderMaterial({}, vertexShaderCode, fragmentShaderCode) に渡す。

7. デバッグ

  • シェーダー内では console.log が使えません。
  • コンパイルエラー はブラウザのコンソールに出るが、英語かつ行番号が分かりにくいこともある。
  • gl_FragColor を使って変数を可視化する (例: ある変数を色に反映し、予期通りの値か確認)。
void main() {
  float debugVal = something;
  // debugVal を0-1に正規化して色に対応させる
  gl_FragColor = vec4(debugVal, 0., 0., 1.);
}

8. 応用例: 時間を使った頂点アニメーション

// Vertex Shader
uniform float uTime;
void main() {
  vec3 pos = position;
  // 例えば Sin カーブで上下移動
  pos.y += sin(uTime + pos.x*3.0)*0.2;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// JavaScript 側で uTime を更新
useFrame(({clock}) => {
  materialRef.current.uTime = clock.getElapsedTime()
})

これだけで、メッシュが揺れるようなアニメーションを実現可能。
想像力次第 で波動やブロブ、炎っぽい動きなど様々な表現が作れます。


まとめ

  • Vertex Shader: 頂点をスクリーン空間に投影。position やカスタム attribute を操作し、gl_Position を設定。
  • Fragment Shader: ピクセルの色を決定。gl_FragColor を最終的に設定。
  • Uniform: JavaScript 側からシェーダに一貫した値を渡す。時間や色、カメラ情報など。
  • Attribute: 各頂点ごとに異なる値 (例: 頂点カラー、速度) を GPU に渡す。
  • Varying: 頂点シェーダ → フラグメントシェーダ間で補間される値。
はる@フルスタックチャンネルはる@フルスタックチャンネル

シェーピング関数

GLSL シェーダー内で使われる「シェーピング関数 (Shaping Functions)」を中心にまとめています。フラグメントシェーダーでの応用例を通じて、どのようにピクセルの色(または頂点位置など)を制御するかを学びましょう。後から見直してもわかりやすいように、サンプルコードポイント解説を交えて解説します。

1. シェーピング関数とは?

シェーダー(特にフラグメントシェーダー)では、「座標 (UV など) → 出力色」という処理を行いますが、
線や図形、グラデーションなどのパターンをどうやって生成するかがポイントになります。
シェーピング関数とは、こうしたパターンを形成するために便利な数学関数の総称で、以下のように使われます。

  1. step(edge, x): しきい値 edge よりも x が小さい場合 0、大きい場合 1
  2. mix(x, y, a): 値 xy を “a の比率” で線形補間
  3. smoothstep(edge0, edge1, x): x を [edge0, edge1] の間だけ滑らかに 0→1 へ補間
  4. mod(x, y): 剰余演算(周期パターンを作るのに便利)
  5. min, max: 値を挟み込んだり、上下限を指定するのに使う
  6. sin, cos, fract, abs, floor, ceil ... GLSL には各種数学関数が多数
// 例: mix関数で0~1の範囲のxに応じて白→青のグラデーション
void main() {
  vec3 whiteColor = vec3(1.0);
  vec3 blueColor  = vec3(0.0, 0.0, 1.0);
  float pct = vUv.x;  // vUv.xは[0,1]の範囲
  vec3 finalColor = mix(whiteColor, blueColor, pct);
  gl_FragColor = vec4(finalColor, 1.0);
}

2. サンプル環境(React Three Fiber + GLSL シェーダー)

  1. @react-three/dreishaderMaterial を使い、独自の頂点シェーダ・フラグメントシェーダを定義
  2. <mesh><planeGeometry> を張り、このカスタムシェーダマテリアルを適用

シンプルなカスタムシェーダの例

import { shaderMaterial } from "@react-three/drei"
import { extend, useFrame } from "@react-three/fiber"
import { Color } from "three"
import { useRef } from "react"

// 1) シェーダMaterial定義
const MyShaderMaterial = shaderMaterial(
  // uniforms
  {
    uColor: new Color("pink"),
    uTime: 0,
  },
  // vertexShader
  /* glsl */`
  varying vec2 vUv;
  void main() {
    vUv = uv;  // フラグメントシェーダにUVを渡す
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
  `,
  // fragmentShader
  /* glsl */`
  uniform vec3 uColor;
  uniform float uTime;
  varying vec2 vUv;

  void main() {
    // (ここで shaping関数を駆使して色を計算)
    gl_FragColor = vec4(uColor, 1.0);
  }
  `
)

// R3F で <myShaderMaterial> を使えるように
extend({ MyShaderMaterial })

export function ShaderPlane() {
  const materialRef = useRef()

  // time を更新
  useFrame(({ clock }) => {
    materialRef.current.uTime = clock.getElapsedTime()
  })

  return (
    <mesh>
      {/* 横 1, 縦 1 の平面 */}
      <planeGeometry args={[1,1]} />
      {/* カスタムシェーダ */}
      <myShaderMaterial ref={materialRef} uColor="lightblue" />
    </mesh>
  )
}

3. step(edge, x)

step(edge, x) は、x < edge → 0、x >= edge → 1 を返す非常にシンプルな関数です。

void main() {
  // 0.5 をしきい値として、vUv.x が小さい部分を黒、 >= 0.5 の部分をピンク
  float pct = step(0.5, vUv.x);
  vec3 finalColor = vec3(1.0, 0.0, 0.5) * pct; // ピンク
  gl_FragColor = vec4(finalColor, 1.0);
}
  • 画面の左半分(x<0.5)→黒(0)
  • 右半分(x>=0.5)→ピンク(1)

4. mix(x, y, a)

mix(x, y, a) は、x*(1-a) + y*a を計算する線形補間関数。
これを使って、値の間をグラデーションや補間できます。

void main() {
  vec3 whiteColor = vec3(1.0);
  vec3 blueColor  = vec3(0.0, 0.0, 1.0);

  // vUv.xに応じて [0,1] の範囲で白→青
  vec3 finalColor = mix(whiteColor, blueColor, vUv.x);
  gl_FragColor = vec4(finalColor, 1.0);
}

5. smoothstep(edge0, edge1, x)

step の「境界」が急激すぎる場合に、滑らかな移行を作るには smoothstep を使います。

  • x < edge0 → 0
  • x > edge1 → 1
  • その中間 → 0 から 1 へ滑らかに補間
void main() {
  float pct = smoothstep(0.3, 0.7, vUv.y); // 0.3~0.7の間で0→1
  vec3 finalColor = mix(vec3(1.0), vec3(0.0,1.0,0.0), pct);
  gl_FragColor = vec4(finalColor, 1.0);
}

→ 下部は白、上部は緑、0.3~0.7あたりで徐々に色が補間される。


6. mod(x, y) (周期パターン)

void main() {
  // vUv.x を 5回繰り返し, その部分(>0.5)を色つきに
  float stripe = step(0.5, mod(vUv.x * 5.0, 1.0));
  vec3 finalColor = vec3(1.,0.,0.) * stripe;
  gl_FragColor = vec4(finalColor, 1.0);
}
  • mod(x, 1.0)fract(x) と同じで、小数部分を返すため、[0,1) の繰り返しパターンを作れる
  • これを応用してストライプ模様やチェッカーボードを作成可能

7. min / max / clamp

  • min(a,b) → a と b の小さい方
  • max(a,b) → a と b の大きい方
  • これで数値を上下限に挟む場合は clamp(x, lower, upper) 相当を実装できる
float val = someCalculation();
val = max(val, 0.2); // 0.2未満にならない
val = min(val, 0.8); // 0.8を超えない

// まとめて clamp(val, 0.2, 0.8) として書ける

8. 実践例:パターン合成

縦ストライプ横ストライプ を掛け合わせて、チェッカーボードにする例:

void main() {
  // 繰り返し: fract() は mod(x,1) と同じ
  vec2 repeated = fract(vUv * 8.0);

  float stripeX = step(0.5, repeated.x); // 横方向ストライプ
  float stripeY = step(0.5, repeated.y); // 縦方向ストライプ

  // 交差部で 2.0 にならないよう min で抑える
  float combined = min(stripeX + stripeY, 1.0);

  vec3 finalColor = mix(vec3(1.), vec3(1.,0.,0.), combined);
  gl_FragColor = vec4(finalColor, 1.0);
}

9. sin, cos, time を使ってアニメーション

uniform float uTime;

void main() {
  // UV座標を中央基準 [-1,1] に変換
  vec2 uvCentered = (vUv - 0.5)*2.0;

  // 時間と組み合わせて、波形アニメーション
  float dist = length(uvCentered); 
  float wave = sin(dist * 10.0 - uTime*2.0);

  float pct = smoothstep(0.0, 0.1, wave); // しきい値で形状を切り出す
  vec3 col = mix(vec3(0.), vec3(1.,0.,1.), pct);
  gl_FragColor = vec4(col, 1.0);
}
  • uTime を使って sin(...)cos(...) でアニメーション
  • 複雑なパターンを混ぜ合わせるとサイケデリックなエフェクトも作れる

10. Signed Distance Functions (SDF)

SDF は、点(UV座標など)から特定の形状までの「符号付き距離」を返す関数。

  • 0より小さい → 形状の内部
  • 0より大きい → 外部
  • ちょうど0 → 形状の境界

たとえば丸み角のボックス六芒星などを簡単に表現でき、数値の大小で図形の内外を判定したり、smoothstep などと組み合わせて「柔らかい境界」を作成。

// 例: sdHexagram, sdRoundedBox など Inigo Quilez のSDF例が多数
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
  // ... SDF ロジック ...
}

// 中心に小さなrounded boxを描く
void main() {
  vec2 uv = (vUv - 0.5)*2.;
  float d = sdRoundedBox(uv, vec2(0.2), vec4(0.05));
  float pct = step(d, 0.0); // 内部なら1.0
  gl_FragColor = vec4(vec3(pct), 1.0);
}

まとめ

  • step, smoothstep, mix, mod, min/max などを組み合わせると、線やブロック、ストライプ等の形状を作成・合成可能。
  • sin / cos / time で動きを加えれば、アニメーション表現が生まれる。
  • SDF (Signed Distance Functions) を使うと、複雑な図形を簡単に表現できる。
  • シェーダーはデバッグや実験に時間が必要。GraphtoyShaderToy を活用して数式を可視化し、練習するとよい。
はる@フルスタックチャンネルはる@フルスタックチャンネル

画像シェーダー

画像をシェーダーで表示しつつ、トランジションや歪みエフェクトなどを加えるイメージスライダー」を例に、GLSL シェーダーを使ったテクスチャの扱いを解説します。
React Three Fiber@react-three/drei を活用し、複数枚の画像を滑らかに切り替えるシェーダーを作る流れを確認しましょう。

1. 準備

1.1 テクスチャ画像の用意

  • public/textures/ フォルダーなどに画像を配置します。
  • AI やフリー素材などを用意し、可能なら圧縮 (Squoosh など) してファイルサイズを抑える。

1.2 State 管理 (optional)

  • 今回は複数の画像を切り替えるために、Zustand やフックなどで curSlide (現在スライド) / prevSlide (前スライド) を管理しておく方法があります。
  • ここでは簡単に curSlide / prevSlide だけ保持している想定とします。
// useSlider.js (Zustandの例)
import { create } from "zustand"

export const useSlider = create((set) => ({
  curSlide: 0,
  // スライドデータ (パス, タイトルなど)
  items: [
    { image: "textures/img1.jpg" },
    { image: "textures/img2.jpg" },
    // ...
  ],
  next: () => set((state) => ({ curSlide: (state.curSlide + 1) % state.items.length })),
  prev: () => set((state) => ({ curSlide: (state.curSlide - 1 + state.items.length) % state.items.length })),
}))

1.3 Canvas & Plane

import { Canvas } from "@react-three/fiber"
import { useSlider } from "./useSlider"
import { ImageSlider } from "./ImageSlider"

function App() {
  return (
    <main style={{ width: "100vw", height: "100vh", position: "relative" }}>
      <Canvas style={{ position: "absolute", width: "100%", height: "100%" }}>
        <ImageSlider />
      </Canvas>
      {/* ここにHTMLのUI (タイトル, ボタンなど) */}
    </main>
  )
}

export default App

2. シェーダーマテリアルを定義する

2.1 shaderMaterialextend

@react-three/dreishaderMaterial を使って、頂点/フラグメントシェーダーコードと uniforms を定義した ImageSliderMaterial を作成します。

// ImageSlider.jsx
import { shaderMaterial, useTexture } from "@react-three/drei"
import { extend, useFrame, useThree } from "@react-three/fiber"
import { useRef, useEffect, useState } from "react"
import { Color, MathUtils } from "three"
import { useSlider } from "./useSlider"

// カスタムshaderMaterial (GLSL)
const ImageSliderMaterial = shaderMaterial(
  {
    // uniform
    uTexture: null,         // 現在の画像
    uPrevTexture: null,     // 前の画像
    uProgress: 1.0,         // 画像切り替え時の進捗 (0→1)
    uResolution: [1,1],
  },
  // vertexShader
  /* glsl */`
  varying vec2 vUv;

  void main() {
    vUv = uv; // フラグメントシェーダにUVを渡す
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
  `,
  // fragmentShader
  /* glsl */`
  varying vec2 vUv;
  uniform sampler2D uTexture;
  uniform sampler2D uPrevTexture;
  uniform float uProgress;

  // optional: three.js の色処理を再利用
  #include <common> 
  #include <dithering_pars_fragment>
  #include <tonemapping_pars_fragment>
  #include <encodings_pars_fragment>

  void main() {
    // 現在テクスチャと前テクスチャを補間
    vec4 curColor  = texture2D(uTexture, vUv);
    vec4 prevColor = texture2D(uPrevTexture, vUv);

    // 0→1 の progress でトランジション
    vec4 finalColor = mix(prevColor, curColor, uProgress);

    gl_FragColor = finalColor;

    // three.js のトーンマッピング, エンコード
    #include <tonemapping_fragment>
    #include <encodings_fragment>
  }
  `
)

// R3Fで <imageSliderMaterial /> を使えるように
extend({ ImageSliderMaterial })

export function ImageSlider() {
  const { curSlide, items } = useSlider()
  
  // texture
  const currentImg = items[curSlide].image
  // useStateで前の画像を保持
  const [lastImg, setLastImg] = useState(currentImg)

  // テクスチャ読込
  const tex = useTexture(currentImg)
  const prevTex = useTexture(lastImg)

  // 画像変化時に lastImg を更新
  useEffect(() => {
    return () => {
      setLastImg(currentImg)
    }
  }, [currentImg])

  // Materialへの参照
  const matRef = useRef()

  // トランジション進捗
  useEffect(() => {
    // 新画像が来たらprogressをリセット
    if (matRef.current) {
      matRef.current.uProgress = 0.0
    }
  }, [currentImg])

  // フレームごとにprogressを0→1へ補間
  useFrame(() => {
    if (!matRef.current) return
    matRef.current.uProgress = MathUtils.lerp(matRef.current.uProgress, 1.0, 0.08)
  })

  // レスポンシブ対応 (planeの大きさ計算)
  const { width, height } = usePlaneSize(3, 4, 0.75) // 後述のカスタムフック

  return (
    <mesh>
      <planeGeometry args={[width, height, 32, 32]} />
      <imageSliderMaterial
        ref={matRef}
        transparent
        uTexture={tex}
        uPrevTexture={prevTex}
      />
    </mesh>
  )
}

// カスタムフック: 画面サイズに合わせてplaneを調整
function usePlaneSize(origW, origH, fillPct=0.75) {
  const { width: vpW, height: vpH } = useThree((s) => s.viewport)
  
  let ratio = vpH / (origH / fillPct)
  if (vpW < vpH) {
    ratio = vpW / (origW / fillPct)
  }
  return {
    width: origW * ratio,
    height: origH * ratio
  }
}

主要ポイント

  1. uTextureuPrevTexture:
    • 現在の画像と以前の画像のテクスチャをシェーダーに送る
  2. uProgress:
    • トランジションが 0→1 に進むにつれて、mix(前画像, 現画像, uProgress) で補間
  3. レスポンシブ計算:
    • 画像の縦横比に合わせて平面ジオメトリをスケーリング
  4. progress の更新:
    • useEffect で新画像読み込み時に 0 にリセット → useFrame で徐々に 1 へ

3. 応用: ディスプレイスメントやノイズを掛けたエフェクト

3.1 フラグメントシェーダで座標を歪ませる

// フラグメントシェーダ一例
uniform sampler2D uDisp; // 追加のディスプレイスメントテクスチャ
uniform float uProgress;
varying vec2 vUv;

void main() {
  vec2 dispUv = vUv;
  // たとえば, ディスプレイスメントテクスチャのr成分を読み込み
  float d = texture2D(uDisp, vUv).r; 
  // uProgressに応じて歪み
  dispUv.x += (d - 0.5) * 0.2 * uProgress;

  // 前後のテクスチャ
  vec4 cTex = texture2D(uTexture,  dispUv);
  vec4 pTex = texture2D(uPrevTexture, dispUv);
  gl_FragColor = mix(pTex, cTex, uProgress);

  #include <tonemapping_fragment>
  #include <encodings_fragment>
}
  • uDisp(モノクロテクスチャ) を useTexture で読み込み、 uDisp={dispTex} として渡す
  • dispUv を変形して texture2D(uTexture, dispUv) で取り出すと、波打つトランジション氷が砕けるようなエフェクトなどを作れる

4. マウスオーバーで歪みを加える

頂点シェーダーで plane の z 座標を操作し、マウスオーバー時に盛り上がるような演出:

// vertexShader
uniform vec2 uMouse; // -1~1のマウス座標
void main() {
  vUv = uv;
  vec2 centerUv = (uv - 0.5)*2.0; // [-1,1]範囲
  float dist = length(centerUv - uMouse);
  float push = 1.0 - smoothstep(0.0, 1.2, dist);
  // Zに反映
  vec3 newPos = position;
  newPos.z += push * 0.3;
  
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos,1.);
}

R3F 側で onPointerMove or mouse 情報を受け取り、uMouse を更新すれば、マウス近辺が盛り上がるエフェクトを作れる。


5. まとめ

  1. テクスチャをシェーダーで扱うには:
    • uniform sampler2D uTexture で受け取り、texture2D(uTexture, uv) で色を取得
  2. 前の画像とのトランジション:
    • mix(prevColor, curColor, progress) でエフェクト
  3. 歪み・ノイズ:
    • 頂点シェーダーで頂点位置を変形
    • フラグメントシェーダーで UV 座標を変形
    • sin, cos, noise, displacement texture などを組み合わせると多彩な演出
  4. レスポンシブ対応:
    • 3D ジオメトリのサイズを viewport.width / viewport.height に応じてスケーリング
  5. ホバーエフェクト:
    • R3F の onPointerEnter, onPointerLeave とシェーダーの uniform を連動させる
はる@フルスタックチャンネルはる@フルスタックチャンネル

水面シェーダー

React Three Fiber水面のシェーダー を実装してプールの水面のようなエフェクトを作る方法をまとめています。
最終的には、泡のある水面オブジェクトとの接触部分で泡が濃くなるなどの表現を行うために、Lygia シェーダーライブラリレンダーターゲットなどのテクニックを使います。

1. 概要とスタータープロジェクト

構成イメージ

  1. プールのモデルアヒルモデル があるシーン
  2. 水面 は単なる <planeGeometry> だが、カスタム水シェーダーマテリアル を適用
  3. Lygia シェーダーライブラリ でノイズや生成関数 (pnoiseなど) を利用して水面の泡や動きを演出
  4. レンダーターゲット (FBO) + depthMaterial でシーンの深度を取得し、水と他オブジェクトの接触部分に泡が多くなる効果を実装

以下のファイル構成イメージ:

src/
  - main.jsx                   # エントリーポイント
  - App.jsx                    # メインApp
  - Water.jsx                  # 水のplane
  - WaterMaterial.jsx          # 水用のシェーダーマテリアル
  - components/PoolModel.jsx   # プールモデル
  - components/DuckModel.jsx   # アヒルモデル
  - ...
public/
  - models/
    - pool.gltf
    - duck.gltf
  - textures/
  - ...

2. カスタム水シェーダーマテリアル

2.1 初期ボイラープレート

// WaterMaterial.jsx
import { shaderMaterial } from "@react-three/drei"
import { resolveLygia } from "resolve-lygia"  // Lygiaライブラリを使うため
import { Color } from "three"

// WaterMaterial: 頂点+フラグメントシェーダーの定義
export const WaterMaterial = shaderMaterial(
  {
    // uniforms
    uColor: new Color("skyblue"),
    uOpacity: 0.8,
    uTime: 0,        // アニメーション用
    uSpeed: 0.5,     // 泡の動き速度
    uRepeat: 20.0,   // ノイズの繰り返し
    uFoam: 0.4,      // 泡の開始閾値
    uFoamTop: 0.7,   // 泡が強くなる閾値

    // Depth-related (レンダーターゲット)
    uDepth: null,
    uMaxDepth: 1.5,
    uResolution: [0,0],
    uCameraNear: 0,
    uCameraFar: 0,
  },
  /* glsl */`
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  `,
  // fragmentShader: Lygia の pnoiseを使うために resolveLygia() で包む
  resolveLygia(/* glsl */`

    // Lygiaのpnoiseなどの関数を使う
    #include "lygia/generative/pnoise.glsl"

    // Depth関連
    #include <packing>  // three.jsのpack/unpack utils

    varying vec2 vUv;
    uniform vec3 uColor;
    uniform float uOpacity;
    uniform float uTime;
    uniform float uSpeed;
    uniform float uRepeat;
    uniform float uFoam;
    uniform float uFoamTop;

    uniform sampler2D uDepth;     // レンダーターゲットの深度
    uniform float uMaxDepth;
    uniform vec2 uResolution;
    uniform float uCameraNear;
    uniform float uCameraFar;

    // three.js の utils: Depth => 目線空間Zに変換
    float getViewZ(const in float depth) {
      return perspectiveDepthToViewZ(depth, uCameraNear, uCameraFar);
    }
    // Depth texture から 深度を取得
    float getDepth(vec2 coord) {
      return unpackRGBAToDepth(texture2D(uDepth, coord));
    }

    void main() {
      float timeAdjusted = uTime * uSpeed;
      
      // *** 1) ノイズによる泡の生成 ***
      float noise = pnoise(vec3(vUv * uRepeat, timeAdjusted), vec3(100.0,24.0,112.0));
      // 泡を smoothstep で強調
      noise = smoothstep(uFoam, uFoamTop, noise);

      // *** 2) Depth-based foam (オブジェクトやプール壁との接触で泡が濃くなる) ***
      // 画面座標:
      vec2 screenUV = gl_FragCoord.xy / uResolution;
      // フラグメントの線形深度
      float fragmentLinDepth = getViewZ(gl_FragCoord.z);
      // シーン全体を深度レンダーしたテクスチャから深度を取得
      float sceneDepthVal = getDepth(screenUV);
      float sceneLinDepth = getViewZ(sceneDepthVal);

      // 現在フラグメントとの距離 -> (camera to fragment) - (camera to object) = 差分
      float depthDelta = fragmentLinDepth - sceneLinDepth;

      // depthDelta が小さいほどオブジェクトに近い
      // uMaxDepthを越えないようにsmoothstepなど使う
      float foamFromDepth = smoothstep(uMaxDepth, 0.0, depthDelta);

      // 総合バブル値を合成
      float bubbleVal = noise + foamFromDepth;

      // *** 3) カラー ***
      vec3 col = mix(uColor, uColor*2.0, bubbleVal);

      gl_FragColor = vec4(col, uOpacity);

      // three.js トーンマッピング & エンコード
      #include <tonemapping_fragment>
      #include <encodings_fragment>
    }
  `)
)

ポイント:

  • pnoise(...) 関数を Lygia からインクルードし、のようなノイズを生成
  • uDepth (レンダーターゲットの深度テクスチャ) と シーンの Depth を比較し、物体やプールの壁付近で泡が強くなるように
  • uFoam, uFoamTop はノイズで泡をどの程度出すかを調整
  • uMaxDepth で泡の影響範囲をコントロール

2.2 Material を extend して使う

// main.jsx (or a place where you do the extends)
import { extend } from "@react-three/fiber"
import { WaterMaterial } from "./WaterMaterial"

extend({ WaterMaterial })
  • これにより <waterMaterial> として JSX で使えるようになります。

3. 水の <mesh> 実装 (Water.jsx)

// Water.jsx
import { useRef } from "react"
import { useFrame, useThree } from "@react-three/fiber"
import { useControls } from "leva"
import { WaterMaterial } from "./WaterMaterial"
import { MeshDepthMaterial, NoBlending, RGBADepthPacking, FloatType } from "three"
import { useFBO } from "@react-three/drei"

export function Water(props) {
  // マテリアル参照
  const waterRef = useRef()
  const matRef = useRef()

  // Depthを描画するための material
  const depthMaterial = new MeshDepthMaterial()
  depthMaterial.depthPacking = RGBADepthPacking
  depthMaterial.blending = NoBlending

  // レンダーターゲット(FBO)
  const renderTarget = useFBO({
    depth: true,
    type: FloatType,
  })

  // Levaで調整可能なパラメータ
  const { 
    uSpeed, uRepeat, uFoam, uFoamTop, uMaxDepth, waterOpacity
  } = useControls("Water Shader", {
    uSpeed: { value: 0.5, min: 0, max: 5 },
    uRepeat: { value: 20, min: 1, max: 100 },
    uFoam: { value: 0.4, min: 0, max: 1 },
    uFoamTop: { value: 0.7, min: 0, max: 1 },
    uMaxDepth: { value: 1.5, min: 0, max: 5 },
    waterOpacity: { value: 0.8, min: 0, max: 1 },
  })

  // Plane geometry
  // (例: size 15x32 subdiv=22)
  // waterRef で mesh 参照
  // matRef で WaterMaterial 参照
  return (
    <mesh ref={waterRef} rotation-x={-Math.PI/2} {...props}>
      <planeGeometry args={[15, 32, 22, 22]} />
      <waterMaterial
        ref={matRef}
        transparent
        uOpacity={waterOpacity}
        uSpeed={uSpeed}
        uRepeat={uRepeat}
        uFoam={uFoam}
        uFoamTop={uFoamTop}
        uMaxDepth={uMaxDepth}
      />
    </mesh>
  )
}

////////////////////////////////////////////////////////
// useFrame などで depthTexture のレンダリングと uniform 代入
////////////////////////////////////////////////////////
useFrame((state) => {
  const { gl, scene, camera, clock } = state

  // 1) 水の mesh を不可視にして (自分自身がdepthに映りこまないように)
  if (waterRef.current) waterRef.current.visible = false

  // 2) overrideMaterial で 全オブジェクトに depthMaterial を適用 → レンダーターゲットへ
  gl.setRenderTarget(renderTarget)
  scene.overrideMaterial = depthMaterial
  gl.render(scene, camera)

  // 戻す
  scene.overrideMaterial = null
  if (waterRef.current) waterRef.current.visible = true
  gl.setRenderTarget(null)

  // シェーダーの uniform 設定
  if (matRef.current) {
    matRef.current.uTime = clock.getElapsedTime()
    matRef.current.uDepth = renderTarget.texture // Depthテクスチャ
    matRef.current.uCameraNear = camera.near
    matRef.current.uCameraFar  = camera.far

    // 画面サイズ
    const pixelRatio = gl.getPixelRatio()
    matRef.current.uResolution = [
      gl.domElement.width  / pixelRatio,
      gl.domElement.height / pixelRatio,
    ]
  }
})

ポイント解説:

  1. レンダーターゲット (renderTarget) にシーンを depthMaterial で再描画し、深度値をテクスチャに保存
  2. waterRef.current.visible = false; で水自身は深度に含まれないようにする
  3. overrideMaterial = depthMaterial; で全メッシュが一時的に深度マテリアルに置き換えられる
  4. matRef.current.uDepth = renderTarget.texture; で深度テクスチャを水シェーダーに渡す
  5. uCameraNear, uCameraFar, そして gl_FragCoord.z などから計算する線形深度を使い、水面近くにある物体周辺で泡を濃くする

4. 結果: 泡のある水面シェーダー

  • ノイズによる泡の揺らぎ (Lygia の pnoise とか)
  • Depth との比較 でオブジェクト周辺の泡を強調
  • レスポンシブに planeGeometry のサイズを調整すれば、プールのサイズに合わせて水面を配置

最終的な見え方: 水面が波打つような動き&アヒルやプール壁面付近に泡が多く集まる効果などが実現できます。
Leva 等で uFoamuMaxDepth をいじってみると、泡の出現具合や濃さをリアルタイムで調整可能です。

はる@フルスタックチャンネルはる@フルスタックチャンネル

画面遷移シェーダー

画面やモデルのトランジション(遷移)をシェーダーで演出する方法を、React Three FiberGLSLの文脈で解説します。

  • 画面トランジション: 画面全体を覆うような「フェードイン/アウト、スパイラル」のエフェクト
  • モデルトランジション: 既存の Three.js マテリアルに onBeforeCompile などでシェーダーコードを注入し、モデルそのものの溶解やフェードを演出

1. 画面トランジションのシェーダー

1.1 画面トランジションの概要

  • 画面全体を覆う PlaneOrthographic カメラ で映し、シェーダーを使って「何らかの形でアルファ値を計算してピクセルを隠す/表示する」
  • React Three Fiber の <Hud><OrthographicCamera> を使うと、シーンとは独立して常に前面に Plane を描画できる

ざっくり実装フロー

  1. ScreenTransitionMaterial (画面遷移用のシェーダーマテリアル) を定義
  2. <mesh> + <planeGeometry> + <screenTransitionMaterial> を HUD レイヤーに置く
  3. uProgression (0→1) で「画面が表示される or 隠れる度合い」を制御
  4. useFrameuseSpring (Framer Motion) などで uProgression をアニメーション

1.2 ScreenTransitionMaterial

// ScreenTransitionMaterial.js
import { shaderMaterial } from "@react-three/drei"
import { Color } from "three"

// シェーダーマテリアル (フルスクリーンplane用)
export const ScreenTransitionMaterial = shaderMaterial(
  {
    uColor: new Color("#ffffff"),  // トランジションの色
    uProgression: 0,              // 0~1 で進行度
    uResolution: [1,1],           // 画面サイズ(オプション、歪みなどで使用)
  },
  /*glsl*/`
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  `,
  /*glsl*/`
    uniform vec3 uColor;
    uniform float uProgression;
    varying vec2 vUv;
    uniform vec2 uResolution;

    void main() {
      // 基本: alpha = 1 - uProgression で単純なフェード
      float alpha = 1.0 - smoothstep(0.0, 1.0, uProgression);

      // たとえばスパイラルエフェクトを加えたいなら:
      // vec2 centerUV = (vUv - 0.5)*2.;
      // centerUV.x *= uResolution.x / uResolution.y;
      // ... スパイラル演算 ...
      // alpha = min(alpha, spiralAlpha);

      gl_FragColor = vec4(uColor, alpha);
      #include <encodings_fragment>
    }
  `
)

ポイント:

  • uProgression を 0→1 にすれば、色付き Plane が 1→0 のアルファでフェードアウト(or フェードイン)
  • スパイラルやノイズなどのロジックを GLSL 内に埋め込んで、より凝った演出が可能

1.3 ScreenTransition コンポーネント

// ScreenTransition.jsx
import { Hud, OrthographicCamera } from "@react-three/drei"
import { useFrame } from "@react-three/fiber"
import { useRef, useEffect } from "react"
import { ScreenTransitionMaterial } from "./ScreenTransitionMaterial" // 上記で定義
import { MathUtils } from "three"

export function ScreenTransition({ active, color }) {
  // シェーダーマテリアル参照
  const matRef = useRef()

  // progression: 0 -> 1
  const transitionState = useRef({ from: 0, to: 0, startTime: 0 })
  useEffect(() => {
    transitionState.current.from = active ? 0 : 1
    transitionState.current.to   = active ? 1 : 0
    transitionState.current.startTime = performance.now()
  }, [active])

  useFrame(() => {
    if (!matRef.current) return
    const t = MathUtils.clamp(
      (performance.now() - transitionState.current.startTime)/1500,
      0, 1
    )
    const val = MathUtils.lerp(transitionState.current.from, transitionState.current.to, t)
    matRef.current.uProgression = val
  })

  return (
    <Hud>
      <OrthographicCamera makeDefault top={1} right={1} bottom={-1} left={-1} near={0} far={1}/>
      <mesh>
        <planeGeometry args={[2,2]}/>
        <screenTransitionMaterial ref={matRef} uColor={color} transparent />
      </mesh>
    </Hud>
  )
}
  • <Hud><OrthographicCamera> を使い、画面最前面に Plane を描画
  • active プロップが true のときトランジションを開始し、uProgression を 0→1 (または 1→0) で補間
  • シェーダー内で alpha = 1.0 - uProgression みたいにすれば、フェード効果が得られる

2. モデルトランジション (onBeforeCompile)

Three.js には onBeforeCompile で既存マテリアル (例: MeshBasicMaterial) に対してシェーダーコードを注入できる仕組みがあります。
これを利用して、モデルの頂点/フラグメントシェーダーを改変し、フェードやディゾルブ等の効果を追加します。


2.1 モデルの出入りをフェード (基本)

ざっくり

  1. 既存の model の material を参照し、 material.transparent = true
  2. material.onBeforeCompile = (shader) => { ... }shader.fragmentShader にカスタムコード (uniform定義, diffuseColor.a *= など) を挿入
  3. uniforms.myProgress などを追加し、 useFrame で 0→1 を補間する
// ModelTransition.jsx
import { useRef, useEffect } from "react"
import { useFrame } from "@react-three/fiber"
import { useGLTF } from "@react-three/drei"
import { MathUtils } from "three"

export function ModelTransition({ url, visible }) {
  const { scene, materials } = useGLTF(url)
  const stateRef = useRef({ from: 0, to: 1, time: 0 })
  
  useEffect(() => {
    // fade in / out
    stateRef.current.from = visible ? 0 : 1
    stateRef.current.to   = visible ? 1 : 0
    stateRef.current.time = performance.now()

    Object.values(materials).forEach((mat) => {
      mat.transparent = true
      mat.onBeforeCompile = (shader) => {
        // 1) uniform追加
        shader.uniforms.myProgress = { value: visible ? 0 : 1 }
        // 2) fragmentShaderに uniform定義
        shader.fragmentShader = shader.fragmentShader.replace(
          `void main() {`,
          `uniform float myProgress;
           void main(){`
        )
        // 3) アルファ操作を inject
        shader.fragmentShader = shader.fragmentShader.replace(
          `#include <alphatest_fragment>`,
          `#include <alphatest_fragment>
           diffuseColor.a *= myProgress;`
        )
        // 4) material.userData.shader に保存
        mat.userData.shader = shader
      }
    })
  }, [visible])

  useFrame(() => {
    // 進捗を補間
    const t = MathUtils.clamp((performance.now()-stateRef.current.time)/1500, 0,1)
    const val = MathUtils.lerp(stateRef.current.from, stateRef.current.to, t)
    // uniformsに適用
    Object.values(materials).forEach((mat) => {
      if (mat.userData.shader) {
        mat.userData.shader.uniforms.myProgress.value = val
      }
    })
  })

  return <primitive object={scene} />
}

ポイント:

  • onBeforeCompileshader.uniformsmyProgress を追加
  • diffuseColor.a *= myProgress; でアルファ値を乗算 → 0 に近いと透明化
  • どの場所に置くかは #include <alphatest_fragment> 等のフックで注入

2.2 ディゾルブ (頂点 or ピクセル)

フラグメント側のみ (ノイズでアルファカット)

  1. ノイズを使ってある閾値以下のピクセルをアルファ 0 にする
  2. onBeforeCompile でノイズ関数や uniform float myThreshold; を注入し、 if(noiseVal < myThreshold) discard; など
  3. myThreshold を 0→1 にすれば、徐々に全体が散っていく演出
// 例: ディゾルブ断片のフラグメント注入コード
uniform float myProgress;
float noiseVal = myNoise(vec3(vUv*10.0, time));
if (noiseVal < myProgress) {
  discard;
}

頂点も混ぜて上から下へ

  • 頂点シェーダーに varying vWorldPos; (onBeforeCompile の vertexShader)
  • フラグメントで if(vWorldPos.y < threshold) discard; or diffuseColor.a = ...

2.3 サンプル: Model の onBeforeCompile

// ModelDissolve.jsx
import { useFrame } from "@react-three/fiber"
import { useGLTF } from "@react-three/drei"
import { useRef, useEffect } from "react"
import { MathUtils } from "three"

const DECLARATIONS = /* glsl */`
  uniform float dissolveProgress;
  float rand(vec2 co){
    return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
  }
`;

const DISSOLVE_FRAGMENT = /* glsl */`
  float noiseVal = rand(gl_FragCoord.xy*0.04);
  if (noiseVal < dissolveProgress) {
    discard; 
  }
`;

export function ModelDissolve({ url, dissolveIn }) {
  const { scene, materials } = useGLTF(url)
  const stateRef = useRef({ from: 0, to: 0, startTime: 0 })

  // fade in/out
  useEffect(() => {
    stateRef.current.from = dissolveIn ? 1 : 0
    stateRef.current.to   = dissolveIn ? 0 : 1
    stateRef.current.startTime = performance.now()
    Object.values(materials).forEach(mat => {
      mat.transparent = true
      mat.onBeforeCompile = (shader) => {
        shader.uniforms.dissolveProgress = { value: 0 }
        // 1) uniform宣言
        shader.fragmentShader = shader.fragmentShader.replace(
          `void main() {`,
          DECLARATIONS + `\nvoid main(){`
        )
        // 2) alphatest_fragment のあと
        shader.fragmentShader = shader.fragmentShader.replace(
          `#include <alphatest_fragment>`,
          `#include <alphatest_fragment>\n${DISSOLVE_FRAGMENT}`
        )
        mat.userData.shader = shader
      }
    })
  }, [dissolveIn, materials])

  // dissolveProgress補間
  useFrame(() => {
    const elapsed = performance.now()-stateRef.current.startTime
    const t = MathUtils.clamp(elapsed/2000, 0,1)
    const val = MathUtils.lerp(stateRef.current.from, stateRef.current.to, t)
    Object.values(materials).forEach(mat => {
      if (mat.userData.shader) {
        mat.userData.shader.uniforms.dissolveProgress.value = val
      }
    })
  })

  return <primitive object={scene} />
}

動作イメージ:

  • dissolveIn === truestateRef.current.from=1, to=0 → ノイズ閾値が 1→0 へ下がる → ピクセルが if(noiseVal < progress) discard; しなくなる → 再表示
  • dissolveIn === false → 逆に 0→1 → ピクセルが段々破棄され溶解して消える

まとめ

  1. 画面遷移
    • フルスクリーン Plane にオルソカメラ / HUD
    • uProgression でアルファなど計算し、フェード・スパイラル等のエフェクト
  2. モデル遷移
    • 既存マテリアルを onBeforeCompile で改変 or THREE-CustomShaderMaterial など
    • uniform を追加し、フラグメント内で discard;diffuseColor.a *= ...; でフェード・ディゾルブ演出
  3. 注入場所#include <alphatest_fragment> 後に記述するとアルファの最終合成へ影響を与えられる
  4. 頂点も改変すれば位置ベースのディゾルブなど、より高度な演出が可能
はる@フルスタックチャンネルはる@フルスタックチャンネル

ポートフォリオアイデアまとめ

操作型(1画面ゲーム風)

  • キャラクターを操作する
    キャラクターをWSADで操作し、オブジェクト前でスキルや経験を表示。地形は限定的で宙に浮いたようなデザイン。
    例: Mike Fernandez

  • 車を操作する
    車を動かしてスキルや経験を地面上に表示。オブジェクトにぶつかると物理演算で倒れる。
    例: Bruno Simon

  • 特定場所でスキル表示
    キャラクターを移動し特定場所でスキルを表示。
    例: Michael Durkin

  • 鉄道列車を操作
    列車にスキルを貨物として積み運行。年が進むと線路を自由に敷設可能。かわいい色合いで「トイ・ストーリー」風のデザイン。
    例: Fikri Emre

  • NPCとの会話
    キャラクターを移動し、NPCに話しかけるシステム。
    例: Coastal World


スクロール型サイト

  • 3Dモデルとスクロール連動
    スクロールでモデルが動き、スムーズなトランジションを実現。
    例: David Hckh, 84-24

  • スキルや経歴とモデル表示
    スクロールに合わせてスキルや経歴に対応するモデルを次々と表示。
    例: WLT Design

  • ロケットとスキル変化
    スクロールでロケットが変化。クリックでスキルに応じたモデル変化も可能。
    例: Loic Nasdetourris

  • 物理演算とマウス連動
    Blenderで作成したシーンを再生し、マウスやスクロールで物理演算と連携。スムーズなトランジションも実現。
    例: JUNNI

  • モデルの回転と変化
    モデルがスクロールで回転し次の画面に移行。スムーズなトランジションを実現。
    例: Needle Tools


固定型(1画面中心)

  • 固定カメラでスキル表示
    画面内の3Dモデルを動かしスキルや経歴を表示。カメラ操作も可能。
    例: Jesse Zhou

  • モデル選択でズーム/遷移
    モデル選択で画面が遷移、またはズームして詳細を表示。
    例: Merodev, Newborn Brew

  • モデル内での表現
    モデル内のコーナーを選択するとブラウザ表示や特定の演出。
    例: Impossible Box

  • 部屋を舞台にした演出
    モデルをクリックするとアニメーションやゲームが起動。
    例: Andrii Babintsev

  • 宝箱探索風
    家のモデルにカーソルを合わせるとテキストが表示され、宝箱探索のような楽しさを提供。
    例: Kaino Wagon

  • 広いフィールドとカメラ移動
    大きなフィールドでクリックに応じてカメラがモデルへ移動。
    例: Portfolio 3JS

  • マンションモデルを使用
    各部屋でスキルや経歴を表示、カメラがズームして詳細を確認可能。
    例: Brett Williams


メモ

  • ゲーム系の注意点

    • 複数のモデルを購入して配置すると魅力的になる。
    • フィールドの広さは無限に設定可能。
    • モデル読み込みは時間がかかるため、ベイクするなど工夫が必要。
  • 固定型の特徴

    • モデル数が少ないためBlenderで自作可能。
  • 共通事項

    • いずれもローディング画面を設け、開始ボタンで起動。
  • 参考コード
    車を動かすコードはこちら

1. 1画面ゲームのように操作する系ポートフォリオ

メリット

  1. 高いインタラクション性
    WSAD操作やキャラクター移動など、ユーザーが「操作している感」を強く味わえます。自然にサイトに滞在してもらいやすく、他のポートフォリオと差別化しやすいです。
  2. ストーリーや世界観の演出がしやすい
    ゲーム的要素を取り入れることで、独自のストーリーや世界観を伝えやすく、ブランディングに大きく貢献します。
  3. 実装力・クリエイティビティのアピール
    キャラクターコントロールや物理演算など、高度な実装が必要になるため、エンジニアとしてのスキルをアピールしやすいです。

デメリット

  1. 開発コスト・学習コストが高い
    キャラクターや車など複数の3Dモデル、物理演算やアニメーション、サウンド演出など、作り込む範囲が広くなりがちです。
  2. 読み込み時間・パフォーマンスが課題になりやすい
    3Dモデル点数が多かったり、テクスチャが重かったりすると、ロードに時間がかかってしまいます。最適化やベイク処理などの工夫が必須です。
  3. 訪問者の操作ハードル
    スクロールだけのサイトとは異なり、キーボードやマウス操作の説明が必要になります。慣れない人には操作がわかりづらい場合があるかもしれません。

アイデア

  • 鉄道列車を走らせるコンセプト
    特に「トイ・ストーリーのような色合い」「走っている列車にスキルや経験が載っている」というアイデアはユニークです。
    • 列車の車両を年代別に分けて、積んでいる貨物をクリックすると、その年の実績やスキル詳細を表示する。
    • 最後の車両は「これから(フリーに線路を引いていく)」を表す空の貨車にして、将来の展望やビジョンを示す。
  • 車を運転する
    Bruno Simonのように車でフィールドを回りながら、気になるオブジェクトに近づくと詳細表示。トイ・ストーリー風のカラフルな世界観で統一するとポップなイメージに。
  • NPCとの会話やイベント
    Michael Durkinやcoastalworld.comのように、NPCを配置して会話イベントを入れたりすると楽しさがアップします。

2. スクロールするサイト系ポートフォリオ

メリット

  1. ユーザーが慣れた操作
    スクロールで進行するサイトは、一般的なWebサイトの操作感に近く、誰でも直感的に情報を追いやすいです。
  2. 没入感の演出がしやすい
    スクロールをトリガーにモデルやカメラ位置を変化させることで、段階的に世界観を見せたり、ストーリー性を演出できます。
  3. 比較的実装負荷が低め
    ゲーム系ほど複雑なユーザー操作を処理しなくて済む分、構成をシンプルにできるケースが多いです。

デメリット

  1. インタラクションが限られる
    スクロールが主になるため、ゲームのような自由度のある操作性は出しにくいです。
  2. 既視感のリスク
    スクロール連動の演出は流行しているため、似た印象のサイトになってしまう場合があります。
  3. 3Dモデルとスクロールの同期調整
    スムーズな演出を実現するためには、カメラアニメーションやモデルアニメーションのタイミング合わせにやや工夫が必要です。

アイデア

  • シーンごとに切り替わるトランジションを工夫
    例:ロケットの形が変化していくサイトのように、各セクションに合わせてモデルが形状変化したり、色がガラッと変わったりすると印象的。
  • Blenderや物理演算を使った短いアニメーションを順次再生
    ユーザーがスクロールするタイミングで、準備しておいたアニメーションが再生され、ストーリーを感じさせる演出。
  • 2.5D的な効果
    背景や手前のオブジェクトにパララックス効果を取り入れ、スクロールによってレイヤーが動くシーンが楽しめるようにする。

3. 1画面モデル固定系ポートフォリオ

メリット

  1. シンプルな操作感
    初期画面で3Dモデルが表示され、クリックやドラッグでカメラを回転・ズームするような直感的UXを提供できます。
  2. 比較的軽量に実装可能
    フィールド全体やキャラクターの移動を実装する必要はなく、1つの3Dシーンを作り込むだけでOK。
  3. モデル制作の自由度が高い
    必要なオブジェクトやインテリアだけ作り込めばよいので、自作モデルやシンプルなモデリングで完結しやすいです。

デメリット

  1. サイトの動きに変化が少なくなりがち
    固定カメラや固定シーンがベースになるため、ゲーム系のような動的演出は難しく、ユーザーに単調と思われる可能性があります。
  2. アピールできる内容が少ないと平凡に見える
    せっかく3Dを使っているのに、ただの「モデル鑑賞」だけで終わるとインパクトに欠けるかもしれません。
  3. 設計次第で導線が複雑になる
    一画面で全てを表現しようとして情報過多になり、ユーザーが何をすればいいか迷う可能性があります。

アイデア

  • 部屋や建物を作り込んでクリック誘導
    部屋の各コーナーに「Work」「Skill」「Contact」などの看板やアイテムを配置して、クリックするとカメラがズーム→詳細情報表示。
  • ズームやパーティクルなどの微細演出
    シーン移動がない分、クリック時にズームイン+文字が浮かび上がるパーティクル表現などを入れることで、華やかさを演出。
  • 宝探しやオブジェクト集めの要素
    「宝箱」や「隠されたアイテム」を探す感じで、ポートフォリオ内を探索できる仕掛けを用意すると、単純な操作でもワクワク感を演出できます。

全体的な最適化・制作のポイント

  1. モデル・テクスチャの軽量化

    • 不要なポリゴンを削減し、テクスチャを圧縮する。
    • GLTF/GLB形式を使い、ベイク処理やDraco圧縮などでファイルを軽量化。
  2. ローディング演出

    • どのタイプでも初回読み込みが長くなりがちなため、ローディング画面や進捗バーを設けて、ユーザーが離脱しないようにする。
  3. 段階的ロード (Lazy Loading)

    • 最初はメインのモデルやUIのみ読み込み、ユーザーが近づいたり進行した際に追加でモデルを読み込むなど、分割ロードを検討するとスムーズ。
  4. UXを意識した情報配置

    • ゲーム系:どこに行けば情報が得られるか、操作方法を明示。
    • スクロール系:セクションごとの情報量や、アニメーションのタイミングを慎重に調整。
    • 1画面系:インタラクションの導線やUI要素の分かりやすさを重視。
  5. 演出とコンテンツのバランス

    • 見た目・演出ばかりに注力しすぎず、実績やスキルの内容がちゃんと伝わるUI/UXを目指す。

まとめ

  • 1画面ゲームのように操作する系
    高度な演出・インタラクションが魅力的ですが、開発コストと読み込み最適化が課題。面白い世界観を構築しやすく、技術力をアピールしたい場合におすすめ。特に「トイ・ストーリーっぽい鉄道」を使うアイデアは目立ちます。

  • スクロールするサイト系
    一般的なユーザーが操作しやすく、ストーリー仕立ての展開がしやすいスタイル。ただし他サイトと差別化する演出やモデル制作がポイントになります。

  • 1画面モデル固定系
    開発は比較的シンプルになりますが、退屈にならないためのギミックをどう入れるかが鍵。モデルの作り込みやクリック・ズーム演出を工夫すると面白いポートフォリオになります。

はる@フルスタックチャンネルはる@フルスタックチャンネル

世界観アイデア

1. 「世界地図」型ワールドマップを歩き回る

  • 概要
    スキルや経歴を国や都市に見立て、ユーザーはキャラクターでマップを移動。都市(ポイント)に到着すると、そのスキル・経歴が表示される。
  • ポイント
    • 地図上の国名をスキル名にして「フロントエンド王国」「バックエンド連邦」など、ユーモアを交えて表現すると面白い。
    • 国境・山・海など障害物を適度に配置して、移動ルートを考える楽しさを演出。

2. 「部屋+ポータル」型

  • 概要
    キャラクターがスタート地点(ロビー)にいて、各部屋の扉に入るとポータルで別の小さな世界へ飛び、そこにスキルや経歴、コンタクトフォームが存在する。
  • ポイント
    • 部屋のデザインをスキルごとに変える(例えば「UI/UX部屋」ならポップなカラフル空間、「3D部屋」ならVR空間っぽいサイバー空間 など)。
    • コンタクト用の部屋では特別な演出を入れる(光の柱や星のゲートなど)とワクワク感が出る。

3. 「年表レース」型

  • 概要
    キャラクターが線路やレール状の道を走りながら、年代順にスキルや経歴が並んでいる。各スポットに着くと、その年に取得したスキルや実績が表示される。
  • ポイント
    • ポートフォリオ自体を「タイムトラベルレース」に見立て、通過地点で「2019年: ○○開発に携わる」「2020年: 新技術学習」など。
    • 最後に現在の年に到達するとフリーに動ける広場があり、そこに今後のビジョンやコンタクトフォームがあると自然。

4. 「ダンジョン探索」型

  • 概要
    迷路やダンジョンを探索しながら、各部屋に宝箱のようなオブジェクトがあり、開けるとスキル情報や経歴が表示される。
  • ポイント
    • 迷路内にモンスター(可愛い敵キャラ)がいて、話しかけるとヒントをくれたり、スキルの説明をしてくれる。
    • ダンジョンの最深部には「ボス部屋」があり、クリアすると連絡フォーム(コンタクト)が開く演出にする。

5. 「アミューズメントパーク」型

  • 概要
    テーマパーク内にアトラクションがいくつかあり、それぞれがスキルや経歴を表現している。ユーザーはキャラクターでアトラクションに近づくと説明がポップアップ。
  • ポイント
    • コースターエリア、ホラーハウス、メリーゴーランドなど各アトラクションの雰囲気をスキルの内容に合わせる。
    • コンタクトは「観覧車」の頂上や、お土産ショップのレジカウンターに設定しておき、行くとフォームが表示されるなど遊び心を入れる。

6. 「ステージライブ」型

  • 概要
    広いステージやライブ会場を舞台にし、ユーザーキャラクターがステージ上を歩き回る。ステージ上にいる複数のキャラクター(NPC)に話しかけると、それぞれがスキルや経歴を教えてくれる。
  • ポイント
    • スキルをバンドメンバーに見立て、「ギター担当: フロントエンド」「ドラム担当: バックエンド」「ボーカル担当: UI/UX」といった設定で紹介。
    • ステージの袖(裏手)に行くとコンタクトフォームがあったり、実績を示すポスターが貼ってあるなど、演出の幅が広い。

7. 「森の小道を散策」型

  • 概要
    フィールドは自然豊かな森や小道。道の途中に立て札や彫刻があり、近づくとスキルや経歴が浮き上がる。最後に山小屋でコンタクトフォームが開く。
  • ポイント
    • 森の中に光るクリスタルやキノコなどファンタジー要素を散りばめて、世界観を強調。
    • スキルによってオブジェクトの色や形を変える(赤色のクリスタルはフロントエンド、青色はバックエンドなど)。

8. 「空中都市」型

  • 概要
    街や島が空中に浮かんでおり、キャラクターがジェットパックや浮遊船で各島へ移動する。島ごとにスキルや経歴が存在する。
  • ポイント
    • 島間を移動するときに「テレポートのゲート」や「雲の道」を通るなど、移動方法を工夫する。
    • コンタクト用の島は「通信基地」や「放送タワー」をイメージし、そちらに行くと連絡先フォームやSNSリンクが表示される。

9. 「街づくり」型

  • 概要
    キャラクターではなく、街の住民として行動しているイメージ。最初は更地状態のマップに、プレイヤーが経歴・スキルの「建物」を次々に建設していくと、その詳細が見られるようになる。
  • ポイント
    • 経歴1つ取得するたびに「会社ビル」が建つなど、スキルごとに異なるタイプの建物が完成。
    • 建物に近づくと中に入る演出→詳細情報が表示。最後に「市役所」がコンタクトフォームとして機能する形でも面白い。

10. 「絵本のページをめくる」型

  • 概要
    一見、絵本の世界のようにページが1枚のフィールドになっており、キャラクターは紙の上を歩く。ページの端まで行くと次のページにめくれて、スキル・経歴が進んでいく。
  • ポイント
    • ページをめくるたびに世界観がガラッと変わる(春のページ、夜空のページ、宇宙のページ、など)。
    • ページの最後に「手紙のコーナー」があり、クリックするとコンタクトフォームが開くなど、ストーリー絵本の最後の手紙演出を取り入れる。

11. 「雪山登山」型

  • 概要
    キャラクターが雪山を徐々に登っていき、各登山キャンプ(ベースキャンプ1、2、3…)に到着すると、そこに経歴やスキルが貼り出されている。最頂上の山小屋でコンタクトフォームが開く。
  • ポイント
    • 各キャンプで焚き火やテントがあり、そこにスキル紹介の看板やアイテムを配置。
    • 山頂に着くと一望できる景色とともに「これからの展望」もあわせて表示すると、演出として映える。

12. 「博物館巡り」型

  • 概要
    大きな博物館の内部をキャラクターで移動し、展示エリアごとにスキルや経歴が展示されている。オブジェクト(本、アート作品、ホログラムなど)を近づいて見ると詳細がポップアップ。
  • ポイント
    • 入口にはマップ(館内案内図)を置いておき、各エリアへの移動先をわかりやすくする。
    • 最奥のVIPルームや管理室がコンタクトページの入り口になっているイメージ。

13. 「宇宙ステーション探索」型

  • 概要
    未来的な宇宙ステーションの中を歩き回り、各モジュール(区画)にてスキルや経歴がホログラムとして表示される。コンタクトは宇宙船のコックピットで行える。
  • ポイント
    • スキルや経歴の発表を「研究レポート」的に見せるとSF感が増す。
    • 窓の外を眺めると地球や銀河が見え、没入感のある演出ができる。

14. 「海底探検」型

  • 概要
    キャラクターが潜水服を着たり、小型潜水艇に乗って海底を探検。コーラルリーフや沈没船などの各ポイントでスキルや経歴が出現。
  • ポイント
    • 海洋生物(魚やクラゲなど)を泳がせて、近づくと手がかりをくれる仕掛けも面白い。
    • 魚群の向こうに「Contact」と書かれた発光クラゲがいて、接触するとコンタクトフォームが開くなど幻想的な演出をプラス。

15. 「レストラン巡り」型

  • 概要
    キャラクターが小さな街のレストランをはしごして回る。各レストランで「フロントエンドコース」「バックエンドコース」と名付けられたメニューを注文すると、そのスキル情報が見られる。
  • ポイント
    • 街並みをカフェ、バー、ブッフェなど多彩にして、「どの順に行く?」という自由度を演出できる。
    • 最後に「デザート店」に行くとコンタクトフォームが開き、“感想をぜひ教えて!”というストーリーに。

16. 「魔法学園」型

  • 概要
    魔法学校の敷地内を自由に動き回り、教室や実験室でスキルを学んだ形にして紹介。卒業ホールで経歴や最終的なコンタクトが整っている。
  • ポイント
    • スキルを魔法の科目に例える(例:「UI魔法学」「3D召喚術」「バックエンド錬金術」など)と遊び心が出る。
    • 学園長室(校長室)でコンタクトフォームを設ける。ユーザーに「卒業証書」的な演出を与えるのも面白い。

17. 「農場ライフ」型

  • 概要
    農場の敷地内をキャラクターで歩き回り、畑や牧場、小屋などにスキルのアイテムが育っている。収穫や世話をすることで経歴・スキル情報が読める。
  • ポイント
    • スキル収穫時にはアニメーション(野菜や果物がポップアップ)で情報が表示されるなど、ほんわかした世界観づくりが可能。
    • 農場主の家(母屋)にコンタクトフォームを用意し、訪問者がドアをノックするとフォームが開く演出。

18. 「秘密の洞窟」型

  • 概要
    山や崖の奥にある洞窟を探索。各洞窟内に光る壁画や遺跡があり、そこにスキルや経歴が刻まれている。最深部には遺跡の祭壇があり、コンタクトにつながる。
  • ポイント
    • 壁画をクリックすると内容が浮き上がるように表示されるなど、冒険感を演出。
    • 洞窟内の崖や仕掛けをジャンプや回避で乗り越えるミニアクションを入れるとゲーム性アップ。

19. 「世界のお祭り巡り」型

  • 概要
    キャラクターが飛行船や気球で移動し、世界各地の祭りを巡る。各お祭り会場では異なるスキルや経歴が披露される。
  • ポイント
    • 会場によって民族衣装や屋台を用意し、それぞれのスキル紹介を“祭りの演目”として表示。
    • 最後のお祭りが「あなたを歓迎するパーティー」として、そこにコンタクトフォームを配置する流れ。

20. 「おとぎ話の童話世界」型

  • 概要
    絵本の挿絵のような、パステルカラーや手描き風のテクスチャを使い、森やお城、湖などを回る。シーンごとに登場する動物や精霊たちがスキルや経歴を案内。
  • ポイント
    • お城の門を開けると実績紹介のホールがあり、周囲のステンドグラスに経歴を描くなど、視覚的な演出が華やか。
    • 最後はお城の一番上の塔で魔法の鏡をクリック→コンタクトフォーム表示、といった演出もドラマチック。

まとめ

「キャラクターを操作してポイントにたどり着いたら表示される」という基本システムは共通ですが、世界観をどう演出するかスキルや経歴の見せ方をどんなオブジェクトで表現するかでオリジナリティを出せます。

  • ゲームらしさを追求したいなら、NPCや仕掛けを充実させる。
  • 物語やテーマパーク的にしたいなら、ステージやアトラクションなどエリアを区切る。
  • ユーザーが操作を楽しむ工夫(飛ぶ・迷路・レース・建設など)を組み合わせると、さらに面白い体験になるでしょう。
  • どれも基本要素として「1)キャラクターが移動」「2)近づいたりアクションするとスキル・経歴が開示」「3)最終的にコンタクトフォームへ」の流れがあります。
  • テーマや世界観を決めるだけでなく、スキル紹介をどう“アイテム化”や“物語化”するかがポイントです。
  • 移動手段や探索方法を工夫すると、よりゲームらしい“冒険”感や“発見”感を演出できます。
はる@フルスタックチャンネルはる@フルスタックチャンネル

キャラクターコントローラー

シンプルなキャラクター操作です。

  • WSAD操作
  • ジャンプ
  • カメラ追従

import { useKeyboardControls } from "@react-three/drei"
import { useFrame } from "@react-three/fiber"
import { CapsuleCollider, RigidBody } from "@react-three/rapier"
import { useControls } from "leva"
import { useRef, useState } from "react"
import { MathUtils, Vector3 } from "three"
import { Character } from "./Character"
import { Controls } from "../App"

const lerpAngle = (start, end, t) => {
  start = MathUtils.euclideanModulo(start, Math.PI * 2)
  end = MathUtils.euclideanModulo(end, Math.PI * 2)

  if (Math.abs(end - start) > Math.PI) {
    if (end > start) start += 2 * Math.PI
    else end += 2 * Math.PI
  }

  return MathUtils.euclideanModulo(start + (end - start) * t, Math.PI * 2)
}

export const CharacterController = () => {
  const { MOVEMENT_SPEED, JUMP_FORCE } = useControls("Character Control", {
    MOVEMENT_SPEED: { value: 5, min: 3, max: 10, step: 1 },
    JUMP_FORCE: { value: 6, min: 3, max: 10, step: 1 },
  })

  const rb = useRef()
  const container = useRef()
  const character = useRef()

  const [animation, setAnimation] = useState("CharacterArmature|Idle")
  const cameraTarget = useRef()
  const cameraPosition = useRef()
  const cameraWorldPosition = useRef(new Vector3())
  const cameraLookAtWorldPosition = useRef(new Vector3())
  const cameraLookAt = useRef(new Vector3())
  const [, get] = useKeyboardControls()
  const movement = new Vector3()
  const inTheAir = useRef(false)

  useFrame(({ camera }) => {
    if (!rb.current) return

    const vel = rb.current.linvel()

    // 動きのリセット
    movement.set(0, 0, 0)

    // WASD入力による移動
    if (get()[Controls.forward]) movement.z += MOVEMENT_SPEED
    if (get()[Controls.back]) movement.z -= MOVEMENT_SPEED
    if (get()[Controls.left]) movement.x += MOVEMENT_SPEED
    if (get()[Controls.right]) movement.x -= MOVEMENT_SPEED

    // ジャンプ処理
    if (get()[Controls.jump] && !inTheAir.current) {
      movement.y += JUMP_FORCE
      inTheAir.current = true
      setAnimation("CharacterArmature|Jump")
    } else {
      // 垂直速度を保持
      movement.y = vel.y
    }

    // キャラクターのアニメーションと回転
    if (movement.x || movement.z) {
      const angle = Math.atan2(movement.x, movement.z)

      // キャラクターの回転補間
      character.current.rotation.y = lerpAngle(
        character.current.rotation.y,
        angle,
        0.2
      )

      if (!inTheAir.current) {
        setAnimation("CharacterArmature|Run")
      }
    } else if (!inTheAir.current) {
      setAnimation("CharacterArmature|Idle")
    }

    // 速度を設定
    rb.current.setLinvel(movement, true)

    // カメラ追従処理
    cameraPosition.current.getWorldPosition(cameraWorldPosition.current)
    camera.position.lerp(cameraWorldPosition.current, 0.1)

    if (cameraTarget.current) {
      cameraTarget.current.getWorldPosition(cameraLookAtWorldPosition.current)
      cameraLookAt.current.lerp(cameraLookAtWorldPosition.current, 0.1)
      camera.lookAt(cameraLookAt.current)
    }
  })

  return (
    <RigidBody
      colliders={false}
      lockRotations
      ref={rb}
      position={[0, 0, 0]}
      onCollisionEnter={({ other }) => {
        // 地面との接触判定
        if (other.rigidBodyObject.name === "ground") {
          inTheAir.current = false
        }
      }}
      gravityScale={2.5}
    >
      <group ref={container}>
        {/* カメラ関連 */}
        <group ref={cameraTarget} />
        <group ref={cameraPosition} position-y={3} position-z={-5} />
        {/* キャラクター */}
        <group ref={character}>
          <Character scale={0.3} animation={animation} />
        </group>
      </group>
      {/* コライダー */}
      <CapsuleCollider args={[0.23, 0.25]} position={[0, 0.37, 0]} />
    </RigidBody>
  )
}

はる@フルスタックチャンネルはる@フルスタックチャンネル

EnterでURL遷移

キャラクターが特定のオブジェクトに近づいたときに、Enterの文字を表示させて、キーボードのEnterを押すと、指定したURLに遷移する

import React, { forwardRef, useMemo, useRef, useState } from "react"
import { Canvas, useFrame } from "@react-three/fiber"
import {
  Environment,
  OrbitControls,
  useKeyboardControls,
  KeyboardControls,
  Text,
} from "@react-three/drei"
import { Physics, RigidBody } from "@react-three/rapier"
import { Bloom, EffectComposer, Vignette } from "@react-three/postprocessing"

import * as THREE from "three"

export const Controls = {
  forward: "forward",
  back: "back",
  left: "left",
  right: "right",
  jump: "jump",
}

export default function App() {
  const map = useMemo(
    () => [
      { name: Controls.forward, keys: ["ArrowUp", "KeyW"] },
      { name: Controls.back, keys: ["ArrowDown", "KeyS"] },
      { name: Controls.left, keys: ["ArrowLeft", "KeyA"] },
      { name: Controls.right, keys: ["ArrowRight", "KeyD"] },
    ],
    []
  )

  const playerRef = useRef()

  return (
    <KeyboardControls map={map}>
      <Canvas camera={{ position: [0, 5, 10], fov: 50 }}>
        <OrbitControls />
        <Environment preset="sunset" />
        <ambientLight intensity={0.5} />
        <pointLight position={[0, 10, 10]} />
        <Physics debug>
          <Ground />
          <Player ref={playerRef} />
          <InteractiveObject
            position={[0, 0, -5]}
            url="https://google.com"
            playerRef={playerRef}
          />
        </Physics>
        <EffectComposer>
          <Bloom mipmapBlur luminanceThreshold={1} intensity={2.4} />
          <Vignette offset={0.1} darkness={0.6} />
        </EffectComposer>
      </Canvas>
    </KeyboardControls>
  )
}

function Ground() {
  return (
    <RigidBody type="fixed" colliders="cuboid" position={[0, -0.5, 0]}>
      <mesh receiveShadow>
        <boxGeometry args={[100, 1, 100]} />
        <meshStandardMaterial color="brown" />
      </mesh>
    </RigidBody>
  )
}

const Player = forwardRef(function Player(_, ref) {
  const [test, getKeys] = useKeyboardControls()
  const SPEED = 7

  useFrame(() => {
    if (!ref?.current) return

    const { forward, back, left, right } = getKeys()

    const velocity = { x: 0, y: 0, z: 0 }

    if (forward) velocity.z -= SPEED
    if (back) velocity.z += SPEED
    if (left) velocity.x -= SPEED
    if (right) velocity.x += SPEED

    const currentVel = ref.current.linvel()
    velocity.y = currentVel.y

    ref.current.setLinvel(velocity, true)
  })

  return (
    <RigidBody
      ref={ref}
      colliders="cuboid"
      position={[0, 0.5, 0]}
      type="dynamic"
      name="Player"
    >
      <mesh>
        <boxGeometry args={[1, 1, 1]} />
        <meshStandardMaterial color="orange" />
      </mesh>
    </RigidBody>
  )
})

function GlowingText({ text, position, fontSize, color, scale }) {
  return (
    <Text
      position={position}
      fontSize={fontSize}
      scale={scale}
      color={color}
      anchorX="center"
      anchorY="middle"
    >
      {text}
      <meshStandardMaterial
        color={color}
        toneMapped={false}
        emissive={color}
        emissiveIntensity={1.5}
      />
    </Text>
  )
}

function InteractiveObject({ position, url, playerRef }) {
  const objectRef = useRef()
  const [isNearby, setIsNearby] = useState(false)
  const [scale, setScale] = useState(0)

  useFrame(() => {
    if (!objectRef.current || !playerRef.current) return

    const objectTranslation = objectRef.current.translation()
    const objectPos = new THREE.Vector3(
      objectTranslation.x,
      objectTranslation.y,
      objectTranslation.z
    )

    const playerTranslation = playerRef.current.translation()
    const playerPos = new THREE.Vector3(
      playerTranslation.x,
      playerTranslation.y,
      playerTranslation.z
    )

    const distance = objectPos.distanceTo(playerPos)
    const isClose = distance < 3
    setIsNearby(isClose)

    const speed = 0.1

    if (isClose && scale < 1) {
      setScale((prev) => Math.min(prev + speed, 1))
    } else if (!isClose && scale > 0) {
      setScale((prev) => Math.max(prev - speed, 0))
    }
  })

  const handleKeyPress = (event) => {
    if (event.key === "Enter" && isNearby) {
      window.open(url, "_blank")
    }
  }

  React.useEffect(() => {
    window.addEventListener("keydown", handleKeyPress)
    return () => {
      window.removeEventListener("keydown", handleKeyPress)
    }
  }, [isNearby])

  return (
    <RigidBody ref={objectRef} position={position} type="fixed">
      <mesh>
        <boxGeometry args={[2, 2, 2]} />
        <meshStandardMaterial color="green" />
      </mesh>
      {isNearby && (
        <GlowingText
          text="Enter"
          position={[0, 2, 0]}
          fontSize={1}
          color="#F3E3DA"
          scale={[scale, scale, scale]}
        />
      )}
    </RigidBody>
  )
}
はる@フルスタックチャンネルはる@フルスタックチャンネル

シェーダーコード内で使用できる変数(vertexShaderfragmentShaderそれぞれで自由に使えるビルトイン変数)について説明します。それぞれの変数はWebGLのシェーダー言語(GLSL)の仕様で定義されています。

頂点シェーダー(vertexShader)で使用できる変数

  1. position

    • 意味: 頂点のローカル座標(モデル空間での位置)。
    • : vec3
  2. normal

    • 意味: 頂点のローカル法線ベクトル。
    • : vec3
  3. uv

    • 意味: テクスチャ座標(頂点に割り当てられたUVマッピング値)。
    • : vec2
  4. projectionMatrix

    • 意味: カメラの射影行列。ビュー空間をクリップ空間に変換します。
    • : mat4
  5. modelViewMatrix

    • 意味: モデルビュー行列。モデル空間をビュー空間に変換します。
    • : mat4
  6. viewMatrix

    • 意味: ビュー行列。カメラの座標系を扱うための行列。
    • : mat4
  7. modelMatrix

    • 意味: モデル行列。オブジェクトの位置、回転、スケールを含む変換行列。
    • : mat4
  8. normalMatrix

    • 意味: 法線のためのモデルビュー行列。法線の回転やスケーリングを正しく扱うために使用します。
    • : mat3
  9. gl_Position

    • 意味: 頂点の最終的な位置を指定する出力変数。
    • : vec4

フラグメントシェーダー(fragmentShader)で使用できる変数

  1. gl_FragCoord

    • 意味: フラグメントの画面座標(ピクセル単位)。
    • : vec4
  2. gl_FrontFacing

    • 意味: フラグメントがポリゴンの表面に属しているか(true)裏面に属しているか(false)。
    • : bool
  3. gl_PointCoord

    • 意味: ポイントスプライトのテクスチャ座標(0.0から1.0の範囲)。
    • : vec2
  4. gl_FragColor (古いバージョンのGLSLで使用)

    • 意味: フラグメントの出力色。
    • : vec4

    注意: GLSLの最新バージョンでは、カスタムの出力変数(例: out vec4 fragColor)を使用するのが推奨されています。

  5. dFdx, dFdy, fwidth

    • 意味: フラグメント間の微分値を計算する関数。
    • : 各関数に応じた型(floatまたはvec2/vec3/vec4)。
はる@フルスタックチャンネルはる@フルスタックチャンネル

useFBO

useFBOFramebuffer Object (FBO) を簡単に管理するための便利なフックで、オフスクリーンレンダリング を行いたい場合に利用します。以下に、useFBO の利用が適している状況や、具体的な例を挙げて詳しく説明します。


useFBO が適している状況

1. ポストプロセスエフェクト

  • シーン全体 をレンダリングした後に、スクリーン全体にエフェクト(例: ブルーム、モーションブラー、色調整)をかけたい場合。
  • シーンの結果をオフスクリーンバッファにレンダリングし、それをテクスチャとして加工します。

例: ブルームエフェクト

  • useFBO を使ってシーンをレンダリングし、その結果を明るい部分だけ抽出してぼかし、スクリーン上に再描画します。

2. 複数の視点を同時に表示

  • 複数のカメラ で異なる角度から見たシーンを、それぞれ FBO にレンダリングして表示。
  • 上記のコードのように、モード(fronttopcorner)に応じて異なるカメラ視点を切り替える場合や、監視カメラ風のインターフェースを作る場合に有効です。

例: マルチカメラレンダリング

  • 各カメラでオフスクリーンレンダリングし、その結果をテクスチャとしてモニターやクアッドに表示します。

3. シミュレーション (GPGPU)

  • GPUでのシミュレーション結果(例: 粒子の位置や速度)を FBO にレンダリングし、その結果を次のフレームで利用します。
  • シミュレーションを更新するレンダリングと、可視化用のレンダリングを分けて管理する場合に便利です。

例: 粒子のシミュレーション

  • 粒子の位置や速度を FBO に保存し、それを利用して次のステップの位置計算や描画を行います。

4. ミラーリングや反射

  • シーンの一部をレンダリングし、その結果を鏡や水面などに投影する場合。
  • useFBO を使うことで、オフスクリーンで鏡専用のカメラからの視点をレンダリングし、その結果を反射テクスチャとして適用できます。

例: 水面反射

  • 水面下から見たシーンをオフスクリーンレンダリングし、それを水面の反射として使用します。

5. カスタムエフェクトの中間処理

  • 一度シーンをレンダリングし、その結果を再利用して複数のエフェクトを組み合わせる場合。
  • useFBO を中間処理用のバッファとして使い、複数のレンダリングパスを実現します。

例: トーンマッピングとフィルタの適用

  • FBO にシーン全体をレンダリング → 明るさ調整 → 色補正といった処理を順次行います。

コードでのポイント

useFBO の使い方

  • 初期化: 必要に応じて解像度やフォーマットを指定可能。
    const fbo = useFBO(width, height, { format: THREE.RGBAFormat, stencil: false });
    
  • レンダリング: gl.setRenderTarget(fbo) を使用して、レンダリング先を指定。
    gl.setRenderTarget(fbo);
    gl.render(scene, camera);
    gl.setRenderTarget(null); // レンダリング先を元に戻す
    
  • 結果の利用: fbo.texture をテクスチャとして参照可能。
    <planeGeometry args={[1, 1]}>
      <meshBasicMaterial map={fbo.texture} />
    </planeGeometry>
    

具体的な応用例

例1: ミニマップの実装

const MiniMap = () => {
  const mapCamera = useRef();
  const fbo = useFBO();

  useFrame(({ gl, scene }) => {
    mapCamera.current.position.set(0, 10, 0);
    mapCamera.current.lookAt(0, 0, 0);

    gl.setRenderTarget(fbo);
    gl.render(scene, mapCamera.current);
    gl.setRenderTarget(null);
  });

  return (
    <>
      {/* ミニマップカメラ */}
      <orthographicCamera ref={mapCamera} />
      {/* ミニマップ表示 */}
      <mesh position={[2, 2, 0]} scale={[0.5, 0.5, 1]}>
        <planeGeometry args={[1, 1]} />
        <meshBasicMaterial map={fbo.texture} />
      </mesh>
    </>
  );
};

例2: 水面反射

const WaterReflection = () => {
  const waterCamera = useRef();
  const fbo = useFBO();

  useFrame(({ gl, scene }) => {
    waterCamera.current.position.set(0, -5, 0); // 水面下にカメラを配置
    waterCamera.current.lookAt(0, 0, 0);

    gl.setRenderTarget(fbo);
    gl.render(scene, waterCamera.current);
    gl.setRenderTarget(null);
  });

  return (
    <>
      <orthographicCamera ref={waterCamera} />
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        <planeGeometry args={[10, 10]} />
        <meshBasicMaterial map={fbo.texture} />
      </mesh>
    </>
  );
};

例3: 複数のモニターで異なる視点を表示

const MultiMonitor = () => {
  const cameras = useRef([]);
  const fbos = [useFBO(), useFBO(), useFBO()];

  useFrame(({ gl, scene }) => {
    cameras.current.forEach((camera, i) => {
      gl.setRenderTarget(fbos[i]);
      gl.render(scene, camera);
      gl.setRenderTarget(null);
    });
  });

  return (
    <>
      {Array(3)
        .fill()
        .map((_, i) => (
          <perspectiveCamera
            ref={(el) => (cameras.current[i] = el)}
            position={[i * 5, 5, 5]}
            key={i}
          />
        ))}
      {fbos.map((fbo, i) => (
        <mesh position={[i * 2 - 2, 0, -1]} scale={[1, 1, 1]} key={i}>
          <planeGeometry args={[1, 1]} />
          <meshBasicMaterial map={fbo.texture} />
        </mesh>
      ))}
    </>
  );
};

まとめ

  • useFBO を使う場面:

    1. ポストプロセスエフェクト
    2. 複数のカメラ視点の描画
    3. GPUシミュレーション結果の管理
    4. ミラーリングや反射
    5. カスタムエフェクトや中間処理
  • 主な利点:

    • 簡単な初期化 (useFBO でフォーマットやサイズを自動管理)
    • 複数のレンダリング結果をテクスチャとして利用
    • レンダリング結果をリアルタイムに更新しながら表示可能

シーンの再利用や高度なエフェクトの実現が必要な場合には、useFBO を活用することで効率的に管理できます。

はる@フルスタックチャンネルはる@フルスタックチャンネル

colorspace_fragment

encodings_fragment は、three.js のバージョン r152 で colorspace_fragment に名称変更されました。

この変更に伴い、シェーダーコード内で #include <encodings_fragment> を使用している場合は、#include <colorspace_fragment> に置き換える必要があります。

import {
  extend,
  Canvas,
  useFrame,
  useLoader,
  useThree,
} from "@react-three/fiber"
import {
  Environment,
  OrbitControls,
  PerspectiveCamera,
  shaderMaterial,
  Stats,
  useFBO,
  useTexture,
} from "@react-three/drei"
import { useRef } from "react"

import * as THREE from "three"

// 頂点シェーダー
const vertexShader = /* glsl */ `
  varying vec2 vUv;
  
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`

// フラグメントシェーダー
const fragmentShader = /* glsl */ `
  uniform sampler2D uTexture;

  varying vec2 vUv;
  
  void main() {
    vec4 curTexture = texture2D(uTexture, vUv);
    gl_FragColor = curTexture;

    #include <tonemapping_fragment>
    #include <colorspace_fragment>
  }
`

export const SimpleMaterial = shaderMaterial(
  {
    uTexture: undefined,
  },
  vertexShader,
  fragmentShader
)

extend({ SimpleMaterial })

const Experience = ({ width = 5, height = 5, fillPercent = 0.6 }) => {
  const viewport = useThree((state) => state.viewport)
  let ratio = viewport.height / (height / fillPercent)

  if (viewport.width < viewport.height) {
    ratio = viewport.width / (width / fillPercent)
  }

  const texture = useTexture("textures/bond.png")

  return (
    <>
      <mesh>
        <planeGeometry args={[width * ratio, height * ratio]} />
        <simpleMaterial side={THREE.DoubleSide} uTexture={texture} />
      </mesh>
    </>
  )
}

function App() {
  return (
    <Canvas>
      <Stats />
      <color attach="background" args={["#17181f"]} />
      <Environment preset="city" />
      <ambientLight intensity={0.5} />
      <directionalLight position={[5, 5, 5]} />

      <OrbitControls />

      <Experience />
    </Canvas>
  )
}

export default App

はる@フルスタックチャンネルはる@フルスタックチャンネル

CatmullRomCurve3

CatmullRomCurve3 は、Three.js で複数の制御点(Vector3)を通る滑らかな曲線を生成するためのクラスです。たとえば、オブジェクトをパスに沿って移動させたい場合や、カメラのアニメーション経路を作る場合に使われます。以下に、基本的な使い方とチュートリアル形式の説明を示します。

1. 基本概念

  • 制御点
    曲線が通過する座標を表す THREE.Vector3 オブジェクトの配列です。
  • 閉じた曲線 / 開いた曲線
    CatmullRomCurve3 の第二引数に true を渡すと、曲線は閉じたループ(始点と終点が連結)として扱われ、false(または省略)だと開いた曲線になります。
  • 補間
    曲線上の任意の位置は、curve.getPoint(t)(t は 0~1 の値)で取得でき、t=0 が始点、t=1 が終点に対応します。

2. 基本的な使い方

2.1 制御点の定義

まず、曲線を構成する制御点(Vector3)を用意します。たとえば:

const points = [
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(5, 10, 0),
  new THREE.Vector3(10, 0, 0)
];

2.2 CatmullRomCurve3 の生成

上記の制御点の配列を使って曲線を作成します。

  • 閉じない曲線の場合(オープンなパス):
const curve = new THREE.CatmullRomCurve3(points, false);
  • 閉じた曲線の場合:
const closedCurve = new THREE.CatmullRomCurve3(points, true);

2.3 曲線上の点を取得する

曲線上の点は、0~1 の範囲の t 値を指定して取得します:

const t = 0.5; // 曲線の中間点
const pointOnCurve = curve.getPoint(t);
console.log(pointOnCurve);  // THREE.Vector3 インスタンスが得られる

3. チュートリアル形式の実例

ここでは、曲線に沿ってオブジェクト(例えばカメラ)の位置を更新する例を示します。

3.1 曲線の作成

まず、カメラの移動パスとなる曲線を定義します。下記のコード例は、複数の座標を指定して曲線を生成しています。

const cameraCurve = new THREE.CatmullRomCurve3(
  [
    new THREE.Vector3(2, 65, 47.5),
    new THREE.Vector3(1.4, 65, 39),
    new THREE.Vector3(-2, 70, 17),
    new THREE.Vector3(-2.6, 68.5, 4.8),
    new THREE.Vector3(-2.45, 67.9, 0),
    // ... 必要な制御点を追加
    new THREE.Vector3(1.4, 65, 39)  // ループする場合、始点に戻るようにする
  ],
  true // 曲線を閉じたループにする
);

3.2 アニメーションでの利用

React Three Fiber の useFrame を使って、毎フレームごとにカメラグループの位置を曲線に沿って更新します。たとえば、スクロールや時間の経過によって進行度 newProgress(0~1)を算出し、曲線上の位置を取得します。

useFrame((state) => {
  // newProgress はスクロール量やアニメーション進捗に基づく 0~1 の値
  const newProgress = /* 進行度の計算 */;
  
  // 曲線上の点を取得
  const basePoint = cameraCurve.getPoint(newProgress);

  // カメラグループの位置を更新(滑らかに補間する場合は THREE.MathUtils.lerp を使用)
  cameraGroup.current.position.x = THREE.MathUtils.lerp(
    cameraGroup.current.position.x,
    basePoint.x,
    0.1
  );
  cameraGroup.current.position.y = THREE.MathUtils.lerp(
    cameraGroup.current.position.y,
    basePoint.y,
    0.1
  );
  cameraGroup.current.position.z = THREE.MathUtils.lerp(
    cameraGroup.current.position.z,
    basePoint.z,
    0.1
  );
});

3.3 曲線の可視化

デバッグやデザイン確認のために、曲線自体をシーンに表示することも可能です。

const points = cameraCurve.getPoints(50);  // 曲線上の50個の点を取得
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
const curveObject = new THREE.Line(geometry, material);
scene.add(curveObject);

これにより、赤いラインとして曲線が表示され、オブジェクトがどのように動くかを視覚的に確認できます。

4. 応用例

ご提示のコードでは、カメラグループの位置を更新するだけでなく、進行度に応じた回転補間も行っています。

  • getPoint(newProgress) で曲線上の位置を取得し、カメラグループの位置に設定。
  • 別途、各進行度に対応する回転(rotationTargets)を定義し、slerp(球面線形補間)を使って滑らかに回転を補間しています。

これにより、カメラが曲線に沿って移動するだけでなく、シーンに合わせた角度の補正も行い、より自然なカメラワークが実現されています。

5. まとめ

  • CatmullRomCurve3 の生成
    制御点の配列を渡して曲線を作成。
  • getPoint(t) の利用
    t (0~1) を渡すことで、曲線上の任意の点を取得可能。
  • 応用例
    アニメーションの経路やカメラ移動、パスに沿ったオブジェクトの動きを実現するのに非常に有用。
  • 可視化
    getPoints() で得られた点を使い、THREE.Line で曲線を表示することでデバッグにも利用できる。

参考リンク

はる@フルスタックチャンネルはる@フルスタックチャンネル

Octreeを使う場面

Three.js の標準的な衝突判定(特に静的なメッシュとの当たり判定など)に関しては、

  • すでに物理演算エンジン(Rapier, Cannon, Ammo, PhysX など)を使用する場合は、そのエンジン内部でオブジェクトの空間分割(BroadPhase)や NarrowPhase の衝突判定が行われるため、開発者がわざわざ Octree を自前で使う必要はあまりありません
  • もし @react-three/rapier のようなライブラリを導入しているなら、ラッピングされた Rapier の API を通してコライダーや剛体を登録することで、すべての衝突判定がカバーされます。

では、Octree を使うとしたらどんなとき?

Rapier がカバーする衝突判定や物理演算のシナリオとは別に、

  1. 自前のシステム(例: カスタムレイキャストやカメラ判定)を高速化したいとき

    • ゲーム内でキャラクターの視線判定・視界判定をする、独自のロジックで “どこに敵がいるか” を空間的に問い合わせたい……など、物理演算(剛体の動き)とは直接関係ない 処理を高速化したいときに Octree を導入することがあります。
    • Rapier 自体にもレイキャスト機能はありますが、それは「Physics World」に登録された物理コライダーに対する問い合わせです。たとえば “シーン内の 描画だけ されている物体” を高速検索したい場合などに、Octree の空間分割を使うことがあります。
  2. 巨大なマップや大量オブジェクトがあるが、物理演算は一部しか使わないケース

    • 物理エンジンに登録すると、少なくともコライダーの数だけリソースが消費されます。もしマップの一部だけにしか本格的な衝突判定が必要なくて、その他は「見た目(レンダリング)だけ」というケースもあるでしょう。
    • そういった「物理エンジンに載せない部分」についてだけ、自前で Octree を使い、最低限の衝突判定や探索をしたり、あるいは単に “どのオブジェクトが近いか” を見つけるための空間検索に使うことがあります。
  3. 描画の最適化 (Frustum Culling や Level of Detail に近い用途)

    • たとえば、より細かい独自のカリング手法を実装したい場合(視錐台カリング + Octree でさらに細かく絞るなど)には Octree が使われることがあります。Rapier はあくまで “物理演算” が目的なので、単に “画面外のモデルを消したい / 軽量化したい” という用途だと過剰です。
    • 一方で、シンプルに Three.js の組み込みフラスタムカリングで十分なことも多いので、ここまでやるケースはそれほど多くはありません。
  4. “物理エンジン外” の特殊なコリジョン形状を扱うとき

    • Rapier が対応していない特殊なコリジョン形状・判定を自作したいときに、空間分割として Octree を使うことがあります。
    • たとえばマーチングキューブで作ったボクセルワールドのような “超膨大な三角形メッシュ” に対して、自前でカスタムな “当たり判定” を行う必要がある場合などです。

そもそも Rapier はどうやって衝突判定をしているのか

Rapier などの物理エンジンでは、BroadPhase(大まかな衝突候補を絞り込む段階)と NarrowPhase(実際の形状同士を詳細に判定する段階)の2段階またはそれに類する工程を用いるのが一般的です。多くの場合、BroadPhase では AABB を使ったツリー構造(SAP: Sort and Sweep や BVH: Bounding Volume Hierarchy など) が使われます。

  • BroadPhase: 物理エンジン内で、すべてのコライダーを含む大まかなバウンディングボリューム(AABBなど)を使った階層構造やソートリストを使い、衝突の可能性があるペアのみを洗い出す。
  • NarrowPhase: BroadPhase で絞り込まれたオブジェクト同士に対し、実際のコリジョン形状(カプセル・球・凸ポリ・三角メッシュなど)で正確な衝突を判定する。

この BroadPhase~NarrowPhase の流れがあるので、ユーザが改めて Octree を作って衝突判定を最適化する必要は(通常は)ありません。Rapier が内部で類似のことをすでに行っているためです。

まとめ

  1. 単純に物理演算や衝突判定がしたいだけなら Rapier 側の機能で十分

    • @react-three/rapier の Body や Collider をセットアップすれば、オブジェクト同士の当たり判定・レイキャスト(物理レイキャスト)などがカバーされる。
    • Rapier 自体が高速化のためのデータ構造(BVH など)を内部でもっているので、Octree をユーザー側が用意する場面は少ない。
  2. 次のような場合に Octree が必要になることがある

    • 物理演算とは別の 独自ロジック(レイキャストや検索など)を最適化したい。
    • マップ全体を Rapier に登録するとコストが大きすぎるので、一部だけ物理衝突に使い、残りは軽い当たり判定や探索だけに留めたい。
    • 描画最適化 (フラスタムカリング、Level of Detail、など物理以外の用途) で空間分割を活用したい。
    • Rapier が扱えない 特殊なコリジョン形状やボクセルレベルの判定を自前で行いたい。

このように Rapier を使っているプロジェクトでは、通常は Rapier 側の衝突判定機能があるので、Octree をわざわざ追加で使う機会は多くありません。 しかし、あえて “物理エンジン外” のロジックや、特殊な大規模構造を扱うための空間分割 として、Octree を活用するケースも 稀に あります。