takumifukasawa’s blog

WebGL, Shader, Unity, UE4

【Javascript】WebWorkerのスクリプトをインラインで記述する

WebWorkerのスクリプトを別ファイルではなくバンドルファイルに含めたい場面があったので、インラインで記述する方法を試しました。

developer.mozilla.org

blob経由でworkerスクリプトのオブジェクトURLを生成することによって実現させました。

ただ欠点として文字列での記述になるので、

  • エディターのsyntaxが利かない
  • typescriptの場合は型指定ができない

という点があります。

WebWorkerのスクリプトの記述量が多くなりそうな場合は別ファイルにしておくのが良さそうです。


(2020.11.20: 追記)

あとで知ったのですが、この方法は「Inline Worker」や「Inline WebWorker」と呼ばれている昔からある方法だったみたいです。

www.html5rocks.com

【GatsbyJS】Basic認証下でiOS safariからページを開くとリロードする度に認証を求められる場合の対処法

先日、GatsbyJSでサイトを構築していた際、Basic認証がかけられた開発環境用のサーバーにビルドしたファイル群を置くと、表題のようにページをロードする度にBasic認証の入力を求められてしまう現象が発生しました。iOS13 safariでは発生し、PC Chrome では発生しませんでした。Androidは不明です。

その時にあれこれ試行錯誤したのでメモとして残しておきます。

(対策を調べていても同じ現象が発生したひとは見当たらなかった & GatsbyJSをBasic認証下で開発する際に毎回同じ現象が起こるとは少し考えづらいので、環境次第の可能性がありそうです)

原因

GatsbyJSで生成されたプリロード用のタグ(ページ情報のjsonを読み込んでる)がBasic認証に必ず引っかかることがわかりました。GatsbyJSのビルドではhtmlに以下のようなタグが生成されています。

<link rel="preload" as="fetch" href="~~~~~~~.json">

具体的には prealoadBasic認証に引っかかっていました。

まずは対策を書いていきます。

対策

preloadそのものはあくまでもリソースの先読みに関するものでGatsbyJSの機能には影響しないと考えました。

そこで、preloadをprefetchに変える or タグごと消すのどちらかを行うと、この現象が起こらなくなりました。

ビルドするたびに手動で行うのは手間なので、書き換え用のnodeのスクリプトをGatsbyJSのビルドの後に自動的に実行するようにします。

↓ npm-scriptsでgatsbyのビルド後にスクリプトを実行します。

...
  "build": gatsby build --prefix-paths && node replace-link-preload-props.js",
...

↓ replace-link-preload-props.js: 書き換え用のスクリプト。タグごと消して上書き保存する形になっています。

preload

preloadは今まで使ったことがなかったので調べたのですが、結局Basic認証に引っかかる理由はわからずでした・・・。

<link rel="preload"> を使用してフェッチされたリソースが 3 秒以内に現在のページで使用されない場合、Chrome デベロッパー ツールのコンソールに警告が表示されるので注意してください。

とのことで、実際、Basic認証が求められて3秒以上経つと警告が表示されていたので、少なくともBasic認証が求められたタイミングは、リソースの要求が行われた後のようです。

developer.mozilla.org

developers.google.com

www.w3.org

【Unity】LeapMotion と VFXGraph を連携させる

f:id:takumifukasawa:20200408200921p:plain

以前LeapMotionを入手した際に、UE4Niagaraと連携させてみました。

takumifukasawa.hatenablog.com

今回はUnityでVFXGraphと連携させてみたいと思います。

最終的にはこのようなものをつくっていきます。

f:id:takumifukasawa:20200408200935g:plain

環境

  • windows10
  • Unity2019.2.17f1 HDRP
  • LeapMotion

方針

このような流れで実現していきたいと思います。

  1. LeapMotionを使い、手のメッシュと手の動きを連動させる
  2. 手のメッシュから頂点情報(位置など)を取得
  3. 頂点情報に応じてVFXでパーティクルを生成

1. LeapMotionの導入

