takumifukasawa’s blog

WebGL, Shader, Unity, UE4

【vscode】MacのUnity開発でC#の補完が動かなくなったので対処

MacvscodeでUnity C#の補完が効かなくなったので原因を探りました。

起こっていたこと

環境は、 mac OSbig sur, unity は 2019.4.25f です。

vscode の Omnisharp Log を見てみると、大きく2つのエラーが発生していました。

  • Could not load file or assembly 'Microsoft.CodeAnalysis.Workspaces ...

  • OmniSharp server load timed out. Use the 'omnisharp.projectLoadTimeout' setting to override the default delay (one minute).

どうやら読み込めていないモジュールがあり、読み込みのタイムアウトが起こっているようです。

解決

まず2つ目のエラーに着目して、 omnisharp.projectLoadTimeout の時間をデフォルトの60秒よりも長い設定にしても変わらずでした。

最終的に、vscode の拡張の C# for Visual Studio Code のバージョンを最新の 1.23.12 から 1.23.11 に戻すことで直りました。

f:id:takumifukasawa:20210618235857p:plain

必要な assembly がなかった関係でロードのタイムアウトが起こっていたようです。

【Unity】Timelineで枠外に隠れたカーブの全体を表示するショートカット

現象

Timeline の graph view で key の位置やイージングの調整をしたいとき、下の画像のようにカーブ全体が表示されないという現象が度々発生して困ることが多くありました。

f:id:takumifukasawa:20210309222434p:plain

[Edit] -> [Shortcuts...] を覗いてみても、カーブ全体を表示するようなショートカットはありませんでした。

f:id:takumifukasawa:20210309222458p:plain

解決

F キーを押すことでカーブの全体表示がされるようになります。

FキーはScene View などで特定のオブジェクトにフォーカスする用途で頻繁に使いますが、Timeline のカーブにも同様の処理がされるようです。

f:id:takumifukasawa:20210309222509p:plain

Timeline に限らず、VFX Graph などでも特定のノード・ブロックにフォーカスしたいときはFキーを使うので、どのTabかを問わず「何かが隠れている or 小さく表示されている」->「全体表示する or フォーカスする」という挙動をさせたいときにはひとまずFキーを押してみるのは有用かもしれません。

【Node.js】画像群を一括圧縮するスクリプト

画像圧縮をする際、jpegminiやimagealphaなどのアプリを使ったりPCにインストールしたcli経由で行うことが多かったのですが、これらの方法だと画像圧縮の方法がプロジェクトの各人に依存してしまうという問題があります。 そこで、画像圧縮ツールもnpmで管理する形だと人に依存しない & スクリプトを使い回すことができるなと思ったので、nodeスクリプトを作成してみました。

ディレクトリ指定をするとそのディレクトリ以下を再帰的に、単一ファイル指定だとそのファイルのみを圧縮するようにしました。pngとjpgに対応しています。

圧縮には imagemin を用いました。ファイルにもよりますが、100近いファイルを圧縮したところ全体で3分の1ぐらいまでに減りました。

github.com

サービスがクローズしたjsdoitのコードをgithub-pagesに移行するまで

1年半ほど前にクローズになった、jsdoitというソースコード共有サービスがあります。

サービスが稼働している時は500以上のコードを上げていて、サービスクローズ前に全てのコードをローカルに落として個人のgoogle driveに保管していたのですが、パッと見返したいコードなどがあるため、git管理をしてgithub-pagesで見られるように移行しました。

takumifukasawa.github.io

f:id:takumifukasawa:20210304194024p:plain

ソースコードのOGP対応もさせてみました。

takumifukasawa.github.io

いざ作業に取り掛かると、知らなかったツール・使ってみたいと思っていたツールに触れることができたのでよい機会でした。

せっかくなので、移行までの簡単なフローと、便利だと思ったツールを書いていこうと思います。

jsdoitのファイル群

jsdoitからソースをダウンロードするとhtml,js,cssのファイルが格納されています。 ただ、画像や動画、音声などのアセットは含まれていませんでした。

https://github.com/takumifukasawa/jsdoitArchives/tree/master/src/archives/%5B2016.2.4%5D%20%E3%82%B5%E3%82%AB%E3%83%8A%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E3%80%8C%E5%A4%9C%E3%81%AE%E8%B8%8A%E3%82%8A%E5%AD%90%E3%80%8D%E3%82%AB%E3%83%90%E3%83%BC%E9%A2%A8

