バーチャルキャスト内で使用可能なVCIスライドを自動作成

![a338432f-3479-4614-86c6-d52e073b644e_base_resized.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286581/662bddc4-1de2-f58c-ae32-8e2af2becd9c.jpeg)

#概要
画像のようにバーチャルキャスト内で使用可能なVCIスライドを作成し、そのVCIスライドを画像から自動作成できるツールを作りました。苦労話の愚痴に、もしかしたら役に立つかもしれない小ネタを混ぜつつ作り方？を書きます。似たような事を初めてする人の参考になれば幸いです。
https://120byte.booth.pm/items/1297409

#バージョン
Unity : 2018.4.0f1
バーチャルキャスト : 1.6.4b
VCI : 0.19

#まずVCIを作る
普通にVCIを作ります。同期が地獄です。HMD付けたり外したり、SteamVRがダダこねたり、RiftSが映らなかったり、コミュ障メンタルに鞭打って凸ったりしながら動確を取ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286581/1bfae127-c548-7b06-3a15-124acd5410a7.png)

##ポイント
VCI Sub Itemが提供してくれる拡縮機能には下限サイズ(おそらく0.2)があります。
上記から逃げるにはVCI Sub Itemの子に可視オブジェクトを入れて任意のサイズ(xyz比が重要)に設定します。
GetPositionの値にオフセットを乗せる場合は、オブジェクトにオフセットを乗せるとluaが綺麗に書けます。
オブジェクト移動の基準位置には、アンカーのようにして使う透明オブジェクトが便利だと思います。
アップしたVCIにluaがない場合は、ローカルで更新されるまでゲスト側に反映されない時があるようです。
oculusとviveでトリガーとグリップが逆になってるので、操作を説明する時には注意が必要です。
所有権は移動できる状態の握った時に移る。という事を忘れてはいけません。
on某系の関数は所有権を持った人側でしか動きませんのでログ(print)も出ません。
not ownerというログがあるようですが、エラーではないのでビビってはいけません。
オブジェクト非表示が無いのでサイズ0にすると中間フレームが補間されて縮む様子が見えてしまいます。
上記を逃げるためには極端に遠い位置へ移動させるのが良さそうです。
同期は状態変数、共有変数、メッセージ、ダミーオブジェクトなどがあり、適材適所に使い分けましょう。
奥の手発動！の例としてupdate関数に対し、on某系から遅延フレームを設定して遅延処理ができそうです。
状態変数(vci.state)は完全に予想ですが、挙動的にはアイテム生成者が真の持ち主感です。
同期はグループID全部1で大体揃ってほしいくらい雑な捉え方してますが、複数人で掴まれると崩壊します。
確証はないんですが、更新アップロードは怪しい気がするので、作業中は削除>新規が良いような気がします。
アップロードしたVCIはバーチャルキャスト再起動しないとダメっぽいです。

##詳細
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286581/8ee39e73-d0a2-eb23-7ec7-4c2f831ba222.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286581/86ba6ef6-9f70-1009-9218-4d28832929ae.png)
0.2の下限から逃げるためスライドはSub Itemの子にします。が、これは私がスライドにCubeを使っていた頃の名残なので、Planeでやるなら要らなかったかもしれません。書いてて今気づきました。あとは、スライド自体は掴めるようにはせず、スライドを表示させる場所を取得するための透明オブジェクト(anchor)を使います。大きさや向きもこの透明オブジェクトを使ってユーザー操作を受け付け、全スライドに反映します。スライドはforの連番で取得するので連番名にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286581/6ade4d72-de74-1ffe-c229-b22c212f3034.png)
ページ送り用のボタンとレーザーポインタのレーザー部分は、レーザーポインタにFixed Jointでくっつけます。luaでも似た事ができますが、追従はこちらの方が綺麗に動くようです。jointの親子関係に気を付けましょう。jointを付けるとたぶん握れないので、握る方を親にした方が良さそうです。

##lua
```lua
local out_pos = Vector3.__new(0, 1000, 0)

if vci.assets.isMine then
    for i = 1, 1000 do
        local item = vci.assets.GetSubItem(i)
        if item == nil then
            -- 最大ページの取得
            vci.state.Set('max', i - 1)
            break
        end
    end
    vci.state.Set('page', 0)
end

function onGrab(target)
    if target == "pointer" then
        vci.assets.GetSubItem("laser").SetLocalScale(Vector3.one)
    end
end

function onUngrab(target)
    if target == "pointer" then
        vci.assets.GetSubItem("laser").SetLocalScale(Vector3.zero)
    end
end

function onUse(use)
    if use == "back" or use == "next" then
        local max = vci.state.Get('max')
        local page = vci.state.Get('page')

        -- onUseでは共有変数の加減算のみ
        if use == "next" and page < max then
            vci.state.Set('page', page + 1)
        end
        if use == "back" and page > 0 then
            vci.state.Set('page', page - 1)
        end
    end
end

function updateAll()
    local max = vci.state.Get('max')
    local page = vci.state.Get('page')
    if max == nil or page == nil then
        return
    end

    local anchor = vci.assets.GetSubItem("anchor")
    for i = 0, max do
        -- 一旦全ページを遠くへ移動して向きと大きさを揃える
        vci.assets.GetSubItem(i).SetPosition(out_pos)
        vci.assets.GetSubItem(i).SetRotation(anchor.GetRotation())
        vci.assets.GetSubItem(i).SetLocalScale(anchor.GetLocalScale())
    end
    -- 現在ページを指定位置に移動
    vci.assets.GetSubItem(page).SetPosition(anchor.GetPosition())
end

```

