React Three Fiberで動く3Dアバター

Three.js vs R3F コード比較

従来のThree.js(命令的)
DOMの生成からループ処理まで、全て自分で指示しないといけません。

// 😫 Three.jsの場合:準備が大変!
import * as THREE from 'three'

// 1. シーン、カメラ、レンダラーを作る(場所作り)
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
const renderer = new THREE.WebGLRenderer()
document.body.appendChild(renderer.domElement) // DOMに手動で追加

// 2. 箱を作る(メッシュ作成)
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshStandardMaterial({ color: 'orange' })
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh) // シーンに追加命令

// 3. 光を足す
const light = new THREE.AmbientLight()
scene.add(light) // これも追加命令

// 4. アニメーションループ(毎フレームの指示)
function animate() {
  requestAnimationFrame(animate)
  mesh.rotation.x += 0.01 // 回転命令
  mesh.rotation.y += 0.01
  renderer.render(scene, camera) // 描画命令
}
animate() // 実行!

React Three Fiber(宣言的)
「完成図(構造)」を書くイメージです。Reactコンポーネントとしてタグを並べるだけで、R3Fが裏で勝手にセットアップしてくれます。

// R3Fの場合:コンポーネントを置くだけ!
import { Canvas } from '@react-three/fiber'

export default function App() {
  return (
    // 1. Canvasを置けば、シーン・カメラ・レンダラーが自動完成
    <Canvas>
      {/* 2. 光を置く */}
      <ambientLight />
      
      {/* 3. 箱を置く(Meshの中にGeometryとMaterialを入れるだけ) */}
      <mesh rotation-x={0.01} rotation-y={0.01}>
        <boxGeometry />
        <meshStandardMaterial color="orange" />
      </mesh>
    </Canvas>
  )
}

開発環境の準備:Viteで爆速構築&R3Fの『三種の神器』をインストール

  • Create React App(CRA)ではなくViteで:シェーダーや3Dモデルとの相性が良い
  • 三種の神器その1:three本体
  • 三種の神器その2:@react-three/fiber (R3F)
  • 三種の神器その3:@react-three/drei (便利な道具箱)

特に Drei が重要です!これがないと、例えばカメラを動かすだけでも数十行のコードを書く羽目になりますが、Dreiがあれば<OrbitControls /> 1行で終わります!

//ベースを作る (Vite)
npm create vite@latest プロジェクト名 -- --template react

cd プロジェクト名

npm install

//R3Fの「三種の神器」をインストール
npm install three @types/three @react-three/fiber @react-three/drei

「Canvasの罠」を防ぐCSS設定
*3D空間(Canvas)は親要素の大きさに合わせて広がります。なので、大元の html や body に高さ100%を指定しておかないと、ペチャンコになったりします。

/* src/index.css */
* {
  box-sizing: border-box;
}

html,
body,
#root {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden; /* スクロールバーを出さない*/
}

コラム:Canvasは「液体」のようなもの?

実は 従来のThree.js: 画面サイズが変わるたびに「カメラの比率を直して!」「再描画して!」と命令を書く必要がありました(超面倒くさい!)。
R3FのCanvas:置かれた場所(親要素)のサイズに合わせて、中身を全自動でリサイズしてくれます
まるで「器に合わせて形を変える水」のような性質です。 だからこそ、器となる親要素(bodyやdiv)に height: 100% を指定してあげないと、Canvasは高さ0になって消滅してしまうのです。

Canvasの「三つの設定」

① dpr(描画解像度)
② gl(WebGLレンダラーの設定)
③ camera(カメラの初期設定)

camera(カメラの初期設定)

Canvasを置いただけだと、カメラは座標 [0, 0, 0](世界の中心)にいます。 もし3Dモデルも [0, 0, 0] に置くと、カメラがモデルの中に埋まってしまい、画面が真っ暗(または内側が見える)になります!

カメラを少し後ろに下げてあげるのが鉄則
position: [横, 高さ, 奥行き](5くらい下げると丁度いいことが多い)
fov: 視野角(ズーム具合)。50〜75が一般的。

<Canvas camera={{ position: [0, 0, 5], fov: 50 }}>

dpr(描画解像度 / Device Pixel Ratio)

最近のスマホは画質が良すぎます(Retinaなど)。これをそのまま真面目に描画すると、スマホがアツアツになって動きがカクカクします。
R3Fには「最低1倍、最大2倍まで」という賢い設定があります。これで十分きれいで、かつ重くなりすぎません。