LeapMotionをPCで使えるようにツール群をインストールします。

以前書いた記事の LeapMotionの導入 と同じ作業になりますので、こちらを参照いただければと思います。

takumifukasawa.hatenablog.com

2. Unityプロジェクトを作成

VFXGraphと連携させたいのでSRP環境を選択します。今回はHDRPを使いました。

f:id:takumifukasawa:20200408170951p:plain

3. LeapMotionのアセットをUnityプロジェクトにインストール

Unity上でLeapMotionを扱う際、PCにツール群をインストールするだけでは使うことができません。

こちらのURLからLeapMotionのアセットが入ったunitypackageをダウンロードし、プロジェクトに追加します。

自分がダウンロードした際のバージョンは 4.4.0 でした。

developer.leapmotion.com

4. LeapMotionとUnityを連動させる

まずはUnity上でLeapMotionと連携させた手のメッシュを表示させたいと思います。

最低限必要なのはこの3つです。

  1. LeapMotionを扱うprefab
  2. 手を管理するスクリプト
  3. 両手それぞれを管理するprefab

4-1. LeapMotionを扱うprefab

Leap Motion Controller をシーンに追加します。LeapMotionを制御するスクリプトがアタッチされたprefabです。

f:id:takumifukasawa:20200408175337p:plain

4-2. 手を管理するスクリプト

こちらはprefabがなかったので手動でスクリプトをつけました。

空のGameObjectを作成し(Hand Modelsとしました)、HandModelManagerスクリプトをアタッチします。設定は以下のようになっています。

Rigged Handsのleftとrightの参照先はそれぞれ、次に作成するprefabです。

f:id:takumifukasawa:20200408180829p:plain

4-3. 両手それぞれを管理するprefab

LoPoly Rigged Hand LeftLoPoly Rigged Hand Left をシーンに追加します。それぞれ、手のメッシュを制御するprefabです。

これを2番で作成したGameObjectの子に入れておきます。

f:id:takumifukasawa:20200408175651p:plain

メッシュの色がピンクになっていますが、こちらはそのままで問題ありません。

アタッチされているのはビルトインパイプライン用のマテリアルで、HDRP下だとシェーディングの仕組みの違いでシェーダーのコンパイルエラーが起こるためです。

最終的なデモではメッシュは表示しないので、そのままでも問題ありません。

4-4. 実行

この状態でPlayを押して実行してみると、ピンク色の手が動きます。

f:id:takumifukasawa:20200408183055p:plain

ここまで出来ていれば、無事にLeapMotionとUnityが連動できたことがわかります。

5. 手の動きに応じてパーティクルを発生させる

いよいよVFXGraphを連携させるパートに入っていきます。最初に記載した方針の2,3の部分ですね。

  1. 手のメッシュから座標を取得
  2. 座標に応じてパーティクルを生成

LeapMotionの手のメッシュはSkinnedMeshとなっており、LeapMotionで検出した手の指の位置などを元に毎フレーム座標を更新することで、手の動きとメッシュが連動する仕組みとなっています。

しかし、SkinnedMeshの座標を毎フレーム取得してVFXGraphと連携させる部分は自分で実装する必要があります

今回はkeijiroさんのこちらのリポジトリを使わせていただき、上記部分を実現します。

computeshaderを使ってSkinnedMeshの各頂点の座標・法線・速度をテクスチャに焼いてくれます(速度は、最後に更新された位置と現在位置との差分)。

github.com

6. SkinnedMeshから頂点座標を取得する

上記リポジトリAssets/Smrvfx の中にあるこの3つのファイルを、プロジェクトの Assets/Smrvfx に落としてきます。

リポジトリをダウンロードしてきてファイルをドラッグ&ドロップするのが一番簡単かな方法かと思います。

  1. SkinnedMeshBaker.compute
  2. SkinnedMeshBaker.cs
  3. Utility.cs

SkinnedMeshBaker.csをGameObjectにアタッチすることで、SkinnedMeshの各頂点の情報(座標・法線・速度)をテクスチャに焼いてくれます。

