バーチャルキャストですきすきチュッチュラブビームを撃ってみた

# はじめに
本項目はバーチャルキャスト 1.8.3aで、バーチャルキャストでユーザーが自由に作成、取り込みが可能なVCIアイテムにアバター情報の取得機能が追加されました。
アバター情報の取得機能を使用し、ポーズに応じてエフェクトを再生するアイテムを作った際の備忘録です。
![2020010514054637.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/51333ac4-6ab5-9768-e0b9-8c04a13c4063.jpeg)


# やりたいこと
1．アバター情報からボーンの情報を取得し、指の形を判定する。
2．指の形がハートかつ手が胸の前に1秒以上ある場合、待機エフェクトとして上昇するハートのエフェクトを再生する。
3. 待機エフェクト再生状態で腕を前に突き出すと、ハートの渦のエフェクトを再生する。
4. 指の形がハートから変化した場合、エフェクトの再生を取りやめる。

# 指の形の判定
**GetBoneTransform fun(boneName: string)**を使用して、ボーンの情報を取得する。
boneNameについては[こちら](https://docs.unity3d.com/ja/current/ScriptReference/HumanBodyBones.html)を参照

## ボーンの回転量から指の形を判定する
GetBoneTransform(”ボーンの名前”).rotationでボーンの回転量を取得することが出来る。

Cubeを握り、コントローラーのボタンを押した際に、指のボーンの回転量を表示するようにしてみた
1回目
![指の角度1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/29d8a317-7568-7886-7962-0740e434a741.jpeg)

2回目
![指の角度2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/8548745b-7fae-2194-6e36-522c2a67adcf.jpeg)

**！！**同じ指の形なのに、回転量が全く異なる...
![コメント1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/0afea54f-59bf-7842-85ac-07564d8a7886.jpeg)
指の曲がり具合(ボーンのローカル回転量)から指の形を判定しようとしていたが、変換処理を挟まないと出来ないらしい。


## ボーンの位置情報から指の形を判定する
ボーンの回転量から判定するには、変換処理が必要になる。どうしようかと検討していると、以下のコメントを頂いた。
![コメント2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/60d6f03a-5a20-44cc-b455-f607167c2c6b.jpeg)

作戦を変更し、ボーンの座標を取得し、位置情報(ボーン間の距離)から指の形がハートか以下の関数で判定する。

```
function LeftFingerHeartChk()
    local str = ""
    local flag = true -- 指の形の条件に合致するかのフラグ

    local handPos = Vector3.__new(0,0,0)
    local hand = owner.GetBoneTransform("LeftHand") -- 左手のボーン情報を取得する。
    local retry = 0
    while hand == nil and retry < 5 do -- ボーン情報取得APIは初回実行時にnilが返るため、リトライ処理を入れる
        hand = owner.GetBoneTransform("LeftHand")
        retry = retry +1
    end

    if hand ~= nil then -- ボーン情報がnilでない場合、ローカル変数に手の位置情報をセットする。
        handPos.x = hand.position.x
        handPos.y = hand.position.y
        handPos.z = hand.position.z
    else
        --print("hand")
        flag = false 
    end

    local LeftFingerList = {
        LeftIndexIntermediate = { name = "LeftIndexIntermediate", pos={x=0,y=0,z=0}, dist = 0.0 },
        LeftIndexDistal = { name = "LeftIndexDistal", pos={x=0,y=0,z=0}, dist = 0.0 },
        LeftMiddleIntermediate = { name = "LeftMiddleIntermediate", pos={x=0,y=0,z=0}, dist = 0.0 },
        LeftMiddleDistal = { name = "LeftMiddleDistal", pos={x=0,y=0,z=0}, dist = 0.0 },
        LeftRingIntermediate = { name = "LeftRingIntermediate", pos={x=0,y=0,z=0}, dist = 0.0 },
        LeftRingDistal = { name = "LeftRingDistal", pos={x=0,y=0,z=0}, dist = 0.0 },
        LeftLittleIntermediate = { name = "LeftLittleIntermediate", pos={x=0,y=0,z=0}, dist = 0.0 },
        LeftLittleDistal = { name = "LeftLittleDistal", pos={x=0,y=0,z=0}, dist = 0.0 }
    }
    for k, v  in pairs(LeftFingerList) do -- テーブルをなめて、指の各ボーンの位置情報を取得、手のボーンとの距離を求める。
        local trans = owner.GetBoneTransform(k) -- ボーン情報の取得
        local retry = 0
        while trans == nil and retry < 5 do
            trans = owner.GetBoneTransform(k)
            retry = retry +1
        end
        if trans ~= nil and flag ~= false then
            v.dist = Vector3.Distance(handPos, trans.position) -- 手のボーンとの距離計算
            v.pos = trans.position
            local idx = string.find(k, "Distal")
            if idx ~= nil then
                -- 第3関節のボーンの場合、距離に補正を入れる
                local header = string.sub(k, 1, idx-1)
                local name = header.."Intermediate"
                local dist = Vector3.Distance(LeftFingerList[name].pos, trans.position)
                v.dist = v.dist - (dist * 0.9) 
            end
        else
            --print(k)
        end
    end
    -- 各指の第2関節と手のボーンとの距離、第3関節と手のボーンとの距離を比較する。
    -- 第3関節が第2関節より、手首に近い場合、手の形がハートと判定する。
    if flag ~= false then
        if LeftFingerList.LeftIndexIntermediate.dist < LeftFingerList.LeftIndexDistal.dist then
            --print("index")
            flag = false
        end
        if LeftFingerList.LeftMiddleIntermediate.dist < LeftFingerList.LeftMiddleDistal.dist then
            --print("middle")
            flag = false
        end
        if LeftFingerList.LeftRingIntermediate.dist < LeftFingerList.LeftRingDistal.dist then
            --print("ring")
            flag = false
        end
        if LeftFingerList.LeftLittleIntermediate.dist < LeftFingerList.LeftLittleDistal.dist then
            --print("little")
            flag = false
        end
    end
    return flag
end
```

※単純に第2関節と第3関節と手首の距離を比較した場合だと、アバターによっては第2関節の方が手首に近いと判定される場合があったので、補正をかけています。
上記は左手の場合、右手の指の形の判定も同様に判定関数を作成する。

# 手が胸の前にあるか判定する。
手が胸の前にあるか判定する方法としては、胸から手までの距離が、肩からヒジまでの距離より短いかで判定することが出来る。
getBoneDist()を作成し、判定を行う。

```
function getBoneDist()
    local flag = true
    
    -- 右手の座標取得
    -- 左手の座標取得
    -- 胸の座標取得
    -- 右肩の座標取得
    -- 左肩の座標取得
    -- 右ヒジの座標取得
    -- 左ヒジの座標取得

    do 
        if 右肩から右ヒジまでの距離 < 胸から右手までの距離 then
            flag = false 
        end
        if 左肩から左ヒジまでの距離 < 胸から左手までの距離 then
            flag = false 
        end
    end
    return flag
end
```

# 指の形がハートかつ手が胸の前に1秒以上あるか判定する
指の形と手の位置のチェックは全ユーザーで毎フレーム呼ばれるupdateAll()のイベントハンドラの中で行う。
エフェクトを仕込んだオブジェクトを移動させる必要があるため、オブジェクトの所有権を持つユーザーで判定処理は行う。

```
---全ユーザーで毎フレーム呼ばれる
function updateAll() 
    local effItem = vci.assets.GetSubItem("effect")
    if effItem.IsMine == true then -- エフェクトを仕込んだオブジェクトを移動させるため、オブジェクトの所有権を持つユーザーで実行する。
        local flag = LeftFingerHeartChk()
        local flag1 = RightFingerHeartChk()
        local flag2 = getBoneDist()

        if flag == true and flag1 == true and flag2 == true then -- 指の形がハートかつ手が胸の前にある条件を全て満たした場合
            if effortTime == nil then -- 時間オブジェクトがnilの場合
                -- 現在時刻から1秒後を時間オブジェクトにセットする。
                effortTime = vci.me.UnscaledTime.Add(TimeSpan.FromMilliseconds(1000))
            else
                local curr = vci.me.UnscaledTime
                local diff = TimeSpan.Compare(curr, effortTime)
                -- 現在時刻と時間オブジェクトを比較し、現在時刻が時間オブジェクトより大きい場合は、1秒経過と判定する。
                if diff >= 0 then
                    -- 待機エフェクトの再生
                end
            end
        end
    end
end
```
# 待機エフェクトの再生
エフェクトはループ再生かつ、後で停止させたいため、グローバル変数でエフェクトのオブジェクトを覚えておく。
エフェクトオブジェクトがnilの場合のみ、エフェクトを取得し、再生する。

```
if eff == nil then
    eff = vci.assets.GetEffekseerEmitter("Cube1")
    eff._ALL_Play()
end
```

# 待機エフェクト再生状態で腕を前に突き出すと、ハートの渦のエフェクトを再生する。
待機エフェクト再生状態で腕を前に突き出したかの判定と、エフェクトの再生もupdateAll()の中で同様に行う。
すでに記載した「指の形がハートかつ手が胸の前に1秒以上あるかの判定」は省略する。

```
---全ユーザーで毎フレーム呼ばれる
function updateAll() 
    local effItem = vci.assets.GetSubItem("effect")
    if effItem.IsMine == true then -- エフェクトを仕込んだオブジェクトを移動させるため、オブジェクトの所有権を持つユーザーで実行する。
        local flag = LeftFingerHeartChk()
        local flag1 = RightFingerHeartChk()
        local flag2 = getBoneDist()

        if flag == true and flag1 == true and flag2 == true then -- 指の形がハートの場合
            -- 処理
        elseif flag == true and flag1 == true then -- 指の形がハートの場合
            if eff == nil then -- 待機エフェクトやハートの渦のエフェクトが再生状態でない場合
                effortTime = nil -- 手の形や位置が変わったとして、経過時間の判定をキャンセル
            else
                -- グローバル変数で覚えているエフェクトの名前が待機エフェクトの場合
                if eff.EffectName == "heart_upstream" then
                    eff._ALL_Stop() -- 待機エフェクトを停止する
                    eff = vci.assets.GetEffekseerEmitter("Cube2")
                    -- ハートの渦のエフェクトを1度だけ実行する
                    eff._ALL_PlayOneShot()
                end
            end
        end
    end
end
```


# 指の形がハートから変化した場合、エフェクトの再生を取りやめる。
本項も前述の項目と同様に、updateAll()の中で同様に行う。

```
---全ユーザーで毎フレーム呼ばれる
function updateAll() 
    local effItem = vci.assets.GetSubItem("effect")
    if effItem.IsMine == true then -- エフェクトを仕込んだオブジェクトを移動させるため、オブジェクトの所有権を持つユーザーで実行する。
        local flag = LeftFingerHeartChk()
        local flag1 = RightFingerHeartChk()
        local flag2 = getBoneDist()

        if flag == true and flag1 == true and flag2 == true then -- 指の形がハートの場合
            -- 処理
        elseif flag == true and flag1 == true then -- 指の形がハートの場合
            -- 処理
        else
            effortTime = nil -- 時間オブジェクトはクリア
            if eff ~= nil then -- エフェクトのオブジェクトがnilでない場合はエフェクトを停止し、オブジェクトもnilクリアする
                eff._ALL_Stop()
                eff = nil
            end
        end
    end
end
```

# 最後に
スタジオ内にいる誰のボーン情報を取得するかの機能も入れ込んだが、こちらは基本的にこれまでもVCIで使用可能な機能を用いて実装したため、割愛する。
従来は装着可能オブジェクトをアバターに装着し、コントローラーを操作することでエフェクトの再生する等しなければいけませんでしたが、アバター情報の取得機能を使用することで、アバターの体勢変更などをトリガーに処理を行うことが出来るようになりました。
私も「魔貫光殺砲」を作られていた方に触発され、すきすきチュッチュラブビームをバーチャルキャスト内で撃てるようにしてみました。
皆様も自由な発想で、色々な技を発動してみてください。