<Canvas dpr={[1, 2]}>

gl(レンダラー設定 / 色味と光)

Blenderで見た色と、「Webで表示した色が違う!なんか白っぽくて薄い…」 これは、色の計算方法(トーンマッピング)の設定がデフォルトのままだから起きることが多いです。
「くっきり鮮やか」に見せたい場合などは、ここで設定をいじります。

また、影(Shadows)を有効にするのもここです。

<Canvas
   shadows // 影を有効化
   gl={{ antialias: true, toneMapping: THREE.ACESFilmicToneMapping }}
>

3Dモデルの配置と設定(argsとprops)

argsとpropsの決定的違い

Reactのコンポーネント(例えばボタン)だと、color も size も全部ただの「props」として扱いうが、R3F(Three.js)では、この2つは「生まれ(args)」と「育ち(props)」くらい別物

① 生まれる瞬間 (args) 、身長や骨格が決まります。 これは後から簡単には変えられません。変えるなら、「生まれ変わる(作り直し)」必要があります。 これが args です。

② 生まれた後 (props) 生まれた後に、「赤い服を着る(color)」「右に移動する(position)」「横を向く(rotation)」ことは、いつでも自由に変えられます。 これが props(属性)です。

// 1. args: 生まれる瞬間 (new する時のカッコの中身!)
const geometry = new THREE.BoxGeometry(1, 2, 3); 
// ↑ 幅1, 高さ2, 奥行3 で生まれる

// 2. props: 生まれた後の設定 (ドットで繋いで代入)
const mesh = new THREE.Mesh(geometry, material);
mesh.position.x = 5; // 右へ移動
mesh.rotation.y = 1.5; // 回転
mesh.visible = true; // 表示する
  • geometry のサイズ → args で OK(初期値だから)
  • mesh や material の色・位置・回転 → 属性(prop)として直接書く方が正しい
// 全部タグの中に書くけど、裏側の役割が違う!
<mesh 
  position={[5, 0, 0]} // props: 後から設定できるもの
  rotation={[0, 1.5, 0]} // props: 後から設定できるもの
>
  <boxGeometry 
    args={[1, 2, 3]} // args: new BoxGeometry(1, 2, 3) になる!
  />
</mesh>

Three.jsの公式ドキュメントを見て Constructor と書いてある部分が args になり、 Properties と書いてある部分がそのままタグの属性(props)になります!

propsの変更(例:色を変える)は 軽い処理です。
argsの変更(例:箱のサイズを変える)は 重い処理です。 いったん今の箱を破壊して、メモリから消して、新しいサイズの箱を new し直します。

【教訓】アニメーションで毎フレーム args を変えると、再生成が繰り返されてパソコンが爆熱になるから気をつけよう!

便利技:position-xなどの短縮記法

「X座標だけちょっとズラしたい」という時、わざわざ [x, y, z] 全部を書かなくても、ハイフン繋ぎで 「中身のプロパティを直接ピンポイントで書き換える」 ことができます。

普通の書き方: position={[10, 0, 0]} → 「住所をまるごと書き換える」イメージ(丁寧だけど、YとZも指定しないといけない)。

便利な書き方:position-x={10} → 「番地だけ書き換える」イメージ(スマート!)。

rotation-z scale-y なども同様に使えます。

groupを使った親子関係の整理

  • <group> = (親)電車のイメージで、position={[100, 0, 0]}
    電車が東京から大阪へ移動
  • <mesh>= (子)乗客のイメージで、position={[1, 0, 0]}
    乗客は「電車の中(ローカル座標)」で、席を1メートル移動

*乗客の「世界での位置(グローバル座標)」は 101 になりますが、乗客のコードを書くときは「電車の中でどこか?」だけを気にすればOK

group(置き場所)と、中身の微調整(オフセット)は分けると便利です

<group position={[5, 0, 5]}> {/* ① 親:ワールド上の「配置場所」を決める */}
  
  <mesh position-y={1.5}>    {/* ② 子:地面からの「高さ(オフセット)」を調整する */}
    <boxGeometry />
    <meshStandardMaterial color="red" />
  </mesh>

</group>

ライトと影

R3Fのライトの種類はThree.jsと同じで、オプション(props)も全部Three.jsと同じです。

//(環境光):全体をぼんやり明るくする。これがないと影の部分が真っ黒になる。
<ambientLight intensity={0.5} />

