21
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社VISIONARY JAPANAdvent Calendar 2024

Day 6

React Three Fiberで宙に浮かぶ3Dカードを作ってみよう

Last updated at Posted at 2024-12-05

Three.jsをReactで扱いやすくしたライブラリ「React Three Fiber」を使って、3Dカードを作成する方法を紹介します。

全体のコードとデモ

image.png

使用するライブラリ

プロジェクト内で必要なライブラリをインストールします

terminal
npm install @react-three/fiber @react-three/drei three
# or 
yarn add @react-three/fiber @react-three/drei three

各ライブラリの大まかな役割は以下の通りです

@react-three/fiber

Three.jsのReactレンダラーです。Three.jsをReactの宣言的な方法で扱うことができます。

@react-three/drei

React Three Fiber用のサポートライブラリで、
よく使われるThree.jsの機能やコンポーネントを簡単に提供します

three

3D表現のコアライブラリです。テクスチャローディングなどで使用します。

実装手順

ベースとなるCanvasの設定

まず、基本となるシーンの設定をします。親コンポーネントに以下のコードを記述します:


function App() {
  return (
    <Canvas camera={{ position: [0, 1, 12], fov: 45 }}>
      <color attach="background" args={['#ffffff']} />
      <ambientLight intensity={1} />
      <directionalLight position={[0, 2, 5]} intensity={1.5} />
      <OrbitControls />
    </Canvas>
  )
}

各要素は以下の用途で使われています。

  • Canvas: Three.jsのレンダリング領域を作成します
  • camera: 3D空間を見る視点を設定します。(デフォルトでは中心の[0,0,0]をみる)
    • position: カメラの位置 [x, y, z]
    • fov: 視野角(Field of View)を度数で指定
  • color: 背景色を設定
  • ambientLight: 環境光(全体の明るさ)
  • directionalLight: 太陽光のような指向性のある光源
  • OrbitControls: マウスによる視点操作を可能にする

デフォルトのThree.js(react-three-fiber)の座標系について:
カメラからみて
x軸: 左右方向(右が+、左が-)
y軸: 上下方向(上が+、下が-)
z軸: 前後方向(手前が+、奥が-)

カードの実装

useLoaderはテクスチャを読み込むことができます。この例ではペンギンの画像を読み込んでいます。
useFrameは1フレーム毎に動作するのでアニメーション処理を行うことができます。

return内の構造がよくわからない場合は
基本的に
mesh →入れ物
geometry →オブジェクトの形
material(材質)→見た目(色や質感)
上記以外の構造のものはdreiからコンポーネントを呼び出しているか例外的なものという
理解で大丈夫な気がします。:v_tone1:

const Card = () => {
  const ref = useRef()
  const texture = useLoader(THREE.TextureLoader, '/doubutu_penguin.png')

  const description = `水中の泳ぐスピードはとても速く
時速40kmに達することもある
これは捕食者から逃げるためや
エサを追いかけるために
発達した能力です
`

  useFrame((state) => {
    const time = state.clock.getElapsedTime()
    ref.current.rotation.y = Math.sin(time / 2) * 0.2
    ref.current.rotation.x = Math.cos(time / 2) * 0.1
  })

  return (
    <group ref={ref} position={[0, 1, 0]}>
      {/* カードの背景 - 枠(角丸) */}
      <RoundedBox position={[0, 0, -0.3]} args={[3.2, 5.2, 0.1]} radius={0.1} smoothness={4}>
        <meshStandardMaterial color="#2d3436" />
      </RoundedBox>

      {/* カードの背景 - メイン */}
      <mesh position={[0, 0, -0.15]}>
        <planeGeometry args={[3, 5]} />
        <meshStandardMaterial color="#34495e" />
      </mesh>

      {/* ヘッダー部分 */}
      <mesh position={[0, 2.1, -0.1]}>
        <planeGeometry args={[3, 0.5]} />
        <meshStandardMaterial color="#3498db" />
      </mesh>

      {/* タイトル */}
      <Text position={[0, 2.1, 0]} fontSize={0.275} color="white" anchorX="center" anchorY="middle" outlineWidth={0.02} outlineColor="#000">
        🐧ペンギン🐧
      </Text>

      {/* 画像部分 - 枠 */}
      <mesh position={[0, 0.5, -0.05]}>
        <planeGeometry args={[2.3, 2.3]} />
        <meshStandardMaterial color="yellow" />
      </mesh>

      {/* 画像部分 */}
      <mesh position={[0, 0.5, 0]}>
        <planeGeometry args={[2.2, 2.2]} />
        <meshStandardMaterial map={texture} />
      </mesh>

      {/* 説明文の背景 */}
      <mesh position={[0, -1.6, -0.05]}>
        <planeGeometry args={[2.8, 1.5]} />
        <meshStandardMaterial color="#2c3e50" />
      </mesh>

      {/* 説明文 */}
      <Text
        position={[0, -1.6, 0]}
        fontSize={0.15}
        color="white"
        maxWidth={2}
        textAlign="center"
        anchorX="center"
        anchorY="middle"
        lineHeight={1.5}
        whiteSpace="pre">
        {description}
      </Text>
    </group>
  )
}

