react-three-fiberで自分の部屋を表示してみる

Three.jsはWeb上で3D表現が気軽にでき、過去に何度か触れたことがあります。当時は、Vue.jsまたはReactで構築できるテンプレートが存在せず、環境を自分で構築するか、手続的に書いていくかのどちらかでした。

久々に調べてみると[react-three-fiber](https://github.com/pmndrs/react-three-fiber/releases)という良さげなライブラリを発見しました。
これで[RoomPlan](https://developer.apple.com/jp/augmented-reality/roomplan/)でスキャンした部屋を表示するまでをやってみました。

RoomPlanについては以下の2記事もご参考にどうぞ。

https://qiita.com/kitanote/items/a914069a11503707fd4c

https://qiita.com/kitanote/items/efcc3f03cf1a84703e6c

## 環境構築

```
npx create-react-app your-app-name --template typescript
```

```
npm install three @react-three/fiber @react-three/drei
npm install --save-dev @types/three
```

## 部屋のデータ

今回は以下のデータを使用しました（私の住居のスキャンデータです）

<details><summary>部屋データ（長いので閉じてます）</summary><div>

``` data.ts
export const data = {
  "windows":[],
  "doors":[{"category":{"door":{"isOpen":false}},"confidence":{"high":{}},"dimensions":[0.70346218347549438,1.8716535568237305,0],"completedEdges":[],"parentIdentifier":"91C3F0A6-6696-41AB-967A-325D0798C327","identifier":"22798192-01B7-40D2-AA33-68CEE75821EC","curve":null,"transform":[0,0,1,0,0,1,0,0,-1,0,0,0,1.2999999523162842,-0.66180282831192017,-0.28146862983703613,1]},{"category":{"door":{"isOpen":false}},"confidence":{"high":{}},"dimensions":[0.60060214996337891,1.9556064605712891,0],"completedEdges":[],"parentIdentifier":"D67A37E8-1078-45ED-92AF-A0F9A7FA5CDB","identifier":"188AFD66-F873-4E5C-BFA8-231DB8F07D7E","curve":null,"transform":[1,0,0,0,0,1,0,0,0,0,1,0,0.78633666038513184,-0.61982637643814087,-0.69999998807907104,1]},{"category":{"door":{"isOpen":false}},"confidence":{"high":{}},"dimensions":[0.66166341304779053,2.1284506320953369,0],"completedEdges":[],"parentIdentifier":"B42B3B39-C7DE-43C4-8284-33B2415650D2","identifier":"DB968143-D22A-4B2E-949F-A9E5F9DC31E0","curve":null,"transform":[1,0,0,0,0,1,0,0,0,0,1,0,0.0036210163962095976,-0.53340440988540649,-2,1]},{"category":{"door":{"isOpen":false}},"confidence":{"high":{}},"dimensions":[0.71239167451858521,1.9506680965423584,0],"completedEdges":[],"parentIdentifier":"4F08BBA7-0F66-4390-97CD-7E78612611D3","identifier":"3AAAA40C-14CE-41DA-BFE8-E3686424A46C","curve":null,"transform":[0,0,-1,0,0,1,0,0,1,0,0,0,-0.60000002384185791,-0.62229567766189575,-0.29999998211860657,1]},{"category":{"door":{"isOpen":false}},"confidence":{"high":{}},"dimensions":[0.75842225551605225,1.7383166551589966,0],"completedEdges":[],"parentIdentifier":"1616E478-F2B6-4E90-A464-FF1396A2CC94","identifier":"663C376A-5CFC-4BB3-853F-359CEF5C36E1","curve":null,"transform":[-1,0,0,0,0,1,0,0,0,0,-1,0,0.96780824661254883,-0.72847127914428711,1.1000000238418579,1]},{"category":{"door":{"isOpen":true}},"confidence":{"high":{}},"dimensions":[0.66552311182022095,2.049436092376709,0],"completedEdges":[],"parentIdentifier":"D67A37E8-1078-45ED-92AF-A0F9A7FA5CDB","identifier":"1F73C1D0-C409-4BE6-ABA5-9A912A242EAB","curve":null,"transform":[1,0,0,0,0,1,0,0,0,0,1,0,0.084048442542552948,-0.57291167974472046,-0.69999998807907104,1]}],
  "walls":[{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1.6999999284744263,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"755A0F9A-5371-48E1-AEBC-F6055F211E07","curve":null,"transform":[1,0,0,0,0,1,0,0,0,0,1,0,0.44999998807907104,-0.49142807722091675,-3.2000000476837158,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1.2000000476837158,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"3D19BDDD-496B-4381-AEAF-4BD7EDCF5AC9","curve":null,"transform":[0,0,-1.0000002384185791,0,0,1,0,0,1.0000002384185791,0,0,0,-0.40000000596046448,-0.49142807722091675,-2.5999999046325684,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1.1000000238418579,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"86C0543F-BBD5-4E6C-9D67-3C43202E68EB","curve":null,"transform":[-1,0,0,0,0,1,0,0,0,0,-1,0,-0.050000011920928955,-0.49142807722091675,0.10000000149011612,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1.2999999523162842,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"59B6880F-2BF2-440F-9A6A-6F71A779086D","curve":null,"transform":[0,0,-1,0,0,1,0,0,1,0,0,0,-1.1000000238418579,-0.49142807722091675,-1.3500000238418579,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[0.70000004768371582,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"9401651F-F32D-47FB-87D5-A0403152562C","curve":null,"transform":[0.99999994039535522,0,0,0,0,1,0,0,0,0,0.99999994039535522,0,-0.75,-0.49142807722091675,-2,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"096AD339-9428-4FA7-8072-EBC52279982F","curve":null,"transform":[0,0,1,0,0,1,0,0,-1,0,0,0,0.5,-0.49142807722091675,0.60000002384185791,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[0.89999997615814209,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"B42B3B39-C7DE-43C4-8284-33B2415650D2","curve":null,"transform":[1,0,0,0,0,1,0,0,0,0,1,0,0.049999997019767761,-0.49142807722091675,-2,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[0.60000002384185791,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"DAA835BE-90F4-4A44-B411-5180A2F5AD5C","curve":null,"transform":[0,0,1.0000002384185791,0,0,1,0,0,-1.0000002384185791,0,0,0,0.5,-0.49142807722091675,-1.7000000476837158,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[0.80000001192092896,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"4F08BBA7-0F66-4390-97CD-7E78612611D3","curve":null,"transform":[0,0,-1,0,0,1,0,0,1,0,0,0,-0.60000002384185791,-0.49142807722091675,-0.29999998211860657,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[0.5,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"46FA06C5-56F0-4BCD-A700-C181D48BDCD4","curve":null,"transform":[-1,0,0,0,0,1,0,0,0,0,-1,0,-0.85000002384185791,-0.49142807722091675,-0.69999998807907104,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[0.79999995231628418,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"1616E478-F2B6-4E90-A464-FF1396A2CC94","curve":null,"transform":[-1,0,0,0,0,1,0,0,0,0,-1,0,0.89999997615814209,-0.49142807722091675,1.1000000238418579,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1.7999999523162842,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"91C3F0A6-6696-41AB-967A-325D0798C327","curve":null,"transform":[0,0,1,0,0,1,0,0,-1,0,0,0,1.2999999523162842,-0.49142807722091675,0.20000001788139343,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1.2999999523162842,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"16CD9E81-7B6E-4761-BC93-2FF423FAE7E7","curve":null,"transform":[0,0,1.0000001192092896,0,0,1,0,0,-1.0000001192092896,0,0,0,1.2999999523162842,-0.49142807722091675,-1.3500000238418579,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1.2000000476837158,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"ADC1886C-BA32-4580-9CBC-0D4D533C1DC3","curve":null,"transform":[0,0,0.99999982118606567,0,0,1,0,0,-0.99999982118606567,0,0,0,1.2999999523162842,-0.49142807722091675,-2.5999999046325684,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[0.79999995231628418,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"092DDAF9-A950-4EB0-9B86-9A20F428B20F","curve":null,"transform":[1,0,0,0,0,1,0,0,0,0,1,0,0.89999997615814209,-0.49142807722091675,-2,1]},{"category":{"wall":{}},"confidence":{"high":{}},"dimensions":[1.8999999761581421,2.2124032974243164,0],"completedEdges":[],"parentIdentifier":null,"identifier":"D67A37E8-1078-45ED-92AF-A0F9A7FA5CDB","curve":null,"transform":[1,0,0,0,0,1,0,0,0,0,1,0,0.34999996423721313,-0.49142807722091675,-0.69999998807907104,1]}],
  "openings":[],
  "objects":[
    {
      "dimensions":[0.4962867796421051,0.85711753368377686,0.8464047908782959],
      "transform":[0.99992614984512329,0,0.012144289910793304,0,0,1,0,0,-0.012144260108470917,0,0.99992614984512329,0,0.9520525336265564,-1.1690709590911865,-1.6141649484634399,1],
      "identifier":"2450FA76-8D2A-4348-8235-6DED39B535EB","category":{"toilet":{}},"confidence":{"high":{}}
    },
    {
      "dimensions":[0.48041149973869324,0.30000007152557373,0.3875267505645752],
      "transform":[0.99908584356307983,0,-0.042745564132928848,0,0,1.0000001192092896,0,0,0.042745586484670639,0,0.99908602237701416,0,-0.74199879169464111,-0.7986028790473938,-1.8306409120559692,1],
      "identifier":"FAF093D7-2114-46C5-8FC6-EC47C689C1D8","category":{"sink":{}},"confidence":{"medium":{}}
    },
    {
      "dimensions":[1.1133368015289307,0.51811295747756958,0.82873803377151489],
      "transform":[0.025234300643205643,0,0.99968159198760986,0,0,0.99999994039535522,0,0,-0.99968153238296509,0,0.025234298780560493,0,0.90238362550735474,-1.3385732173919678,-2.6174006462097168,1],
      "identifier":"BDEA9FCE-4A74-4185-932D-4399565A8250","category":{"bathtub":{}},"confidence":{"high":{}}
    }
  ],
  "version":1
}
```

</div></details>

## 表示

![スクリーンショット 2023-02-01 1.18.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510204/8a6e2405-2b28-8c05-c0c0-2adb82bc787a.png)


## 一部コード紹介（床生成）

上の画像では赤色の床が表示されていますが、RoomPlanでスキャンしたデータに床の情報はありません。
全ての壁のバウンディングボックスを取得し、それを使って床データを生成しています。

``` WallsModel.ts
export default class WallsModel {
  public walls: WallModel[]

  constructor(
    walls: any[],
  ) {
    this.walls = walls.map(v => new WallModel(v))
  }

  get boundingBox(): BoundingBoxModel {
    const boundingBox = {
      min: { x: Infinity, y: Infinity, z: Infinity },
      max: { x:-Infinity, y:-Infinity, z:-Infinity },
    }
    const allPoints: any[] = []
    for (const wall of this.walls) {
      const bb = wall.createBoundinBox()
      allPoints.push(bb.min)
      allPoints.push(bb.max)
    }
    for (const point of allPoints) {
      boundingBox.min.x = Math.min(boundingBox.min.x, point.x)
      boundingBox.min.y = Math.min(boundingBox.min.y, point.y)
      boundingBox.min.z = Math.min(boundingBox.min.z, point.z)

      boundingBox.max.x = Math.max(boundingBox.max.x, point.x)
      boundingBox.max.y = Math.max(boundingBox.max.y, point.y)
      boundingBox.max.z = Math.max(boundingBox.max.z, point.z)
    }
    return new BoundingBoxModel(boundingBox.min, boundingBox.max)
  }
  ...
```

そのBoundingBoxからdimensions, transformを生成。
Floorコンポーネントに渡して表示しています。

``` Floor.tsx
    ...
    <mesh
      matrix={matrix}
      matrixAutoUpdate={false} 
    >
      <boxGeometry args={dimensions} />
      <meshStandardMaterial color={FloorModel.Color} opacity={0.7} transparent={true} />
    </mesh>
```

## さいごに

開発環境がすぐ用意でき、開発体験もよく、three.jsが宣言的に書けるreact-three-fiber最高です。
3D開発モデルぐるぐるするだけでも楽しいのでぜひ触ってみてください:cat:


リモデラでは現在エンジニア募集をしています。この記事の内容に少しでも興味を持った方は採用サイトからのお問い合わせ、私のTwitterへのDMなどから気軽にメッセージを送っていただけると嬉しいです。