f:id:takumifukasawa:20200408174535p:plain

両手のSkinnedMeshの情報を取得したいので、SkinnedMeshBaker.csをアタッチしたGameObjectを両手分作成します。

sourceにはテクスチャに焼きたいSkinnedMeshを割り当てる必要があります。手のprefabの直下にあるSkinnedMesh(LoPoly_Hand_Mesh_LeftLoPoly_Hand_Mesh_Right)をそれぞれアタッチします。

f:id:takumifukasawa:20200408183612p:plain

テクスチャも座標、法線、速度の3つ分作成する必要があります。今回は両手なので6つになります。

テクスチャの設定はこのようになっています。座標、法線、速度の3つとも同じにしました。

f:id:takumifukasawa:20200408183623p:plain

7. 手のメッシュを非表示にする

VFXだけを表示させたいので、手のメッシュは非表示にします。非表示にするメッシュはこの2つです。

f:id:takumifukasawa:20200408185233p:plain

8. VFX

両手から発生させるパーティクル群を作成していきます。VFXオブジェクトを作成し、GameObjectを2つ作成してそれらにVFXオブジェクトをアタッチします。

f:id:takumifukasawa:20200408205225p:plain

頂点情報をテクスチャから参照する

Set Position from Mapブロックに座標を焼いたテクスチャを渡すことで、座標をテクスチャから取得することができます。

また同様の方法で、Set Velocity from Mapブロックは速度を焼いたテクスチャから速度を参照することができます。

1つのVFXで両手ごと参照させるテクスチャを変えたいので、各テクスチャをパラメーター化し、Inspectorからアタッチできるようにしました。

f:id:takumifukasawa:20200408190110p:plain

頂点情報を焼いたテクスチャをアタッチ

SkinnedMeshの頂点情報を焼いたテクスチャをInspectorからアタッチします。

f:id:takumifukasawa:20200408190119p:plain

9. VFXやポスプロの調整

好みの見た目に調整していきます。ポスプロでブルームを入れ発光感を出しました。

VFXのUpdateではTurbulanceブロックを追加し、ランダムに乱気流のような動きをつけます。

f:id:takumifukasawa:20200408215239p:plain

10. 完成

f:id:takumifukasawa:20200408200935g:plain

最後に

手のメッシュの頂点数の関係で第三関節と付け根のあたりがスカスカになってしまっているのですが、これは元のメッシュのポリゴンを細分化して頂点数を増やすことで改善できそうです。

【three.js】ビルボードシェーダー

最終的なデモはこちらです。ビルボードの板ポリのパーティクルを生成しています。

f:id:takumifukasawa:20200330235049p:plain

See the Pen 【Threejs】Billboard Shader: Particles by takumifukasawa (@takumifukasawa) on CodePen.


ビルボードとは

リアルタイムレンダリング におけるビルボードとは常にカメラを向いているオブジェクトのことです。常に視点を向いていますが、2DレイヤーにUIとして乗っかっているオブジェクトではなく、3D空間にあるオブジェクト(ほとんどの場合は板ポリ)を指します。

いつ使うか

エフェクトで頂点プリミティブの代わりとして使うことが多いかなと思います。草など視点が近づきやすいかつ大量に発生させたいオブジェクトにも向いているようです。

また、LODの代わりとしてカメラから距離が離れている位置のオブジェクト(遠景など)ビルボードとして使うこともあるそうです。

three.jsでビルボード

主な方法は2つです。

  1. 板ポリをCameraに対してLookAtする処理を毎フレーム行う
  2. シェーダーで常にカメラを向くようにする

どちらにもメリットはありますが、今回は2番をやってみます。

パーティクルのようなエフェクトの用途の場合は、たいてい大量にオブジェクトを出現させたいので、その数が多ければ多くなるほど毎フレームCPU側で処理することは効率が悪くなります。常にカメラを向くような処理をシェーダーで行うことにより、Javascript側でカメラの方を向かせるよりも高速化を図ることができるので、2番が適しています。

