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
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>
<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と通信できるようにしていきます。
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をインポートします。
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を追加します。
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メソッドを使います。
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へのデータ送信ボタンとして使います。
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メソッドを追加します。
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メソッドの中で、データを受信した時の処理を書きます。
//state型定義
public state: {
cubeRotation: any,
height: number,
wantsResult: boolean,
width: number,
};
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
で管理されているので送受信した値を読み込めるように編集していきます。
export interface IColorCubeProps {
rotation: any;
height: number;
width: number;
}
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を追加します。
<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ボタンを押すことで、リアルタイムに全てのブラウザでキューブの形が同期されていれば成功です。
最後に
今回作ったものはここでみることができます。
慣れない記事の投稿で説明が荒いところがあったと思いますが、最後まで読んでいただきありがとうございました。