groupコンポーネント

<group ref={ref} position={[0, 1, 0]}>
  • group: オブジェクトをグループ化して移動やアニメーションを可能とする
  • ref={ref}: アニメーションのための参照を設定
  • position: 位置指定

カードの外枠(RoundedBox)

<RoundedBox 
  position={[0, 0, -0.3]} 
  args={[3.2, 5.2, 0.1]} 
  radius={0.1} 
  smoothness={4}
>
  <meshStandardMaterial color="#2d3436" />
</RoundedBox>
  • position: 配置位置
  • args: サイズ指定[幅, 高さ, 厚さ]
  • radius: 角の丸み
  • smoothness: 丸みの品質

メイン背景(mesh)

<mesh position={[0, 0, -0.15]}>
  <planeGeometry args={[3, 5]} />
  <meshStandardMaterial color="#34495e" />
</mesh>
  • planeGeometry: 平面の形状定義
  • args: [幅, 高さ]で指定

タイトル(Text)

<Text
  position={[0, 2.1, 0]}
  fontSize={0.275}
  color="white"
  anchorX="center"
  anchorY="middle"
  outlineWidth={0.02}
  outlineColor="#000"
>
  🐧ペンギン🐧
</Text>
  • position: 最前面(z=0)
  • fontSize: 文字サイズ
  • anchorX/Y: テキストの基準点
  • outline: 縁取りの設定

画像エリア

{/* 画像の枠 */}
<mesh position={[0, 0.5, -0.05]}>
  <planeGeometry args={[2.3, 2.3]} />
  <meshStandardMaterial color="yellow" />
</mesh>

{/* 画像本体 */}
<mesh position={[0, 0.5, 0]}>
  <planeGeometry args={[2.2, 2.2]} />
  <meshStandardMaterial map={texture} />
</mesh>
  • 枠と画像を別々に作成
  • map={texture}: テクスチャの適用(ここではペンギンの画像)

説明文エリア

<Text
  position={[0, -1.6, 0]}
  fontSize={0.15}
  color="white"
  maxWidth={2}
  textAlign="center"
  anchorX="center"
  anchorY="middle"
  lineHeight={1.5}
  whiteSpace="pre"
>
  {description}
</Text>
  • maxWidth: テキストの最大幅
  • lineHeight: 行間
  • whiteSpace="pre": 改行を保持

反射する地面の配置

 ReflectiveGround = () => {
  return (
    <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -2, 0]}>
      <planeGeometry args={[15, 15]} />
      <MeshReflectorMaterial
        blur={[300, 100]}
        resolution={1024}
        mixBlur={0.8}
        mixStrength={0.5}
        roughness={0.3}
        depthScale={1.2}
        color="#fff"
        metalness={0.5}
      />
    </mesh>
  )
}

meshの設定

  • rotation={[-Math.PI / 2, 0, 0]}: 平面を90度回転して床にする
  • position={[0, -2, 0]}: カードの下に配置
  • planeGeometry args={[15, 15]}: 15x15の平面を作成

MeshReflectorMaterialのパラメータ解説

<MeshReflectorMaterial
  blur={[300, 100]}         // ぼかしの強さ [x, y]
  resolution={1024}         // 反射の解像度
  mixBlur={0.8}            // ぼかしの混ざり具合
  mixStrength={0.5}        // 反射の強さ
  roughness={0.3}          // 表面の粗さ(低いほど鏡面的)
  depthScale={1.2}         // 深度スケール
  color="#fff"             // 地面自体の色
  metalness={0.5}          // 金属的な質感の強さ
/>

パフォーマンスの注意点:

  • resolutionは高いほど品質が上がるが処理が重くなる
  • blur値が大きいほど処理負荷が増加

作成したカードと地面を追加

作成したコンポーネントを入れて完成です!

function App() {
  return (
    <Canvas camera={{ position: [0, 1, 12], fov: 45 }}>
      <color attach="background" args={['#ffffff']} />
      <ambientLight intensity={1} />
      <directionalLight position={[0, 2, 5]} intensity={1.5} />
     + <Card />
     + <ReflectiveGround />
      <OrbitControls />
    </Canvas>
  )
}

まとめ

全体のコードは大体100行くらいのコードで3D表現ができました。
はじめてQiita記事を書いたので要領よく書けていない部分もありますが、
React Three Fiberを使うことで、Reactの書き方で手軽に3D表現が実装できそうと感じていただけたら嬉しいです。:relaxed:

21
4
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?