当たり判定を行いたいときはCPU側で計算をとりまわしたいので、1番が適しています。

考え方・実装方法

  1. 板ポリ用の頂点4つを全て同じ位置に置く
  2. 頂点シェーダーで、ビュー座標系で頂点をxy方向に移動させて板ポリを作る(= カメラからの視点で頂点をずらし四角形を作る)
  3. フラグメントシェーダーで描画

1,2に関しては、この図のようなイメージです。原点を前提とし、横幅と縦幅の大きさを2とした場合です。

f:id:takumifukasawa:20200401232717p:plain

メリット

プリミティブの頂点は常にカメラを向くオブジェクトですが Point_Size の大きさの限界があり(かつ、端末によって限界値が違う)、それ以上の大きさを表現することができません。つまりカメラがどれだけそのオブジェクトに近づいていってもある一定のサイズよりは大きくなりません。

板ポリのビルボードであればポリゴンとして表現することができるのでPoint_Size の大きさの制限を気にする必要はなく、大きさを自由自在にコントロールすることができます

また、頂点プリミティブの場合は描画領域は必ず正方形になりますが、板ポリは自由に縦横の比率を変えることができます

デメリット

板ポリなので、ポリゴンは最低2枚必要です。つまり頂点パーティクルと比べると頂点数は4倍になります

1. 板ポリのデモ

まず、板ポリを作成してシェーダーでカメラを向かせるデモを作っていきます。

縦横の比率も自由に変更できていることが分かるかと思います。

f:id:takumifukasawa:20200331001206g:plain

See the Pen 【Threejs】Billboard Shader: Basic Plane by takumifukasawa (@takumifukasawa) on CodePen.

こちらがコードの抜粋です。コメントをつけて説明を書いていきます。

javascript

まず 板ポリ用の頂点4つを全て同じ位置に置く Meshを用意します。

three.jsのBufferGeometyを使うことで、頂点ごとの値を自分で定義することができます。

const geometry = new THREE.BufferGeometry();

// この順番でポリゴンを張っていく
// polygon indexes
// 3 -- 2
// |       |
// 0 -- 1

// 頂点を全て同じ位置(0)に
const vertices = [
  0, 0, 0,
  0, 0, 0,
  0, 0, 0,
  0, 0, 0,
];
const uvs = [
  0, 0,
  1, 0,
  1, 1,
  0, 1
];
// ビュー座標系にてオフセットする方向を頂点ごとに定める
const offsets = [
  -1, -1,
  1, -1,
  1, 1,
  -1, 1
];
const indices = [
  0, 1, 2,
  2, 3, 0
];
geometry.setIndex(indices);
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setAttribute('uv', new THREE.Uint16BufferAttribute(uvs, 2));
geometry.setAttribute('offset', new THREE.Float32BufferAttribute(offsets, 2));

const material = new THREE.RawShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms: {
    // ビルボードの横幅・立幅
    uWidth: {
      value: 1,
    },
    uHeight: {
      value: 1,
    },
  },
});

// meshを作成
const billboardPlane = new THREE.Mesh(geometry, material);
billboardPlane.position.set(-1.5, 2, 0);
scene.add(billboardPlane);

頂点シェーダー

頂点シェーダーで頂点ごとに位置をずらします。

uniformで横幅縦幅の値を受け取るようにしていますが、パーティクルでビルボードを使う場合はオブジェクトごとに大きさを変えて違いを持たせたいと思うのでattributeで広げる横幅縦幅の値を受け渡しするようにしてあげるのがよいと思います。コードについては後述します。

attribute vec3 position;
attribute vec2 uv;
attribute vec2 offset;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

// 拡大する大きさを変えられるようにuniformで値をjavascriptから受け取る
uniform float uWidth;
uniform float uHeight;

varying vec2 vUv;
void main() {
  vUv = uv;
  // ローカル座標をビュー座標に変換
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.);
  // ビュー座標系を元に頂点位置を移動
  mvPosition.xy += offset * vec2(uWidth, uHeight);
  gl_Position = projectionMatrix * mvPosition;
}

