初心者がReactでGLSLを使う
2020-04-28
azblob://2022/11/11/eyecatch/2020-04-22-react-glsl-beginner-000.jpg

初めまして!FIXER 新入社員の谷口です。 FIXER tech blog デビュー! ということで、本題を後回しにして自己紹介します。

プロフィール

  • 生年月日:2000年3月11日 20歳
  • 出身地:富山県
  • 出身校:富山高専 電子情報工学科
  • 趣味:ゲーム、アニメ、ゆるいプログラミング
  • 好きな食べ物:甘いもの
  • 苦手なもの:朝
  • 言語:shaderlab(HLSL/Cg),GLSL,C#(Unity,WPF)

初の引っ越し&一人暮らしで東京生活に悪戦苦闘しています。料理経験は皆無ですが自炊をはじめてみました。最近はもやしと豆腐と冷凍うどんで命を繋いでいます。

クラウドはほぼ未経験です。よろしくお願いします。

趣味

ゲームはSplatoon2やチェス、ゴッドフィールドが好きです。

アニメはジャンル問わず何でも見ます。最近見たのは四畳半神話大系です。

趣味でゆるくプログラミングをしています。高専5年のときは主にシェーダーを書いていました。最近はSonic Pi,Blender,TouchDesignerなどを少しずつ触っています。

本題

ReactでGLSLを使った描画をします。

私はフロントエンドをほとんど触ったことがなく(授業でhtmlとjsを少しだけ使ったくらい。Reactは初)、知識も乏しいので間違っていたらご指摘ください。

環境

Reactの環境はcreate-react-appで構築したものを用います。

node.jsのバージョンは v12.16.1 です。

手順

GLSLを使うために以下のコマンドで REACT-VFX をインストールします。REACT-VFX は、多分テキストや画像にシェーダーを当てて見た目をいい感じにしてくれるものです。 REACT-VFX はプリセットのシェーダーを適応する機能とカスタムシェーダーを適応する機能があります。

npm i -S react-vfx

使いたい画像をpublicフォルダに投げます。今回使う画像の名前は”cctlogo.png”です。

srcフォルダ内のApp.jsを編集します。今回はREACT-VFX内蔵のプリセットシェーダーの"rgbshift"をテックブログの画像に適応させました。ついでに最近若者に人気上昇中の技術であるレイマーチングを用いて簡単にメンガーのスポンジを描画しました。

import React from 'react';
import './App.css';
import * as VFX from 'react-vfx';
import { VFXSpan } from 'react-vfx';

const glsl =`
uniform float time;
uniform vec2  mouse;
uniform vec2  resolution;

const float sphereSize = 1.0;
const vec3 lightDir = vec3(-0.577, 0.577, 0.577);


float sdCross(vec3 p, float c) {
    p = abs(p);
    float dxy = max(p.x, p.y);
    float dyz = max(p.y, p.z);
    float dxz = max(p.x, p.z);
    return min(dxy, min(dyz, dxz)) - c;
}

float sdBox(vec3 p, vec3 b) {
    p = abs(p) - b;
    return length(max(p, 0.0)) + min(max(p.x, max(p.y, p.z)), 0.0);
}

#define ITERATIONS 5
float deMengerSponge1(vec3 p, float scale, float width) {
    float d = sdBox(p, vec3(1.0));
    float s = 1.0;
    for (int i = 0; i < ITERATIONS; i++) {
        vec3 a = mod(p * s, 2.0) - 1.0;
        s *= scale;
        vec3 r = 1.0 - scale * abs(a);
        float c = sdCross(r, width) / s;
        d = max(d, c);
    }
    return d;
}


float distanceFunc(vec3 p){
    return deMengerSponge1(p, 3.0, 1.0);
}

vec3 getNormal(vec3 p){
    float d = 0.0001;
    return normalize(vec3(
        distanceFunc(p + vec3(  d, 0.0, 0.0)) - distanceFunc(p + vec3( -d, 0.0, 0.0)),
        distanceFunc(p + vec3(0.0,   d, 0.0)) - distanceFunc(p + vec3(0.0,  -d, 0.0)),
        distanceFunc(p + vec3(0.0, 0.0,   d)) - distanceFunc(p + vec3(0.0, 0.0,  -d))
    ));
}

void main(void){
    // fragment position
    vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);
    
    // camera
    vec3 cPos = vec3(0.0,  0.0,  2.2);
    vec3 cDir = vec3(0.0,  0.0, -1.0);
    vec3 cUp  = vec3(0.0,  1.0,  0.0);
    vec3 cSide = cross(cDir, cUp);
    float targetDepth = 1.0;
    
    // ray
    vec3 ray = normalize(cSide * p.x + cUp * p.y + cDir * targetDepth);
    
    // marching loop
    float distance = 0.0;
    float rLen = 0.0;
    vec3  rPos = cPos;
    for(int i = 0; i < 16; i++){
        distance = distanceFunc(rPos);
        rLen += distance;
        rPos = cPos + ray * rLen;
    }
    
    // hit check
    if(abs(distance) < 0.001){
        vec3 normal = getNormal(rPos);
        float diff = clamp(dot(lightDir, normal), 0.1, 1.0);
        gl_FragColor = vec4(vec3(diff), 1.0);
    }else{
        gl_FragColor = vec4(vec3(0.0), 1.0);
    }
}`


class App extends React.Component{
  render()
  {
    return (
      <div className="vfx">
      <VFX.VFXProvider>
        <VFX.VFXImg className="tex" src="logo192.png" shader={glsl}/>
        <br></br>
        <VFX.VFXImg className="tex" src="cctlogo.png" alt="image" shader="rgbShift"/>
      </VFX.VFXProvider>
      </div>
    );
  }
}

export default App;

結果

こうなります。

ちょっとだけ解説

REACT-VFXは全体をCanvasで囲っているらしいです。詳しいことは知らないです。<VFX.VFXProvider>の部分のことです。

<VRF.VFXImg> で画像にシェーダーを適用します。カスタムシェーダーを使う場合はシェーダー指定部分を{}で囲います。

class App extends React.Component{
  render()
  {
    return (
      <div className="vfx">
      <VFX.VFXProvider>
        <VFX.VFXImg className="tex" src="logo192.png" shader={glsl}/>
        <br></br>
        <VFX.VFXImg className="tex" src="cctlogo.png" alt="image" shader="rgbShift"/>
      </VFX.VFXProvider>
      </div>
    );
  }
}

おまけ

GPUアーキテクチャ上if文は効率が悪いらしいです。簡単に言うとGLSLのfor文の中でif文を多く回すとGPUが暖房になります。

おわりに

ReactでGLSLを使う方法の1つを紹介しました。業務で使うことはほぼないと思いますがシェーダーは書いていて楽しいですね。

次にブログを書くことがあれば、Unityのシェーダーについて書こうと思います。