TypeScriptのReact上でthree.js情報をWEB Socket同期させる

HALアドベントカレンダー4日目を担当する、HAL東京IT学部2年のゆうたろうです。

最近勉強したフロントエンドに関する技術をテーマに記事を書いてみます。

## TypeScriptのReactでthree.js

Reactで WEB GLをthree.jsを使って描画させるときに使うライブラリは*react-three-renderer*だと思うのですが、このライブラリをTypeScriptで使おうと思っても型定義ファイルが用意されていなくてエラーを起こしてしまいます。そこで、代わりになるものを探していたところGitHub上に*react-three-renderer-fiber*というリポジトリを見つけて、TypeScriptでも*react-three-renderer*みたいなものを使えるようにしてくれるものがあったのでこれを活用していきます。

```shell
git clone https://github.com/toxicFork/react-three-renderer-fiber
cd react-three-renderer-fiber
yarn install
cd example
yarn install
cd ../
yarn start
```

上記のコマンドを実行すると、localhost:8080にWEBサーバが建てられてブラウザでアクセスするとthreejsのサンプルをいろいろ見ることができると思います。

今回はこのサンプルの中の*experiment*というサンプルを使ってWEB Socketを実装していきたいと思います。

## Socket.ioとthree.jsの連携

WEB Socketを実装するときはSocket.ioというライブラリを使うのがポピュラーなのでこれを使っていきます。

最近AWSのApp SyncでWEB Socketのフルマネージドサービスがリリースされるというニュースをみて、ぜひこれを使ってみたいのですが、まだリリースされていないので普通に自分でサーバを建ててWEB Socketを実装していこうと思います。

### プロジェクト構成

```shell
mkdir SocketIoServer
cd SocketIoServer
yarn init -y
yarn add express socket.io
toutch index.js
```

```js:index.js
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var express = require('express');

app.use(express.static(__dirname + '/react-three-renderer-fiber'));
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/react-three-renderer-fiber/examples/experiment.html');
});

io.on('connection', (socket) => {
  console.log('a user connected');
  socket.on('disconnect', () => {
    console.log('user disconnected');
  });
  socket.on('senddata', (data) => {
    io.emit('recevedata', msg);
  });
});
http.listen(3000, function(){
  console.log('listening on *:3000');
});
```

expressで先ほどgit cloneした*react-three-renderer-fiber*をアクセスできるようにホスティングさせたいので*SocketIoServer*ディレクトリ配下に移動させます。



現段階のディレクトリ構成が以下の通りになります。

```
SocketIoServer/
 ├ node_modules/
 │　└ (yarn addした内容)
 ├ react-three-renderer-fiber/
 │　└ (リポジトリの内容)
 ├ index.js
 ├ package.json
 └ yarn.lock(yarnを使っていた場合)
```

### サンプルのカスタマイズ
expressのホスティングの関係でパス関係が変わったので、それに合わせて*socketServer/react-three-renderer-fiber/examples/experiment.html*のなかに記述されている依存関係のファイルとメインの*experiment.js*のパスを変えます。

```html:experiment.html
<html>
<head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
</head>
<body>
<div id="example"></div>

<!-- Dependencies -->
<script src="/examples/node_modules/react/umd/react.development.js"></script>
<script src="/examples/node_modules/react-dom/umd/react-dom.development.js"></script>

<!-- Main -->
<script src="/examples/dist/experiment.js"></script>
</body>
</html>
```