フラグメントシェーダー

こちらは特にビルボードの実装とは関係のない部分です。頂点シェーダーからUV座標を受け取り色を出力しています。

precision mediump float;
varying vec2 vUv;
void main() {
  gl_FragColor = vec4(vUv, 1., 1.);
}

2. パーティクルのデモ

それでは記事冒頭のパーティクルのデモに入っていきたいと思います。ビルボードそのものの実装は、基本的には板ポリ1枚の時と考え方は変わりません。

こちらもコード内にコメントを書いて補足していきたいと思います。また、デモをもう一度貼っておきます。

See the Pen 【Threejs】Billboard Shader: Particles by takumifukasawa (@takumifukasawa) on CodePen.

javascript

前述のように、パーティクルで板ポリを扱う場合はそれぞれのオブジェクトに違いを持たせるためにattributeにデータを格納していくようにしておいた方が取り回しがよいです。

板ポリをBufferGeometryで2000個生成し、色の違いと大きさの違いはランダムに生成してattributeに登録し差をつけるようにしています。また、拡大・縮小するタイミングはインデックスを参照して少しずつずらすことでパーティクルっぽさを出しています。

const geometry = new THREE.BufferGeometry();

// polygon indexes
// 3 -- 2
// |    |
// 0 -- 1
const index = [];
const vertices = [];
const uvs = [];
const offsets = [];
const indices = [];
const sizes = [];
const colors = [];

const particleNum = 2000;
const randomOffsetRange = 12;
const sizeRange = 1.1;
const sizeMin = 0.1;

// 頂点ごとにデータを生成
for(let i = 0; i < particleNum; i++) {
  // 座標をランダムに生成
  const px = Math.random() * randomOffsetRange - randomOffsetRange * 0.5;
  const py = Math.random() * randomOffsetRange - randomOffsetRange * 0.5;
  const pz = Math.random() * randomOffsetRange - randomOffsetRange * 0.5;

  // 大きさをランダムに生成
  const size = Math.random() * sizeRange + sizeMin;

  // 赤〜黄ぐらいの色をランダムに作成。
  const color = {
    x: Math.random() * .4 + .6,
    y: Math.random() * .4 + .4,
    z: Math.random() * .2 + .2,
  };
  
  // 頂点ごとにデータを格納
  for(let j = 0; j < 4; j++) {
    index.push(i);
    vertices.push(px, py, pz);
    sizes.push(size, size);
    colors.push(color.x, color.y, color.z);
  }
  uvs.push(
    0, 0,
    1, 0,
    1, 1,
    0, 1
  );
  offsets.push(
    -1, -1,
    1, -1,
    1, 1,
    -1, 1
  );

  // インデックスを貼る
  const vertexIndex = i * 4;
  indices.push(
    vertexIndex + 0, vertexIndex + 1, vertexIndex + 2,
    vertexIndex + 2, vertexIndex + 3, vertexIndex + 0
  );
}

geometry.setIndex(indices);

// attributeに登録
geometry.setAttribute('index', new THREE.Uint16BufferAttribute(index, 1));
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setAttribute('uv', new THREE.Uint16BufferAttribute(uvs, 2));
geometry.setAttribute('offset', new THREE.Float32BufferAttribute(offsets, 2));
geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 2));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));

const material = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  transparent: true,
  blending: THREE.AdditiveBlending,
  alphaTest: 0.01,
  depthWrite: false,
  uniforms: {
    uTime: {
      value: 0, // 拡大縮小のアニメーション用に、毎フレームアニメーションループ内で経過時間を渡す
    },
  },
});

const billboardParticles = new THREE.Mesh(geometry, material);
scene.add(billboardParticles);

頂点シェーダー