//(平行光源):太陽光のような強い光。これがないと影(Shadow)が落ちない。
<directionalLight position={[10, 10, 5]} intensity={1} />

影の基本ルール(超ざっくり)

  • Canvas が「影を計算する」モードになっている → shadows をオン
  • 光が「影を落とすライト」になっている → castShadow
  • オブジェクトが「影を落とす/受ける」設定になっている → castShadow / receiveShadow
<Canvas
      shadows // ← Canvas全体で影を有効にする
      camera={{ position: [3, 3, 6], fov: 45 }} // 適当な視点
    >
      {/* 全体をほんのり明るくする補助光 */}
      <ambientLight intensity={0.3} />

      {/* ★ ② 影を落とすライト(directionalLight の例) */}
      <directionalLight
        position={[5, 5, 5]} // 右上後ろくらいから当てる
        intensity={1.2}      // 明るさ
        castShadow           // ← このライトで影を計算する
        shadow-mapSize-width={1024}  // 影テクスチャの解像度(横)
        shadow-mapSize-height={1024} // 影テクスチャの解像度(縦)
        shadow-camera-near={1}       // 影に使うカメラの手前
        shadow-camera-far={20}       // 影に使うカメラの奥
      />

      {/* ★ ③ 影を「落とす」オブジェクト */}
      <mesh
        position={[0, 1, 0]} // 台から少し浮かせる
        castShadow           // ← このメッシュが影を落とす
      >
        <boxGeometry args={[1, 1, 1]} />
        <meshStandardMaterial color="hotpink" />
      </mesh>

      {/* ★ ④ 影を「受ける」床 */}
      <mesh
        rotation-x={-Math.PI / 2} // 平面を横倒し(床にする)
        position={[0, 0, 0]}      // 原点に床
        receiveShadow             // ← ここに影が落ちる
      >
        <planeGeometry args={[10, 10]} />
        <meshStandardMaterial color="#555555" />
      </mesh>
    </Canvas>

自分でライトを配置するのは、スタジオに電球を一個ずつセットするようなもので大変です。 Dreiの を使うと、『いい感じの風景画像(HDRI)』で全体を包み込み、リアルな反射と光を一瞬で作ってくれます。
また、標準の影設定(castShadow とか receiveShadow とか)を使わなくても、Dreiで爆速!プロ級の『光と影』の設定ができます。

便利な道具箱『Drei』

R3F でよく使う便利な3Dコンポーネントやツールをまとめたパック。

モデルの読み込み (useGLTF)

GLBファイルをキャッシュ付きで効率よく読み込む。

const { scene } = useGLTF('/model.glb')
return <primitive object={scene} />

カメラ操作

基本はこれ:<OrbitControls />
置くだけで、マウスでの「回転・ズーム・移動」が可能になる。

発展:<CameraControls />
プログラムでカメラを動かせる(例:ボタンを押したら顔にズームする、など)。

import { OrbitControls} from '@react-three/drei'

<OrbitControls />

光と影(EnvironmentとContactShadows)

3Dシーン全体に “背景の光” と “環境光の反射” を与える HDRI 環境マップ。
Environment は 環境光 なので基本はこれだけでも明るい。

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

<Environment preset="sunset" />

//背景を Environment にする(スカイボックス的)
<Environment preset="sunset" background />

//カスタム HDRI を使う
<Environment files="/textures/studio.hdr" background />

//明るさを変える
<Environment preset="sunset" environmentIntensity={0.8} />

よく使う preset 一覧()

  • sunset 暖かいオレンジの夕日
  • city 都会の反射、製品表示向き
  • dawn 暗めの青朝景
  • forest 緑の樹々に囲まれた柔らかい光
  • studio プロ用写真スタジオ(製品表示の鉄板)
  • warehouse 工房っぽい反射が映える
  • apartment 室内の柔らかいライティング
  • park 野外の自然
  • lobby モダンな屋内反射

ContactShadows
設定不要。足元のふんわりした影を自動生成!

//Canvas に shadows は不要
<Canvas camera={{ position: [0, 2, 5] }}>
  
  {/* ① 光:これだけでリアルな環境光 */}
  <Environment preset="city" />

  {/* ② 影:これだけで足元にふんわり影が出る(設定不要!) */}
  <ContactShadows 
    opacity={0.5}   // 影の濃さ
    scale={10}      // 影の範囲
    blur={2}        // ぼかし具合
    far={10}        // どのくらいの高さまで影を計算するか
    resolution={256} // 解像度(軽くするなら下げる)
    color="#000000" // 影の色
  />

  {/* モデル(castShadowとか書かなくていい!) */}
  <Model />
  
