takumifukasawa’s blog

WebGL, Shader, Unity, UE4

【WebGL】GLTFのUV座標系の向きについて

GLTFのUV座標系について知識不足だったので、メモを残しておきたいと思います。


結論、GLTF自体のフォーマットでは以下の図のようにUV座標は左上が原点(左上が(0,0)で右下が(1,1))に設定されています。

※画像は下記URLの図をスクショしたもの

glTF™ 2.0 Specification

普段html,jsを触っている感覚からすると自然な感覚かなと思います。


しかし、「あえてWebGLのUVの原点が左下になるように設計・実装している」場合には問題が発生します。具体的には、gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); を呼んでいるときです。

まず、画像データを何かしらの方法で読み込む(fetchなど)と座標系の原点は左上になります。 一方、Blenderなどの各種DCCツールでは以下のように UVの座標系の原点を左下 にしているケースが大半だと思います。つまり、Yが反転した状態になっているのです。

(0, 1) ---- (1, 1)
   |           |
   |           |
(0, 0) ---- (1, 0)

Blender の UVエディタウインドウのスクショ


しかし、WebGLでは gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); を呼ぶことによって左下を原点とすることができます。

個人的には各種DCCツールに合わせて左下に原点を持ってきた方がわかりやすいと思っていてこの UNPACK... の指定は入れるようにしているのですが、GLTFをパースしているときに「UVのY軸がなぜか逆だな」と思って調べてみると、そもそもGLTFでは左上が原点になっていた、と気がついた経緯でした。

最終的には以下のような実装にまとまりました。

  • 左下を原点とするために gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); は指定する
  • GLTFからUVを読み込む際、Yを反転させてからBufferにデータを送る

【WebGL】CubeMap(環境マップ)の軸について整理する

生のWebGLを書いているときにCubeMap(環境マップ)がサンプルされる軸についてややこしくなったので整理したいと思います。

具体的には、WebGL2においてCubeMapの色を取得するために texture(samplerCube, vec3) で呼び出す際、 「どうサンプルされるのが正しいのか」「渡したベクトルに対してCubeMapのどの軸がサンプルされるか?」というような内容になります。

skyboxも実装したサンプル図です(skyboxそのものの実装については割愛します)

こちらは開発中のWebGLの自作ライブラリになるのですが動作デモになります。

https://takumifukasawa.github.io/PaleGL/workspace/011_cube-mapping/

目次

CubeMapの考え方

この画像のように、シェーダー内ではカメラから物体の表面に対して、法線を軸に反射させたベクトルを使うのが基本的な考え方です。

映り込み(specular)に使うと、画像から色をサンプルしているかつ視点によってサンプルする色も変化するので情報量が増します。(ex. UnityのReflectionProbeや、Skyboxのreflection成分)

WebGLAPIも踏まえた具体的な説明はこちらがとてもわかりやすいです。

wgld.org | WebGL: キューブ環境マッピング |

サンプルするベクトルを変えながら確認

以下のような状態で、球体を置いてCubeMappingを実装していきます。

  • 球体に出力する色はCubeMappingからサンプルした色をそのまま出す
  • js側(各種行列など)は右手座標系で実装。z+が手前
  • 球体の位置は(0,0,0)。カメラの位置は(0,0,5)で球体を向く。つまり-zを向く状態。
  • CubeMapに使う6面の画像は+x,-x,+y,-y,+z,-zが分かるような画像

1. reflectしたものをそのまま使う

// PtoE ... 描画する点のワールド座標 -> カメラのワールド座標 の正規化ベクトル
// N ... 正規化した法線
vec3 reflectDir = reflect(-PtoE, N);
vec3 cubeColor = texture(uCubeTexture, reflectDir).xyz;

どうやら軸は手前がz+になっているようです。しかし、「反転して鏡のように映る」期待は逆の結果になります。

CubeMapの軸

このサイトによると、CubeMapの参照する軸についてこう書かれています。

Pragmatic physically based rendering : HDR

