こんにちは。
株式会社iimonでエンジニアをしている遠藤です。
つい最近年が明けたな〜なんて思っていたら、あっという間にもうすぐ3月になりますね。
3月といえば、新生活に向けて引越しをされる方も多いのではないのでしょうか。
ということで、今回は引越しにちなんで(?)Three.jsで試しに簡単なお部屋を作ってみました。
Three.jsとは
手軽にWebページ上に3Dコンテンツを表示するJavaScriptのライブラリです。
環境構築
普段業務でReactを使用しているので、今回はreact-three-fiber
を使用してReactで実装していきます。
(Node.jsをインストール済みの前提で進めるので、インストールしていない方は先にインストールしてください。)
以下のコマンドでReactのプロジェクトを作成し、ディレクトリを移動してnpm install
します。
npm create vite@latest room-simulator -- --template react-ts cd room-simulator npm install
今回使用するライブラリをインストールします。
npm install three @types/three @react-three/fiber @react-three/drei
ライブラリ | 用途 |
---|---|
@react-three/fiber | threejs用のReactレンダラー |
@react-three/drei | 便利なヘルパーコンポーネント |
開発サーバを立ち上げます
npm run dev
ディレクトリ構成
こんな感じのディレクトリ構成にしていきます。
room-simulator/ │── public/ │ ├── models/ │ │ ├── bed.glb │ │ ├── oven.glb │ │ ├── refrigerator.glb │ │ ├── sofa.glb │ │ ├── table.glb │ │ ├── tv.glb │ ├── textures/ | | ├── wood.jpg │── src/ │ ├── components/ │ │ ├── Floor.tsx │ │ ├── Furniture.tsx │ │ ├── FurnitureMenu.tsx │ │ ├── Room.tsx │ │ ├── Wall.tsx │ ├── App.tsx │ ├── main.tsx │ ├── constants/ │ │ ├── furniture.ts
実装手順
1. 3Dコンテンツを表示するための設定
Three.jsで3Dシーンを表示するためには、以下の三つの基本要素が必要です。
- Scene: 「舞台」のような役割で、3Dオブジェクトや光を配置する空間
- Camera: 「視点」の役割で、どこから3D空間を見るかの設定
- Renderer: Cameraの視点からSceneを画面に描画するもの
@react-three/fiber
を使用する際は、Canvas
コンポーネントをReactツリーに配置することで、レンダリングに必要なSceneとCameraを設定できます。
また、@react-three/drei
のOrbitControls
コンポーネントを追加すると、カメラの視点をマウスやタッチ操作で動かすことができるようになるので入れておきます。
上記を踏まえて、App.tsx
を以下のように編集します。
この状態では、まだ何も要素を配置していないので何も表示されません。
import { Canvas } from "@react-three/fiber"; import { OrbitControls } from "@react-three/drei"; const App = () => { return ( <Canvas style={{ height: "100vh", width: "100vw" }}> <OrbitControls /> </Canvas> ); }; export default App;
2. 部屋の作成
とりあえず、部屋っぽい箱を表示させたいので壁と床の要素を作って組み合わせます。
シーンに3Dオブジェクトを表示させるために、meshを追加します。
meshは、
・geometry: オブジェクトの形
・material: オブジェクトの外観(色や質感)
を組み合わせることで表現できる3Dオブジェクトです。
2-1. 壁の作成
src/
配下にcomponents/
フォルダを作成し、Wall.tsx
というファイルを作成します。
mkdir src/components && touch src/components/Wall.tsx
作成したファイルに以下のコードを貼り付けます。
Wall.tsx
type WallProps = { position: [number, number, number]; size: [number, number, number]; }; const Wall = ({ position, size }: WallProps) => { return ( <mesh position={position}> <boxGeometry args={size} /> <meshLambertMaterial color="white" /> </mesh> ); }; export default Wall;
壁なので、立方体を表現できるboxGeometry
と、マットな質感を表現できるmeshLambertMaterial
というマテリアルを使用してみます。
propsでは壁の位置と大きさを受け取るようにしておきます。
2-2. 床の作成
同様にcomponents/
配下にFloor.tsx
というファイルを作成します。
touch src/components/Floor.tsx
以下のようにコードを書きます。
Floor.tsx
import { useLoader } from "@react-three/fiber"; import { TextureLoader } from "three"; type FloorProps = { position: [number, number, number]; size: [number, number, number]; }; const Floor = ({ position, size }: FloorProps) => { const woodTexture = useLoader(TextureLoader, "/textures/wood.jpg"); return ( <mesh position={position}> <boxGeometry args={size} /> <meshStandardMaterial map={woodTexture} roughness={0.8} /> </mesh> ); }; export default Floor;
壁と同様に立方体を表現できるboxGeometry
を使用します。
せっかくなので、以下のリンクの画像をテクスチャとして使用させていただき、床っぽさを出してみます。
https://ambientcg.com/view?id=WoodFloor051
jpg形式でダウンロードした画像をpublic > textures
配下に置いてファイル名をwoodにします。
こちらもpropsで床の位置と大きさを受け取ります。
2-3. 部屋の作成
壁と床を組み合わせて部屋を作ります。
components
配下にRoom.tsx
を作成します。
touch src/components/Room.tsx
以下のようにコードを書きます。
部屋全体の大きさ(幅x高さx奥行き)をpropsで受け取り、そこから床と四方の壁にそれぞれの位置や大きさを渡します。
Room.tsx
import Floor from "./Floor"; import Wall from "./Wall"; type RoomProps = { // 部屋の幅x高さx奥行きをpropsで受け取る size: { width: number; height: number; depth: number }; }; const Room = ({ size }: RoomProps) => { return ( <> {/** 壁も床も厚さは0 */} <Floor position={[0, -size.height / 2, 0]} size={[size.width, 0, size.depth]} /> <Wall position={[0, 0, size.depth / 2]} size={[size.width, size.height, 0]} /> <Wall position={[0, 0, -size.depth / 2]} size={[size.width, size.height, 0]} /> <Wall position={[size.width / 2, 0, 0]} size={[0, size.height, size.depth]} /> <Wall position={[-size.width / 2, 0, 0]} size={[0, size.height, size.depth]} /> </> ); }; export default Room;
2-4. アプリケーションから部屋を呼び出す
App.tsx
でRoomコンポーネントを呼び出します。
また、このままでは暗いのでambientLight
で全体に光を追加します。
import { Canvas } from "@react-three/fiber"; import Room from "./components/Room"; import { OrbitControls } from "@react-three/drei"; const App = () => { return ( <Canvas style={{ height: "100vh", width: "100vw" }} camera={{ position: [-5, 30, 20] }} > <OrbitControls /> {/* 追加①: 全体に光を当てる */} <ambientLight intensity={1} /> {/* 追加②: Roomを呼び出して部屋を描画 */} <Room size={{ width: 50, height: 10, depth: 25 }} /> </Canvas> ); }; export default App;
こんな感じの箱ができます。
3. 家具や家電の配置
部屋の箱ができたので、家具や家電を置いてみます。
3-1. 3Dモデルの用意
家具・家電の3Dのモデルが必要なので用意します。
今回は以下のサイトからダウンロードさせていただきました。
https://sketchfab.com/feed
ダウンロードしたglbファイルをpublic/models/
配下に配置します。
3-2. 家具・家電の表示
ダウンロードした家具・家電を表示させます。
今回は、家具・家電の表示 / 非表示を切り替えるために画面上にチェックボックスを表示させておこうと思います。
constants/
フォルダを作成し、その配下にfurniture.ts
ファイルを作成します。
mkdir src/constants/ && touch src/constants/furniture.ts
以下のような感じで、先ほどメニューに表示させる家具・家電の名称とmodels/
配下に置いたファイルのpathをマッピングします。
export const FURNITURE_PATH = { 冷蔵庫: "models/refrigerator.glb", オーブン: "models/oven.glb", テーブル: "models/table.glb", ソファ: "models/sofa.glb", テレビ: "models/tv.glb", ベッド: "models/bed.glb", };
components/
配下にFurnitureMenu.tsx
を作成します。
touch src/components/FurnitureMenu.tsx
以下のコードを貼り付けて、チェックボックスをクリックすると該当アイテムのpathが配列に追加・削除されるようにします。
import React, { Dispatch, SetStateAction } from "react"; import { FURNITURE_PATH } from "../constants/furniture"; type FurnitureMenuProps = { setFurniturePathList: Dispatch<SetStateAction<string[]>>; }; const FurnitureMenu = ({ setFurniturePathList }: FurnitureMenuProps) => { const handleChange = (checked: boolean, path: string) => { if (checked) { setFurniturePathList((prev) => [...prev, path]); } else { setFurniturePathList((prev) => prev.filter((furniturePath) => furniturePath !== path), ); } }; return ( <div style={{ position: "fixed", top: 0, left: 0, padding: 10, color: "white", }} > <p>表示する家具・家電</p> {Object.keys(FURNITURE_PATH).map((name) => ( <React.Fragment key={name}> <input type="checkbox" id={name} value={name} onChange={(e) => handleChange( e.target.checked, FURNITURE_PATH[name as keyof typeof FURNITURE_PATH], ) } /> <label>{name}</label> </React.Fragment> ))} </div> ); }; export default FurnitureMenu;
同様にcomponents/
配下にFurniture.tsx
を作ります。
touch src/components/Furniture.tsx
以下のコードで、pathを読み込んで3Dモデルを表示できます。
import { useGLTF } from "@react-three/drei"; type FurnitureProps = { path: string; }; const Furniture = ({ path }: FurnitureProps) => { const { scene } = useGLTF(path); return <primitive object={scene} />; }; export default Furniture;
App.tsx
を以下のように修正します
import { Canvas } from "@react-three/fiber"; import Room from "./components/Room"; import { OrbitControls } from "@react-three/drei"; import FurnitureMenu from "./components/FurnitureMenu"; import { useState } from "react"; import Furniture from "./components/Furniture"; const App = () => { // 追加① 表示中の家具・家電のファイルのpathを管理する状態 const [furniturePathList, setFurniturePathList] = useState<string[]>([]); return ( <> <Canvas style={{ height: "100vh", width: "100vw" }} camera={{ position: [-5, 30, 20] }} > <OrbitControls /> <ambientLight intensity={1} /> <Room size={{ width: 50, height: 10, depth: 25 }} /> {/** 追加② 選択した家具・家電を表示 */} {furniturePathList.map((path, index) => ( <Furniture key={index} path={path} /> ))} </Canvas> {/** 追加③ 表示する家具・家電のチェックメニューの表示 */} <FurnitureMenu setFurniturePathList={setFurniturePathList} /> </> ); }; export default App;
これで、家具・家電を表示することができるようになりました。 FurnitureMenuコンポーネントは3Dのオブジェクトを表示するわけではないので、Canvasの外で呼び出します。
3-3. 家具・家電の位置やサイズの調整
このままだと画面上で家具・家電の位置やサイズの調整ができないので機能を追加します。
Three.jsのTransformControls
というクラスを使用すると、Meshの移動、回転、拡大・縮小が簡単に実装できるようです。
@react-three/drei
のTransformControls
はThree.jsのTransformControls
のラッパーで、これでmeshを囲ってあげます。
また、移動、回転、拡大・縮小のモードがあるので、今回はオブジェクトをクリックするとモードが切り替わるようにします。
Furniture.tsx
を以下のように書き換えました。
import { useGLTF, TransformControls } from "@react-three/drei"; import { useState, useRef } from "react"; type FurnitureProps = { path: string; setIsTransforming: (state: boolean) => void; }; const Furniture = ({ path, setIsTransforming }: FurnitureProps) => { const { scene } = useGLTF(path); const [mode, setMode] = useState< "scale" | "rotate" | "translate" | undefined >(); const transformRef = useRef(null); // クリックで次のモードに切り替える const changeMode = () => { if (mode === "scale") { setMode("rotate"); } else if (mode === "rotate") { setMode("translate"); } else { setMode("scale"); } }; return ( <TransformControls ref={transformRef} mode={mode} showX={mode !== "rotate"} // Y方向にのみ回転できれば良いので、回転の際は非表示 showZ={mode !== "rotate"} // Y方向にのみ回転できれば良いので、回転の際は非表示 onMouseDown={() => setIsTransforming(true)} // 操作開始時にカメラ無効 onMouseUp={() => setIsTransforming(false)} // 操作終了時にカメラ有効 onClick={changeMode} > <primitive object={scene} /> </TransformControls> ); }; export default Furniture;
App.tsx
は、家具・家電を操作している際にカメラが一緒に動かないように修正します。
import { Canvas } from "@react-three/fiber"; import Room from "./components/Room"; import { OrbitControls } from "@react-three/drei"; import FurnitureMenu from "./components/FurnitureMenu"; import { useState } from "react"; import Furniture from "./components/Furniture"; const App = () => { const [furniturePathList, setFurniturePathList] = useState<string[]>([]); // 追加①: 家具・家電の操作中にカメラが動かないように、操作中かどうかを管理する状態 const [isTransforming, setIsTransforming] = useState(false); return ( <> <Canvas style={{ height: "100vh", width: "100vw" }} camera={{ position: [-5, 30, 20] }} > {/** 追加②: 操作中はOrbitControlsを無効にする */} <OrbitControls enabled={!isTransforming} /> <ambientLight intensity={1} /> <Room size={{ width: 50, height: 10, depth: 25 }} /> {furniturePathList.map((path, index) => ( <Furniture key={index} path={path} // 追加③ 状態をセットする関数をpropsで渡す setIsTransforming={setIsTransforming} /> ))} </Canvas> <FurnitureMenu setFurniturePathList={setFurniturePathList} /> </> ); }; export default App;
これで表示した家具・家電を画面上で動かせるようになりました。
家具・家電をコネコネして最終的にこんな感じの部屋になりました。
まとめ
家具や家電が扱いづらかったり雑なところもありますが、一応簡単なお部屋を作ることができました。
初めてThree.jsを触ったので、最初はあまり実装のイメージが湧かなかったのですが、単純なものであれば結構簡単に表示できるものなのだなあと感動しました。
ユーザーが画面上でもう少し部屋や家具の色や柄などデザインを弄れるようにしたり、作成したデータを保存できるようにするなど追加したかった機能が色々とあるので、気が向いたらまたやってみようと思います。
最後まで読んでくださりありがとうございます。
また、現在弊社ではエンジニアを募集しています!
この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!
iimon採用サイト / Wantedly / Green