UnityでPoint Cloudを表示する方法

3次元点群を3Dビューアでグリグリとただ表示するだけならPoint Cloud Libraryを使ってすぐにできるんだけど、インタラクティブなコンテンツとして点群を利用したい場合にはPoint Cloud Libraryだけだとちょっと味気ない。Point Cloud Libraryだと視点を動かす時は点群の更新が止まっちゃうし。

何でUnityで点群を扱いたいかというと、お察しの通り、Kinectで取得した点群をカッコよく表示したいのです。Kinect V2になってからMicrosoftが公式にUnityプラグインを配布し始めたし、せっかくだから自前でOpenCVとかPoint Cloud Libraryでゴリゴリ頑張るよりも、Unityで他のAssetと組み合わせてサクッとリッチなコンテンツに仕上げたいのだ。

Microsoft公式のKinect v2 for Unityプラグインには、サンプルとしてKinectのColorとDepthをMesh化して表示するScriptコードが付属しているんだけど、無理やりMesh化して表示してるからちょっと汚らしい。Microsoft公式のUnityプラグインおよびサンプルプロジェクトは以下のページの”Unity Pro packages“のリンクから。↓


こちらの公式サンプルでは、Kinect V2から取得した点群を4分の1の量にダウンサンプリングしてから、それらを頂点としたポリゴンメッシュ化してリアルタイム表示している。
Kinect V2で取得可能な点群(つまりDepth画像の解像度)は
512 × 424 = 217,088
だけど、Unityには「1つのMeshオブジェクトが持つ三角形の数は最大65,535まで」という制限があるため、ダウンサンプリングで頂点の数を減らしてMesh化してるんだと思う。解像度をダウンサンプリングする代わりに、Depth値を近傍ピクセルの平均から求めたりもしてるんだけど、結果として表示されるMeshがあんまりきれいじゃない。

ということで、Unityで「面」ではなく「点」の描画を実現する方法を調べてみた。


スポンサーリンク



ちょっと調べてみると、どうやら点群ファイルを表示するための有料のAssetでPoint Cloud Viewer and Toolsというものがすでにあるようだ。以下サポートページに詳細が載っている。↓

Point Cloud Viewer & Tools for Unity

Point Cloud Viewer and Tools

このツールはファイル読み込み用なので、どうやら動的にKinectの情報を表示はできなさそう。読み込みをサポートしているフォーマットはXYZ, XYZRGB, CGO, ASC, CATIA ASC, PLY (ASC)。読み込んだファイルを自動でUnityのポリゴン制限数で分割して表示と、バイナリデータへの保存ができるようだ。

Kinectの点群をリアルタイムで表示するにはやっぱり自分でScriptを書くしかなさそうなので点の描画方法を調べてみたら、ドンピシャなブログ記事を見つけた。↓

Rendering a Point Cloud inside Unity

Here is a short example of how to render a point cloud using MeshRenderer inside Unity, have in mind that you have a limit of 65k points per mesh, so if you want to render more points, you need to split them.

ソースコードも全部載せてくれている。OpenGLのAPIコマンドを有効にしてPoint SizeとSmooth PointをEnableにして、Shader側で受け取ってやるってことね。

上記の記事だけだとShaderへのパラメータ渡しの記述が欠けてるけど、以下のフォーラムの記事に全部載っている。↓

How do I use PSIZE in a Unity 4.5.4 shader?

このフォーラム記事では、DirectX環境ではPoint Sizeの指定が描画に反映されないことについて話題になっている。要するに、残念ながらOpenGL環境でしか動かない仕様っぽい。

こちらが描画用のPoint Cloudをランダムに生成するサンプルコード。↓

using UnityEngine;
using System.Collections;
 
 
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class PointCloud : MonoBehaviour {
 
    private Mesh mesh;
    int numPoints = 60000;
 
    // Use this for initialization
    void Start () {
        mesh = new Mesh();
 
        GetComponent<MeshFilter>().mesh = mesh;
        CreateMesh();
    }
 
    void CreateMesh() {
        Vector3[] points = new Vector3[numPoints];
        int[] indecies = new int[numPoints];
        Color[] colors = new Color[numPoints];
        for(int i=0;i<points.Length;++i) {
            points[i] = new Vector3(Random.Range(-10,10), Random.Range (-10,10), Random.Range (-10,10));
            indecies[i] = i;
            colors[i] = new Color(Random.Range(0.0f,1.0f),Random.Range (0.0f,1.0f),Random.Range(0.0f,1.0f),1.0f);
        }
 
        mesh.vertices = points;
        mesh.colors = colors;
        mesh.SetIndices(indecies, MeshTopology.Points,0);
 
    }
}

点を受け取って描画するShader。↓

Shader "Custom/VertexColor" {
     Properties {
         _PointSize("PointSize", Float) = 1
     }
     SubShader {
         Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
         LOD 200
         Pass {
         Cull Off ZWrite On Blend SrcAlpha OneMinusSrcAlpha
         
             CGPROGRAM
              
                 #pragma exclude_renderers flash
                 #pragma vertex vert
                 #pragma fragment frag
                 
                                    
                 struct appdata {
                     float4 pos : POSITION;
                     fixed4 color : COLOR;
                 };
 
                 
                 struct v2f {
                     float4 pos : SV_POSITION;
                     float size : PSIZE;
                     fixed4 color : COLOR;
                     };
                 float _PointSize;
 
                 v2f vert(appdata v){
                     v2f o;
                     o.pos = mul(UNITY_MATRIX_MVP, v.pos);
                     o.size = _PointSize; 
                     o.color = v.color;
                     return o;
                 }
 
                 half4 frag(v2f i) : COLOR0
                 {
                     return i.color; 
                 }
             ENDCG
         }
     } 
 }

そしてUnityでOpenGLのPoint Sizeコマンドを有効にするコード。(これはMain Cameraとかにアタッチする)↓

#if UNITY_STANDALONE
 #define IMPORT_GLENABLE
 #endif
 
 using UnityEngine;
 using System;
 using System.Collections;
 using System.Runtime.InteropServices;
 
 public class EnablePointSize : MonoBehaviour 
 {
     const UInt32 GL_VERTEX_PROGRAM_POINT_SIZE = 0x8642;
 
     const string LibGLPath =
 #if UNITY_STANDALONE_WIN
         "opengl32.dll";
 #elif UNITY_STANDALONE_OSX
         "/System/Library/Frameworks/OpenGL.framework/OpenGL";
 #elif UNITY_STANDALONE_LINUX
         "libGL";    // Untested on Linux, this may not be correct
 #else
         null;   // OpenGL ES platforms don't require this feature
 #endif
     
 #if IMPORT_GLENABLE
     [DllImport(LibGLPath)]
     public static extern void glEnable(UInt32 cap);
 
     private bool mIsOpenGL;
 
     void Start()
     {
         mIsOpenGL = SystemInfo.graphicsDeviceVersion.Contains("OpenGL");
     }
 
     void OnPreRender()
     {
         if (mIsOpenGL)
             glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
     }
 #endif
 }

初めて知ったけど、UnityのMeshってMeshTopologyを指定すれば三角形じゃなくてもいけるのね。

リアルタイムで動く点群をグリグリできると、スターウォーズに出てくる立体映像みたいでカッコイイね。


スポンサーリンク

関連記事


Also published on Medium.