Cubemaps spec comes from the time when RenderMap ruled the world and Renderman it's using Left-Handed Coordinate system so do the cubemaps. That means to we will need to flip the X axis in our shader whenever we sample from a CubeMap texture. Additionally you will need to point your camera towards +Z instead of the usual -Z in order to start at expected direction. Otherwise you might end up looking at the wall like in case of the Pisa texture we are using.

どうやら、CubeMapはRenderMap(Autodesk製のソフト?ツール?)にならって左手座標系を基準としているようです。

しかしWebGLは右手座標系で設計されている(ポリゴンの正面判定など)ので、参照する軸が異なります。

つまりCubeMapの考え方に沿って左手座標系に沿って実装するためには、まずx軸を反転する必要があり、場合によってはz軸も反転する必要がある、ということのようです。

(↑ 解釈が間違っていたらすいません)

試しにx軸を反転させてみます。

2. reflect & x軸を反転

// PtoE ... 描画する点のワールド座標 -> カメラのワールド座標 の正規化ベクトル
// N ... 正規化した法線
vec3 reflectDir = reflect(-PtoE, N);
reflectDir.x *= -1.;
vec3 cubeColor = texture(uCubeTexture, reflectDir).xyz;

z+が手前、x+が左側になりました。両方反転させると左手座標系になるので、180度回転をさせます。

3. reflect & x軸を反転 & xzを180度回転

// PtoE ... 描画する点のワールド座標 -> カメラのワールド座標 の正規化ベクトル
// N ... 正規化した法線

mat2 rotate(float r) {
    float c = cos(r);
    float s = sin(r);
    return mat2(c, s, -s, c);
}

vec3 reflectDir = reflect(-PtoE, N);
reflectDir.x *= -1.;
reflectDir.xz *= rotate(3.14);
vec3 cubeColor = texture(uCubeTexture, reflectDir).xyz;

手前がz-、右がz+となっています。また、それぞれ反転しているので無事に左手座標系に直りました。

参考

Pragmatic physically based rendering : HDR

WebGLでキューブマッピング - Qiita

wgld.org | WebGL: キューブ環境マッピング |

0~1のfloatを32bitRGBAに格納する

デモはこちらにおきました。

デモ https://takumifukasawa.github.io/float-to-rgba-tester/

リポジトリ GitHub - takumifukasawa/float-to-rgba-tester: float to rgba tester


こちらが計算部分のjavascriptのソースになります。

class FloatPacker {
    static packToRGBA(num) {
        const rawR = num * 255;
        const r = Math.floor(rawR);
        const rawG = (rawR - r) * 255;
        const g = Math.floor(rawG);
        const rawB = (rawG - g) * 255;
        const b = Math.floor(rawB);
        const rawA = (rawB - b) * 255;
        const a = Math.floor(rawA);
        return { r, g, b, a };
    }
    static unpackToFloat({ r, g, b, a }) {
        return r / 255 + g / (255 * 255) + b / (255 * 255 * 255) + a / (255 * 255 * 255 * 255);
    }
}

// usage example
const { r, g, b, a } = FloatPackage.packToRGBA(0.87185264);
const unpackedFloat = FloatPackage.unpackToFloat({ r, g, b, a });

概要

jpgやpngなど、普段使うことの多いテクスチャの形式ではRGBAの各チャンネルの表現できる値は8bitなので0~255までの256段階になります。

そのため、VATなど頂点シェーダーであらかじめ用意されたデータをテクスチャから読み込んで取り扱うときは、0~255の256段階でしか使うことができません。

しかし、頂点シェーダーでは0.5145や5.4514など、floatな値を使いたい場面があり、256段階では精度が足りないことがほとんどです。

浮動小数点テクスチャを扱うことができればその限りではないのですが、jpgやpngなどの 8bit x 4 = 32bit/pixel なテクスチャの場合は、浮動小数点的をそのままチャンネルに詰めることができません。チャンネルごとに32bitであればもちろんそのまま浮動小数点を入れることができるのですが、8bitだと浮動小数点を入れるにはbit数が足らないからですね。

