VCIで3D空間にグラフを描画する試み

# はじめに

この記事は、[VCI Advent Calendar 2019](https://qiita.com/advent-calendar/2019/vci)の14日目の記事です。

バーチャルキャストの世界の中で、データを３次元のグラフとして描画したい。
無理やり実装ですが、それを実現したVCI「ぐらんまてぃか」の実現方法を記載するものになります。


## 3D世界にグラフを描きたい！

バーチャルキャストでVCIのスクリプトが使えるようになって、やりたいと思ったこと。
その一つがSFアニメなので出てくるグラフ描写。
あれを実データでちゃんと描画したいとわたしは思ったのでした。

#### 要件は以下のようなもの
* Vキャス内で意図した数値データをグラフにして描画したい
* カラフルで半透明できれいにしたい
* 同じ物を使って、いろんなグラフが描けるようにしたい

#### 今回の実現方法は以下の通り

#### 考え方
* 当たったアイテムの名前をデータ受け渡し文字列として使い「データ構文」を決めておく
* そこそこの数の小さな箱と球体を本体VCIに格納しておく
* 小さな箱と球体のXYZ方向への拡大縮小を調整してグラフを書く

#### 実際の処理の流れ
1. 当たったアイテムの名前から配列を作る
2. 配列の2つ目の文字列でなんのグラフを描くのかを判定する
3. グラフ毎に配列の3つめの文字列以降を使って描画処理を実行する

「データ構文」に従った名前を持つアイテムを「ぐらんまてぃか」に当てればグラフが描画できる
といった形で要件を実装します。

## 本体VCIを作る

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222644/1b642597-6d7c-965a-edb5-dafee0dcfcf4.png)
グラフの描画に使う箱と球体をとりあえず入れておきます

![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222644/90cea40d-3214-31d4-42a7-39ace725fc65.png)
箱の設定はこんな感じ


## データ構文を考える

まず、データ描画に使うデータ構文を考えます。

* "_"を区切り文字とする。
* 区切った時の1番目の文字列で、本VCI「ぐらんまてぃか」対応であるかを判定する。
* 区切った時の2番目の文字列で、どんなグラフを描画するか判定する。
* 区切った時の3番目以降の文字列は、グラフによって利用意味を変える。

現時点では「棒グラフ（縦）」「棒グラフ（横）」「ネットワークグラフ」に対応するようにしました。

構文に従った文字列は以下のようになります

* 棒グラフ（縦） `MEME_BarChart_12_12_3_41_46_11_43_23_5_8_9_12_3`
* 棒グラフ（横） `MAME_BarGraph_1_5_21_21_2_11_13_30_52_8_19_22_7`
* ネットワークグラフ `MAME_Network_Node_a_12_b_12_c_3_d_41_e_46_f_11_g_43_h_23_i_5_j_8_Edge_a-b_3_a-c_1_a-d_4_a-g_3_b-j_6_d-f_4_d-g_6_e-f_3_e-h_4_g-h_1_g-i_3_g-j_3_h-j_2`

グラフ毎の構文については、後半で解説します。

## 実際の処理とコード

### 1. アイテム名から配列を作る

アイテムが触れたらアイテム名の文字列内の"_"を区切り文字として分離し、配列にします。

```lua

function split(str, ts)
    -- 引数がないときは空tableを返す
    if ts == nil then return {} end
    local t = {} ; 
    i=1
    for s in string.gmatch(str, "([^"..ts.."]+)") do
        t[i] = s
        i = i + 1
    end  
    return t
end
function onTriggerEnter(item, hit)
    local array = split(hit, "_")
~~~~~~~~~~~~以下略~~~~~~~~~~~~
```
split構文がデフォルトではないようなので、コピペでsplit構文は作ってあります。

`onTriggerEnter(item, hit)`は、Unity上の設定でisTriggerにチェックをつけて出力したVCIの場合にアイテム同士が触れると発生してくれるイベントです。
* 第一引数（item）が触れられたアイテムの名前
* 第二引数（hit）は触れたアイテムの名前
になります。

ここでの処理は、触れたアイテムの名前文字列（hit）を"_"で区切って配列（array）にしています。


### 2. 配列の2つ目の文字列でなんのグラフを描くのかを判定する

```lua

~~~~~~~~~~~~以下略~~~~~~~~~~~~
if array[2] == "BarChart" then
~~~~~~~~~~~~以下略~~~~~~~~~~~~
elseif array[2] == "BarGraph" then
~~~~~~~~~~~~以下略~~~~~~~~~~~~
elseif array[2] == "Network" then
~~~~~~~~~~~~以下略~~~~~~~~~~~~
```
考えた構文に従って、配列の２番目の文字列で描画するグラフに処理を分岐させます。

### 3.グラフ毎に配列の3つめの文字列以降を使って描画処理を実行する

