アバターを基準に制御するVCI(CameraSwitcher)

ExportStudioを使用した、スタジオでアバターを基準に制御するサンプルVCIです。

サンプルデータ

https://virtualcast.jp/products/feb68b3db0ef71fc80576cf96212fa586004ac425ff30ac041a5bc7b5e0de017

cameraswitcher.zip

コンポーネント設定

操作のために使うGrab可能なSubitemを3つ用意してあるだけで、VCI本体は特別な事をしてありません。
ただし、ONになってる時に緑、OFFになってる時に赤とマテリアルを操作するために、Subitem毎にマテリアルを割り当てて、Subitemの名前とマテリアルの名前を統一しておくと便利です。

VCIスクリプト

main.lua
---------------------- ホストのみBonePositionを管理する ----------------------
local _ownerAvater
 
-- 初期化 (ホスト実行)
if vci.assets.IsMine then
    vci.studio.shared.Set('CameraMode', "STOP")
    _ownerAvater = vci.studio.GetOwner()
    print("CameraSwitcher Owner : ".._ownerAvater.GetName())
end
-- リモートの確認
if vci.assets.IsMine == false then
    print("あなたはCameraSwitcherのOwnerではりません。")
end
 
---------------------- モーションのプロパティ -----------------------
-- スイッチング時のNeckの位置・回転 (ホスト管理)
local _fixNeckPosition = Vector3.zero
local _fixNeckRotation = Quaternion.identity
local _fixHipsPosition = Vector3.zero
local _fixHipsRotation = Quaternion.identity
 
-- LookAtの補正
 -- +で上方向 -で下方向 を注視する
local _LookAtoffset = -2.0
 
-- カメラ位置のランダム設定
-- Min ~ Max の範囲でランダムな値になります
local _randomPosition = Vector3.zero
local _posXmax = 1.5 -- X軸(左右)
local _posXmin = -1.5
local _posYmax = 1.5 -- Y軸(上下)
local _posYmin = 0.5
local _posZmax = 2.0 -- Z軸(前後)
local _posZmin = 0.8
 
-- カメラの位置をランダムで決定
function randomPosition()
    local x = 0.1 * math.random(10.0 * _posXmin, 10.0 * _posXmax)
    local y = 0.1 * math.random(10.0 * _posYmin, 10.0 * _posYmax)
    local z = 0.1 * math.random(10.0 * _posZmin, 10.0 * _posZmax)
    _randomPosition = Vector3.__new(x, y, z)
end
 
-- Lookat Lerpで追従する (ホスト実行)
function LookAtLerp()
    local cam = vci.studio.GetHandiCamera()
    local forward = _ownerAvater.GetBoneTransform("Neck").position - cam.GetPosition()
    forward = forward.normalized
    local target = Quaternion.LookRotation(forward, Vector3.up)
    target = target * Quaternion.AngleAxis(_LookAtoffset, Vector3.left) --向き補正
    local rot = Quaternion.Lerp(cam.GetRotation(), target, 0.02)
    cam.SetRotation(rot)
end
 
-- Lookat 切り替え時に注視するが、固定 (ホスト実行)
function LookAtFix()
    local cam = vci.studio.GetHandiCamera()
    local forward = _fixNeckPosition - cam.GetPosition()
    forward = forward.normalized
    local rot = Quaternion.LookRotation(forward, Vector3.up)
    rot = rot * Quaternion.AngleAxis(_LookAtoffset, Vector3.left) --向き補正
    cam.SetRotation(rot)
end
 
-- LookATカメラ切り替え時、即座に正面に移動 (ホスト実行)
function fixPosition()
    local cam = vci.studio.GetHandiCamera()
    --回転
    local forward = _ownerAvater.GetBoneTransform("Head").position - cam.GetPosition()
    forward = forward.normalized
    local rot = Quaternion.LookRotation(forward)
    cam.SetRotation(rot)
    --移動
    local pos = _ownerAvater.GetBoneTransform("Head").rotation * Vector3.__new(0, 0, 1.0)
    pos = pos + _ownerAvater.GetBoneTransform("Head").position
    cam.SetPosition(pos)
end
 
function updateAll()
    CameraMotion()
end
 
-- カメラモーションの監視 (ホスト実行)
function CameraMotion()
    --リモートの場合は終了
    if vci.assets.IsMine == false then
        return
    end
 
    --ハンディカメラが存在しないので終了
    if vci.studio.HasHandiCamera() == false then
        print("スタジオにハンディカメラが存在しません")
        return
    end
 
    -- mode が STOP なので終了
    local mode = vci.studio.shared.Get('CameraMode')
    if mode ==  "STOP" then
        return
    end
 
    -- バストアップ
    if mode == "LOOKAT" then
        modeLOOKAT()
    end
 
    -- 固定位置ランダム
    if mode == "RANDOM" then
        modeFixpos()
    end
end
 
