■ 2023年10月27日 [Unity][RealSense] RealSense (1) SDK の Unity Package の使い方
Intel RealSense SDK の Unity Package の使い方
なんと初めて Unity とか RealSense とかいう Blog カテゴリを使います。私としては画期的ですね。もちろん自分自身は OpenGL / GLSL を使ったプログラムを C や C++ でゴリゴリに書くってことを長らくやってきたわけです。でも、そういう実装を他の人に渡しても(自分がメンテする場合を除いて)結局活用してもら無かったりします。それで、かつては「Maya から使えんか」とか言われ、今は「Unity で何とかならんか」と言われるわけです。それで自分も遅まきながら Unity とか勉強したりしているんですが…
やっぱり難しいですね。これ、やっぱり CG やリアルタイムグラフィックスなんかを理解していて、その上で API やプログラミングの知識を持ってないと、ランタイムを使いこなせないんじゃないでしょうか。当たり前ですね。ゲームエンジンですから。でも、エディタがすごくよくできているせいで「誰でも」「簡単に」「ゲームが」作れるようになっています。素晴らしいです。それで「Unity でゲームを作ったことがあります」という学生さんに「じゃあ Unity を卒研の道具に使えば」と言ったりするんですけど(うちでは「研究テーマ」と「研究ツール」の両方を決めることを求めています)、なぜか途中で詰んでしまう人がたまに出ます。
実はそれがちょっと不思議だったんですけど、なんだかわかった気がしました。彼らにとっても Unity は実は難しかったんですね。私自身が低レイヤの実装ばかりしているせいかもしれませんけど、うちの学生さんたちの企画する研究内容が要素技術に偏る傾向があるみたいで、ゲームエンジンに追加する機能とかゲームエンジン自体を作るみたいな話になりがちなんですね。それなのに、そういうテーマに対して(彼らのこれまでの使い方で)ゲームエンジンを使うということは、今のゲームエンジンに無い機能をエディタを作って作るとか結構ややこしい話になってしまいます。本当はゲームエンジンの本体であるランタイム・ライブラリの機能を読み解いて使いこなさないといけないのに、それは「ガチプログラミング」になってしまって手も足も出なくなってしまうというのが、「詰む」理由のように思えます。
以前、他の大学の先生と「Unity を使うと卒論が企画書っぽくなりがち」という話をしたことがあります。確かに「~する」とは書いてあっても、どうやってやったか、何を考えてそうしたかという説明が不足しがちに感じます。だから読んでもおぼろげなイメージとか曖昧なビジョンみたいなものは感じとれても、具体的なものが見えてきません。確かに作ってはいるのですけど、多分、試行錯誤の結果なんだか知らないけどそうなったとか、やろうと思ってたことに似たものを見つけたので、そのコードをはめてみたらそれらしく動いたみたいなものの積み重ねでゴールまで来ちゃったんじゃないか、などと思ってしまいます。でも、それは今後の(AI 時代の?)「プログラミングスタイル」に通じるものなのかもしれませんけど。
自分自身のテーマは CG というかインタラクティブ CG だと思ってはいるんですけど、そういうものの応用を考えていると、いつの間にかゲームとか VR とかに関わることになってしまいます。それで VR から AR、MR、そして XR とかいう方向に歩いていくと (と言っても、これらはみんなおんなじだと思っているんですけどね)、CV や点群やセンサ類などにも関係してしまいます。もともと CG は数学・物理から認知科学とか芸術とかにも関わる総合科学だと思ってはいたのですけど、何か一つシステムを作ろうと思ったときに考えないといけない要素が結構たくさんあるんですよね。だから、そういうものをあらかじめパッケージにしたものを使わないと、手間と時間がかかりすぎます。それでも、本当に必要なものは、それらの基礎的な理論だなあといつも思います。時間がないという言い訳をしながら、それらの成果物を借りてごまかしていますけど。
SDK のインストール
というわけで、今回は自分自身が RealSense SDK (librealsense) の Unity Package の使い方を勉強しながら、Teams 上で学生さんに説明したので、その内容をここにメモっています。
- GitHub の librealsense の Releases の Assets にある SDK のインストーラ
Intel.RealSense.SDK-WIN10-2.*.*.*.exe
をダウンロードしてください。また Unity PackageIntel.RealSense.unitypackage
もダウンロードしてください。 - SDK のインストーラを起動して SDK をインストールしてください。インストール完了後に
Realsense Viewer
の起動を促されます。これを起動すると PC に接続している RealSense の Firmware のアップデートを促されますので、アップデートしておいてください。RealSense Viewer はこのあと終了してください。 - Unity Hub か Unity のエディタを起動して、新しいプロジェクトを作ってください。テンプレートは
3D
か3D (URP)
を選んでください。3D
は古いビルトインパイプライン、3D (URP)
は Universal Render Pipeline と言って自分でレンダリングパイプラインを記述できる Scriptable Render Pipeline で、今はもっぱらこれが使われるみたいです。ここでは3D (URP)
を選ぶことにします (このプロジェクトはうっかり3D
すなわちビルトインパイプラインで作っちゃったけど、3D (URP)
でも同じ手順でできると思います)。 Assets
メニューからImport Package... >
Custum Package...
を選び、先ほどダウンロードした RealSense の Unity Package を開いてください。Import Unity Package
というウィンドウがポップアップしますから、そのままImport
をクリックしてください。- Unity Package の Import が終わると Project ウィンドウに RealSenseSDK2.0 というフォルダができますから、その中の Scenes というフォルダを開いてください。中に StartHere というシーンがあります。これは、ここからすべてのシーンを起動するものです。これをダブルクリックして
▶
をクリックすれば、エディタ内で実行を開始します。 - 学生さんが最初に必要になるのは、このなかの PointCloudDepthAndColor だと思います。このシーン自体は Samples のフォルダの中にあります。また、同じフォルダにある PointCloudProcessingBlock には、Decimation や時間方向のフィルタ、穴ふさぎフィルタなどが組み込まれています。これには UI もついているので、UI を付けるときの参考にもなると思います。
PointCloudDepthAndColor を実行すると、こんな具合になります。視点はマウスで動かせます。
ただ、これは実際には点で描いているので、クローズアップするとこういうことになります。
これを三角形のメッシュで描いて、クローズアップしても隙間が空かないようにしたいと思います。
新しいシーンを作る
- 自分で新しいシーンを作るときは、
File
メニューのNew Scene
を選ぶか、とりあえず Project ウィンドウの Assets の中にある Scenes に作られている SampleScene を使ってください。 - Project ウィンドウの Assets の RealSenseSDK2.0 の Prefabs にある
PointCloud
、RsDevice
、RsProcessingPipe
の3つの Prefab を Hierarchy ウィンドウにドラッグ&ドロップします。
RsDevice
が RealSense のインタフェースなので、RealSense が複数あるときは、これも複数 Hierarchy に置きます。その場合、センサごとにRsDevice
の名前を変えておいた方が良いでしょう (RsDevice0
,RsDevice1
, ...)。- どの
RsDevice
がどの RealSense を担当するかは、Requested Serial Number で指定すればいいんじゃないかと思います。これは RealSense Viewer で調べることができます。 - 実空間上の RealSense の位置を Unity の 3D 空間に反映する場合のように点群を
RsDevice
に連動して動かしたければ、PointCloud をRsDevice
の下の階層に置いておくと良いじゃないかと思います。 - 複数の RealSense から得た複数の点群をシーンに配置する場合は、それぞれの
RsDevice
の下にPointCloud
を置いた方が良いでしょう。
- Hierarchy ウィンドウの
RsProcessingPipe
がRsDevice
のデータを加工するパイプラインになります。したがって、この Source には使用する RealSense を担当するRsDevice
を指定し、Profile には使用する設定を選びます。いずれも⊕
をクリックすれば、選択可能なものの一覧が出ます。検索欄に数文字打ち込めば、目的のものを見つけられると思います。
- これも RealSense が複数あるときは、複数 Hierarchy に置き、
RsDevice
ごとにRsProcessingPipe
の名前を変えておいた方が良いんじゃないかと思います (RsProcessingPipe0
,RsProcessingPipe1
, ...)。
- これも RealSense が複数あるときは、複数 Hierarchy に置き、
- Profile は Project ウィンドウの Assets の RealSenseSDK2.0 の ProcessingPipe フォルダに入っており、PointCloudProcessingBlocks はここにあるすべての設定をまとめたものです。とりあえずこれを設定しておけばいいと思います。デプスとカラーしか使わないなら PointCloudDepthAndColor でもいいと思います。
- Hierarchy ウィンドウの
PointCloud
は、このRsProcessingPipe
からデータを受け取ります。これは Mesh Filter でメッシュを作成しない代わりに、RsPointCloudRenderer が Source に指定されているRsProcessingPipe
からデータを受け取って描画します。
この状態で ▶
をクリックすると、黒い点群が表示されます。
- Hierarchy ウィンドウで
RsDevice
のColor
ストリームを選択してください。
- Inspector でこれを有効にします。
- RsStreamTextureRenderer の Source には
RsProcessingPipe
、Stream には Color、Format には Rgb8 を選んでください。RealSense のカラーの生データは多分 RGB ではないので (確認してないけど YUV とか YUY2?)、RsDevice
で RGB に変換したものを使うんだと思います。
- 次に、Color の Texture Binding のマテリアルに PointCloudMat を選んでください。これも検索欄に何文字か打ち込めば見つけられると思います。
- そうすると No Function のところで Material が選べるようになりますから、mainTexture を選んでください。これによって、カラーのテクスチャが RsPointCloudRenderer のマテリアルの (シェーダが参照する _MainTex) テクスチャに設定されます。
この状態で ▶
をクリックすると、テクスチャが貼られます。
ただし、これは三角形メッシュでではなく点 (というか四角形) です。点のサイズは PointCloud
の RsPointCloudRenderer の Point Size で指定します。三角形のメッシュにするにはインデックスデータを作るなど、もう一工夫必要になります。
Orbit Camera Control を組み込む
このままではカメラが遠いし動かすこともできないので、Project ウィンドウの Assets の RealSenseSDK2.0 の Misc の Utils にある OrbitCameraControl というスクリプトを組み込みます。Hierarchy ウィンドウの Main Camera
を選択し Inspector の一番下の Add Component
をクリックして、OrbitCameraControl を選んでください。
これでマウスを使ってカメラを操作することができるようになります。この OrbitCameraControl は結構便利で、他のものにも流用できると思います (他では Unity Editor のエラーが出ることがありますが、スクリプトの Start() の当該の部分を削除すると動きます)。
スクリプトの解説
あと、スクリプトとシェーダについて、少し説明します。スクリプトは Assetes の RealSenseSDK2.0 の Scripts フォルダにあります。
まず RsDevice というスクリプトを開いてください。ダブルクリックすれば VisualStudio が起動すると思います。デフォルトの Multithread の場合、下記のように RealSense の起動時に開始されたスレッドで RealSense のフレームの取得を待ち、データが到着したら SampleEvent というイベントを発行するという処理を繰り返しています。
/// <summary> /// Worker Thread for multithreaded operations /// </summary> private void WaitForFrames() { while (!stopEvent.WaitOne(0)) { using (var frames = m_pipeline.WaitForFrames()) RaiseSampleEvent(frames); } }
なお Unity Thread の場合は、このスクリプトの最後の Update() で、次のようにフレームを取得して同様に SampleEvent を発行しています。
void Update() { if (!Streaming) return; if (processMode != ProcessMode.UnityThread) return; FrameSet frames; if (m_pipeline.PollForFrames(out frames)) { using (frames) RaiseSampleEvent(frames); } }
点群の描画を行っているのは RsPointCloudRenderer というスクリプトです。ここでは次のように入力フレーム frame がコンポジットフレーム (複数のデータが合成されているとき) はフレームセットのデプスのフレームから点の位置 Xyz32f を取り出してキュー q
に入れています。
private void OnNewSample(Frame frame) { if (q == null) return; try { if (frame.IsComposite) { using (var fs = frame.As<FrameSet>()) using (var points = fs.FirstOrDefault<Points>(Stream.Depth, Format.Xyz32f)) { if (points != null) { q.Enqueue(points); } } return; } if (frame.Is(Extension.Points)) { q.Enqueue(frame); } } catch (Exception e) { Debug.LogException(e); } }
そのあと (すべての Update()
より後に実行される) LateUpdate()
において、以下のように (メッシュサイズが変わったときにメッシュの作り直しをしたり、テクスチャがあればそれを更新したりした後で) points.CopyVertices(vertices);
により点群から頂点位置を取り出した後、それを使って mesh.vertices = vertices;
としてメッシュの頂点を更新しています。mesh.UploadMeshData(false);
はそのメッシュのデータがこれ以降使われず描画後に破棄しても問題ないことを Mesh Renderer に伝えています。
protected void LateUpdate() { if (q != null) { Points points; if (q.PollForFrame<Points>(out points)) using (points) { if (points.Count != mesh.vertexCount) { using (var p = points.GetProfile<VideoStreamProfile>()) ResetMesh(p.Width, p.Height); } if (points.TextureData != IntPtr.Zero) { uvmap.LoadRawTextureData(points.TextureData, points.Count * sizeof(float) * 2); uvmap.Apply(); } if (points.VertexData != IntPtr.Zero) { points.CopyVertices(vertices); mesh.vertices = vertices; mesh.UploadMeshData(false); } } } }
描画処理を Update()
ではなく LastUpdate()
で行っているのは、(デフォルトでは使われない) Unity Thread の場合に Upload()
で点群の収集を行っているために、それより後で描画するためだと思います。(デフォルトの) Multithread なら描画と並行して点群の収集が行われるので、Update()
で描画しても問題ない気がします。
点群自体はこの vertices
に入っているので、点群そのものを使うならこれか、その前の points を使えばいいと思いおます。デプスに対してごにょごにょしようとするなら RsDevice スクリプトでフレームをいじるか、RsProcessingPIpe
の ProcessFrame をいじる (この中で使っている Process っていう Abstruct クラスをオーバーライドする?) といいんじゃないでしょうか。
点群を三角形メッシュ化する
いよいよ本題に入ります。最初に Renderer スクリプトを作成します。
- Project ウィンドウの Assets の中に Scripts というフォルダを作ります。この Assets を選択した状態で、上の
Assets
メニューからCreate >
Folder
を選び、フォルダを作成してください。フォルダ名は Scripts にしておくことにします。 - Project ウィンドウの Assets の RealSenseSDK2.0 の Scripts にある RsPointCloudRenderer を、先ほど作成した Assets 直下の Scripts にコピーします。RsPointCloudRenderer を選択して
Ctrl+C
した後、Assets 直下の Scripts を選択してCtrl+V
してください。 - コピーした Assets 直下の Scripts の RsPointCloudRenderer の名前を、(例えば) TriangleMeshRenderer に変更します。
- TriangleMeshRenderer をダブルクリックして編集します。
- クラス名を (例えば)
TriangleMeshRenderer
にすることにします。クラス名の変更は、クラス名を右クリックして "名前の変更Ctrl+R
,Ctrl+R
" で行った方が無難だと思います。[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] //public class RsPointCloudRenderer : MonoBehaviour public class TriangleMeshRenderer : MonoBehaviour {
ResetMesh()
の前あたりに以下のCreateTriangleMeshIndex()
というメソッドを追加します。このインデックスの作り方については、メディアデザインセミナー2Bの資料などを参考にしてください。private static int[] CreateTriangleMeshIndex(int slices, int stacks) { // 三角形の頂点の数 int vertices = slices * stacks * 6; // 三角形のデータ int[] indeces = new int[vertices]; // 三角形の頂点番号を求める for (int j = 0; j < stacks; ++j) { // 各段の左端の下側の頂点番号の格納先 int f0 = j * slices * 6; // 各段の左端の下側の頂点の頂点番号 int p0 = j * (slices + 1); for (int i = 0; i < slices; ++i) { // 左から i 番目の四角形の左下の頂点番号の格納先 int fi = f0 + i * 6; // 左から i 番目の四角形の左下の頂点番号 int pi = p0 + i; // 1つ目の三角形の頂点番号 indeces[fi + 0] = pi; indeces[fi + 1] = pi + 1; indeces[fi + 2] = pi + slices + 1; // 2つ目の三角形の頂点番号 indeces[fi + 3] = pi + slices + 2; indeces[fi + 4] = pi + slices + 1; indeces[fi + 5] = pi + 1; } } // 作成したインデックスを返す return indeces; }
ResetMesh()
で行っているインデックスの作成にCreateTriangleMeshIndex()
をいます。//var indices = new int[vertices.Length]; //for (int i = 0; i < vertices.Length; i++) // indices[i] = i; var indices = CreateTriangleMeshIndex(width - 1, height - 1);
- このインデックスを
MeshTopology.Triangles
でメッシュに設定します。//mesh.SetIndices(indices, MeshTopology.Points, 0, false); mesh.SetIndices(indices, MeshTopology.Triangles, 0, false);
シェーダの作成
次にシェーダを作成します。もちろん GLSL ではなく HLSL です。色々勝手が違います。
- Project ウィンドウの Assets の中に Shaders というフォルダを作ります。この Assets を選択した状態で上の
Assets
メニューからCreate >
Folder
を選び、フォルダを作成してください。フォルダ名は Shaders にしておくことにします。 - 作成した Shaders フォルダを選択した状態で上の
Assets
メニューからCreate >
Shader >
Unlit Shader
を選択して、シェーダを作成してください。シェーダ名は TriangleMesh にすることにします。 - このシェーダに
_UVMap
というプロパティを追加します。RealSense はデプスセンサ (ステレオカメラ) とカラーセンサが独立していて違う位置にあり、解像度も異なるため、点群の各点におけるカラーデータの画素位置 (テクスチャ座標, UV) を別のデータとして持っているからです。Shader "Unlit/TriangleMesh" { Properties { _MainTex ("Texture", 2D) = "white" {} _UVMap("UV", 2D) = "" {} // 追加 }
- フォグは使わないのですが、この部分には変更を加えず、設定を残したままにしています。
SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; };
_UVMap
のサンプラ変数を追加します。sampler2D _MainTex; sampler2D _UVMap; // 追加 float4 _MainTex_ST;
- RealSense の
RsProcessingPipe
から得られる点群データは上下が反転しているので、バーテックスシェーダで y 座標値の符号を反転します。v2f vert (appdata v) { // y 座標値を反転する v.vertex.y = -v.vertex.y; v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; }
- フラグメントシェーダでは、バーテックスシェーダから与えられるテクスチャ座標
i.uv
を使って_UVMap
をサンプリングし、カラーデータのテクスチャ座標uv
を得ます。このテクスチャ座標が範囲外の時は、画素を捨てます。fixed4 frag (v2f i) : SV_Target { // _UVMap をサンプリングしてテクスチャ座標 uv を得る float2 uv = tex2D(_UVMap, i.uv); if (any(float4(uv, 1.0 - uv) <= 0.0)) discard;
- このテクスチャ座標
uv
を使ってカラーデータ _MainTex をサンプリングして、フラグメントの色を求めます。// sample the texture fixed4 col = tex2D(_MainTex, uv); // uv をテクスチャ座標に使う // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }
多分、この用途ではフォグや TRANSFORM_TEX
によるテクスチャ座標の位置調整は不要だと思うので、削除しても構わない (むしろ削除した方がいい) んじゃないかと思います。
マテリアルの作成
マテリアルを作成して作成したシェーダを組み込みます。
- Project ウィンドウの Assets の中に Resources というフォルダを作ります。この Assets を選択した状態で上の
Assets
メニューからCreate >
Folder
を選び、フォルダを作成してください。フォルダ名は Resources にしてください。 - この Resources フォルダを選択した上で、再度、上の
Assets
メニューからCreate >
Folder
を選び、フォルダを作成してください。フォルダ名は Materials にすることにします。このプロジェクトではこんなに深いフォルダを作る必要はないと思うのですが、Unity のスクリプトから参照するファイルのパスはこの Assets の下の Resources からの相対パスになっているみたいなので、それに合わせます。 - 作成したフォルダ Materials を選択した状態で上の
Assets
メニューからCreate >
Material
を選択して、マテリアルを作成してください。マテリアル名は TriangleMeshMat にすることにします。 - Inspector で Shader に先ほど作った TriangleMesh を選びます。
- Hierarchy ウィンドウで
RsDevice
のColor
ストリームを選択し、Inspector で RsStreamTextureRenderer の Source にRsProcessingPipe
、Stream に Color、Format に Rgb8 が選ばれていることを確認してください。 - Texture Binding のマテリアルに TriangleMeshMat を選んでください。その際、Material.mainTexture が選ばれていることを確認してください。
TriangleMesh オブジェクトの作成
空の Game Object を作って、そこに作成したスクリプトを組み込みます。
- Hierarchy ウィンドウで
PointCloud
を選択し、上のEdit
メニューからDelete
を選ぶか、Delete キー
をタイプして削除します。 - 上の
GameObject
メニューからCreate Empty
Ctrl+Shift+N
を選び、空の Game Object を作成します。オブジェクト名は、これもTriangleMesh
とかにしておきます。 - この
TriangleMesh
を選択した状態で上のComponent
メニューからMesh >
Mesh Filter
を選ぶか、Inspactor の一番下のAdd Component
をクリックして Mesh Filter を選んで、Mesh Filter を追加します。 - 同様に、この
TriangleMesh
を選択した状態で上のComponent
メニューからMesh >
Mesh Renderer
を選ぶか、Inspactor の一番下のAdd Component
をクリックして Mesh Renderer を選んで、Mesh Renderer を追加します。 - 追加した Mesh Renderer の Material の Element0 に TriangleMeshMat を選びます。
- Inspector の一番下の
Add Component
をクリックして、Triangle Mesh Renderer を選んでください。
- Triangle Mesh Renderer の Source に
RsProcessingPipe
を選びます。
- Triangle Mesh Renderer の Material の Shader に TriangleMesh を選びます。
これでようやくこういう結果が得られます (点群と色が若干ずれている気がするのは RealSense をしょっちゅう落っことしたりしたのにキャリブレーションし直していないからですかね?)。
でも残念ながら、視点を動かすと、これはこういう表示になっています。
これは RealSense が計測不能だったりした点の位置を (0, 0, 0) にしてしまうため、三角形のメッシュで表示したときに3つの頂点のうち1つが (0, 0, 0) の三角形がそこまで伸びて表示されるからです。そこで、これを避けるために、どれか1つの頂点でも (0, 0, 0) になっている三角形は描かないようにします。それには (あんまり使いたくないけど) ジオメトリシェーダを使います。
ジオメトリシェーダの追加
伸びてしまった三角形をジオメトリシェーダで削除します。
- Project ウィンドウの Assets 直下の Shaders フォルダに入っている (はずの) TriangleMesh シェーダのソースファイルを編集します。
- このシェーダが
geom
という名前のジオメトリシェーダを含むことを宣言します。Shader "Unlit/TriangleMesh" { Properties { _MainTex ("Texture", 2D) = "white" {} _UVMap("UV", 2D) = "" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma geometry geom // 追加 #pragma fragment frag // make fog work #pragma multi_compile_fog
- バーテックスシェーダで頂点の位置が (0, 0, 0) なら、その w も 0 にして次段のジオメトリシェーダに送ります。RealSense の計測範囲は有限なので、w = 0 すなわち無限遠のデータを取得していることはないはずです。
#include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; sampler2D _UVMap; float4 _MainTex_ST; v2f vert (appdata v) { // 頂点の位置が (0, 0, 0) なら w も 0 にして出力する if (all((float3)v.vertex == 0.0)) { v.vertex.w = 0.0; return v; } v.vertex.y = -v.vertex.y; v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; }
- 以下の内容のジオメトリシェーダを追加します。これは渡された3つの頂点
v[3]
のどれか一つでも w = 0 だったら頂点を一つも出力しません。[maxvertexcount(3)]
はこのシェーダが最大で3つの頂点を出力することを示しています。[maxvertexcount(3)] void geom (triangle v2f v[3], inout TriangleStream<v2f> ts) { if (any(float3(v[0].vertex.w, v[1].vertex.w, v[2].vertex.w) == 0.0)) return; ts.Append(v[0]); ts.Append(v[1]); ts.Append(v[2]); }
- フラグメントシェーダには変更ありません。
fixed4 frag (v2f i) : SV_Target { float2 uv = tex2D(_UVMap, i.uv); if (any(float4(uv, 1.0 - uv) <= 0.0)) discard; // sample the texture fixed4 col = tex2D(_MainTex, uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }
これで (0, 0, 0) に向かって伸びていた三角形が削除されます。
RealSense の D400 シリーズは (アクティブ) ステレオカメラなので、カメラに近い計測対象の周囲に視差による閉塞 (occulusion) により計測できない場所が生じることがあります (これが生じない LiDAR の L515 がディスコンなのは残念)。ここではその部分の三角形を単に削除しているので、そこに隙間 (穴) が生じています。
RealSense SDK にはそういう穴をふさぐ関数が用意されていますが (サンプルの PointCloudProcessingBlocks というシーンで使っています)、このプログラムではそれを使わず、GPU を使って自前実装しています。今後はそれも Unity で実装しようと考えています。
おまけ
以上はフォグの処理に手を加えずにコードの追加のみを行って実装しましたが、フォグの処理を削除すればシェーダの記述がもっと簡単になります。以下のバーテックスシェーダ vert()
は戻り値の型が v2f
ですが、戻り値として返している v
は appdata
型の引数です。v2f
と appdata
はシェーダセマンティクスが異なりますが、構造を同じにしておけば問題なくデータを渡すことができるようです (多分そういうことはしない方が良いと思うけど)。
Shader "Unlit/TriangleMesh" { Properties { _MainTex ("Texture", 2D) = "white" {} _UVMap("UV", 2D) = "" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma geometry geom #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; sampler2D _MainTex; sampler2D _UVMap; v2f vert (appdata v) { if (all((float3)v.vertex == 0.0)) { v.vertex.w = 0.0; return v; } v.vertex.y = -v.vertex.y; v.vertex = UnityObjectToClipPos(v.vertex); return v; } [maxvertexcount(3)] void geom (triangle v2f v[3], inout TriangleStream<v2f> ts) { if (any(float3(v[0].vertex.w, v[1].vertex.w, v[2].vertex.w) == 0.0)) return; ts.Append(v[0]); ts.Append(v[1]); ts.Append(v[2]); } fixed4 frag (v2f i) : SV_Target { float2 uv = tex2D(_UVMap, i.uv); if (any(float4(uv, 1.0 - uv) <= 0.0)) discard; return tex2D(_MainTex, uv); } ENDCG } } }