#### 3-A.棒グラフ
```lua
        --- 棒グラフ（縦）
        for v in pairs(array) do
            if v > 2 then
                local item =  vci.assets.GetSubItem("Cubeword"..(v-2))
                item.SetVelocity(Vector3.__new(0,0,0))
                item.SetLocalPosition(Vector3.__new(0,0,0))
                item.SetRotation(Quaternion.__new(0,0,0,1.0))
                item.SetLocalPosition(Vector3.__new(0.15*v,0.2+0.1*array[v]/2,0))
                item.SetLocalScale(Vector3.__new(0.1,0.1*array[v],0.1))
            end
        end
```

区切った３番目以降の値は、棒グラフの縦の高さとして処理します。
`local item =  vci.assets.GetSubItem("Cubeword"..(v-2))`
で箱を指定し、array[v]のサイズにY軸(縦)方向に拡大、起点からY軸の＋ー両方に拡大するので、位置も半分サイズ分あげます。
![D5ArKfTUwAAATpY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222644/6be56cde-a776-426f-71db-da5b58f38ecf.png)


#### 3-B.ネットワークグラフ

ネットワークグラフは、3文字以降に「Node」か「Edge」の文字列が存在するかどうかで、そこからの処理を切り替えます。
Nodeがネットワークの点になる部分で、Edgeがネットワークの線の部分です。
文字列を再度みます。

`MAME_Network_Node_a_12_b_12_c_3_d_41_e_46_f_11_g_43_h_23_i_5_j_8_Edge_a-b_3_a-c_1_a-d_4_a-g_3_b-j_6_d-f_4_d-g_6_e-f_3_e-h_4_g-h_1_g-i_3_g-j_3_h-j_2`

* Node
Node_a_12_b_12_c_3...
の部分は、点aを描画し、サイズは12です。点bを描画し、サイズは12です。点cを描画し、サイズは3です。というような意味になります。

* Edge
Edge_a-b_3_a-c_1_a-d_4
の部分は、点aと点bの間に線を描き、その線の太さは3です。点aと点cの間に線を描き、その線の太さは1です。点aと点dの間に線を描き、その線の太さは4です。というような意味になります。


コードは以下のようになっています。

```lua
--- ネットワーク図
        local flg = ""
        local node = {}
        local nodeids = {}
        local edge = {}
        local nodeNo = 0
        local edgeNo = 0
        for v in pairs(array) do
            if array[v]=="Node" then
                flg = "Node"
            elseif array[v]=="Edge" then
                flg = "Edge"
            else
                if flg == "Node" and v%2==0 then
                    nodeNo = nodeNo +1
                    node[array[v]] = {0.5*math.random(10),0.5*math.random(10),0.5*math.random(10)}
                    nodeids[array[v]] = nodeNo
                    local item =  vci.assets.GetSubItem("SphereWord"..nodeNo)
                    item.SetLocalPosition(Vector3.__new(node[array[v]][1],node[array[v]][2],node[array[v]][3]))
                    item.SetLocalScale(Vector3.__new(0.02*array[v+1],0.02*array[v+1],0.02*array[v+1]))
                elseif flg == "Edge" and v%2==1 then
                    edgeNo = edgeNo+1
                    local set = split(array[v], "-")
                    local ds = math.sqrt((node[set[1]][1]-node[set[2]][1])*(node[set[1]][1]-node[set[2]][1]) + (node[set[1]][2]-node[set[2]][2])*(node[set[1]][2]-node[set[2]][2]) + (node[set[1]][3]-node[set[2]][3])*(node[set[1]][3]-node[set[2]][3]))
                    local item = vci.assets.GetSubItem("Cubeword"..edgeNo)
                    item.SetVelocity(Vector3.__new(0,0,0)) -- 動きオフ
                    item.SetRotation(Quaternion.__new(0,0,0,1.0)) -- 角度初期化
                    item.SetLocalPosition(Vector3.zero) -- 位置初期化
                    item.SetLocalRotation(Quaternion.LookRotation(Vector3.__new(node[set[2]][1],node[set[2]][2],node[set[2]][3])-Vector3.__new(node[set[1]][1],node[set[1]][2],node[set[1]][3]))) -- 方向を指定
                    item.SetLocalPosition(Vector3.__new((node[set[1]][1]+node[set[2]][1])/2,(node[set[1]][2]+node[set[2]][2])/2,(node[set[1]][3]+node[set[2]][3])/2)) -- 位置を指定
                    item.SetLocalScale(Vector3.__new(0.02,0.02,ds))
                    edge[array[v]] = array[v+1]
                end
            end
        end
```

実態は棒グラフと同じようなことをやっています。
点は球体を指定されたサイズで置く。
線は箱を引き伸ばして、球を結ぶように見えるように置く。

本当は重みづけだったり、配置の工夫だったりいろいろやってみたいことはありますが、とりあえず描画はこんな感じ。

![20190401-031330_62.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222644/5ad55d08-d5fe-0fe6-0821-9d2b8ced9d66.jpeg)


## 終わりに

こんな感じで、今回は名前でグラフを描画するVCIをどう実現したかをかきました。
こちらを作ったのは、まだ、message構文などがない頃でしたが、現在ではmessage構文などもあるので、それを使って描画したいデータの情報を受け渡すという方がスタンダードかもしれないですね。