タイムテーブルを用いた処理の実行

# はじめに
本記事はVCIアドベントカレンダーの16日目の記事です。
12/12に開催されたVMF(バーチャルミュージックフェスティバル)にAnd U'sの一員として参加し、バーチャルライブを作成しました。

![スクリーンショット 2020-12-16 223429.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/7974e9a5-e56a-a444-78c3-f235e8f7e527.jpeg)

バーチャルライブはメンバーで以下の通り役割を分担し作成しています。
_yukku : モデリング、ダンス
くろのす : エフェクト、アニメーション
八葉ユーマ : カメラワーク
ちんぐ : VCI化、スクリプト

今回は私が担当したVCI化、スクリプトの観点で主に表題の通り「タイムテーブルを用いた処理の実行」について記事にします。

# VCI化
各種オブジェクトは以下のようにあらかじめその場に配置し、マテリアルをCutoutまたはTransparentに設定し透明化しています。

![2020121620191952.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/bbbb3b33-9240-0491-33e3-19f1c5a2d903.png)

# スクリプト
スクリプトでは以下のようなタイムテーブルを作成し、経過時間をキーにタイムテーブルの処理を実行します。

<details><summary>タイムテーブル</summary><div>

```
local ProductionTable = {
    ["ti"]={["title"] = "シャルル"},
    ["length"]={["time"] = "03:48.47"},

    ["00:00.3"]={["function"] = initMaterials },
    ["00:00.9"]={["function"] = onMaterials, ["val"] = { { "noise", "sprite" } } }, -- ノイズエフェクト再生(テクスチャスクロール)

    ["00:03.5"]={["function"] = onMaterials, ["val"] = { {"sorenanoni"} }}, --マテリアルON

    ["00:04.9"]={["function"] = startAnimationFromName, ["val"] = { "sorenanoni3", "sorenanoni6" }}, --アニメーション
    ["00:07.7"]={["function"] = offMaterials, ["val"] = { {"sorenanoni"} } },
    ["00:12.9"]={["function"] = offMaterials, ["val"] = { { "noise","sprite" } } },

    ["00:13.3"]={["function"] = onMaterials, ["val"] = { { "Charles_Kashi5", "光", "黒" } } }, --"笑って" 暗転
    ["00:13.4"]={["function"] = startAndStopEffect, ["val"] = { "smoke", 1 } },
    ["00:14.7"]={["function"] = offMaterials, ["val"] = { { "Charles_Kashi5", "光", "黒" } } }, -- 暗転終わり

    ["00:14.8"]={["function"] = changeColorMaterials, ["val"] = { {"cylinder_ao"}, Color.__new(211/255, 211/255, 211/255, 1)} }, --歌詞なし 建物マテリアル不透明化
    ["00:14.9"]={["function"] = startTitleEffect, ["val"] = { 11.7 } }, -- タイトル
    ["00:15.0"]={["function"] = onMaterials, ["val"] = { { "kaben", "kuki" } } }, -- クロッカス出現

    ["00:39.9"]={["function"] = changeColors2, ["val"] = { 500, "Moya", Color.__new(255/255, 255/255, 255/255, 0), Color.__new(255/255, 255/255, 255/255, 1) } }, -- "空っぽでいよう"
    ["00:40.8"]={["function"] = changeColorMaterials, ["val"] = { {"cylinder_ao"}, Color.gray} }, -- 建物マテリアル 灰色
    ["00:42.3"]={["function"] = changeColors2, ["val"] = { 500, "Moya", Color.__new(255/255, 255/255, 255/255, 1), Color.__new(255/255, 255/255, 255/255, 0) } }, -- 暗転終わり

    ["00:44.5"]={["function"] = onoffMaterials, ["val"] = {500, {"SINobject2", "SINobject1"} } }, -- "深い青で満たしたのなら" -- 複数マテリアル連続透明・不透明化
    ["00:44.8"]={["function"] = onMaterials, ["val"] = { {"bc"} } }, -- 水面不透明化
    ["00:44.9"]={["function"] = startAnimation, ["val"] = { "Water surface" } }, -- 水面上昇
    ["00:45.0"]={["function"] = startAndStopEffect, ["val"] = { "awa", 3.6 } },  -- "どうだろう" -- 泡エフェクト再生
    ["00:46.1"]={["function"] = startAndStopEffect, ["val"] = { "hukaiao", 2.5 } }, -- 泡エフェクト再生
    ["00:48.7"]={["function"] = offMaterials, ["val"] = { {"bc"} } }, -- 水面透明化

--    ["00:48.9"]={["lylic"] = "こんな風に悩めるのかな", ["function"] = changeColorMaterials, ["val"] = { {"cylinder_ao"}, Color.__new(28/255, 44/255, 82/255, 1) } }, -- 建物マテリアル 青色
    ["00:48.8"]={["function"] = changeColorMaterials, ["val"] = { {"cylinder_ao"}, Color.__new(159/255, 217/255, 246/255, 1) } }, --"こんな風に悩めるのかな" 建物マテリアル 青色
    ["00:50.5"]={["function"] = onoffMaterials, ["val"] = {500, {"SINobject4", "SINobject3"} } }, -- 複数マテリアル連続透明・不透明化
    ["00:52.9"]={["function"] = changeColorMaterials, ["val"] = { {"cylinder_ao"}, Color.__new(211/255, 211/255, 211/255, 1)} },

    ["00:53.8"]={["function"] = onMaterials, ["val"] = { {"utatte"} }},
    ["00:54.0"]={["function"] = startAndStopEffect, ["val"] = { "kumo", 3.5 } },
    ["00:54.1"]={["function"] = playKumonoUeAnime, ["val"] = { } },
    
    ["00:55.8"]={["function"] = offMaterials, ["val"] = { {"utatte"} }},

    ["00:59.4"]={["function"] = uvTexture, ["val"] = {500, "pannel4", Vector2.__new(0,0), Vector2.__new(3,0), Vector2.__new(4,5) } }, -- テクスチャUVoffset切り替え

    ["01:01.6"]={["function"] = startAndStopEffect, ["val"] = { "iya", 2.5 } },
    ["01:01.7"]={["function"] = onMaterials, ["val"] = { { "光", "黒" } } }, -- 暗転
    ["01:04.2"]={["function"] = offMaterials, ["val"] = { { "光", "黒" } } }, -- 暗転終わり

    ["01:04.3"]={["function"] = startAnimationFromName, ["val"] = { "Light", "lightBig" }}, --"遠く描いてた日々を" 街灯拡大
    ["01:07.1"]={["function"] = startAnimationFromName, ["val"] = { "Light", "lightSmall" } }, -- 街灯縮小

    ["01:07.2"]={["function"] = changeColor, ["val"] = {1000, "tenkyu", Color.__new(8/255, 35/255, 67/255, 1) } }, -- 夜
    ["01:07.3"]={["function"] = startAnimation, ["val"] = { "katattebone" }},
    ["01:07.4"]={["function"] = onMaterials, ["val"] = { {"katatte"} }},
    ["01:09.9"]={["function"] = offMaterials, ["val"] = { {"katatte"} }},

    ["01:08.3"]={["function"] = startAndStopEffect, ["val"] = { "yorunomure", 2.6 }}, -- effect

    ["01:12.7"]={["function"] = uvTexture, ["val"] = {500, "pannel4", Vector2.__new(0,4), Vector2.__new(3,4), Vector2.__new(4,5) }}, -- テクスチャUVoffset切り替え
    ["01:14.4"]={["function"] = changeColorMaterials, ["val"] = { {"tenkyu"}, Color.__new(0/255, 171/255, 202/255, 1) } }, -- 昼

    ["01:14.7"]={["function"] = startEffect, ["val"] = { "ina" }}, -- effect
    ["01:14.8"]={["function"] = onMaterials, ["val"] = { { "光", "黒" } } }, -- 暗転
    ["01:17.4"]={["function"] = startAndStopEffect, ["val"] = { "smoke", 3 } },
    
    ["01:17.9"]={["function"] = onMaterials, ["val"] = { { "Charles_Kashi26" } }}, --"笑い合ってさよなら"
    ["01:19.0"]={["function"] = changeColorMaterials, ["val"] = { {"tenkyu"}, Color.__new(0/255, 171/255, 202/255, 1) } }, -- 昼

    -- 間奏
    ["01:19.9"]={["lylic"] = "Charles_Kashi66", ["function"] = changeColor, ["val"] = {6500, "tenkyu", Color.__new(255/255, 165/255, 0, 1) } }, -- 昼→夕
    ["01:20.5"]={["function"] = offMaterials, ["val"] = { { "Charles_Kashi26", "光", "黒" } } }, -- 暗転終わり
    ["01:21.0"]={["function"] = startAndStopEffect, ["val"] = { "particle", 12 }}, -- effect
    ["01:26.5"]={["function"] = changeColor, ["val"] = {6500, "tenkyu", Color.__new(8/255, 35/255, 67/255, 1) } }, -- 夕→夜
    ["01:33.3"]={["function"] = changeColorMaterials, ["val"] = { {"tenkyu"}, Color.__new(0/255, 171/255, 202/255, 1) } }, -- 昼

    ["01:45.9"]={["lylic"] = "Charles_Kashi32", ["function"] = changeColors2, ["val"] = { 500, "Moya", Color.__new(255/255, 255/255, 255/255, 0), Color.__new(255/255, 255/255, 255/255, 1) }}, --"黙っていよう　それでいつか苛まれたとしても" 暗転
    ["01:48.6"]={["function"] = changeColors2, ["val"] = { 500, "Moya", Color.__new(255/255, 255/255, 255/255, 1), Color.__new(255/255, 255/255, 255/255, 0) } }, -- 暗転終わり
    ["01:48.9"]={["lylic"] = "Charles_Kashi33" }, -- 複数マテリアル連続透明・不透明化
    ["01:50.2"]={["function"] = onoffMaterials, ["val"] = {600, {"SINobject2", "SINobject1"} } }, -- 複数マテリアル連続透明・不透明化
    ["01:51.0"]={["function"] = changeColorMaterials, ["val"] = { {"cylinder_ao"}, Color.__new(207/255, 115/255, 122/255, 1)}}, -- 建物マテリアル 赤
    ["01:51.1"]={["function"] = startAndStopEffect, ["val"] = { "Sainamare", 2 }}, -- effect
    ["01:52.9"]={["function"] = changeColorMaterials, ["val"] = { {"cylinder_ao"}, Color.__new(211/255, 211/255, 211/255, 1)}},


    ["01:56.6"]={["function"] = onoffMaterials, ["val"] = {500, {"SINobject4", "SINobject3"} }}, -- 複数マテリアル連続透明・不透明化
    ["01:59.5"]={["function"] = onMaterials, ["val"] = { {"koito6"} }},
    
    ["02:00.0"]={["function"] = onMaterials, ["val"] = { {"koito"} }},
    ["02:00.3"]={["function"] = offMaterials, ["val"] = { { "koito6", "koito" } } }, -- 暗転終わり
    ["02:02.0"]={["lylic"] = "Charles_Kashi38"},
    
    ["02:03.8"]={["lylic"] = "Charles_Kashi39"}, --"汚れきった言葉を今"
    ["02:05.6"]={["function"] = uvTexture, ["val"] = {500, "pannel4", Vector2.__new(0,3), Vector2.__new(3,3), Vector2.__new(4,5) }}, -- テクスチャUVoffset切り替え
    ["02:08.0"]={["lylic"] = "Charles_Kashi40", ["function"] = startAndStopEffect, ["val"] = { "ima", 2.2 } }, --"今今" effect
    
    ["02:09.2"]={["function"] = startAnimationFromName, ["val"] = { "CrocusLarge", "default" }}, --アニメーション
    ["02:09.4"]={["function"] = onMaterials, ["val"] = { { "kaben1", "kuki1" } } }, 
    ["02:09.7"]={["function"] = offMaterials, ["val"] = { { "kaben1", "kuki1" } } }, 


    ["02:13.5"]={["function"] = onMaterials, ["val"] = { {"mazatte"} }},

    ["02:13.6"]={["function"] = startAnimationFromNameTransform, ["val"] = { "Armature(mazatte)", "mazatteanim" }}, --"混ざって混ざって二人の果て",
    ["02:15.6"]={["function"] = offMaterials, ["val"] = { {"mazatte"} }},
    
    ["02:18.9"]={["function"] = uvTexture, ["val"] = { 400, "parple5", Vector2.__new(0,0), Vector2.__new(4,0), Vector2.__new(5,1) }}, -- テクスチャUVoffset切り替え

    ["02:20.0"]={["function"] = changeColor, ["val"] = {3000, "tenkyu", Color.__new(255/255, 165/255, 0, 1) } }, -- 昼→夕
    ["02:20.9"]={["function"] = startAndStopEffect, ["val"] = { "ina10", 2.5 }}, -- effect

    ["02:22.0"]={["function"] = startAnimationFromName, ["val"] = { "CrocusLarge", "default" }}, --アニメーション
    ["02:22.3"]={["function"] = onMaterials, ["val"] = { { "kaben1", "kuki1" } } }, 
    ["02:22.6"]={["function"] = offMaterials, ["val"] = { { "kaben1", "kuki1" } } }, 
    --skydomeをオレンジから夜へ

    ["02:26.5"]={["function"] = playKansouEffect, ["val"] = { } },
    ["02:27.6"]={["function"] = changeColor, ["val"] = {1000, "tenkyu", Color.__new(0/255, 171/255, 202/255, 1) } }, -- 夕→昼
    ["02:43.2"]={["function"] = onoffMaterials, ["val"] = {500, {"SINobject1", "SINobject2"} }}, -- 複数マテリアル連続透明・不透明化
    

    ["02:49.6"]={["function"] = onoffMaterials, ["val"] = {500, {"SINobject3", "SINobject4"} }}, -- 複数マテリアル連続透明・不透明化

    ["03:05.3"]={["function"] = uvTexture, ["val"] = {500, "kansou", Vector2.__new(0,1), Vector2.__new(3,1), Vector2.__new(4,5) }}, -- テクスチャUVoffset切り替え
    ["03:07.2"]={["function"] = startEffect, ["val"] = { "iya" } },
    ["03:07.4"]={["function"] = onMaterials, ["val"] = { { "光", "黒" } } }, -- 暗転
    ["03:10.4"]={["function"] = offMaterials, ["val"] = { { "光", "黒" } } }, -- 暗転終わり

    ["03:10.2"]={["function"] = startAndStopEffect, ["val"] = { "moya4", 3 }}, -- effect

    ["03:13.3"]={["function"] = changeColor, ["val"] = {1000, "tenkyu", Color.__new(8/255, 35/255, 67/255, 1) } }, -- 夜

    ["03:13.5"]={["function"] = onMaterials, ["val"] = { {"katatte"} }},
    ["03:13.6"]={["function"] = startAnimation, ["val"] = { "katattebone" }},
    ["03:14.5"]={["function"] = offMaterials, ["val"] = { {"katatte"} }},
    ["03:14.6"]={["function"] = startAndStopEffect, ["val"] = { "yorunomure", 2.4  }}, -- effect

    ["03:18.3"]={["function"] = uvTexture, ["val"] = {500, "pannel4", Vector2.__new(0,2), Vector2.__new(3,2), Vector2.__new(4,5) }}, -- テクスチャUVoffset切り替え
    ["03:20.3"]={["function"] = changeColorMaterials, ["val"] = { {"tenkyu"}, Color.__new(0/255, 171/255, 202/255, 1) } }, -- 昼

    ["03:20.7"]={["function"] = startAndStopEffect, ["val"] = { "ina10", 3 }}, -- effect
    ["03:21.5"]={["function"] = startAnimationFromName, ["val"] = { "CrocusLarge", "default" }}, --アニメーション
    ["03:22.0"]={["function"] = onMaterials, ["val"] = { { "kaben1", "kuki1" } } }, 
    ["03:22.4"]={["function"] = offMaterials, ["val"] = { { "kaben1", "kuki1" } } }, 

    ["03:23.2"]={["function"] = startAnimationFromName, ["val"] = { "CrocusLarge", "colorful" }}, --アニメーション
    ["03:23.4"]={["function"] = onMaterials, ["val"] = { { "kaben1", "kuki1" } } }, 
    ["03:23.5"]={["function"] = switchColor, ["val"] = { 300, {"kaben", "kaben1"} }}, -- effect
    ["03:24.8"]={["function"] = offMaterials, ["val"] = { { "kaben1", "kuki1" } } }, 


    ["03:25.6"]={["function"] = playMovie, ["val"] = { "nc79270_movie_halfclear", 15, 30 } }, --"愛を謳って謳って雲の上"
    ["03:26.1"]={["function"] = onMaterials, ["val"] = { {"utatte"} }},
    ["03:26.2"]={["function"] = startAnimation, ["val"] = { "utatteroot" }},
    ["03:26.7"]={["function"] = startEffect, ["val"] = { "utatteeffe" } },
    ["03:28.4"]={["function"] = offMaterials, ["val"] = { {"utatte"} }},

    ["03:33.2"]={["function"] = startAnimation, ["val"] = { "katattebone" }},
    ["03:33.3"]={["function"] = onMaterials, ["val"] = { {"katatte"} }},
    ["03:34.9"]={["function"] = offMaterials, ["val"] = { {"katatte"} }},
}
```
</div></details>

