はじめに
みなさんは、pdfをアップロードするだけでVキャス内でスライド発表できる 「スライドVCI」 は使ったことありますか?
ポインタで、スライド内を指し示せるのは便利ですよね
しばらく前に、VCIの ダウンロード機能 が実装されたため、スライドVCIの中身を確認できるようになりました!
実は、こちらのスクリプトを流用すると、VR内動作するタブレット型のUI が簡単にできてしまうんです!
本シリーズは3部構成で、「スライドVCIの解析」、「スライドVCIからタブレットVCIの発展」、「ボタンやスライド操作の実装」の順に進んでいきます!
それでは、本記事にてスライドVCIの内部スクリプトを解析し、スライド上でポインタ位置を計算している仕組み を解説します!
このシリーズでわかること
- スライドVCIの仕組み(←今回でわかること)
- ペンが平面に対して指している場所の、ローカル座標の算出方法
- タブレットの、ボタンの認識方法やスライダーの動作方法
1. スライドVCI解析編 ←今回
2. スライドVCIからタブレットVCIへの発展編
3. ボタンやスライド操作の実装編
スライドVCIの仕組みを見てみよう!
スクリプトは長いですが、ポイントは「ペンの向きから、どのようにスライド平面の座標を算出しているか」の部分なので、それがどの部分か見つけていきます
スライドVCIの中身(全部見る必要がないので畳んで省略)
-- Generatorから設定される
MAX_PAGE = 9
PAGE_WIDTH = 2048
PAGE_HEIGHT = 1152
MAX_X_PAGE = 1
MAX_Y_PAGE = 1
-- ここまで
ga = PAGE_WIDTH / 2048
gb = PAGE_HEIGHT / 2048
gc = MAX_X_PAGE * MAX_Y_PAGE
gd = "SlideMain "
ge = "currentPage"
local gf = vci.assets.GetTransform("SubSlide")
local gg = vci.assets.GetTransform("Next")
local gh = vci.assets.GetTransform("Priv")
local gi = vci.assets.GetTransform("Pointer")
local gj = vci.assets.GetTransform("SubPointer")
local gk = vci.assets.GetTransform("StickOrigin")
local gl = vci.assets.GetTransform("MainOrigin")
local gm = vci.assets.GetTransform("MainT")
local gn = vci.assets.GetTransform("MainR")
local go = vci.assets.GetTransform("MainTR")
local gp = vci.assets.GetTransform("SubOrigin")
local gq = vci.assets.GetTransform("SubT")
local gr = vci.assets.GetTransform("SubR")
local gs = vci.assets.GetTransform("SubTR")
function g_a(currentPage)
vci.state.Set(ge, currentPage)
end
function g_b()
local la = vci.state.Get(ge)
if la == nil then
la = 0
end
return la
end
function g_c()
local la = g_b()
la = la + 1
if la >= MAX_PAGE then
la = 0
end
g_a(la)
end
function g_d()
local la = g_b()
la = la - 1
if la < 0 then
la = (MAX_PAGE - 1)
end
g_a(la)
end
function onUse(use)
if use == "Next" or use == "Stick" then
g_c()
end
if use == "Priv" then
g_d()
end
end
function updateAll()
gk.SetLocalRotation(Quaternion.identity)
local la = g_b()
local lb = math.floor(la / gc) + 1
for i = 1, 20 do
local lc = vci.assets.GetTransform("Page_" .. i)
local ld = vci.assets.GetTransform("SubPage_" .. i)
lc.SetActive(false)
ld.SetActive(false)
if i == lb then
lc.SetActive(true)
ld.SetActive(true)
end
end
local le = la % gc
local lf = le % MAX_X_PAGE * ga
local lg = (1 + math.floor(le / MAX_X_PAGE)) * gb
vci.assets.material.SetTextureOffset(gd .. lb, Vector2.__new(lf, 1 - lg))
local lh = gf.GetLocalScale()
gg.SetLocalScale(lh)
gh.SetLocalScale(lh)
local li = g_e(gk, gp, gq, gr, gs)
gj.SetActive(li.enable)
gj.SetPosition(li.value)
if li.enable == false then
local lj = g_e(gk, gl, gm, gn, go)
gi.SetActive(lj.enable)
gi.SetPosition(lj.value)
if lj.enable then
gj.SetActive(true)
gj.SetLocalPosition(gi.GetLocalPosition() * (3 / 10))
end
else
gi.SetActive(true)
gi.SetLocalPosition(gj.GetLocalPosition() * (10 / 3))
end
end
function g_e(pointStart, origin, top, right, topRight)
local la = origin.GetForward()
local lb = origin.GetPosition()
local lc = pointStart.GetPosition()
local ld = pointStart.GetUp()
local le = Vector3.Dot(la, lb);
local lf = lc + ((le - Vector3.Dot(la, lc)) / (Vector3.Dot(la, ld))) * ld;
local lg = origin.GetPosition()
local lh = top.GetPosition()
local li = topRight.GetPosition()
local lj = right.GetPosition()
local lk = lf
local ll = lg - lk
local ln = lh - lk
local lm = li - lk
local lo = lj - lk
local lp = lh - lg
local lq = li - lg
local lr = Vector3.Cross(ll, ln).normalized
local ls = Vector3.Cross(ln, lm).normalized
local lt = Vector3.Cross(lm, lo).normalized
local lu = Vector3.Cross(lo, ll).normalized
local lv = Vector3.Cross(lp, lq).normalized
local lw = {}
if Vector3.Dot(lv, lr) > 0 and
Vector3.Dot(lv, ls) > 0 and
Vector3.Dot(lv, lt) > 0 and
Vector3.Dot(lv, lu) > 0
then
lw.enable = true
lw.value = lf
else
lw.enable = false
lw.value = Vector3.zero
end
return lw
end
if vci.assets.IsMine then
g_a(0)
end
中身を見ると、 g_e の関数で、ペンがタブレットを指している位置(ポインタを置くべき位置)を計算して、ポインタを表示するか・非表示にするかのフラグと、位置座標を返しているようです
function g_e(pointStart, origin, top, right, topRight)
local la = origin.GetForward()
local lb = origin.GetPosition()
local lc = pointStart.GetPosition()
local ld = pointStart.GetUp()
local le = Vector3.Dot(la, lb);
local lf = lc + ((le - Vector3.Dot(la, lc)) / (Vector3.Dot(la, ld))) * ld;
local lg = origin.GetPosition()
local lh = top.GetPosition()
local li = topRight.GetPosition()
local lj = right.GetPosition()
local lk = lf
local ll = lg - lk
local ln = lh - lk
local lm = li - lk
local lo = lj - lk
local lp = lh - lg
local lq = li - lg
local lr = Vector3.Cross(ll, ln).normalized
local ls = Vector3.Cross(ln, lm).normalized
local lt = Vector3.Cross(lm, lo).normalized
local lu = Vector3.Cross(lo, ll).normalized
local lv = Vector3.Cross(lp, lq).normalized
local lw = {}
if Vector3.Dot(lv, lr) > 0 and
Vector3.Dot(lv, ls) > 0 and
Vector3.Dot(lv, lt) > 0 and
Vector3.Dot(lv, lu) > 0
then
lw.enable = true
lw.value = lf
else
lw.enable = false
lw.value = Vector3.zero
end
return lw
end
このg_eの関数の中身を見ていくと、ベクトルの内積や外積を用いて、計算しているようです
(ベクトルの内積と外積というと、高校の数学で習ったのを思い出しますね)
では、g_eの関数の中身を順々に見ていきましょう!
g_e関数の前半:ペンの延長線上とスライド平面の交点を求める
g_e関数の前半部分では、スライド平面上のペンの指している位置計算を行っています
前半部分は、以下の通りです
local la = origin.GetForward()
local lb = origin.GetPosition()
local lc = pointStart.GetPosition()
local ld = pointStart.GetUp()
local le = Vector3.Dot(la, lb);
local lf = lc + ((le - Vector3.Dot(la, lc)) / (Vector3.Dot(la, ld))) * ld
-
la:スライドの法線ベクトル -
lb:スライドの原点 -
lc:ペンの先端の点 -
ld:ペンの軸方向のベクトル
la、lb、lc、ldは、上記のような点とベクトルです
そして、xをスライド平面上の点とした時、以下のような関係が成り立ちます
- スライド平面上のベクトル
x - lbと、スライドの法線ベクトルlaは直交する - ペンの軸先の延長線上に、
xがある
以上の2つの条件から、次の2つの式が成り立ちます
la・(x - lb) = 0・・・ ①
x = lc + t・ld ・・・ ②
①を式変形すると以下のようになり、
la・x = la・lb・・・ ①'
①'に②を代入して、tについて解くと、以下のようになり、
t = (la・lb - la・lc) / (la・ld)
tを②に代入すると、lfと同じ式が出来上がり、lfはスライド平面とペンの延長線上の交点を示していることがわかりました
x = lc + (la・lb - la・lc) / (la・ld)・ld
この関数を使う上で重要な条件は次の2つです
重要なポイント
1. スライド原点(origin)のz軸が、スライド平面に垂直であること
2. ペン先の点(pointStart)は、ペンの軸に対してy軸を合わせて置くこと
前半部分の導出を理解できてなくても、この2点だけ守れば問題なく使用できます!
シンプルで便利な式ですね!
g_e関数の前半:スライド範囲内かどうかを判定する
後半部分は、交点lfがスライド平面の内部にいるかどうかの確認を行っています
後半部分は、以下の通りです
local lg = origin.GetPosition()
local lh = top.GetPosition()
local li = topRight.GetPosition()
local lj = right.GetPosition()
local lk = lf
local ll = lg - lk
local ln = lh - lk
local lm = li - lk
local lo = lj - lk
local lp = lh - lg
local lq = li - lg
local lr = Vector3.Cross(ll, ln).normalized
local ls = Vector3.Cross(ln, lm).normalized
local lt = Vector3.Cross(lm, lo).normalized
local lu = Vector3.Cross(lo, ll).normalized
local lv = Vector3.Cross(lp, lq).normalized
local lw = {}
if Vector3.Dot(lv, lr) > 0 and
Vector3.Dot(lv, ls) > 0 and
Vector3.Dot(lv, lt) > 0 and
Vector3.Dot(lv, lu) > 0
then
lw.enable = true
lw.value = lf
else
lw.enable = false
lw.value = Vector3.zero
end
順番に見ていきましょう
まず、この部分で、交点lf(スクリプト内では、lkに置き換えている)からのスライドの四隅へのベクトルll,ln,lm,loと、スライド平面のX軸Y軸ベクトルlp,lqを作ります
local ll = lg - lk
local ln = lh - lk
local lm = li - lk
local lo = lj - lk
local lp = lh - lg
local lq = li - lg
続いて、交点lk(lf)から原点へのベクトルから時計回りに外積をとり、法線ベクトルlr,ls,lr,luと、スライド平面の法線ベクトルlvを作っていきます
local lr = Vector3.Cross(ll, ln).normalized
local ls = Vector3.Cross(ln, lm).normalized
local lt = Vector3.Cross(lm, lo).normalized
local lu = Vector3.Cross(lo, ll).normalized
local lv = Vector3.Cross(lp, lq).normalized
続いて、lvとぞれぞれの法線ベクトルlr,ls,lr,luで内積をとり、それぞれのベクトルがlvと同じ方向かどうかの判定を行い、スライド平面の法線ベクトルlvと同じであれば、スライドの内側に交点lk(lf)があるという判定を行っています
外積(Vector3.Cross)で「面の向き」、内積(Vector3.Dot)で「向きの一致」を判定しています
交点がスライド外部にあると、外積の右ネジの法則で、ベクトルが反転し、内積を計算した際に負の値となります
内積(Vector3.Dot(a,b))のa,bの順番は重要ではありませんが、外積(Vector3.Cross(a,b))のa,bの順番は重要です
必ず、外積をとる向きは、時計回りか、逆時計回りで統一しましょう
if Vector3.Dot(lv, lr) > 0 and
Vector3.Dot(lv, ls) > 0 and
Vector3.Dot(lv, lt) > 0 and
Vector3.Dot(lv, lu) > 0
then
外積をとって、内積をとるだけで、スライド平面上の点がスライド範囲の内部にあるか外部にあるか判断できるなんて面白いですね!
ここで重要なのは、以下の一点だけです
重要なポイント
1. スライドの四隅の点lg,lh,li,ljを用意すること
最終的な戻り値
ここまでで、ペンの軸の延長線上とスライド平面上の交点の位置の計算と、その点がスライドの内側に点があるかどうかの判定ができたので、それを以下のように、lw.enableで内側の可否、lw.valueで位置情報を返してます
local lw = {}
if Vector3.Dot(lv, lr) > 0 and
Vector3.Dot(lv, ls) > 0 and
Vector3.Dot(lv, lt) > 0 and
Vector3.Dot(lv, lu) > 0
then
lw.enable = true
lw.value = lf
else
lw.enable = false
lw.value = Vector3.zero
end
まとめ
今回、スライドVCIの仕組みについて紐解き、ペンの向きからスライド平面上のどこを指しているのか計算する方法を解説しました
この計算方法を扱う上で重要な点は以下の通りです
重要なポイント
1. スライド平面の四隅にEmptyObjectを置くこと
2. 四隅の左下のEmptyObjectは、ローカルZ軸をスライド平面と垂直になるようにすること
3. ペン先に、ペンの軸にY軸を合わてEmptyObjectを置くこと
おわり
いかがでしたか?
難しそうに見えて、原理はシンプルで簡単なことがわかったのではないでしょうか?
「高校数学わからんー」っていう方も、上記の重要な点さえ押さえていれば、理解できなくともタブレット型UIを作れます!
挫けずに続いての記事をご覧になって、タブレット型UIにチャレンジしてみてくださいね!