移行フロー

大まかな流れはこちらです。全てnodeのスクリプトを書いています。

https://github.com/takumifukasawa/jsdoitArchives/tree/master/batches

1. ソースコードのパース・変換
  - 画像など切れているリンクの置き換え
  - opg用のmetaタグ挿入
2. サムネイル生成
  - アーカイブ一覧ページのhtml用の画像(465x465)
  - ogp画像(1200x630)
3. アーカイブ一覧ページのhtml生成

使用ツール・サービス

unpkg

リンクの置き換えに関して、jsdoitのサーバーにアップされていたライブラリのリンクは全て置き換えていく必要があるので、 可能な限り洗い出し、正規表現で検知・置き換えをしていきました。

ライブラリに関しては、動くデモを作るために可能な限りコードを書いた当時のバージョンを使っていきたい意図がありました。 主要ライブラリだと何かしらのcdnに上がっているのですが、ライブラリに依存するライブラリは見つからない場合があります。 threejsのOrbitControlsはまさにそのパターンです。バージョンが異なるとエラーが出て動かない場合があります。

unpkgはnpmのバージョンごとにコンテンツを配信してくれているサービスです。 threejsや関連モジュールはunpkgを参照するようにリンクを変換し、各ライブラリのバージョンを揃えることができました。

unpkg.com

node-html-parser

ダウンロードしたhtmlの中にはogp向けのmetaタグは入っていなかったので、ogp用のmetaタグは全て新規追加していく必要がありました。

htmlそのものはテキストファイルなので、htmlファイルを読み込んでもdocument.appendChildなどのnodeベースで要素を追加していくことができないため、どう変換するのがよいか考えながらツールを探した結果こちらが便利でした。

www.npmjs.com

htmlの中身をパースしてquerySelectorを用い要素にアクセスすることができるインターフェースになっている & 文字列への変換メソッドが生えている点が便利で、以下のような形でogp用のmetaタグを追加していきました。

import { parse } from 'node-html-parser';

...

// htmlファイルを読み込む
const htmlContent = fs.readFileSync( ... );

// htmlファイルの内容をパースし、head要素を取得
const root = parse(htmlContent);
const head = root.querySelector("head");

// headの中身を文字列にしてキャッシュ
const headText = head.toString();

// ogp向けのmetaを後続に追加
headText += "<meta ...  />";
...

// headの中身を置き換え
head.set_content(haadText);

// htmlファイルを書き込み
fs.writeFileSync( ... );

puppeteer

画像キャプチャはheadlessブラウザのpuppeteerを使いました。

https://pptr.dev/

公式のサンプルを引用させていただくと以下のようなコードのみで画像キャプチャをしてくれます。 headlessブラウザを使ったことがなかったのでよい機会になりました。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'example.png' });

  await browser.close();
})();

余談

playwrightも流行っているみたいです。

playwright.dev


9割方は、ひとまず動くところまで機械的に変換することができました。 残りは手動になりそうですが、見つけ次第整えていきたいと思います。

【Javascript】Promiseを直列実行

内容は短めですが、よく使うのでそのメモになります。

複雑なアニメーションなど、とあるPromiseがresolveになったら違うPromiseの状態の監視を始めたい場合が頻繁にあります。

thenで繋いでもよいのですが、Promiseが増えるごとにthenの記述も合わせて増えていくため少し冗長です。

そこで以下のような関数を用意し、Promiseを返す関数の配列をreduceして繋いであげることで、記述量を減らすようにしました。

rejectな場合への対応がいらない場合に、順番に実行していきたい時に多用しています。

async function execPromiseInSequence (arr) {
  return arr.reduce(
    (chained, func) => chained.then(func),
    Promise.resolve()
  );
};

// ex
async function main() {
  // 上から順に実行される
  await execPromiseInSequence([
    async () => { ... },
    async () => { ... }
    ...
  ]);
}

【firebase】functions x hosting 環境でBasic認証をかけている時のBasic認証の挙動確認

firebase functions と firebase hosting を併用しているプロジェクトで Basic認証をかけている時の Basic認証の対象の挙動が分からなかったので確認しました。

firebase で Basic認証をかける

firebase で Basic 認証をかけるためには functions を使用する必要があります。

このようなフォルダ構成の時、

