Three.jsをReactで扱いやすくしたライブラリ「React Three Fiber」を使って、3Dカードを作成する方法を紹介します。
全体のコードとデモ
使用するライブラリ
プロジェクト内で必要なライブラリをインストールします
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からコンポーネントを呼び出しているか例外的なものという
理解で大丈夫な気がします。
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表現が実装できそうと感じていただけたら嬉しいです。