-- バストアップモーション (ホスト実行)
function modeLOOKAT()
    -- Neckからの相対距離
    local distance = 0.8 --ターゲットとカメラの間隔
    local height = 0.2 --カメラの高さ
    local pos = _ownerAvater.GetBoneTransform("Neck").rotation * Vector3.__new(0, height, 1.0 * distance)
    -- 原点をNeckにオフセット
    pos = pos + _ownerAvater.GetBoneTransform("Neck").position
    -- カメラに位置を適用
    local cam = vci.studio.GetHandiCamera()
    local fixpos = Vector3.Lerp(cam.GetPosition(), pos, 0.02)
    cam.SetPosition(fixpos)
    LookAtLerp()
end
 
-- ランダム位置に出現 (ホスト実行)
function modeFixpos()
    -- 原点をHipsにオフセット
    local pos = _fixNeckRotation * _randomPosition
    pos = pos + _fixNeckPosition
    -- カメラに位置を適用
    local cam = vci.studio.GetHandiCamera()
    cam.SetPosition(pos)
    LookAtLerp()
end
 
function onUse(switch)
    -- スイッチの色変更(リモート/ホスト)
    ColorChange(switch)
    -- カメラモードの切り替え(リモート/ホスト)
    vci.studio.shared.Set('CameraMode', switch)
    -- 各クライアントのパラメータ更新(リモート/ホスト)
    vci.message.Emit("onUseFireOnce", tostring(switch))
end
 
-- Useした時に1回だけ実行する関数
function onUseFireOnce(sender, name, message)
 
    print("message : "..tostring(message))
 
    -- 各クライアントでランダム値を更新
    randomPosition()
 
    -- ホストのみ実行
    if vci.assets.IsMine then
        --オーナーのみがボーンの情報を取得する
        GetBoneTransform()
        print("ボーン情報を更新")
        --バストアップの場合、食座に位置を切り替え
        if message == "LOOKAT" then
            fixPosition()
        end
    end
    -- リモートで確認
    if vci.assets.IsMine == false then
        print("ホストがボーン情報を更新しました")
    end
end
 
-- Emit "onUseFireOnce" で onUseFireOnce()を実行する
vci.message.On("onUseFireOnce", onUseFireOnce)
 
-- スイッチング時のボーン情報の保存 (ホスト実行)
function GetBoneTransform()
    --切り替え時の Neck Hips の位置・回転を保存
    _fixNeckPosition = _ownerAvater.GetBoneTransform("Neck").position
    _fixNeckRotation = _ownerAvater.GetBoneTransform("Neck").rotation
    _fixHipsPosition = _ownerAvater.GetBoneTransform("Hips").position
    _fixHipsRotation = _ownerAvater.GetBoneTransform("Hips").rotation
end
 
-- スイッチの色変更 (リモートorホストで実行可能)
function ColorChange(switch)
    --リセット
    vci.assets.material._ALL_SetColor("STOP", Color.red)
    vci.assets.material._ALL_SetColor("LOOKAT", Color.red)
    vci.assets.material._ALL_SetColor("RANDOM", Color.red)
    -- オンのスイッチを緑色に
    if switch == "STOP" then
        vci.assets.material._ALL_SetColor("STOP", Color.green)
    end
    if switch == "LOOKAT" then
        vci.assets.material._ALL_SetColor("LOOKAT", Color.green)
    end
    if switch == "RANDOM" then
        vci.assets.material._ALL_SetColor("RANDOM", Color.green)
    end
end

VCIスクリプト(解説)

vci.assets.IsMine を使ってホストで行う処理・リモートで行う処理を分ける

基本的に vci.assets.IsMine が true になるクライアント(VCIを出したクライアント)が代表して処理を行います。
ただし、他のクライアントからでも操作可能にしたい為…
同期変数で状態の共有、メッセージを使ってどのクライアントから実行しても、全体のクライアントで実行するように工夫します。
_ALL_で同期が可能な処理については、複雑な事を行わず_ALL_で同期させます。

LookAtの処理について

main.lua
    local cam = vci.studio.GetHandiCamera()
    local forward = _ownerAvatar.GetBoneTransform("Neck").position - cam.GetPosition()
    forward = forward.normalized
    local target = Quaternion.LookRotation(forward, Vector3.up)
    target = target * Quaternion.AngleAxis(_LookAtoffset, Vector3.left) --向き補正
    local rot = Quaternion.Lerp(cam.GetRotation(), target, 0.02)
    cam.SetRotation(rot)

ターゲットの座標(頭のボーン)-カメラの座標 を計算した結果を normalized すると、カメラからターゲットの方向へ向かう単位ベクトルが得られます。
Quaternion.LookRotation() の第一引数に上記で求めたベクトルを入れると、ターゲットの方向へ向くクォータニオンが得られます。
この結果をそのまま SetRotation() してもいいですが、なめらかに補間させるために Quaternion.Lerp() を実行する事で、ゆっくりカメラが振り向くようになります。