ランサーズ(Lancers)エンジニアブログ > JavaScript > ReactNative > React NativeでGLSL書けるか試してみた

React NativeでGLSL書けるか試してみた

blog_admin|2017年12月21日
ReactNative

この記事はランサーズ Advent Calendar 2017の21日目の記事です。
はじめまして、フロントエンドエンジニアの加藤です。8月からランサーズのユーザーファーストデザイン室というチームでデザイナーの人たちと一緒に働いています。

 

突然ですが、みなさんはglslsandboxを見て、知らない外人のお兄さんの作品を見て毎日感動してますよね?そして、うちの仕事では使う機会ないなぁと涙を流していますよね?

 

しかし、仕事は自分で勝ち取るものなのだと誰かが言っていました。どうやって今の仕事の環境の上で書けるかを試してみるといいかもしれません。以前、gl-reactを使ってReactで書いたアプリケーションの上でGLSLを書ける方法を試したことがありますが、現在であれば一番新規フロントエンド開発で選定されそうなReact Nativeで書ければ、もしかしたら仕事で書けるかもしれません!

 

GLSLとは

先ほど紹介したGLSL Sandboxを見てもらえるとわかりやすいんですが、OpenGLのシェーディング言語であるGLSLでフラグメントシェーダーを書いて、画面上のピクセルをどんなルールでどんな色をつけるかをプログラミングすることで、色々なスケッチを作り出すことができます。(多分何か間違っている気がするので気になる方は是非調べてください)

何を使うか

ググると、gl-react-nativereact-native-webgl の組み合わせで表示できそうなことがわかりましたが、react-native-webglは、linkしないと使えなさそうです。今回は、create-react-native-appによって生成されたボイラープレートからExpoのレールの上に乗った状態で開発したかったので、detachしない選択肢であればそれが一番望ましいため、他の選択肢を探します。

gl-react-expo

要望に答えられそうなものがありました。その名の通りexpoでgl-reactを使えるライブラリです。

やってみる

gl-react-expoを調べる限り、導入に関するドキュメントが若干乏しいのですが、公式のサンプルがリポジトリ内にあるので、とりあえずそれを真似して動かすところまでを今回の目標とします。

依存パッケージを追加

create-react-native-appでプロジェクトを生成したら、

yarn add gl-react-expo gl-react

加えて、後述するHOCに必要なパッケージも追加します

yarn add hoist-non-react-statics raf

とにかくコピペしていく

よくわからないですがとにかく動作に必要そうなものをコピペしていきます。

gl-react-implementation.js

let runtime;
export function setRuntime (r) { runtime = r; }
export default () => runtime;

inject-gl-react-implementation.js

import Expo from 'expo';
import { Surface } from 'gl-react-expo';
import { setRuntime } from './gl-react-implementation';

setRuntime({
  name: 'gl-react-expo',
  EXGLView: Expo.GLView,
  Surface,
  endFrame: gl => gl.endFrameEXP(),
  loadThreeJSTexture: (gl, src, texture) => {
    let image = new Image();
    image.onload = function() {
      texture.image = image;
      texture.needsUpdate = true;
    };
    image.src = src;
  }
});

App.jsで、上記のinject-gl-react-implementationをインポートします。

App.js

import getGLReactImplementation from './gl-react-implementation';

これで準備が整いました。

実際にシェーダーを表示するコンポーネントを書いていく

まずは先程つくったファイルをインポートして表示できるSurfaceを取得します。

追加で、gl-reactからシェーダを書くためのReactコンポーネントを呼びます。

GLSL.js

import getGLReactImplementation from './gl-react-implementation';
const { Surface } = getGLReactImplementation();
import { Node, Shaders, GLSL } from 'gl-react';

Shaders, GLSLを使ってシェーダー定義します。内容については今回触れません。

(ちなみにですが、ここで実機プレビューしながらシェーダーを書いていくのは反映に結構ラグがあるのでおすすめしません。GLSLSandboxとか、Atom拡張のVEDAなどを使うのをおすすめします。VEDAは今回初めて使ってみましたが最高でした:))

GLSL.js

const shaders = Shaders.create({
  helloGL: {
    frag: GLSL`
precision mediump float;

uniform float time;
uniform vec2 resolution;

void main( void ) {
  float adjustedTime = time * 0.0001;
  vec2 position = ( gl_FragCoord.xy / resolution.xy ) - 0.8;

  float x = 0.3 * ( position.x + 1. ) * sin( 3.0 * position.x - 8. * adjustedTime );

  float y = 4. / ( 40. * abs(position.y - x));

  gl_FragColor = vec4( (position.x) * y, 0.5 * y, y, 10. );
}
`
  }
});

ここで重要なのが、uniform変数をprops経由で渡すことになる点です。

つまり、ここで使われているtime,resolutionは、自分で計算してpropsに渡さなければなりません。

gl-reactでは、計算が必要なものについてはHOCでラップして渡すことで実現する方法を取っています。

timeを刻んで渡すHOCはアニメーションには必須になってくると思います。これは幸いサンプルにあるので、そのまま拝借します。

timeLoop.js

import React, { PureComponent } from 'react';
import raf from 'raf';
import hoistNonReactStatics from 'hoist-non-react-statics';

// NB this is only an utility for the examples
export default (
  C,
  { refreshRate = 60 } = {}
) => {
  class TL extends PureComponent {
    static displayName = `timeLoop(${C.displayName||C.name||""})`;
    state;
    state = {
      time: 0,
      tick: 0,
    };
    _r;
    componentDidMount() {
      this.onPausedChange(this.props.paused);
    }
    componentWillReceiveProps({ paused }) {
      if (this.props.paused !== paused) {
        this.onPausedChange(paused);
      }
    }
    componentWillUnmount() {
      raf.cancel(this._r);
    }
    startLoop = () => {
      let startTime, lastTime;
      let interval = 1000 / refreshRate;
      lastTime = -interval;
      const loop = (t) => {
        this._r = raf(loop);
        if (!startTime) startTime = t;
        if (t - lastTime > interval) {
          lastTime = t;
          this.setState({
            time: t - startTime,
            tick: this.state.tick + 1,
          });
        }
      };
      this._r = raf(loop);
    };
    onPausedChange = paused => {
      if (paused) {
        raf.cancel(this._r);
      }
      else {
        this.startLoop();
      }
    };
    render() {
      return ;
    }
  };

  hoistNonReactStatics(TL, C);

  return TL;
}

先程のコンポーネントをtimeLoopに渡します。

const Comp = timeLoop(Sketch)

これでほぼ完成です。よしなにスタイルをつけて、App.jsから読み込んで表示します。

export default class GL extends Component {
  constructor(props) {
    super(props);
    this.state = {
      width: 0,
      height: 0
    };
  }
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.logo}>LOGO</Text>
        <Comp width={width} height={height} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center'
  },
  logo: {
    position: 'absolute',
    top: 120,
    zIndex: 2,
    elevation: 2,
    color: '#fff',
    fontSize: 20,
    backgroundColor: 'transparent'
  }
});

うごいた〜

できたもの

サンプルリポジトリはこちら

見たい方はこちらのQRコードをExpoのモバイルアプリから読み込んで見てください。iOSのシュミレーターとAndroid実機では動いたので多分動くと思います。

まとめ

書けることはわかりましたがよく考えたら仕事では画像のフィルターぐらいしか使い道がないかもしれないと気づいて悲しみが深まりました。