</Canvas>

3D空間にHTMLを置く(Html)

import { Html } from "@react-three/drei";

<Html position={[0, 2, 0]} center>
  <div className="tooltip">Happy Birthday!</div>
</Html>

他にも Stars(星空)やSparkles(キラキラ)など、面白い機能がたくさんある。

モデルのロードと表示

useGLTFフックとSuspense

React Three FiberでGLTF/GLBモデルを読み込む際は、Dreiの useGLTF フック を使うのが基本です。

ただし、3Dモデルは画像などと違ってデータ容量が大きく、読み込みに時間がかかります(非同期処理)。 そのため、読み込みが終わるまでReactが困らないように、Suspense 内に配置する必要があります。

// MyModel.jsx
import { useGLTF } from "@react-three/drei";

export function MyModel(props) {
  const gltf = useGLTF("/models/my-model.glb"); // モデルを非同期で読み込む
  return <primitive object={gltf.scene} {...props} />;
}

// モデルを事前読み込み(任意)
useGLTF.preload("/models/my-model.glb");
import { Canvas } from "@react-three/fiber";
import { Suspense } from "react";
import { Stage, OrbitControls } from "@react-three/drei";
import { MyModel } from "./MyModel";

export default function App() {
  return (
    <Canvas camera={{ position: [0, 1.5, 4], fov: 45 }}>
      {/* モデル読み込み待ち */}
      <Suspense fallback={null}>
        {/* ライト・影・環境光は Stageで */}
        <Stage
          environment="city"   // 環境マップ("studio" "sunset" なども)
          intensity={0.7}      // 全体の明るさ
          adjustCamera         // モデルに合わせてカメラをいい感じにする
          shadows="contact"    // 足元の影("contact" or true など)
        >
          <MyModel scale={[1, 1, 1]} position={[0, 0, 0]} />
        </Stage>
      </Suspense>

      {/* カメラ操作はモデルロードと関係ないので外に出してOK */}
      <OrbitControls makeDefault />
    </Canvas>
  );
}

モデルの表示方法1:primitiveのシンプルパターン

React Three Fiber のオブジェクトではなく、three.js のオブジェクトを直接扱う。
モデルを 「そのまま配置するだけ」なら、この方法がシンプル。

primitiveは、読み込んだ three.js のシーンオブジェクト(gltf.scene)をそのまま置くためのタグ


<primitive object={gltf.scene} {...props} />

{/*
<MyModel position={[0,0,0]} scale={[1,1,1]} /> と呼び出したら、
その値がそのまま <primitive> に渡る。
props は
{
  "position": [0, 0, 0],
  "scale": [1, 1, 1]
}
*/}

モデルの表示方法2:gltfjsxで3DモデルをReactコンポーネントに変換する

モデル内部のパーツを細かく操作したい場合に最適

gltfjsx とは、GLTF/GLB モデルの内部構造(nodes / materials)をReact コンポーネントとして扱いやすい形に変換してくれるツールです。

  1. Blenderから書き出した .glb ファイルを、プロジェクトの public フォルダに入れます。
  2. ターミナルで以下のコマンドを叩きます。
// --transform: データを圧縮して軽量化します(Draco圧縮)。Webで表示するなら必須!
// --shadows: 「影を落とす・受ける」設定(cast/receive)を最初からコードに書いてくれます。

npx gltfjsx public/model.glb --transform --shadows

// -o(どこに出力するか)指定しない場合はコンソールに出力
npx gltfjsx ./public/pc.glb -o src/components/PcModel.jsx

生成するコードのイメージ

import { useGLTF } from "@react-three/drei";

export function Model(props) {
  const { nodes, materials } = useGLTF("/models/pc.glb");

  return (
    <group {...props}>
      <mesh geometry={nodes.Screen.geometry} material={materials.ScreenMat} />
      <mesh geometry={nodes.Keyboard.geometry} material={materials.KeysMat} />
      <mesh geometry={nodes.Body.geometry} material={materials.BodyMat} />
    </group>
  );
}

useGLTF.preload("/models/pc.glb");

useGLTF の中身はどうなっている?