- functions // functionsにアップロードされる対象
  - index.js
  - static
    - index.html

- public // hostingにアップロードされる対象
  - sample.png

- firebase.json

以下のようなfunctionsでBasic認証をかけることができます。

import * as functions from "firebase-functions";
import express from "express";
import path from "path";

const basicAuth = require("basic-auth-connect");

//---------------------------------------------------------------------------------------------
// constants
//---------------------------------------------------------------------------------------------

const USE_BASIC_AUTH = true;

const AUTH_USER = "user";
const AUTH_PASSWORD = "pass";

//---------------------------------------------------------------------------------------------
// main
//---------------------------------------------------------------------------------------------

const server = express();

// basic認証
if (USE_BASIC_AUTH) {
  server.use(basicAuth(AUTH_USER, AUTH_PASSWORD));
}

server.use(express.static(path.join(`${__dirname}/static/`)));

export const hostingApp = functions.https.onRequest(server);

firebase.jsonはこのようになります。

{
  "functions": {
    "source": "./functions"
  },
  "hosting": {
    "public": "./public",
    "rewrites": [
      {
        "source": "**",
        "function": "hostingApp"
      }
    ],

    ...

  }
}

hosting にアップされたファイルにはBasic認証がかからない

hosting の rewrite ルールの優先度はドキュメントにこのように書いてあります。

https://firebase.google.com/docs/hosting/functions?hl=ja#use_a_web_framework

注: public ディレクトリ内の静的ファイルは rewrite よりも優先されるため、静的ファイルは Cloud Functions エンドポイントと一緒に提供されます。

つまり、hosting は functions でかけているBasic認証の対象にならないので、先ほどのフォルダ構成とBasic認証がかかるかかからないかの関係性はこのようになります。

- functions // functionsにアップロードされる対象
  - index.js
  - static
    - index.html <- Basic認証がかかる

- public // hostingの対象
  - sample.png <- Basic認証がかからない

逆にいうと、基本的にはBasic認証をかけたいが、とあるファイルだけBasic認証の対象から外したい、という場合には rewrite のルールを書かずに hostingにアップロードするのが良いのかもしれません。

【WebGL】three.js : soft particle

この投稿の内容は会社のアドベントカレンダーにて書いた記事と同じものになっています。

ですが、自分のブログにも同じ内容を残しておきたいので移植させていただいています。


three.js で soft particle (ソフトパーティクル)を実装する方法を紹介していきたいと思います。

ソフトパーティクルを使って、霧のような雰囲気の中をキツネが駆けるシーンを作ってみました。

この動作デモはこちらから、動作デモのソースはこちらのリポジトリからご覧いただくことができます。

f:id:takumifukasawa:20201220163512g:plain

ソフトパーティクルについて

ソフトパーティクルとは

おおまかに一言でまとめると「深度値の比較をして透過率・色を調整する」方法のことです。

特徴

まずはこちらの画像を比較してみます。

f:id:takumifukasawa:20201220163627p:plain

f:id:takumifukasawa:20201220163644p:plain

1枚目はソフトパーティクルを無効にしたもの、2枚目が有効にしたものです。 煙に着目すると1枚目は床(地面)との境界に線が入っており、2枚目は床に馴染んでいるという違いがあります。

煙は、霧感を出すためにキツネが走っている床のあたりに板ポリのビルボードにテクスチャを貼って散りばめています。ほとんどの煙の板ポリは床に突き抜けているような状態 = 板ポリと床は交差している状態にあるのですが、そのままだと1枚目の画像のように床の境界で煙のテクスチャが切れてしまうので不自然な見た目になります。

そこで、「他のオブジェクトとの重なりの境界を馴染ませる方法」としてソフトパーティクルを使います。

実装方針

カメラから見て床と煙の境界付近のピクセルに着目すると、カメラから床・煙それぞれへの距離はかなり近くなります。完全な境界の場合は、それぞれの距離はほぼ同じになっているはずです。この、距離が近い = 深度値が近いことを利用していきます。

まずは床を描画して床の深度値を取得します。その後、煙を描画する際に深度値を計算します。 その際、煙のピクセルシェーダー内で、床の深度値と煙の深度値を比較して近ければ近いほど透過するような処理をつけます。

すると、煙が描画されるピクセルのうち、床に遠い部分から近い部分にフェードアウトするような状態になります。