## タイムテーブルの処理を実行する方法
タイムテーブルの処理は以下のシーケンス図のように実現しています。
その際、コルーチンはvci.StartCoroutineではなく、私の方で公開している[expandedVCICoroutine](https://github.com/chinng-inta/expandedVCICoroutine)モジュールを使用しています。
経過時間をキーにタイムテーブルから値を取り出す方法については[Lua言語でテーブルを用いてswith-case文を疑似的に実現する](https://qiita.com/chinng_inta/items/933d05bb9c44409630e8)を参照ください。

![タイムテーブルを実行する.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/1faa7211-fa22-e12b-b623-55d81795af9a.png)

<details><summary>タイムテーブル実行コルーチン</summary><div>

```
function timerCoroutine()
    print("not all コルーチン開始")
    vci.assets.audio.PlayOneShot("シャルル【ボカロ完成】", 0.3)
    startAnimationFromName("Light", "lightSmall")
    startAnimationFromName( "sorenanoni3", "sorenanonikoteiyou" )
    initMaterials()

    local colorWaku = vci.assets.material.GetColor("waku")
    colorWaku.a = 0
    vci.assets.material.SetColor("waku", colorWaku)

    while isStart == true do
        local curr = vci.me.UnscaledTime
        local baseTime = os.time({["year"] = 1970, ["month"] = 1, ["day"] = 1, ["hour"] = 0, ["min"] = 0, ["sec"] = 0 })
--        local curr = TimeSpan.FromSeconds( os.difftime( os.time(), baseTime ) )
        local tStr = getTimeStr(curr)

        if lastTimeStr ~= tStr then
            local lylic = getLylic(tStr)
            if lylic ~= nil then
                vci.assets.SetText("TimerText", tStr)
            end

            local func = getFunction(tStr)
            local val = getValues(tStr)

            if func == changeColorMaterials then
                func(val[1], val[2] )
            elseif  func == onMaterials then
                expandedVCICoroutine.startCoroutine(func, val[1])
            elseif  func == offMaterials then
                expandedVCICoroutine.startCoroutine(func, val[1])
            elseif func == changeColor then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2], val[3] )
            elseif  func == changeColors2 then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2], val[3], val[4])
            elseif  func == onoffMaterials then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2])
            elseif  func == uvTexture then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2], val[3], val[4], val[5])
            elseif  func == startEffect then
                expandedVCICoroutine.startCoroutine(func, val[1])
            elseif  func == startAndStopEffect then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2])
            elseif  func == startAnimation then
                expandedVCICoroutine.startCoroutine(func, val[1])
            elseif  func == startAnimationFromName then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2])
            elseif  func == startAnimationFromNameTransform then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2])
            elseif func == switchColor then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2])
            elseif func == playMovie then
                expandedVCICoroutine.startCoroutine(func, val[1], val[2], val[3] )
            elseif func == startTitleEffect then
                expandedVCICoroutine.startCoroutine(func, val[1])
            elseif  func == playKumonoUeAnime then
                expandedVCICoroutine.startCoroutine(func)
            elseif func == playKansouEffect then
                expandedVCICoroutine.startCoroutine(func, val[1])
            elseif  func == initMaterials then
                initMaterials()
            end

            lastTimeStr = tStr
        end

        moveCameraModule()
        if tStr > songLength then
            break
        end
        sleepMs(20)
    end
    isStart = false
    vci.assets.SetText("LylicText", ProductionTable["ti"]["title"])
    vci.assets.audio.Stop("シャルル【ボカロ完成】")
    colorWaku.a = 1
    vci.assets.material.SetColor("waku", colorWaku)
    print("not all コルーチン終了")
end
```
</div></details>

### なぜvci.StartCoroutineを使用しないか
vci.StartCoroutineを使用しない理由としては以下の2点が挙げられます。

1.vci.StartCoroutineで実行したコルーチンの中でvci.StartCoroutineを実行すると、VCIシステムが死ぬ
以下の図のようにvci.StartCoroutineで実行したコルーチンの中でコルーチンを実行しようとするとその時点でVCIの大本が死に、スタジオに入りなおさないといけません。
![VCIコルーチン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/3e28e281-5c99-f57d-7355-d6b3a9d79a49.png)

2.コルーチンの実行時に引数が渡せない
例えば、徐々に色を変えるコルーチン

```
function changeColor(time, mat, color)
    print("changeColor 開始")
    print(time)
    local start = vci.assets.material.GetColor(mat)

    local t0 = vci.me.UnscaledTime.Add(TimeSpan.FromMilliseconds(time))
    while TimeSpan.Compare(vci.me.UnscaledTime, t0) < 0 do
        local t1 = TimeSpan.FromTicks((t0 - vci.me.UnscaledTime).Ticks)
        local msec = (t1.Hours * 1000 * 60 * 60) + (t1.Minutes * 1000 * 60) + (t1.Seconds * 1000) + t1.Milliseconds 

        local c = Color.Lerp(color, start, (msec/time))
        vci.assets.material.SetColor(mat, c)
        sleepMs(5)
    end

    vci.assets.material.SetColor(mat, color)
    print("changeColor 終了")
    print(time)
end
```
引数を渡さない場合、専用のコルーチンを作成したり、グローバル変数に使いたい値を覚えることで実現することも出来ますが、同じコルーチンを複数実行する場合に煩雑になります。
そのため、自作のコルーチンモジュール(luaのコルーチンをモジュール化)を使用しています。

<details><summary>専用のコルーチンを作成する例</summary><div>

```
function changeColorFromNoonToEvening()
    print("changeColor 開始")
    local time = 6500
    local mat = "tenkyu"
    local color = Color.__new(255/255, 165/255, 0, 1)
    local start = vci.assets.material.GetColor(mat)


    local t0 = vci.me.UnscaledTime.Add(TimeSpan.FromMilliseconds(time))
    while TimeSpan.Compare(vci.me.UnscaledTime, t0) < 0 do
        local t1 = TimeSpan.FromTicks((t0 - vci.me.UnscaledTime).Ticks)
        local msec = (t1.Hours * 1000 * 60 * 60) + (t1.Minutes * 1000 * 60) + (t1.Seconds * 1000) + t1.Milliseconds 

        local c = Color.Lerp(color, start, (msec/time))
        vci.assets.material.SetColor(mat, c)
        sleepMs(5)
    end

    vci.assets.material.SetColor(mat, color)
    print("changeColor 終了")
    print(time)
end

function changeColorFromEveningToNight()
    print("changeColor 開始")
    local time = 6500
    local mat = "tenkyu"
    local color = Color.__new(8/255, 35/255, 67/255, 1)
    local start = vci.assets.material.GetColor(mat)


    local t0 = vci.me.UnscaledTime.Add(TimeSpan.FromMilliseconds(time))
    while TimeSpan.Compare(vci.me.UnscaledTime, t0) < 0 do
        local t1 = TimeSpan.FromTicks((t0 - vci.me.UnscaledTime).Ticks)
        local msec = (t1.Hours * 1000 * 60 * 60) + (t1.Minutes * 1000 * 60) + (t1.Seconds * 1000) + t1.Milliseconds 

        local c = Color.Lerp(color, start, (msec/time))
        vci.assets.material.SetColor(mat, c)
        sleepMs(5)
    end

    vci.assets.material.SetColor(mat, color)
    print("changeColor 終了")
    print(time)
end

vci.StartCoroutine( coroutine.create( changeColorFromNoonToEvening ) )
vci.StartCoroutine( coroutine.create( changeColorFromEveningToNight) )
```
</div></details>

<details><summary>グローバル変数に使いたい値を覚える例</summary><div>

```
local time
local mat
local color

function changeColor()
    print("changeColor 開始")

    local start = vci.assets.material.GetColor(mat)


    local t0 = vci.me.UnscaledTime.Add(TimeSpan.FromMilliseconds(time))
    while TimeSpan.Compare(vci.me.UnscaledTime, t0) < 0 do
        local t1 = TimeSpan.FromTicks((t0 - vci.me.UnscaledTime).Ticks)
        local msec = (t1.Hours * 1000 * 60 * 60) + (t1.Minutes * 1000 * 60) + (t1.Seconds * 1000) + t1.Milliseconds 

        local c = Color.Lerp(color, start, (msec/time))
        vci.assets.material.SetColor(mat, c)
        sleepMs(5)
    end

    vci.assets.material.SetColor(mat, color)
    print("changeColor 終了")
    print(time)
end

time = 6500
mat = "tenkyu"
color = Color.__new(255/255, 165/255, 0, 1)
vci.StartCoroutine( coroutine.create( changeColor) )
```
</div></details>

### updateとVCIコルーチンでよくない？
前項の内容を以下のようにupdateとVCIコルーチンで実現することも可能です。

![updateを使う方法.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407114/198f0f1b-2dfe-d4e9-f4a1-6f75590e03c8.png)

ただ、**ダサいし**、処理が分割してて管理が面倒なので自作のコルーチンモジュールを使用しています。

### タイムテーブルの作成方法
タイムテーブルを作成する際にはカラオケ等で使用される同期歌詞が非常に役に立ちます。
曲名+LRCファイルで検索すると幸せになります。
ほぼほぼ、luaのテーブルに近い形なのでテキストエディタで少し置換するだけでVCIで使用できるタイムテーブルになります。
タイムテーブルにないタイミングで処理を実行したい場合は録画とにらめっこしながら頑張ってください

# 最後に
今回は実装方法の概要について記載しました。
Vキャスでもいわゆるパーティクルライブを作成することができますので、どんどん作ってください
VFMの放送画面に映らない部分も色々作り込んでいます。近々VR内での体験会を実施しますので、是非見に来てください

2020/12/17
シーケンス図にスリープが抜けていたので修正
「専用のコルーチンを作成する例」、「グローバル変数に使いたい値を覚える例」を追記