概要
画像のようにバーチャルキャスト内で使用可能なVCIスライドを作成し、そのVCIスライドを画像から自動作成できるツールを作りました。苦労話の愚痴に、もしかしたら役に立つかもしれない小ネタを混ぜつつ作り方?を書きます。似たような事を初めてする人の参考になれば幸いです。
https://120byte.booth.pm/items/1297409
バージョン
Unity : 2018.4.0f1
バーチャルキャスト : 1.6.4b
VCI : 0.19
まずVCIを作る
普通にVCIを作ります。同期が地獄です。HMD付けたり外したり、SteamVRがダダこねたり、RiftSが映らなかったり、コミュ障メンタルに鞭打って凸ったりしながら動確を取ります。
ポイント
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はバーチャルキャスト再起動しないとダメっぽいです。
詳細
0.2の下限から逃げるためスライドはSub Itemの子にします。が、これは私がスライドにCubeを使っていた頃の名残なので、Planeでやるなら要らなかったかもしれません。書いてて今気づきました。あとは、スライド自体は掴めるようにはせず、スライドを表示させる場所を取得するための透明オブジェクト(anchor)を使います。大きさや向きもこの透明オブジェクトを使ってユーザー操作を受け付け、全スライドに反映します。スライドはforの連番で取得するので連番名にします。
ページ送り用のボタンとレーザーポインタのレーザー部分は、レーザーポインタにFixed Jointでくっつけます。luaでも似た事ができますが、追従はこちらの方が綺麗に動くようです。jointの親子関係に気を付けましょう。jointを付けるとたぶん握れないので、握る方を親にした方が良さそうです。
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作成用のシーンに複製してページオブジェクトを削除します。
必要な情報の入力UIを作り、ボタンを押したらその情報をVCIに適用して、スライドにしたい画像をページオブジェクトとしてC#スクリプトから生成します。
Cしゃーぷ
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