↓ 赤い枠線の範囲が板ポリゴンの床より上に見えている範囲だとしたら、

f:id:takumifukasawa:20201220163728p:plain

↓ 赤が濃い部分(床に近い部分)ほど透過をかけるようなイメージ

f:id:takumifukasawa:20201220163757p:plain

このように、深度値の比較をして透過率・色を調整することで他のオブジェクトとの重なりを馴染ませることができます。

ソフト「パーティクル」と呼ばれていますが、この後のデモのように、パーティクルのようなたくさん物量をばらまくようなオブジェクトでなくとも同じ見た目を表現することももちろん可能です。

three.jsでの実装

簡易的なデモをこちらに用意しました。重なっている部分にフェードがかかっていることが分かるかなと思います。

赤い箱は赤単色を出力するマテリアルを、白い箱はソフトパーティクルのシェーダーを適用したマテリアルを割り当てています。マテリアルは RawShaderMaterial を使っています。

See the Pen [test] three.js : soft particle by takumifukasawa (@takumifukasawa) on CodePen.

白い箱以外のオブジェクトも試してみました。

ビルボードの白い板ポリ

f:id:takumifukasawa:20201220163939g:plain

↓ 煙のテクスチャを割り当てた板ポリのビルボード: ソフトパーティクル無効

f:id:takumifukasawa:20201220164020g:plain

↓ 煙のテクスチャを割り当てた板ポリのビルボード: ソフトパーティクル有効

f:id:takumifukasawa:20201220164053g:plain

JavaScript

requestAnimationFrame で毎フレーム実行される関数の中身を抜粋してコメントをつけてみます。

// RenderTarget の横幅縦幅は別途更新
const renderTarget = new THREE.WebGLRenderTarget(1, 1);
renderTarget.texture.format = THREE.RGBAFormat;
renderTarget.texture.minFilter = THREE.NearestFilter;
renderTarget.texture.magFilter = THREE.NearestFilter;
renderTarget.texture.generateMipmaps = false;
renderTarget.stencilBuffer = false;
renderTarget.depthBuffer = true;
renderTarget.depthTexture = new THREE.DepthTexture();
renderTarget.depthTexture.type = THREE.UnsignedShortType;
renderTarget.depthTexture.format = THREE.DepthFormat;

...

const tick = () => {
  ...

  const ctx = renderer.getContext();

  // ----------------------------------------------------------------------------
  // 1. ソフトパーティクル以外(= 赤い箱)の深度値を深度テクスチャに描画
  // ----------------------------------------------------------------------------

  // ソフトパーティクルのメッシュを非表示
  softParticleMesh.visible = false;

  // 深度値を描画する renderTarget を割り当て  
  renderer.setRenderTarget(renderTarget);
  
  // depth だけ renderTarget に描画するため、色情報は描画しないようにマスク
  ctx.colorMask(false, false, false, false);
  renderer.render(scene, camera);

  // ----------------------------------------------------------------------------
  // 2. ソフトパーティクルを含め全てのオブジェクトを描画
  // ----------------------------------------------------------------------------

  // 画面に表示させるために renderTarget の指定を解除
  renderer.setRenderTarget(null);

  // ソフトパーティクルのメッシュを表示  
  softParticleMesh.visible = true;

  // 深度値比較用の各種 uniform 値をシェーダーに渡す
  softParticleMesh.material.uniforms.tDepth.value = renderTarget.depthTexture;
  softParticleMesh.material.uniforms.cameraNear.value = camera.near;
  softParticleMesh.material.uniforms.cameraFar.value = camera.far;
  softParticleMesh.material.uniforms.depthFade.value = params.depthFade;
  softParticleMesh.material.uniforms.resolution.value = new THREE.Vector2(
    width * ratio, height * ratio
  );

  // 色情報のマスクを解除し描画
  ctx.colorMask(true, true, true, true);
  renderer.render(scene, camera);
};

流れをまとめると、

ソフトパーティクル以外のオブジェクトのz-bufferを深度テクスチャに焼く → 深度テクスチャなど必要な情報をソフトパーティクルのシェーダーに渡す → 全てのオブジェクトを描画

となっています。

z-buffer を深度テクスチャに焼く際、 gl.colorMask という WebGL API を直接叩いています。この関数は、フレームバッファーに書き込むチャンネルをフラグ形式で指定することができます。