attribute float index;
attribute vec2 offset;
attribute vec2 size;
attribute vec3 color;
varying vec2 vUv;
varying vec3 vColor;
uniform float uTime;
void main() {
  vUv = uv;
  vColor = color;
  // ローカル座標をビュー座標に変換
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.);
  // ビュー座標系を元に頂点位置を移動
  // 拡大縮小のアニメーションを係数として掛ける
  float anim = sin((uTime * 2. + index * 100.) / 1000.) * .5 + .5;
  mvPosition.xy += offset * vec2(size.x, size.y) * anim;
  gl_Position = projectionMatrix * mvPosition;
}

フラグメントシェーダー

precision mediump float;
varying vec2 vUv;
varying vec3 vColor;
void main() {
  vec4 diffuseColor = vec4(vColor, 1.);
  // 円形にフェードをつける
  vec2 p = vUv * 2. - 1.;
  diffuseColor.a = 1. - smoothstep(length(p), 0., .05);
  diffuseColor.a = clamp(diffuseColor.a, 0., 1.);
  // threejsのalphatestのコードをインクルードして引用
  #include <alphatest_fragment>
  gl_FragColor = diffuseColor;
}

CSSスプライトアニメーションをGPUレンダリングさせる

※内容は以前書いたこちらの記事と同じでさらに文章に校正を加えたものになります。jsdo.itをデモとして使っていたのですがjsdo.itがクローズになってデモが動作しなくなったのでデモをcodepenに移したためです。

CSSスプライトアニメーションをGPUレンダリングさせる - Qiita

スプライトアニメーションと再描画

CSSでスプライト画像の連番アニメーションを実装する場合、background-positionをずらす方法を使っていました。しかし、同じ画面内でサイズの大きい画像のスプライトアニメーションをいくつも動かしたい場面があり、パフォーマンスがネックになりました。

実際、background-positionのcss animationはGPUレンダリングではないので再描画処理が走ってしまいます。chromeのデバッガーツールでRenderingPaint Flashingを見ると、このように再描画処理が走っているのがわかります。

f:id:takumifukasawa:20200328232048p:plain

@keyframes sprite-image {
  0% {
    background-position: 0 0;
  }
  100% {
    background-position: 0 -1064px;
  }
}

