クワマイでもできる

クワマイでもわかる

ソーシャルVRに点群を持ち込みたい[2] モバイル対応編

f:id:kuwamai:20201121174738p:plain

VRChatやCluster、Stylyなど、VRやARの空間を簡単に共有できるツールが増えてきた。多くはスクリプトが使用できない制限があるため、点群を表示するにはshaderで頑張る必要がある。最近ではOculus Questやスマホなど、使用できるデバイスが多様になっているのでこれらにも対応したい。今回のお話はPhi16さんからいただいた点群表示shader、PointCloud shaderの紹介とモバイル対応させた記録。

今回はコードを書いた思い出話。肝心の点群を持ち込む方法は前回の記事を見て。

元になったもの

Phi16さんからshaderをいただいた経緯はこちらの記事。

似たような仕組みのFuwaParticleについてPhi16さん自身が解説されている記事があります。

つくったもの

作ったshaderは全部公開している。これで君も点群ワールドをアップしよう。

  • 点群表示用shader

  • 恒星表示用shader

制作の記録

点群をテクスチャに書き込む

点群は点の位置と色がセットになったデータなので、それぞれのデータをテクスチャに書き込む。これは元々Phi16さんからいただいたshaderの方式と同じ。すでにいくつかの作品がこのフォーマットを用いてたので互換性を保つようにした。

色はテクスチャの1ピクセルが点群の1点に対応しているので(r, g, b)がそのまま書き込まれている。同様に点の位置(x, y, z)もテクスチャの色の(r, g, b)に対応して書き込まれているけど、色と比べて扱う数値が大きいので工夫が必要。今回のフォーマットでは単精度浮動小数点数*1を色に変換する処理がされている。単精度浮動小数点数は32 bit、色は8 bitで表現されているので32 bitを8 bitずつ切り出すとちょうど4ピクセルに書き込める。それっぽい図を下に書いてみた。VRChatのshader芸として、ワールドの状態保存にこの方式がたびたび使われてるみたい*2

f:id:kuwamai:20201205194157p:plain

コードだとこの部分。uvが4ピクセルのちょうど真ん中なので、半ピクセル分動いてそれぞれの値を取得してる感じ。

float3 unpack(float2 uv) {
    float texWidth = _Pos_TexelSize.z;
    float2 e = float2(-1.0/texWidth/2, 1.0/texWidth/2);
    uint3 v0 = uint3(DecodeHDR(tex2Dlod(_Pos, float4(uv + e.xy,0,0)), _Pos_HDR).xyz * 255.) << 0;
    uint3 v1 = uint3(DecodeHDR(tex2Dlod(_Pos, float4(uv + e.yy,0,0)), _Pos_HDR).xyz * 255.) << 8;
    uint3 v2 = uint3(DecodeHDR(tex2Dlod(_Pos, float4(uv + e.xx,0,0)), _Pos_HDR).xyz * 255.) << 16;
    uint3 v3 = uint3(DecodeHDR(tex2Dlod(_Pos, float4(uv + e.yx,0,0)), _Pos_HDR).xyz * 255.) << 24;
    uint3 v = v0 + v1 + v2 + v3;    return asfloat(v);
}

Geometry shaderが使えない環境について

Geometry shaderでパーティクル表現することを一部ではGPUパーティクルと呼ばれている。いただいたshaderもGeometry shaderを使っていたけど、大半のモバイル環境ではGeometry shaderが使えない。代わりにVertex shaderでパーティクル表現をすることにした。

Geometry shaderは1ポリゴンずつ処理できるので繋がったメッシュをバラバラにしたりできる*3けど、Vertex shaderは1頂点ずつ処理されるので下図左側みたいにポリゴンをたくさん並べたメッシュを作った。

f:id:kuwamai:20201210023902j:plain

この三角ポリゴン1つが点群の1点に対応する。z軸(図の青矢印)の位置で何番目のポリゴンか判別できるので、下記のコードでどのポリゴンがテクスチャのどのピクセル(uv)に対応するか計算できる。

float2 uv = float2((floor(v.vertex.z / texWidth) + 0.5) / texWidth, (fmod(v.vertex.z, texWidth) + 0.5) / texWidth);

また、y軸(緑矢印)の位置で三角形のどの頂点なのか判別できるので、下記のコードで点を表示する正三角形のどの頂点に対応するか計算できる。triVertに(ほぼ)正三角形の各頂点の位置を保存してるので、pを中心にそれぞれ頂点をずらして上図右側みたいに変形してる。xAxisyAxisは描画した点が常にカメラの方を向くようにポリゴンを回転してくれてる。

float3x2 triVert = float3x2(
    float2(0, 1),
    float2(-0.9, -0.5),
    float2(0.9, -0.5));

o.uv = triVert[round(v.vertex.y)];
offset = o.uv * sz;
o.vertex = UnityWorldToClipPos(worldPos + xAxis * offset.x + yAxis * offset.y);

その後のclipで三角形を丸くくり抜いてあげることで点を描画している。clipがないと三角形がそのまま描画されるし、lに応じて透明度を変更すれば円の端をぼかすようなこともできる。

float l = length(i.uv);
clip(0.5-l);
return float4(i.color,1);

iOS対応

上記の変更でQuest等のAndroid端末で表示ができるようになった。ただ下記TweetのようにiOS端末で表示が崩れてしまった。

この件に関しては勉強不足で詳しく説明できないけど、iOS端末はネイティブで浮動小数点テクスチャをサポートしておらず、DecodeHDR関数でデコードしてあげる必要があるとのこと。例えばこんな感じ。

DecodeHDR(tex2Dlod(_Pos, float4(uv + e.xy,0,0)), _Pos_HDR).xyz

デコードしてあげるとこんな感じでキレイに点が表示された。

えっじゃあTex2Dlod使っただけじゃ正しい色が取れてないの?と気になるところだけど全然情報がない。闇を感じた。

感想

これでPC、Oculus Quest、その他Android端末、iOS端末の主要なデバイスで点群表示ができるようになった。iPhone 12 Proが発売されてかなりお手軽に点群が撮れるようになってきたのでそのうち点群展でも開きたいな。