Socket.ioサーバと通信するときは、*socket.io-client*を使っていくので*SocketIoServer/react-three-renderer-fiver/examples/*にインストールします。

```shell
yarn add socket.io-client
yarn add --dev @types/socket.io-client
```



次に*SocketIoServer/react-three-renderer-fiver/examples/src/Experiment.tsx*を編集してSocket.ioと通信できるようにしていきます。

```tsx:Experiment.tsx
import * as React from "react";
import {Component} from "react";
import * as ReactDOM from "react-dom";
import * as THREE from "three";
import React3 from "../../../src";

import {connect} from "socket.io-client";

import ColorCube from "./ColorCube";
```

最初にsocket.io-clientのconnectをインポートします。

```tsx:Experiment.tsx
private readonly cameraPosition: any;
private readonly onAnimate: (callback: any) => any;
private rafRequest: number;
private renderer: any;
private scene: any;
private camera: any;
private animateInterval: number;
private socketIo: SocketIOClient.Socket;
```

次にプロパティにsocketIoを追加します。

```tsx:Experiment.tsx
this.renderer = null;
this.scene = null;
this.camera = null;
this.rafRequest = 0;
this.socketIo = connect("localhost:3000");
```

最後にconstructorの中でsocket.Ioプロパティの値を代入します。

### socket.ioでリアルタイムにデータ送受信する

socket.ioでは受信に*on*メソッド送信に*emit*メソッドを使います。

```tsx:Experiment.tsx
const cube: any = <ColorCube
                        rotation={this.state.cubeRotation}
                        height={this.state.height}
                        width={this.state.width}
                      />;

if (this.state.wantsResult) {
	testResult = <div key="result">Yay</div>;
} else {
    react3 = <div>
        <button onClick={this.addData}>change</button>
        <React3>
          <webGLRenderer
            ref={this.rendererRef}

            width={width}
            height={height}
          >
            <scene
              ref={this.sceneRef}
            >
              <perspectiveCamera
                fov={75}
                aspect={width / height}
                near={0.1}
                far={1000}

                position={this.cameraPosition}

                ref={this.cameraRef}
              />
              {cube}
            </scene>
          </webGLRenderer>
        </React3>
        </div>;
}

```

cube定数の中を編集し、buttonタグを追加して、Socket.ioへのデータ送信ボタンとして使います。



```tsx:Experiment.tsx
public addData = () => {
    const randomHeight: number = Math.floor(Math.random() * Math.floor(3));
    const randomWidth: number = Math.floor(Math.random() * Math.floor(3));

    this.setState({
      height: randomHeight,
      width: randomWidth,
    });

    this.socketIo.emit("senddata", {
      height: randomHeight,
      width: randomWidth,
    });
  };
```

addDataメソッドを追加します。

```tsx:Experiment.tsx
  public componentDidMount() {
    console.log("mounted");

    this.renderer.render(this.scene, this.camera);

    this.animateInterval = window.setInterval(() => {
      this.onAnimate(() => {
        if (this.rafRequest === 0) {
          this.rafRequest = requestAnimationFrame(renderFunction);
        }
      });
    }, 20);

    const renderFunction = () => {
      this.renderer.render(this.scene, this.camera);

      this.rafRequest = 0;
    };

    this.socketIo.on("recevedata", (data: any) => {
      this.setState({
        height: data.height,
        width: data.width,
      });
    });
  }
```

componentDidMountメソッドの中で、データを受信した時の処理を書きます。

```tsx:Experiment.tsx
//state型定義  
public state: {
    cubeRotation: any,
    height: number,
    wantsResult: boolean,
    width: number,
  };
```

```tsx:Experiment.tsx
this.state = {
      cubeRotation: new THREE.Euler(),
      height: 1,
      wantsResult: false,
      width: 1,
    };
```

Socket.ioで送受信したデータはstateで管理したいので新しくwidthプロパティとheightプロパティを定義します。

### 送受信した値をthreejsのキューブに反映させる

キューブの主な情報は*sockerServer/react-three-renderer-fiber/examples/src/app/ColorCube.tsx*

で管理されているので送受信した値を読み込めるように編集していきます。

```tsx:ColorCube.tsx
export interface IColorCubeProps {
  rotation: any;
  height: number;
  width: number;
}
```

```tsx:ColorCube.tsx
public render() {
    const {
      color,
    } = this.state;

    const {
      rotation,
      height,
      width,
    } = this.props;

    return (<mesh
      rotation={rotation}
    >
      <boxGeometry
        width={width}
        height={height}
        depth={1}
      />
      <meshBasicMaterial
        color={color}
      />
    </mesh>);
  }
```

propsの値を定数に代入するところで先ほどstateとして定義した、heightとweightを追加します。

```tsx:ColorCube.tsx
<boxGeometry
	width={width}
	height={height}
	depth={1}
/>
```

ジオメトリーのwidthとheightにpropsの値が反映されるように修正します。


## 動かしてみる

expressからjsを実行できるように、*SocketIoServer/react-three-renderer-fiver*ディレクトリに移ってからexamplesをビルドします。

```shell
yarn build-examples
```

ビルドが終わったら、*SocketIoServer/*ディレクトリに移ってexpressを起動します。

```shell
node .
```

複数ブラウザを立ち上げて、それぞれ*localhost:3000*にアクセスし、changeボタンを押すことで、リアルタイムに全てのブラウザでキューブの形が同期されていれば成功です。



## 最後に

今回作ったものは[ここ](http://ec2-13-113-194-0.ap-northeast-1.compute.amazonaws.com:3000/)でみることができます。

慣れない記事の投稿で説明が荒いところがあったと思いますが、最後まで読んでいただきありがとうございました。