そのため、浮動小数点をとある変換式にかけることでRGBAの 8bit x 4 の形式に変換し、多少誤差があるものの 8bit x 4 = 32bit/pixel から浮動小数点に直す方法をとることで、浮動小数点を 8bit x 4 = 32bit/pixel な形式のテクスチャに格納することができます。

ただし、0~1の間であることが条件になります。

変換

まず、0.87185264という値があるとします。

この数値に 255 をかけて、整数部分のみ切り出します。 これがRチャンネルに入る要素になります。

0.87185264 * 255 = 222.3224232 => 222

次に、先ほどの小数部分のみを切り出し、再度255をかけて整数部分のみをきりだします。 これがGチャンネルに入る要素になります。

0.3224232 * 255 = 82.217916 => 82

これを残りのB,Aチャンネル分の2回繰り返します。

0.217916 * 255 = 55.56858 => 55
0.56858 * 255 = 144.9879 => 144

最終的に R=222, G=82, B=55, A=144 という数値が得られました。

それでは、0~1の浮動小数点に復元してみましょう これまでは、255をかけて整数部分を切り出すことをしていたので、この逆の計算をしていきます。

(222 / 255) + (82 / (255 ^ 2)) + (55 / (255 ^ 3)) + (144 / (255 ^ 4)) = 0.8718526397663572

最終的に、差分は 0.8718526397663572 - 0.87185264 = -2.336428e-10 となりました。

この程度の差であれば、場面によってはほぼ誤差に近いと捉えることもできそうです。


参考

www.gamedev.net

【Rider】Windowsで.ideavimrcが読み込まれないとき

おそらく特殊な状況だったと思うのですが、メモしておきます。

経緯としては、IdeaVimのアラート音を消したいためにideavimrcを編集していたのですが反映されない現象に出くわしていました。


RiderでVimキーバインドを使う場合、IdeaVimを使うことになります。 設定は~/.ideavimrcに書くことで適用されます。

Windowsの場合は C:Users/{username}/.ideavimrc に置くことになります。 しかし、.ideavimrc に設定を書いても読み込まれることはありませんでした。

よくよく C:Users/{username}/ を見てみると、.vimrcが存在していませんでした。

そこで、空の.vimrcを作成し.ideavimrcから読み込むようにすることで、設定が反映されました。

ファイルの中身は以下のようになりました。


.ideavimrc

source ~/.vimrc
set visualbell
set noerrorbells

.vimrc は空

WebWorkerではプライベートフィールドを渡すことができない

ちょっとハマったのでメモです。

WebWorkerのpostMessageでデータを送るとき、渡すことのできるデータとそうでないデータがあります。

これは構造化複製アルゴリズムが適用されているからです。たとえばFunctionオブジェクトは送ることができません。

developer.mozilla.org

その中にプライベートフィールドも含まれていました。

chromeで以下のコードを実行すると Object で { hoge: "hogehoge" } が出力されます。

