Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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みたいなものを使えるようにしてくれるものがあったのでこれを活用していきます。

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を実装していこうと思います。

プロジェクト構成

mkdir SocketIoServer
cd SocketIoServer
yarn init -y
yarn add express socket.io
toutch index.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のパスを変えます。

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/にインストールします。

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

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

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をインポートします。

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を追加します。

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メソッドを使います。

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へのデータ送信ボタンとして使います。

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メソッドを追加します。

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メソッドの中で、データを受信した時の処理を書きます。

Experiment.tsx
//state型定義  
public state: {
    cubeRotation: any,
    height: number,
    wantsResult: boolean,
    width: number,
  };
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

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

ColorCube.tsx
export interface IColorCubeProps {
  rotation: any;
  height: number;
  width: number;
}
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を追加します。

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

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

動かしてみる

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

yarn build-examples

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

node .

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

最後に

今回作ったものはここでみることができます。

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away