gltfjsx が生成したコードを見ると、nodes や materials という単語が出てきます。これは useGLTF() が読み込んだデータの中身を分解しています。

  • nodes: 3Dモデルのパーツ(骨、頭、服など)が全部入った辞書。「名前」で指名して取り出せます。
  • materials: 色や質感のデータ。
  • animations: アニメーションデータ。

従来の scene(モデル全体)をポンと置く方法だと細かいパーツがいじれませんが、nodes を使えば「右腕だけ消す」「服の色だけ変える」といったことがコード上で自由にできるようになります。

【重要】Blenderで1つだったのに、勝手に分割される?
Blenderでは「1つのオブジェクト」に「複数のマテリアル(肌、服、金具など)」を割り当てることができます。 しかし、GLTFのルールでは「1つのマテリアルにつき、1つのメッシュ」が原則です。
そのため、書き出し時に自動的にパーツが分割されることがあります。

gltf.pmnd.rs(Web版)

ブラウザで GLB をドラッグ&ドロップすると、React コードが生成される
→ すぐに使えるのがメリット!

アニメーション

まずは「操作ハンドル」を握る (useRef)

ReactでDOMを触る時と同じです。3Dモデルを動かすにも、まずは「掴む」必要があります。

役割: 3Dオブジェクト(など)の実体にアクセスするためのフック。

import { useRef } from 'react'

// 1. ハンドル(Ref)を作成
// 最初は空っぽなので null を入れておくのがお作法
const groupRef = useRef(null)

return (
  // 2. 3Dモデル(group)にハンドルを取り付ける
  <group ref={groupRef}>
    
    {/* ここにモデルの中身(メッシュなど)が入る 
      これで、groupRef.current を通じて
      Three.jsの実体(位置、回転、サイズなど)を
      直接いじれるようになります!
    */}
    
  </group>
)

groupRef.current に実体が入るのは、「画面に描画された後(マウント後)」 です。 コンポーネントの関数のなかでいきなり console.log(groupRef.current) と書いても、まだ中身は null(空っぽ)です。

実際に中身を触るのは、

  • useEffect(() => { … }, []) の中(マウントした直後)
  • useFrame(() => { … }) の中(アニメーション中)
  • onClick などのイベントの中

のどれかになります!

Blenderのアニメーションを再生する (useAnimations)

役割: GLBファイルに含まれている「キーフレームアニメーション(Walk, Idleなど)」を再生する専用フック。
gltfjsx が animations というデータを読み込んでくれます。
これを useAnimations に渡すと、actions (リモコン)が返ってきます。

const { actions } = useAnimations(animations, group)
// actions['Idle'].play() で再生できる!

ロジックで動かす・切り替える (useEffect / useFrame)

useEffect(アクションの切り替え)

親(App)から、子(Avatar)へ、<Avatar animation="Run" /> のようにアニメーション名をPropsで渡す

export function Avatar({ animation }) {
  const { actions } = useAnimations(animations, group)

  // ④ propsの中身が変わるたびに、useEffectが発動して動きが切り替わる
  useEffect(() => {
    // さっきのクロスフェードのロジックがここで動く!
    actions[animation]?.reset().fadeIn(0.5).play()
    // ...
  }, [animation]) // ← ここで監視している!

  return <primitive object={scene} />
}

R3Fのアニメーション切り替えに汎用的に使えて便利なコード

import React, { useEffect, useRef } from 'react'
import { useGLTF, useAnimations } from '@react-three/drei'

// 親コンポーネントから props で "Run" や "Idle" などの文字列を受け取る
export function Avatar({ animation }) {
  const group = useRef()
  // 1. モデルとアニメーションをロード(パスは自分のファイルに合わせて)
  const { scene, animations } = useGLTF('/avatar.glb')
  // 2. アニメーションを取り出す(actions辞書を作る)
  const { actions } = useAnimations(animations, group)

  // 3. アニメーション切り替えのロジック(ここが心臓部!)
  useEffect(() => {
    // 次に再生したいアクションを取得
    const nextAction = actions[animation]
    
    // 安全策:アクションが見つからなければ何もしない
    if (!nextAction) return

    // --- クロスフェード処理 ---
    
    // A. 「現在動いている他のアクション」をじわっと消す
    Object.values(actions).forEach((action) => {
      if (action !== nextAction && action.isRunning()) {
        action.fadeOut(0.5)
      }
    })

    // B. 「次のアクション」をじわっと開始
    // reset()で頭出しして、fadeInで滑らかに再生
    nextAction.reset().fadeIn(0.5).play()

    // C. クリーンアップ(コンポーネントが消える時などにフェードアウト)
    return () => {
      nextAction.fadeOut(0.5)
    }

  }, [animation, actions]) // ← propsのanimationが変わるたびに発動!

  return (
    <group ref={group} dispose={null}>
      <primitive object={scene} />
    </group>
  )
}