深度情報のみ欲しい場合はRGBA情報は必要ないので、gl.colorMask(false, false, false, false) として色情報を書き込む処理を無効にし、負荷軽減対策をしています。

developer.mozilla.org

GLSL

ソフトパーティクルの処理を入れている白い箱の頂点シェーダーとフラグメントシェーダーの抜粋になります。 こちらもコメントをつけていきます。

// ----------------------------------------------------------------------------
// 頂点シェーダー
// ----------------------------------------------------------------------------

attribute vec3 position;
attribute vec2 uv;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

varying vec4 vViewPosition;

void main() {
  // ビュー座標をフラグメントシェーダーに渡す
  vViewPosition = modelViewMatrix * vec4(position, 1.);

  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
}
// ----------------------------------------------------------------------------
// ピクセルシェーダー
// ----------------------------------------------------------------------------

precision highp float;
  
#include <packing>
  
varying vec4 vViewPosition;

uniform sampler2D uDepthTexture;
uniform float uCameraNear;
uniform float uCameraFar;
uniform float uDepthFade;
uniform vec2 uResolution;
  
// refs: https://threejs.org/examples/webgl_depth_texture.html
float readDepth(sampler2D depthSampler, vec2 coord) {
  float fragCoordZ = texture2D(depthSampler, coord).x;
  float viewZ = perspectiveDepthToViewZ(fragCoordZ, cameraNear, cameraFar);
  return viewZToOrthographicDepth(viewZ, cameraNear, cameraFar);
} 
  
void main() {
  // スクリーン上の座標を0~1で取得
  vec2 screenCoord = gl_FragCoord.xy / uResolution.xy;

  // 深度テクスチャから、現在のピクセルの他のオブジェクト(= 赤い箱)の深度値を取得
  float sceneDepth = readDepth(uDepthTexture, screenCoord);

  // 現在のピクセルの、これから描画するオブジェクト(= 白い箱)の深度値を取得
  float viewZ = vViewPosition.z;
  float currentDepth = viewZToOrthographicDepth(viewZ, uCameraNear, uCameraFar);

  // 深度値を比較して、一定の値以下の場合はフェードする
  // 0除算しないように調整 
  float eps = .0001;
  float fade = clamp(abs(currentDepth - sceneDepth) / max(uDepthFade, eps), 0., 1.);
 
  gl_FragColor = vec4(vec3(1.), fade);
}

ポイントは readDepth 関数です。この関数自体はthree.jsのサンプルから引用しました。

まず、深度テクスチャには z-buffer の値が入っているのですが、z-buffer の性質の関係で値は非線形ではありません。 そのため、取り出す際には camera の near clip と far clip を元に線形に直す作業が必要になります。それを行っているのが readDepth です。

z-buffer の性質についてはこちらの記事がとてもわかりやすかったです。

marupeke296.com

learnopengl.com

readDepth 関数の中で呼んでいる viewZToOrthographicDepth , perspectiveDepthToViewZピクセルシェーダー冒頭の #include <packing> で展開される中身に含まれています。

#include <...> は three.js のGLSLのコードを挿入する記述です。fog など three.js の機能に絡むシェーダーを使いたい時に便利です。

float viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {
  return ( viewZ + near ) / ( near - far );
}

float perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) {
  return ( near * far ) / ( ( far - near ) * invClipZ - far );
}

改善点

  • iOS Safari, Chrome で見るとガタついているようなアーティファクトが発生していました。原因は、深度テクスチャの書き込み時もしくは読み取り時に精度が落ちていることにあるようです。

  • フェードする閾値だけを調整できるようにしているのですが、フェード開始地点・フェードする閾値の2つを調整できるようにするとより細かい調整が可能かなと思います。

霧の表現

キツネが駆けるデモでは霧のような雰囲気を出すためにソフトパーティクルを使っているのですが、煙のポリゴンをたくさん出して霧の雰囲気を出そうとするとその分透過の描画・重なりも増えるので描画が重くなりやすいです。

なので、霧のような雰囲気をより出すためにはパーティクルは控えめにしつつ(数を減らす・サイズを小さくするなど)、他の方法と組み合わせるのがよいと思います。例えば distance fog や height fog ですね。キツネが駆けるデモでは three.js の fog (distance fog) と組み合わせています。

f:id:takumifukasawa:20201220164437g:plain