.sprite {
  width: 400px;
  height: 266px;
  margin: 15px;
  background-image: url(https://user-images.githubusercontent.com/947953/77824893-2f663480-7149-11ea-873b-9fda24fe1022.jpg);
  background-size: 400px 1064px;
  animation: sprite-image 1s steps(4) infinite;    
}

css transformを使うことで再描画を防ぐ

そこでGPU処理が適用されるcss transformを使った方法を試してみました。before要素の大きさをスプライト画像と同じ大きさにし、背景にスプライト画像をあて、animationでtransformを操作することにより、background-positionの時と同じDOM構造のままcssを変更するのみで実現できました。

先ほどと同じようにchromeのデバッガーツールを見てみると、再描画が走っていないことがわかります。この方法によって、background-positionを使った時よりも描画負荷を軽減することができました。

f:id:takumifukasawa:20200328232059p:plain

こちらがデモになります。

See the Pen 【CSS】Sprite Animation by GPU Rendering by takumifukasawa (@takumifukasawa) on CodePen.

cssの抜粋です。

@keyframes sprite-image {
  0% {
    transform: translate(0, 0);
  }
  100% {
    transform: translate(0, -100%);
  }
}

.sprite {
  overflow: hidden;
  width: 400px;
  height: 266px;
  margin: 15px;

  // animationでtransformのtranslateを動かす
  &:before {
    content: "";
    display: block;
    width: 400px;
    height: 1064px;
    background-image: url(https://user-images.githubusercontent.com/947953/77824893-2f663480-7149-11ea-873b-9fda24fe1022.jpg);
    background-size: 400px 1064px;
    animation: sprite-image 1s steps(4) infinite;
  }
}

【WebGL】Google Chrome の Hardware Acceleration 無効時の挙動

WebGL開発をする際に、Chromeのハードウェアアクセラレーションを無効にしたときでもWebGLコンテンツを動作させたい機会があり、挙動を調べる機会があったのでメモとして残しておきます。

設定方法の変更

Chrome設定詳細設定ハードウェア アクセラレーションが使用可能な場合は使用する で切り替えることができます。

切り替える際はブラウザを再起動する必要があります。

f:id:takumifukasawa:20200328102201p:plain

f:id:takumifukasawa:20200328102209p:plain

有効・無効時のブラウザの機能の違い

chrome://gpu にアクセスすることでグラフィック回りの使える機能の状態を確認することができます。

↓ ハードウェアアクセラレーション有効時 f:id:takumifukasawa:20200328111007p:plain

↓ ハードウェアアクセラレーション無効時 f:id:takumifukasawa:20200328111015p:plain

無効時のWebGL部分を見るとSoftware only, hardware acceleration unavailableの状態になっています。ハードウェアアクセラレーションは使えなくなっていますが(= GPUを使わない)なんらかのソフトウェアは動作しているようで、そのソフトウェアとはSwiftShaderを指しています。

Chrome と CPUレンダラー

ChromeにはSwiftShaderというCPUベースのレンダラーが組み込まれています。

github.com

SwiftShader is a high-performance CPU-based implementation of the Vulkan, OpenGL ES, and Direct3D 9 graphics APIs12. Its goal is to provide hardware independence for advanced 3D graphics.

つまりSwiftShaderはVulkanやOpenGL ESなどのグラフィックAPI相当の処理をCPUで動作させてくれるソフトウェアということになります。

Googleのこちらの記事によると2009年から組み込まれているようです。

2009 年以来、Chrome はハードウェア アクセラレーションによるレンダリングを完全にはサポートしていないシステムでの 3D レンダリングに SwiftShader を使用しています。WebGL などの 3D コンテンツは GPU 向けに記述されていますが、ユーザーの端末にはこのようなコンテンツを実行できるだけのグラフィック ハードウェアが搭載されていない場合もあります。

developers-jp.googleblog.com

WebGLの挙動

ハードウェアアクセラレーションをオフにしてGPUが使えない状態でも SwiftShader が動作することにより、WebGLを使用したコンテンツも表示することができます。ただし、処理速度はどうしてもGPU動作時よりも劣ります。

処理速度

ハードウェアアクセラレーションのオプションを有効・無効にしたときのWebGLコンテンツのFPSを比較してみました。参考にしたのはthree.jsのこちらのサンプルです。

https://threejs.org/examples/#webgl_buffergeometry

↓ 有効時: 約60FPS f:id:takumifukasawa:20200328110025p:plain

↓ 無効時: 約20FPS f:id:takumifukasawa:20200328110036p:plain

処理の内容にももちろん大きく左右されると思いますが、ハードウェアアクセラレーションを無効にしてもWebGLが使われているページがCPUのみの動作で20FPS近く出るとは驚きでした。見た目にも違いは見受けられませんでした。

注意点

調べきれてはいないのですが、SwiftShaderはあくまでもCPUレンダリングなので処理によってはGPUで動作していたときと同じ挙動・見た目にならないものもあるかもしれません。特にWebGL拡張機能回りを使いたいときはあらかじめ動作確認をしておいた方が安全、と思いました。

【three.js】背面カリングを用いたアウトライン表現

f:id:takumifukasawa:20200326234027p:plain

リアルタイムレンダリングにおいてアウトラインを生成する方法はいくつかあります。 背面カリングを利用した方法や、ポストプロセスによる生成が主かと思います。

今回は前者の、背面カリングを使ったアウトライン表現を行ってみたいと思います。

デモ

最終的なデモはこちらになります。

↓ モデルを使った場合。色と境界線の太さを変えられるようにしています。

See the Pen BackFace Culling Outline: Model by takumifukasawa (@takumifukasawa) on CodePen.

↓ three.jsのTorusGeometryを使った場合。境界線用のメッシュ・境界線内用のメッシュの表示非表示を切り替えられるようにしています。

See the Pen BackFace Culling Outline by takumifukasawa (@takumifukasawa) on CodePen.

手法と流れ

  1. 描画したいモデルを複製する
  2. 一つは、頂点シェーダーで各頂点を法線方向にオフセットし(膨らまし)、境界線につけたい色で背面を描画する。もしくはあらかじめ各頂点を法線方向にオフセット済みのモデルを用意する
  3. もう一つは、任意のマテリアル(境界線の内側に表示したいマテリアル)で通常通りレンダリング

このようにすることで、境界線用に背面描画したモデルの上に、描画したいモデルが乗っかるように描画され、あたかも境界線がついたかのように見えるということになります。


こちらのGIFは、境界線用のMeshと境界線の内側用のMeshの表示・非表示を切り替えてみたものです。

境界線用のMeshは境界線のみを描画しているのではなくベタ塗りされ、その上に境界線内に描画したいMeshが乗っかっているようになっていることが分かるかなと思います。

f:id:takumifukasawa:20200327001814g:plain

three.jsの場合

three.jsの場合はこのような流れで実現することができます。

  1. 描画したいMeshを2つ用意する
  2. 片方のMeshには境界線用に背面描画のMaterialを割り当てる。このマテリアルはシェーダーを書く
  3. もう片方のMeshは任意の見た目のMaterial(境界線の内側に描画させたい見た目のマテリアル)
  4. 描画

コード

こちらが境界線描画回りのコードになります。上のサンプルのTorusGeometryの方から抜粋しました。

コード中にコメントで説明を書いていきたいと思います。

...

const outlineVertexShader = `
void main() {
  // 法線方向に頂点を膨らませる
  // .04 は任意の膨らませたい大きさ
  vec3 _position = position + normal * .04; 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(_position, 1.);
}
`;

const outlineFragmentShader = `
precision mediump float;
void main() {
  // 境界線色として黒を指定
  gl_FragColor = vec4(0., 0., 0., 1.);
}
`;

// 境界線用の背面描画マテリアル
const outlineMaterial = new THREE.ShaderMaterial({
  vertexShader: outlineVertexShader,
  fragmentShader: outlineFragmentShader,
  side: THREE.BackSide,
});

// 境界線の内側に描画する用のPBRマテリアル
const surfaceMaterial = new THREE.MeshStandardMaterial({
  color: 0xff0000
});
  
// 描画したい形状
const torusGeometry = new THREE.TorusGeometry(1, 0.3, 16, 100);

// 境界線用のMeshを作成しSceneに追加
const outlineMesh = new THREE.Mesh(torusGeometry, outlineMaterial);
scene.add(outlineMesh);

// 境界線の内側用のMeshを作成しSceneに追加
const innerMesh = new THREE.Mesh(torusGeometry, surfaceMaterial);
scene.add(innerMesh);

...

こうしてみると、境界線用のコードはそこまで多くないことが分かります。

特徴・注意点

モデルを2回描画する必要がある

  • 特に対策をしない場合は単純にポリゴン数が倍になる
  • そのためポリゴン数が多いGeometryの場合には採用がしづらく、採用する場合はポリゴン数を減らしたモデルにするなどの対策が必要?になるのかなと思います

カメラの位置によって太さが変わる

  • 太さを変えたくない場合は、カメラの位置によって膨らませる大きさを調整するか、ポストプロセスによる境界線の方法を用いる必要があります
  • 見せ方によっては、カメラの位置によって太さが変わっても問題ない場合もあるかと思います。

輪郭以外の境界線のディティールを出しづらい

  • 下のスクショのように、アボガドを横から見たときは種と身の境目に境界線が生じていません。また、種の左側に浮くように境界線が表示されているかと思います
  • 折り目がついているところや段差がある部分(このアボガドの場合は種と身の境目)に境界線をつけやすくしたい場合はポストプロセスの方が向いています
    • ポストプロセスであればシーン内の法線方向や色を用いた境界線の生成を行う方法をとることができるからです

f:id:takumifukasawa:20200327003200p:plain