useFrame(毎フレームの処理)

useFrameは「毎フレームごとに実行される関数」を登録する R3F のフック。
アニメーション・動き・カメラ制御など“常に変化する処理”を書く場所。
requestAnimationFrame と同じ(60fps)。

useFrame((state, delta) => {

})

state:R3F の内部状況(シーン・カメラ・時計など)が全部入ってる。

  • state.camera(カメラ)
  • state.clock.elapsedTime(経過時間)
  • state.pointer.x / y(マウス位置)
  • state.viewport(画面サイズ)
  • state.scene
  • state.gl(WebGLRenderer)

delta:前フレームからの経過秒数(1/60秒前後)

  • 値が安定するのでアニメが滑らかになる
  • += delta が基本の動き方

備考
delta=1フレームごとの差。動く速さを一定にしたいとき。
state.clock.elapsedTime=積み上がる時間。周期や波の動きに使う。

useRefで3D オブジェクトをつかむ → useFrame で毎フレーム動かす → ref を付けた groupやmesh が動く

import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'

export function FloatingText() {
  const groupRef = useRef()

  // 毎フレーム(1/60秒ごと)実行されるループ処理
  useFrame((state, delta) => {
    // 1. 回転(deltaを使う)
    // PCの性能差が出ないように、経過時間(delta)を掛けて一定速度にする
    groupRef.current.rotation.y += delta * 0.3

    // 2. 上下運動(elapsedTimeを使う)
    // 時間(t)経過に合わせて sin波 でゆらゆらさせる
    // state.clock.elapsedTime = アプリ起動からの総秒数
    groupRef.current.position.y = Math.sin(state.clock.elapsedTime * 0.8) * 0.2
  })

  return (
    <group ref={groupRef}>
      <mesh>
        <boxGeometry />
        <meshStandardMaterial color="orange" />
      </mesh>
    </group>
  )
}

補足: 「Reactのコンポーネントは通常、データが変わった時だけ再描画されますが、useFrame の中だけは『1秒間に60回、強制的に呼び出される特区』です。だからここでState(useState)を更新すると、Reactが過労死(無限再レンダリング)するので絶対NGです!」

💡 ふわふわさせたいとき
useFrame と Math.sin を使ってふわふわさせるよりも、実は Drei には<Float> というそのものズバリなコンポーネントがあります。

import { Float } from '@react-three/drei'
<Float 
  speed={2} // アニメーションの速さ 
  rotationIntensity={1} // 回転のふわふわ具合 
  floatIntensity={2} // 上下のふわふわ具合
>
   <mesh>
       <boxGeometry />
       <meshStandardMaterial color="orange" />
   </mesh>
<Float/>

CameraControls で演出をつける

CameraControlsは、OrbitControlsのようにユーザーも動かせるし、コードから命令もできる。
必須メソッド setLookAt

import { useRef } from 'react'
import { CameraControls } from '@react-three/drei'

export default function App() {
  // 1. カメラのハンドル(Ref)を作る
  const cameraRef = useRef()

  // 2. 演出用の関数
  const zoomToFace = () => {
    // cameraRef.current?.setLookAt(
    //   カメラの位置X, Y, Z,
    //   見る対象の位置X, Y, Z,
    //   true // ← これをtrueにするとアニメーションする!(重要)
    // )
    cameraRef.current?.setLookAt(0, 1.5, 2, 0, 1.5, 0, true)
  }

  const resetCamera = () => {
    // 引いて全体を見る
    cameraRef.current?.setLookAt(0, 2, 5, 0, 1, 0, true)
  }

  return (
    <>
      <Canvas>
        {/* OrbitControlsの代わりにこれを置く! */}
        <CameraControls ref={cameraRef} />
        
        {/* ...モデルやライト... */}
      </Canvas>

      {/* 操作ボタン */}
      <div style={{ position: 'absolute', top: '10px', left: '10px' }}>
        <button onClick={zoomToFace}>顔を見る</button>
        <button onClick={resetCamera}>全体を見る</button>
      </div>
    </>
  )
}