class Hoge {
  hoge;
  #fuga;
  constructor(hoge, fuga) {
    this.hoge = hoge;
    this.#fuga = fuga;
  }
  echo() {
    console.log(this.hoge);
    console.log(this.#fuga);
  }
}

const hoge = new Hoge("hogehoge", "fugafuga");

const worker = new Worker(URL.createObjectURL(new Blob([`
self.onmessage = (e) => {
  console.log(e.data[0]); // Object { hoge: "hogehoge" }
}
`])));

worker.postMessage([hoge]);

【Photoshop】レイヤーモードのオーバーレイの原理

Photoshopのレイヤーモードには、加算、乗算など様々な種類があります。

下のレイヤーに対して上のレイヤーの色をどう重ねるかの方式の設定になります。

WebGLで言うところのBlendModeに近い処理ですね。

例えば加算を設定した結果どういう色になるかは、感覚的にも、BlendModeと比較した場合にもイメージがつきやすいのですが、オーバーレイに関してはどういうことをやっているのか良くわからなかったので、計算方法を調べてみました。

計算式

おそらく、各レイヤーモードの計算式はこちらにまとまっているものになるかと思います。

stackoverflow.com

stack overflow の記事に載っている計算式を一部抜粋します。

#define ChannelBlend_Overlay(A,B)    ((uint8)((B < 128) ? (2 * A * B / 255):(255 - 2 * (255 - A) * (255 - B) / 255)))
...
#define ChannelBlend_Multiply(A,B)   ((uint8)((A * B) / 255))
#define ChannelBlend_Screen(A,B)     ((uint8)(255 - (((255 - A) * (255 - B)) >> 8)))

どうやら、下の色が128以上の場合はスクリーンを、128未満の場合は乗算を適用しているのがオーバーレイであるようです。

スクリーンの計算方法については今回は言及せず、オーバーレイについてのみ焦点を当てます。


オーバーレイを使った時の見た目のサンプルと、計算結果のサンプルを2パターン作ってみたので、それぞれ記載していきます。

計算は、オーバーレイの式をjavascript向けに編集したものを使いました。

// Aが下のレイヤーのピクセル色、Bが上のレイヤーのピクセル色(オーバーレイで重ねる色)
const overlay = (A, B) =>
  ((A < 128) ?
  (2 * B * A / 255) :
  (255 - 2 * (255 - B) * (255 - A) / 255));

1. 白黒のグラデにべた塗のレイヤーをオーバーレイで重ねる

中央の矩形が重なっている部分です。

f:id:takumifukasawa:20210926120503j:plain

// 白黒のグラデに真っ白のべた塗レイヤーをオーバーレイで重ねる(図上)

overlay(255, 255);
// 255
overlay(192, 255);
// 255
overlay(128, 255);
// 255
overlay(64, 255);
// 128
overlay(0, 255);
// 0

// 白黒のグラデのレイヤーに真っ黒のべた塗をオーバーレイで重ねる(図下)

overlay(255, 0);
// 255
overlay(192, 0);
// 129
overlay(128, 0);
// 1
overlay(64, 0);
// 0
overlay(0, 0);
// 0

傾向として、

  • 真っ白をオーバーレイで重ねる場合、下のピクセル色が、

    • 128以上: 255になる
    • 128未満: 0~128の範囲が0~255に変換される
  • 真っ黒をオーバーレイで重ねる場合、下のピクセル色が、

    • 128以上: 128~255の範囲が0~255に変換される
    • 128未満: 0になる

ということが分かりました。

見た目的には、白いレイヤーをオーバーレイで重ねると明るい部分の幅が広がり、黒いレイヤーをオーバーレイで重ねると暗い部分の幅が広がっていますね。

ざっくり書くと、全体をより明るく、より暗くする処理の出し分け、としてオーバーレイを使うことになるのかなと思いました。

(例えば、コントラストを部分的に強く、彩度を強くする使い方など?)

2. べた塗のレイヤーの上に白黒のグラデをオーバーレイで重ねる

f:id:takumifukasawa:20210926120514j:plain

// 真っ白のべた塗レイヤーに白黒のグラデをオーバーレイで重ねる(図上)

overlay(255, 255);
// 255
overlay(255, 192);
// 255
overlay(255, 128);
// 255
overlay(255, 64);
// 255
overlay(255, 0);
// 255

// 真っ黒のべた塗レイヤーに白黒のグラデをオーバーレイで重ねる(図下)

overlay(0, 255);
// 0
overlay(0, 192);
// 0
overlay(0, 128);
// 0
overlay(0, 64);
// 0
overlay(0, 0);
// 0

結果を見ると、オーバーレイで重ねる色に関わらず、下のピクセル色が0,255になっている場合はそのままの色(真っ黒か真っ白)になっています。

前述のように、「より明るく」「より暗く」することが目的のレイヤーモードだとすると、重ねる元の色によっては全く変わらない見た目になる場合があることは正しいと言えそうです。

【ffmpeg】ディレクトリ内の複数webmファイルを一括でmp4に変換

$ for i in *.webm; do ffmpeg -i "$i" "${i%.*}.mp4"; done

を叩くことで一括変換できます。

もし mov にしたい場合は mp4mov に変更すればフォーマットが mov になります。