#自動作成ツール
Unityで作ります。作ったスライドVCIをexe作成用のシーンに複製してページオブジェクトを削除します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286581/11922de2-c880-44a4-d4dc-c354984f54b2.png)
必要な情報の入力UIを作り、ボタンを押したらその情報をVCIに適用して、スライドにしたい画像をページオブジェクトとしてC#スクリプトから生成します。

##Cしゃーぷ
```cs
using System;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using VCI;
using VCIGLTF;

public class ExportVCI : MonoBehaviour
{
    [SerializeField]
    GameObject Template;

    [SerializeField]
    InputField Title;

    [SerializeField]
    InputField Version;

    [SerializeField]
    InputField Author;

    [SerializeField]
    InputField Contact;

    [SerializeField]
    InputField Reference;

    [SerializeField]
    Text ExportVCI_Text;

    [SerializeField]
    Material Mat;

    public void Export()
    {
        var title = Title.text;
        var version = Version.text;
        var author = Author.text;
        var contact = Contact.text;
        var reference = Reference.text;

        if (title == "" || author == "")
        {
            ExportVCI_Text.text = "必須の項目を入力してください";
            return;
        }
        else
        {
            ExportVCI_Text.text = "Export VCI";
        }

        var temp = Instantiate(Template);
        temp.name = title;

        var vci = temp.GetComponent<VCIObject>();
        vci.Meta.title = title;
        vci.Meta.version = version;
        vci.Meta.author = author;
        vci.Meta.contactInformation = contact;
        vci.Meta.reference = reference;

        var jpg = Directory.GetFiles(Application.dataPath + "/../IMAGE", "*.jpg");
        var png = Directory.GetFiles(Application.dataPath + "/../IMAGE", "*.png");
        var img = new string[jpg.Length + png.Length];
        jpg.CopyTo(img, 0);
        png.CopyTo(img, jpg.Length);
        Array.Sort(img);

        for (int i = 0; i < img.Length; i++)
        {
            var tex = ReadTexture2D(img[i]);
            float x = tex.width >= tex.height ? 1 : (float)tex.width / tex.height;
            float y = tex.width <= tex.height ? 1 : (float)tex.height / tex.width;

            var go = new GameObject();
            go.name = i.ToString();
            go.transform.parent = temp.transform;
            go.transform.position = new Vector3(0, 1, 0);

            var sub = go.AddComponent<VCISubItem>();
            sub.GroupId = 1;

            var rigid = go.AddComponent<Rigidbody>();
            rigid.useGravity = false;

            var plane = GameObject.CreatePrimitive(PrimitiveType.Plane);
            plane.transform.parent = go.transform;
            plane.transform.position = new Vector3(0, 1, 0);
            plane.transform.eulerAngles = new Vector3(90, 0, 0);
            plane.transform.localScale = new Vector3(x / 10, 0.1f, y / 10);
            plane.GetComponent<Renderer>().material = Mat;
            plane.GetComponent<Renderer>().material.SetTexture("_MainTex", tex);
            Destroy(plane.GetComponent<MeshCollider>());
        }

        var gltf = new glTF();
        var exporter = new VCIExporter(gltf);
        exporter.Prepare(temp);
        exporter.Export();
        var bytes = gltf.ToGlbBytes();
        var path = Application.dataPath + "/../" + title + ".vci";
        File.WriteAllBytes(path, bytes);

        Destroy(exporter.Copy);
        Destroy(temp);
    }

    Texture2D ReadTexture2D(string path)
    {
        byte[] read = ReadFile(path);
        Texture2D texture = new Texture2D(1, 1);
        texture.LoadImage(read);
        return texture;
    }

    byte[] ReadFile(string path)
    {
        FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
        BinaryReader br = new BinaryReader(fs);
        byte[] read = br.ReadBytes((int)br.BaseStream.Length);
        br.Close();
        return read;
    }
}
```
Export()をボタンに割り当てます。Planeのサイズってなんで他の10倍なんでしょうね。

#参考
VCIスクリプトリファレンス
https://virtualcast.jp/wiki/doku.php?id=vci:script:reference

VCIをビルドしたクライアントからExportする
https://qiita.com/Nekomasu/items/5ab21c6d9359f6c18e46