Synthesizer V Pro用レコーディングモーション生成プラグイン
投稿:2021-10-21、更新:2021-11-07
Synthesizer V Proで弦巻マキちゃん(弦巻マキAI)に歌ってもらっても、その楽曲用のMMD(MikuMikuDance)モーションがない。 かといってモーションを自作できる技量もない。 止め絵(イラスト)をお借りしても良い。 けれど動く弦巻マキちゃんを見たい、見せたい❤ そうだ、レコーディングスタジオで収録してる風なモーションなら動きが単純だし、できるんじゃないか?
2021-11-07 【変更点】 ・グループに対応 ・手を上げる挙動の所要時間を設定(速さを設定)
プラグインが1000行越えたので先に使い方を説明します。
初めに出力したいトラックを選択してメニュー「スクリプト|MMD|レコーディングモーション生成(Lua)」を選択してください。
設定ダイアログが表示されます。
上から順に
- モーションを出力するvmdファイル
- 首を拍に合わせて左右に振る
- 首を発音に合わせて上下に振る
- 体を小節に合わせて左右に振る
- 腕を上げる割合
- 腕を上げる時間
こちらをご覧ください。
レコーディングのモーションの他に、まばたきはランダム登録、口パク(リップモーション)はリップモーション生成プラグインで生成しました。
- バグがあるのが前提です
- Synthesizer V Proのプロジェクトを壊したり
- vmdファイルの出力が止まらずに巨大なファイルが生成されたり
- MMDで読み込むとMMDのプロジェクトを壊すかも知れません
- くれぐれもバックアップを確保してから作業してください
- プラグインの元ネタとして好きに書き替えてお使いください
- Synthesizer V Proのプラグインがもっと増えますように
-- レコーディングモーション生成
local plugin_name = "レコーディングモーション生成(Lua)"
function getClientInfo()
return {
name = plugin_name,
category = "MMD",
author = "lemorin_jp",
versionNumber = 2,
minEditorVersion = 0
}
end
local fmt_long = "<I4"
local fmt_float = "<f"
local fmt_byte = "B"
-- 設定
local preferences = {
left_right = 1.5,
up_down = 3.0,
trunk = 0.5,
arm_ratio = 1.0,
arm_up_time = 0.5,
}
-- 変換できる文字列
local check_born_name = [[
レコーディング
上半身2
首
頭
左腕
右腕
左ひじ
右ひじ
左手捩
右手捩
左手首
右手首
左親指0
左親指1
左親指2
左人指1
左人指2
左人指3
左中指1
左中指2
左中指3
左薬指1
左薬指2
左薬指3
左小指1
左小指2
左小指3
右親指0
右親指1
右親指2
右人指1
右人指2
右人指3
右中指1
右中指2
右中指3
右薬指1
右薬指2
右薬指3
右小指1
右小指2
右小指3
下半身
]]
local sjis_char = {
["\n"] = "\n", -- 文字列チェック用(変換に使わない)
["2"] = "\x32",
["ー"] = "\x81\x5b",
["じ"] = "\x82\xb6",
["ひ"] = "\x82\xd0",
["0"] = "\x82\x4f",
["1"] = "\x82\x50",
["2"] = "\x82\x51",
["3"] = "\x82\x52",
["ィ"] = "\x83\x42",
["グ"] = "\x83\x4f",
["コ"] = "\x83\x52",
["デ"] = "\x83\x66",
["レ"] = "\x83\x8c",
["ン"] = "\x83\x93",
["右"] = "\x89\x45",
["下"] = "\x89\xba",
["左"] = "\x8d\xb6",
["指"] = "\x8e\x77",
["手"] = "\x8e\xe8",
["首"] = "\x8e\xf1",
["小"] = "\x8f\xac",
["上"] = "\x8f\xe3",
["親"] = "\x90\x65",
["人"] = "\x90\x6c",
["身"] = "\x90\x67",
["中"] = "\x92\x86",
["頭"] = "\x93\xaa",
["半"] = "\x94\xbc",
["薬"] = "\x96\xf2",
["腕"] = "\x98\x72",
["捩"] = "\x9d\x80",
}
--[[
[""] = "\x\x",
]]
function main()
-- SJISに変換できるかチェック
local checkd = utf8_2_sjis(check_born_name)
-- プロジェクトのパス
local path_func = string.gmatch(SV:getProject():getFileName(), ".+\\")
local project_path = path_func()
-- プロジェクトがない場合
if nil == project_path then
project_path = ""
end
-- ダイアログの内容
local myForm = {
title = plugin_name,
message = "レコーディングモーション",
buttons = "OkCancel",
widgets = {
{
type = "TextBox",
label = "vmdファイル Windowsは漢字NG",
name = "vmdfile",
default = project_path .. "recordingmotion.vmd",
},
{
type = "Slider",
label = "首を振る(左右)",
name = "left_right",
format = "%5.1f°",
minValue = 0.1,
maxValue = 10.0,
interval = 0.1,
default = preferences.left_right,
},
{
type = "Slider",
label = "首を振る(上下)",
name = "up_down",
format = "%5.1f°",
minValue = 0.1,
maxValue = 10.0,
interval = 0.1,
default = preferences.up_down,
},
{
type = "Slider",
label = "体の揺れ",
name = "trunk",
format = "%5.1f°",
minValue = 0.1,
maxValue = 5.0,
interval = 0.1,
default = preferences.trunk,
},
{
type = "Slider",
label = "腕を上げる角度(比率)",
name = "arm_ratio",
format = "%3.0f%%",
minValue = 1,
maxValue = 100,
interval = 1,
default = math.floor(preferences.arm_ratio * 100),
},
{
type = "Slider",
label = "腕を上げる時間",
name = "arm_up_time",
format = "%3.1f秒",
minValue = 0.1,
maxValue = 1.0,
interval = 0.1,
default = preferences.arm_up_time,
},
}
}
-- ダイアログ表示(モーダル)
local result = SV:showCustomDialog(myForm)
if result.status then
-- 設定内容を拾う
local vmdfile = result.answers.vmdfile
preferences.left_right = result.answers.left_right
preferences.up_down = result.answers.up_down
preferences.trunk = result.answers.trunk
preferences.arm_ratio = result.answers.arm_ratio / 100
-- 出力先に同名ファイルがないか、上書き確認した場合
local isgo = false
if file_exists(vmdfile) then
if SV:showOkCancelBox(plugin_name, "vmdファイルが既にあります。上書きしてよいですか?\n" .. vmdfile) then
isgo = true
end
else
isgo = true
end
if isgo then
local phrase, note = grab_phrase()
local motion_phrase, kubi_phrase = phrase2motion(phrase)
local kubi_note = note2motion(note)
-- 2つの首モーションを合成する
local motion_note = kubi2motion(kubi_phrase, kubi_note)
-- 体幹モーション
local motion_trunk = create_trunk()
-- 全モーションをフレーム順にまとめる
local motion = {}
local i
for i = 1, #motion_phrase do
table.insert(motion, motion_phrase[i])
end
for i = 1, #motion_note do
table.insert(motion, motion_note[i])
end
for i = 1, #motion_trunk do
table.insert(motion, motion_trunk[i])
end
-- フレーム順に並べ替え、同フレームの場合はボーン名順
table.sort(motion,
function (a, b)
if a.frame ~= b.frame then
return a.frame < b.frame
end
return a.bone < b.bone
end
)
local rc = write_vmd(vmdfile, motion)
if 1 == rc then
SV:showMessageBox(plugin_name, "vmdファイルを出力しました")
else
SV:showMessageBox(plugin_name, "vmdファイルを開けません\n" .. vmdfile)
end
end
end
-- スクリプト終了
SV:finish()
end
-- 体幹モーション生成
function create_trunk()
local motion = {}
-- 歌い終わった後も揺らすために、プロジェクト全体の長さを使う
local project = SV:getProject()
local timeaxis = project:getTimeAxis()
local size_second = timeaxis:getSecondsFromBlick(project:getDuration())
local trunk_home = {
{"上半身2", 0.0, 0.0, 0.0},
{"頭", 0.0, 0.0, 0.0},
{"下半身", 0.0, 0.0, 0.0},
}
local trunk_left = {
{"上半身2", 0.0, 0.0, preferences.trunk},
{"頭", 0.0, 0.0, preferences.trunk},
{"下半身", 0.0, 0.0, preferences.trunk},
}
local trunk_right = {
{"上半身2", 0.0, 0.0, -preferences.trunk},
{"頭", 0.0, 0.0, -preferences.trunk},
{"下半身", 0.0, 0.0, -preferences.trunk},
}
-- 最初は直立
local second = 0
local frame = math.floor(second * 30)
insert_form(motion, frame, trunk_home)
second = seek_measure(second, 1)
local isLeft = 1
while second < size_second do
-- 小節の頭で左右に揺れる
frame = math.floor(second * 30)
if 1 == isLeft then
insert_form(motion, frame, trunk_left)
else
insert_form(motion, frame, trunk_right)
end
isLeft = 1 - isLeft
second = seek_measure(second, 1)
end
-- 最後は直立に戻る
frame = math.floor(size_second * 30)
insert_form(motion, frame, trunk_home)
return motion
end
-- 1つのボーンで2つのモーションを合成
function kubi2motion(phrase, note)
local motion = {}
table.sort(phrase,
function (a, b)
return a.frame < b.frame
end
)
table.sort(note,
function (a, b)
return a.frame < b.frame
end
)
local frame_max = phrase[#phrase].frame
if frame_max < note[#note].frame then
frame_max = note[#note].frame
end
local bone = "首"
table.insert(phrase, {
frame = frame_max + 1,
x = phrase[#phrase].x,
y = phrase[#phrase].y,
z = phrase[#phrase].z,
})
local p = 1
local p_step = 0
local p_x = 0
local p_y = 0
local p_z = 0
table.insert(note, {
frame = frame_max + 1,
x = note[#note].x,
y = note[#note].y,
z = note[#note].z,
})
local n = 1
local n_step = 0
local n_x = 0
local n_y = 0
local n_z = 0
local frame
for frame = 0, frame_max do
local is_motion = false
while phrase[p + 1].frame < frame do
p = p + 1
p_step = 0
is_motion = true
end
while note[n + 1].frame < frame do
n = n + 1
n_step = 0
is_motion = true
end
if is_motion then
local x
if nil ~= p_x and nil ~= n_x then
x = (p_x + n_x) / 2
elseif nil ~= p_x then
x = p_x
elseif nil ~= n_x then
x = n_x
else
x = 0.0
end
local y
if nil ~= p_y and nil ~= n_y then
y = (p_y + n_y) / 2
elseif nil ~= p_y then
y = p_y
elseif nil ~= n_y then
y = n_y
else
y = 0.0
end
local z
if nil ~= p_z and nil ~= n_z then
z = (p_z + n_z) / 2
elseif nil ~= p_z then
z = p_z
elseif nil ~= n_z then
z = n_z
else
z = 0.0
end
local kubi = {
{bone, x, y, z},
}
insert_form(motion, frame, kubi)
end
if phrase[p].frame < phrase[p + 1].frame then
local dist = phrase[p + 1].frame - phrase[p].frame
if nil ~= phrase[p].x then
p_x = phrase[p].x - phrase[p].x * p_step / dist + phrase[p + 1].x * p_step / dist
end
if nil ~= phrase[p].y then
p_y = phrase[p].y - phrase[p].y * p_step / dist + phrase[p + 1].y * p_step / dist
end
if nil ~= phrase[p].z then
p_z = phrase[p].z - phrase[p].z * p_step / dist + phrase[p + 1].z * p_step / dist
end
end
if note[n].frame < note[n + 1].frame then
local dist = note[n + 1].frame - note[n].frame
if nil ~= note[n].x then
n_x = note[n].x - note[n].x * n_step / dist + note[n + 1].x * n_step / dist
end
if nil ~= note[n].y then
n_y = note[n].y - note[n].y * n_step / dist + note[n + 1].y * n_step / dist
end
if nil ~= note[n].z then
n_z = note[n].z - note[n].z * n_step / dist + note[n + 1].z * n_step / dist
end
end
p_step = p_step + 1
n_step = n_step + 1
end
return motion
end
-- ファイルの有無チェック
function file_exists(file)
local result = false
local fh = io.open(file, "rb")
if nil ~= fh then
local buff = fh:read(1)
if 0 < string.len(buff) then
result = true
end
fh:close(fh)
end
return result
end
-- フレーズ収集
function grab_phrase()
-- 選択中のトラックのメイングループ
local track = SV:getMainEditor():getCurrentTrack()
local timeaxis = SV:getProject():getTimeAxis()
local track_note = {}
local groupnum = track:getNumGroups()
local group_index
for group_index = 1, groupnum do
local notegroup_ref = track:getGroupReference(group_index)
local offset_blick = notegroup_ref:getTimeOffset()
local notegroup = notegroup_ref:getTarget()
local num_note = notegroup:getNumNotes()
local note_index
for note_index = 1, num_note do
local note = notegroup:getNote(note_index)
local n = {
start = offset_blick + note:getOnset(),
stop = offset_blick + note:getEnd(),
}
table.insert(track_note, n)
end
end
table.sort(track_note,
function (a, b)
return a.start < b.start
end
)
-- 選択中のトラックのグループ
local phrase ={}
local note_list = {}
local phrase_start
local phrase_stop
local pronunciation_last
local note_index
for note_index = 1, #track_note do
-- 音符1個
local note = track_note[note_index]
if nil == phrase_start then
phrase_start = note.start
pronunciation_last = phrase_start
phrase_stop = note.stop
end
if note.start <= phrase_stop then
phrase_stop = note.stop
pronunciation_last = note.start
else
local p = {
start = timeaxis:getSecondsFromBlick(phrase_start),
last = timeaxis:getSecondsFromBlick(pronunciation_last),
stop = timeaxis:getSecondsFromBlick(phrase_stop)
}
table.insert(phrase, p)
phrase_start = note.start
pronunciation_last = phrase_start
phrase_stop = note.stop
end
local note_single = {
start = timeaxis:getSecondsFromBlick(note.start),
stop = timeaxis:getSecondsFromBlick(note.stop),
}
table.insert(note_list, note_single)
end
if nil ~= phrase_start then
local p = {
start = timeaxis:getSecondsFromBlick(phrase_start),
last = timeaxis:getSecondsFromBlick(pronunciation_last),
stop = timeaxis:getSecondsFromBlick(phrase_stop)
}
table.insert(phrase, p)
end
SV:showMessageBox(plugin_name, "音符数 " .. #track_note .. "\nフレーズ数 " .. #phrase)
return phrase, note_list
end
-- フレーズからモーション
function phrase2motion(phrase)
local timeaxis = SV:getProject():getTimeAxis()
local motion = {}
local kubi = {}
-- 気を付け開始
local frame = math.floor(0 * 30)
local kiwotsuke = {
{"左腕", 0.0, 0.0, 42.0},
{"左ひじ", 0.0, 0.0, 0.0},
{"左手捩", 0.0, 0.0, 0.0},
{"左手首", 0.0, 0.0, 10.0},
{"右腕", 0.0, 0.0, -45.0},
{"右ひじ", 0.0, 0.0, 0.0},
{"右手捩", 0.0, 0.0, 0.0},
{"右手首", 0.0, 0.0, -10.0},
{"左親指0", 0.0, 0.0, 0.0},
{"左人指1", 0.0, 0.0, 0.0},
{"左人指2", 0.0, 0.0, 0.0},
{"左人指3", 0.0, 0.0, 0.0},
{"左中指1", 0.0, 0.0, 0.0},
{"左中指2", 0.0, 0.0, 0.0},
{"左中指3", 0.0, 0.0, 0.0},
{"左薬指1", 0.0, 0.0, 0.0},
{"左薬指2", 0.0, 0.0, 0.0},
{"左薬指3", 0.0, 0.0, 0.0},
{"左小指1", 0.0, 0.0, 0.0},
{"左小指2", 0.0, 0.0, 0.0},
{"左小指3", 0.0, 0.0, 0.0},
{"右親指0", 0.0, 0.0, 0.0},
{"右人指1", 0.0, 0.0, 0.0},
{"右人指2", 0.0, 0.0, 0.0},
{"右人指3", 0.0, 0.0, 0.0},
{"右中指1", 0.0, 0.0, 0.0},
{"右中指2", 0.0, 0.0, 0.0},
{"右中指3", 0.0, 0.0, 0.0},
{"右薬指1", 0.0, 0.0, 0.0},
{"右薬指2", 0.0, 0.0, 0.0},
{"右薬指3", 0.0, 0.0, 0.0},
{"右小指1", 0.0, 0.0, 0.0},
{"右小指2", 0.0, 0.0, 0.0},
{"右小指3", 0.0, 0.0, 0.0},
}
insert_form(motion, frame, kiwotsuke)
local kubi_home = {"首", nil, 0.0, nil}
local kubi_left = {"首", nil, preferences.left_right, nil}
local kubi_right = {"首", nil, -preferences.left_right, nil}
local utaidashi = {
{"左手捩", 60.0, 90.0, 60.0},
{"左手首", 0.0, 0.0, 0.0},
{"右手捩", 60.0, -90.0, -60.0},
{"右手首", 0.0, 0.0, 0.0},
{"左親指0", -12.2, 9.5, -1.0},
{"左人指1", 0.0, -10.0, 95.0},
{"左人指2", 0.0, 0.0, 60.0},
{"左人指3", 0.0, 1.2, 49.2},
{"左中指1", 0.2, 1.8, 96.7},
{"左中指2", 0.0, 0.0, 60.0},
{"左中指3", 0.1, 0.4, 28.5},
{"左薬指1", -1.0, 12.1, 102.7},
{"左薬指2", 0.0, 0.0, 60.0},
{"左薬指3", 0.0, 0.1, 24.5},
{"左小指1", 10.1, 17.9, 85.1},
{"左小指2", 0.0, 0.0, 80.0},
{"左小指3", 0.0, 0.3, 18.1},
{"右親指0", -12.2, -9.5, 1.0},
{"右人指1", 0.0, 10.0, -95.0},
{"右人指2", 0.0, 0.0, -60.0},
{"右人指3", 0.0, -1.2, -49.2},
{"右中指1", 0.2, -1.8, -96.7},
{"右中指2", 0.0, 0.0, -60.0},
{"右中指3", 0.1, -0.4, -28.5},
{"右薬指1", -1.0, -12.1, -102.7},
{"右薬指2", 0.0, 0.0, -60.0},
{"右薬指3", 0.0, -0.1, -24.5},
{"右小指1", 10.1, -17.9, -85.1},
{"右小指2", 0.0, 0.0, -80.0},
{"右小指3", 0.0, -0.3, -18.1},
}
if 0 < #phrase then
-- 歌い出しの1小節前を探す
local before_second = seek_measure(phrase[1].start, -1)
-- 気を付け終了
frame = math.floor(before_second * 30)
insert_form(motion, frame, kiwotsuke)
-- 歌い出し(フレーズ頭とぶつかるので1フレーム前)
frame = math.floor(phrase[1].start * 30) - 1
insert_form(motion, frame, utaidashi)
table.insert(kubi, {
frame = frame,
x = kubi_home[2],
y = kubi_home[3],
z = kubi_home[4],
})
end
local arm_home = {
{"左腕", 0.0, 0.0, 42.0},
{"左ひじ", 40.0, -100.0, -40.0},
{"右腕", 0.0, 0.0, -45.0},
{"右ひじ", 40.0, 100.0, 40.0},
}
local is_left = true
local i
for i = 1, #phrase do
local is_kiwotsuke = false
-- 前のフレーズから3小節以上空いたら気を付けで休む
if 1 < i and phrase[i - 1].stop <= seek_measure(phrase[i].start, -3) then
-- 前のフレーズ直後に歌い出し
frame = math.floor(phrase[i - 1].stop * 30) + 1
insert_form(motion, frame, utaidashi)
-- 前のフレーズから1小節後~今のフレーズの1小節前で気を付け
local second = seek_measure(phrase[i - 1].stop, 1)
frame = math.floor(second * 30)
insert_form(motion, frame, kiwotsuke)
second = seek_measure(phrase[i].start, -1)
frame = math.floor(second * 30)
insert_form(motion, frame, kiwotsuke)
-- 今のフレーズの直前
frame = math.floor(phrase[i].start * 30) - 1
insert_form(motion, frame, utaidashi)
is_kiwotsuke = true
end
local p = phrase[i]
-- フレーズの開始
local second = p.start
frame = math.floor(second * 30)
local angle = preferences.arm_ratio
if 1 < i and (p.start - phrase[i - 1].stop) < preferences.arm_up_time then
-- フレーズ間が短時間の場合は腕をちょっと上げる
angle = angle * (p.start - phrase[i - 1].stop) / preferences.arm_up_time
local arm_up = {
{"左腕", 0.0 + 30.0 * angle, 0.0, 42.0 - 2.0 * angle},
{"左ひじ", 40.0 - 10.0 * angle, -100.0 - 40.0 * angle, -40.0 - 30.0 * angle},
{"右腕", 0.0 + 30.0 * angle, 0.0, -45.0 + 5.0 * angle},
{"右ひじ", 40.0 - 10.0 * angle, 100.0 + 40.0 * angle, 40.0 + 30.0 * angle},
}
insert_form(motion, frame, arm_up)
elseif not is_kiwotsuke and 1 < i and preferences.arm_up_time < (p.start - phrase[i - 1].stop) then
-- 気を付けの場合を除きフレーズ間が長時間の場合は先に腕を上げて待つ
local arm_up = {
{"左腕", 0.0 + 30.0 * angle, 0.0, 42.0 - 2.0 * angle},
{"左ひじ", 40.0 - 10.0 * angle, -100.0 - 40.0 * angle, -40.0 - 30.0 * angle},
{"右腕", 0.0 + 30.0 * angle, 0.0, -45.0 + 5.0 * angle},
{"右ひじ", 40.0 - 10.0 * angle, 100.0 + 40.0 * angle, 40.0 + 30.0 * angle},
}
insert_form(motion, math.floor((phrase[i - 1].stop + preferences.arm_up_time) * 30), arm_up)
insert_form(motion, frame, arm_up)
else
-- 普通(?)
local arm_up = {
{"左腕", 0.0 + 30.0 * angle, 0.0, 42.0 - 2.0 * angle},
{"左ひじ", 40.0 - 10.0 * angle, -100.0 - 40.0 * angle, -40.0 - 30.0 * angle},
{"右腕", 0.0 + 30.0 * angle, 0.0, -45.0 + 5.0 * angle},
{"右ひじ", 40.0 - 10.0 * angle, 100.0 + 40.0 * angle, 40.0 + 30.0 * angle},
}
insert_form(motion, frame, arm_up)
end
-- 拍に合わせて首を横に振る
local blick = timeaxis:getBlickFromSeconds(second)
local tempo = timeaxis:getTempoMarkAt(blick)
local beat_second = 60 / tempo.bpm
while second < p.last do
-- 拍の頭
frame = math.floor(second * 30)
table.insert(kubi, {
frame = frame,
x = kubi_home[2],
y = kubi_home[3],
z = kubi_home[4],
})
-- 次の拍までの中間点で戻す
frame = math.floor((second + beat_second * 0.5) * 30)
if is_left then
table.insert(kubi, {
frame = frame,
x = kubi_left[2],
y = kubi_left[3],
z = kubi_left[4],
})
is_left = false
else
table.insert(kubi, {
frame = frame,
x = kubi_right[2],
y = kubi_right[3],
z = kubi_right[4],
})
is_left = true
end
second = second + beat_second
end
-- 最後の発音
frame = math.floor(p.last * 30)
insert_form(motion, frame, arm_home)
table.insert(kubi, {
frame = frame,
x = kubi_home[2],
y = kubi_home[3],
z = kubi_home[4],
})
-- フレーズの終了
frame = math.floor(p.stop * 30)
insert_form(motion, frame, arm_home)
end
if 0 < #phrase then
-- 歌い終わり
frame = math.floor(phrase[#phrase].stop * 30)
insert_form(motion, frame, utaidashi)
-- 歌い終わりの1小節後を探す
local after_second = seek_measure(phrase[#phrase].stop, 1)
-- 気を付け開始
frame = math.floor(after_second * 30)
insert_form(motion, frame, kiwotsuke)
end
return motion, kubi
end
-- 小節単位の相対位置
function seek_measure(second, offset)
local timeaxis = SV:getProject():getTimeAxis()
local blick = timeaxis:getBlickFromSeconds(second)
local measure = timeaxis:getMeasureAt(blick)
local measure_mark = timeaxis:getMeasureMarkAt(measure)
local tempo = timeaxis:getTempoMarkAt(blick)
return second + 60 / tempo.bpm * measure_mark.numerator * offset
end
-- 間の小節数
function dist_measure(start, stop)
local start_blick = timeaxis:getBlickFromSeconds(start)
local start_measure = timeaxis:getMeasureAt(start_blick)
local stop_blick = timeaxis:getBlickFromSeconds(stop)
local stop_measure = timeaxis:getMeasureAt(stop_blick)
return stop_measure - start_measure
end
-- ノートからモーション
function note2motion(note)
local timeaxis = SV:getProject():getTimeAxis()
local kubi = {}
local kubi_home = {"首", 0.0, nil, nil}
local kubi_down = {"首", -preferences.up_down, nil, nil}
-- 気を付け開始
local frame = math.floor(0 * 30)
table.insert(kubi, {
frame = frame,
x = kubi_home[2],
y = kubi_home[3],
z = kubi_home[4],
})
local last_frame
local i
for i = 1, #note do
local n = note[i]
-- ノートの開始
frame = math.floor(n.start * 30)
if nil == last_frame or last_frame < frame then
table.insert(kubi, {
frame = frame,
x = kubi_home[2],
y = kubi_home[3],
z = kubi_home[4],
})
end
-- ノートに合わせて首を縦に振る
frame = math.floor((n.start + n.stop) / 2 * 30)
table.insert(kubi, {
frame = frame,
x = kubi_down[2],
y = kubi_down[3],
z = kubi_down[4],
})
-- ノートの終了
frame = math.floor(n.stop * 30)
table.insert(kubi, {
frame = frame,
x = kubi_home[2],
y = kubi_home[3],
z = kubi_home[4],
})
last_frame = frame
end
return kubi
end
function insert_form(motion, frame, form)
local i
for i = 1, #form do
insert_motion(motion, frame, form[i][1], form[i][2], form[i][3], form[i][4])
end
end
function insert_motion(motion, key_frame, bone_name, rotate_x, rotate_y, rotate_z)
table.insert(motion, {
frame = key_frame,
bone = bone_name,
x = rotate_x,
y = rotate_y,
z = rotate_z,
})
end
function write_vmd(vmdfile, motion)
local rc = 0
local fh = io.open(vmdfile, "wb")
if fh then
-- ヘッダ
write_string(fh, "Vocaloid Motion Data 0002", 30)
-- バージョン「レコーティング」
write_string(fh, utf8_2_sjis('レコーディング'), 20)
-- ここからモーション
-- ボーン個数
local motion_count = 0
local i
for i = 1, #motion do
if nil ~= motion[i].frame then
motion_count = motion_count + 1
end
end
write_long(fh, motion_count)
-- ここからキー毎
for i = 1, #motion do
if nil ~= motion[i].frame then
local bone = utf8_2_sjis(motion[i].bone)
-- ボーン名
if nil == bone then
-- 想定外のボーンは名無しで
write_string(fh, "", 15)
else
write_string(fh, bone, 15)
end
-- フレーム番号
write_long(fh, motion[i].frame)
-- 位置
write_float(fh, 0.0)
write_float(fh, 0.0)
write_float(fh, 0.0)
-- 回転
local qx, qy, qz, qw = mmd2quaternion(motion[i].x, motion[i].y, motion[i].z)
write_float(fh, qx)
write_float(fh, qy)
write_float(fh, qz)
write_float(fh, qw)
-- 補完
local j
for j = 0, 63 do
write_byte(fh, 0)
end
end
end
-- ここまで
-- スキン個数
write_long(fh, 0)
-- カメラ個数
write_long(fh, 0)
-- 照明個数
write_long(fh, 0)
-- セルフ影個数
write_long(fh, 0)
-- モデル表示個数
write_long(fh, 0)
fh:close()
rc = 1
end
return rc
end
function utf8_2_sjis(text)
local text_sjis = ""
local len_utf8 = utf8.len(text)
local i
for i = 1, len_utf8 do
local char_utf8 = utf8_sub(text, i, i)
local char_sjis = sjis_char[char_utf8]
if nil == char_sjis then
SV:showMessageBox(plugin_name, "ボーン名に変換できない文字がありました:'" .. char_utf8 .. "'")
else
text_sjis = text_sjis .. char_sjis
end
end
return text_sjis
end
-- string.subのutf8版
function utf8_sub(str, head, tail)
local head_offset = utf8.offset(str, head)
local tail_offset = utf8.offset(str, tail + 1) - 1
return string.sub(str, head_offset, tail_offset)
end
function write_long(fh, v)
fh:write(string.pack(fmt_long, v))
end
function write_float(fh, v)
fh:write(string.pack(fmt_float, v))
end
function write_byte(fh, v)
fh:write(string.pack(fmt_byte, v))
end
function write_string(fh, v, s)
fh:write(padding_0x00(v, s))
end
function padding_0x00(str, byte)
local pad = ""
local i
for i = 1, byte do
pad = pad .. "\0"
end
return string.sub(str .. pad, 1, byte)
end
function mmd2quaternion(mx, my, mz)
local ex = math.rad(mx)
local ey = math.rad(-my)
local ez = math.rad(-mz)
local hx = ex / 2
local hy = ey / 2
local hz = ez / 2
local qx = math.cos(hx) * math.sin(hy) * math.sin(hz) + math.sin(hx) * math.cos(hy) * math.cos(hz)
local qy = -math.sin(hx) * math.cos(hy) * math.sin(hz) + math.cos(hx) * math.sin(hy) * math.cos(hz)
local qz = math.cos(hx) * math.cos(hy) * math.sin(hz) - math.sin(hx) * math.sin(hy) * math.cos(hz)
local qw = math.sin(hx) * math.sin(hy) * math.sin(hz) + math.cos(hx) * math.cos(hy) * math.cos(hz)
return qx, qy, qz, qw
end