Image
-- **************************************************
-- Provide Moho with the name of this script object
-- **************************************************

ScriptName = "MR_BakeBoneDynamics"

-- **************************************************
-- General information about this script
-- **************************************************

MR_BakeBoneDynamics = {}

function MR_BakeBoneDynamics:Name()
	return self:Localize('UILabel')
end

function MR_BakeBoneDynamics:Version()
	return '1.0'
end

function MR_BakeBoneDynamics:UILabel()
	return self:Localize('UILabel')
end

function MR_BakeBoneDynamics:Creator()
	return 'Eugene Babich'
end

function MR_BakeBoneDynamics:Description()
	return self:Localize('Description')
end

-- **************************************************
-- Is Relevant / Is Enabled
-- **************************************************

function MR_BakeBoneDynamics:IsRelevant(moho)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return false
	end
	return true
end

function MR_BakeBoneDynamics:IsEnabled(moho)
	local skel = moho:Skeleton()
	if (moho:CountBones() < 1) then
		return false
	end
	return true
end

-- **************************************************
-- Recurring Values
-- **************************************************

MR_BakeBoneDynamics.from = 1
MR_BakeBoneDynamics.to = 50
MR_BakeBoneDynamics.interval = 1
MR_BakeBoneDynamics.preroll = 10
MR_BakeBoneDynamics.isMouseDragging = false
MR_BakeBoneDynamics.addDynamicsKeys = true
MR_BakeBoneDynamics.selRect = LM.Rect:new_local()

-- **************************************************
-- Prefs
-- **************************************************

function MR_BakeBoneDynamics:LoadPrefs(prefs)
	self.from = prefs:GetInt("MR_BakeBoneDynamics.from", 1)
	self.to = prefs:GetInt("MR_BakeBoneDynamics.to", 50)
	self.interval = prefs:GetInt("MR_BakeBoneDynamics.interval", 1)
	self.preroll = prefs:GetInt("MR_BakeBoneDynamics.preroll", 10)
	self.addDynamicsKeys = prefs:GetBool("MR_BakeBoneDynamics.addDynamicsKeys", true)
end

function MR_BakeBoneDynamics:SavePrefs(prefs)
	prefs:SetInt("MR_BakeBoneDynamics.from", self.from)
	prefs:SetInt("MR_BakeBoneDynamics.to", self.to)
	prefs:SetInt("MR_BakeBoneDynamics.interval", self.interval)
	prefs:SetInt("MR_BakeBoneDynamics.preroll", self.preroll)
	prefs:SetBool("MR_BakeBoneDynamics.addDynamicsKeys", self.addDynamicsKeys)
end

function MR_BakeBoneDynamics:ResetPrefs()
	self.from = 1
	self.to = 50
	self.interval = 1
	self.preroll = 10
	self.addDynamicsKeys = true
end

-- **************************************************
-- Keyboard/Mouse Control
-- **************************************************

function MR_BakeBoneDynamics:OnMouseDown(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	
	self.isMouseDragging = true
	if not mouseEvent.shiftKey and not mouseEvent.altKey and skel ~= nil then
		skel:SelectNone()
	end
	
	self.selRect.left = mouseEvent.startPt.x
	self.selRect.top = mouseEvent.startPt.y
	self.selRect.right = mouseEvent.pt.x
	self.selRect.bottom = mouseEvent.pt.y
	mouseEvent.view:Graphics():SelectionRect(self.selRect)
	mouseEvent.view:DrawMe()
	
end

function MR_BakeBoneDynamics:OnMouseMoved(moho, mouseEvent)
	local skel = moho:Skeleton()

	if (skel == nil) then
		return
	end

	mouseEvent.view:Graphics():SelectionRect(self.selRect)
	self.selRect.right = mouseEvent.pt.x
	self.selRect.bottom = mouseEvent.pt.y
	mouseEvent.view:Graphics():SelectionRect(self.selRect)
	mouseEvent.view:RefreshView()
	mouseEvent.view:DrawMe()
end

function MR_BakeBoneDynamics:OnMouseUp(moho, mouseEvent)
	local skel = moho:Skeleton()
	local mouseDist = math.abs(mouseEvent.pt.x - mouseEvent.startPt.x) + math.abs(mouseEvent.pt.y - mouseEvent.startPt.y)

	local id = mouseEvent.view:PickBone(mouseEvent.pt, mouseEvent.vec, moho.layer, true)
	if not mouseEvent.shiftKey and not mouseEvent.altKey then
		skel:SelectNone()
	end
	
	if id ~= -1 then
		skel:Bone(id).fSelected = not mouseEvent.altKey
	end

	self.isMouseDragging = false

	local v = LM.Vector2:new_local()
	local screenPt = LM.Point:new_local()
	local m = LM.Matrix:new_local()

	self.selRect:Normalize()
	moho.layer:GetFullTransform(moho.frame, m, moho.document)
	if	moho.layer:LayerType() == MOHO.LT_BONE then
		if (skel ~= nil) then
			for i = 0, skel:CountBones() - 1 do
				local bone = skel:Bone(i)
				local boneMatrix = bone.fMovedMatrix
				for j = 0, 10 do
					v:Set(bone.fLength * j / 10.0, 0)
					boneMatrix:Transform(v)
					m:Transform(v)
					mouseEvent.view:Graphics():WorldToScreen(v, screenPt)
					if (self.selRect:Contains(screenPt)) then
						if (mouseEvent.altKey) then
							bone.fSelected = false
						else
							bone.fSelected = true
						end
						break
					end
				end
			end
		end
	end
	moho:UpdateSelectedChannels()
end

function MR_BakeBoneDynamics:DrawMe(moho, view)
	if self.isMouseDragging then
		local g = view:Graphics()
		g:SelectionRect(self.selRect)
	end
end

-- **************************************************
-- Tool Panel Layout
-- **************************************************

MR_BakeBoneDynamics.SELECT_DYNAMIC_BONES = MOHO.MSG_BASE + 1
MR_BakeBoneDynamics.PREROLL = MOHO.MSG_BASE + 2
MR_BakeBoneDynamics.FROM = MOHO.MSG_BASE + 3
MR_BakeBoneDynamics.TO = MOHO.MSG_BASE + 4
MR_BakeBoneDynamics.INTERVAL = MOHO.MSG_BASE + 5
MR_BakeBoneDynamics.INTERVAL_1 = MOHO.MSG_BASE + 6
MR_BakeBoneDynamics.INTERVAL_2 = MOHO.MSG_BASE + 7
MR_BakeBoneDynamics.INTERVAL_3 = MOHO.MSG_BASE + 8
MR_BakeBoneDynamics.INTERVAL_4 = MOHO.MSG_BASE + 9
MR_BakeBoneDynamics.SET_PROJECT_RANGE = MOHO.MSG_BASE + 10
MR_BakeBoneDynamics.SET_PLAYBACK_RANGE = MOHO.MSG_BASE + 11
MR_BakeBoneDynamics.ADD_DYNAMICS_KEYS = MOHO.MSG_BASE + 12
MR_BakeBoneDynamics.BAKE = MOHO.MSG_BASE + 13

function MR_BakeBoneDynamics:DoLayout(moho, layout)
	self.selectDynamicsBonesButton = LM.GUI.Button(self:Localize('Select Dynamic Bones'), self.SELECT_DYNAMIC_BONES)
	layout:AddChild(self.selectDynamicsBonesButton, LM.GUI.ALIGN_LEFT, 0)

	self.preRollInput = LM.GUI.TextControl(0, '10000', self.PREROLL, LM.GUI.FIELD_INT, self:Localize('Pre Roll:'))
	layout:AddChild(self.preRollInput, LM.GUI.ALIGN_LEFT, 0)
	
	self.fromInput = LM.GUI.TextControl(0, '10000', self.FROM, LM.GUI.FIELD_INT, self:Localize('From:'))
	layout:AddChild(self.fromInput, LM.GUI.ALIGN_LEFT, 0)

	self.toInput = LM.GUI.TextControl(0, '10000', self.TO, LM.GUI.FIELD_INT, self:Localize('To:'))
	layout:AddChild(self.toInput, LM.GUI.ALIGN_LEFT, 0)
	
	self.setProjectRangeButton = LM.GUI.ImageButton("ScriptResources/mr_bake_bone_dynamics/mr_set_project_range",self:Localize('Set Project Range'),false, self.SET_PROJECT_RANGE, false)
	layout:AddChild(self.setProjectRangeButton, LM.GUI.ALIGN_LEFT, 0)
	self.setPlaybackRangeButton = LM.GUI.ImageButton("ScriptResources/mr_bake_bone_dynamics/mr_set_playback_range",self:Localize('Set Playback Range'),false, self.SET_PLAYBACK_RANGE, false)
	layout:AddChild(self.setPlaybackRangeButton, LM.GUI.ALIGN_LEFT, 0)
	
	layout:AddChild(LM.GUI.StaticText(self:Localize("IntervalText")))

	self.intervalMenu = LM.GUI.Menu(MOHO.Localize("Interval=Interval"))
	self.intervalMenu:AddItem(MOHO.Localize("1=1"), 0, self.INTERVAL_1)
	self.intervalMenu:AddItemAlphabetically(MOHO.Localize("2=2"), 0, self.INTERVAL_2)
	self.intervalMenu:AddItemAlphabetically(MOHO.Localize("3=3"), 0, self.INTERVAL_3)
	self.intervalMenu:AddItemAlphabetically(MOHO.Localize("4=4"), 0, self.INTERVAL_4)

	self.intervalPopup = LM.GUI.PopupMenu(50, true)
	self.intervalPopup:SetMenu(self.intervalMenu)
	layout:AddChild(self.intervalPopup)
	
	self.addDynamicsKeysCheckbox = LM.GUI.CheckBox(self:Localize('Add Dynamics Keys'), self.ADD_DYNAMICS_KEYS)
	self.addDynamicsKeysCheckbox:SetToolTip(self:Localize('Add Dynamics Keys Tooltip'))
    layout:AddChild(self.addDynamicsKeysCheckbox, LM.GUI.ALIGN_LEFT, 0)

	self.bakeBoneDynamicsButton = LM.GUI.Button(self:Localize('Bake'), self.BAKE)
	layout:AddChild(self.bakeBoneDynamicsButton, LM.GUI.ALIGN_LEFT, 0)
end

function MR_BakeBoneDynamics:UpdateWidgets(moho)
	self.preRollInput:SetValue(self.preroll)
	self.fromInput:SetValue(self.from)
	self.toInput:SetValue(self.to)
	
	if self.interval < 1 or self.interval > 4 or self.interval == nil then
		self.interval = 1
	end
	
	if (self.interval == 1) then
		self.intervalMenu:SetChecked(self.INTERVAL_1, true)
	elseif (self.interval == 2) then
		self.intervalMenu:SetChecked(self.INTERVAL_2, true)
	elseif (self.interval == 3) then
		self.intervalMenu:SetChecked(self.INTERVAL_3, true)
	elseif (self.interval == 4) then
		self.intervalMenu:SetChecked(self.INTERVAL_4, true)
	end

	self.intervalPopup:Redraw()
	self.addDynamicsKeysCheckbox:SetValue(self.addDynamicsKeys)
end

function MR_BakeBoneDynamics:HandleMessage(moho, view, msg)
	if msg == self.SELECT_DYNAMIC_BONES then
		self:SelectDynamicsBones(moho)
	elseif msg == self.PREROLL then
		self.preroll = LM.Clamp(self.preRollInput:IntValue(), 0, 5000)
		self.preRollInput:SetValue(self.preroll)
	elseif msg == self.FROM then
		self.from = self.fromInput:IntValue()
		if self.from < 1 then
			self.from = 1
			self.fromInput:SetValue(self.from)
		end	
		if self.to < self.from then
			self.to = self.from
			self.toInput:SetValue(self.to)
		end
	elseif msg == self.TO then
		self.to = self.toInput:IntValue()
		if self.to < self.from then
			if self.to < 1 then
				self.to = 1
				self.toInput:SetValue(self.to)
			end
			self.from = self.to
			self.fromInput:SetValue(self.from)
		end
	elseif msg == self.SET_PROJECT_RANGE then
		self.from = moho.document:StartFrame()
		self.to = moho.document:EndFrame()
		self.toInput:SetValue(self.to)
		self.fromInput:SetValue(self.from)
	elseif msg == self.SET_PLAYBACK_RANGE then
		local startP = MOHO.MohoGlobals.PlayStart
		local endP = MOHO.MohoGlobals.PlayEnd
		if startP > 0 then
			self.from = startP
		end
		if endP > 0 then
			self.to = endP
		end
		self.toInput:SetValue(self.to)
		self.fromInput:SetValue(self.from)
	elseif (msg >= self.INTERVAL_1 and msg <= self.INTERVAL_4) then
		local int = 1
		if (msg == self.INTERVAL_1) then
			int = 1
		elseif (msg == self.INTERVAL_2) then
			int = 2
		elseif (msg == self.INTERVAL_3) then
			int = 3
		elseif (msg == self.INTERVAL_4) then
			int = 4
		end	
		self.interval = int
		self.isReady = false
		self:UpdateWidgets(moho)
	elseif msg == self.ADD_DYNAMICS_KEYS then
		self.addDynamicsKeys = self.addDynamicsKeysCheckbox:Value()
	elseif msg == self.BAKE then
		self:Bake(moho)
	end
end

function MR_BakeBoneDynamics:Bake(moho)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	
	if moho:CountSelectedBones() < 1 then
		return
	end
	
	moho.document:PrepUndo(moho.layer, true)
	moho.document:SetDirty()
	
	local isSoundtrackMuted = false
	local isDynamicsOn = false
	if not MOHO.MohoGlobals.MuteSoundtrack then
		isSoundtrackMuted = true
		MOHO.MohoGlobals.MuteSoundtrack = true
	end
	if MOHO.MohoGlobals.EnableBoneDynamics then
		isDynamicsOn = true
	end
	
	local preroll = self.preroll
	if preroll >= self.from then
		preroll = self.from - 1
	end
	local targetFrame = self.from - preroll
	local nextIntervalFrame = self.from
	local curFrame = moho.frame
	
	local frameList = {}
	frameList.frame = {}
	local bonesList = {}
	
	for i=0, skel:CountBones()-1 do
		local bone = skel:Bone(i)
		if bone.fSelected then
			local dynamicsChannel = moho:ChannelAsAnimBool(bone.fBoneDynamics)
			if dynamicsChannel:CountKeys() > 1 or dynamicsChannel:GetValue(0) then
				table.insert(bonesList, i)
			end	
		end
	end
	
	if #bonesList < 1 then
		return
	end
	
	MOHO.MohoGlobals.EnableBoneDynamics = true
	
	repeat
		moho:SetCurFrame(targetFrame)
		if targetFrame == nextIntervalFrame then
			local bonesValuesList = {}
			bonesValuesList.boneID = {}
			bonesValuesList.angle = {}
			for i, id in pairs(bonesList) do
				table.insert(bonesValuesList.boneID, id)
				local bone = skel:Bone(id)
				table.insert(bonesValuesList.angle, bone.fAngle)
			end
			table.insert(frameList, bonesValuesList)
			table.insert(frameList.frame, targetFrame)
			nextIntervalFrame = targetFrame + self.interval
		end
		targetFrame = targetFrame + 1
	until targetFrame >= self.to
	
	if frameList.frame[#frameList.frame] < self.to then
		moho:SetCurFrame(self.to)
		local bonesValuesList = {}
		bonesValuesList.boneID = {}
		bonesValuesList.angle = {}
		for i, id in pairs(bonesList) do
			table.insert(bonesValuesList.boneID, id)
			local bone = skel:Bone(id)
			table.insert(bonesValuesList.angle, bone.fAngle)
		end
		table.insert(frameList, bonesValuesList)
		table.insert(frameList.frame, self.to)
	end

	local keyInterp = MOHO.InterpSetting:new_local()
	keyInterp.interval = self.interval
	
	for i, id in pairs(bonesList) do
		local bone = skel:Bone(id)
		local dynamicsChannel = moho:ChannelAsAnimBool(bone.fBoneDynamics)
		local angleChannel = moho:ChannelAsAnimVal(bone.fAnimAngle)
		if self.addDynamicsKeys then
			dynamicsChannel:SetValue(self.to +1, dynamicsChannel:GetValue(self.to +1))
			dynamicsChannel:SetValue(self.from, false)
		end
		MOHO.MohoGlobals.EnableBoneDynamics = false
		local angleKeysToDeleteList = {}
		
		if self.addDynamicsKeys then
			local dynamicsKeysToDeleteList = {}
			for keyID = 0, dynamicsChannel:CountKeys() - 1 do
				local channelFrame = dynamicsChannel:GetKeyWhen(keyID)
				if (channelFrame > self.from and channelFrame <= self.to) then
					table.insert(dynamicsKeysToDeleteList, channelFrame)
				end
			end	
			for c, frame in pairs(dynamicsKeysToDeleteList) do
				dynamicsChannel:DeleteKey(frame)
			end
		end
		
		for keyID = 0, angleChannel:CountKeys() - 1 do
			local channelFrame = angleChannel:GetKeyWhen(keyID)
			if (channelFrame > self.from and channelFrame < self.to) then
				table.insert(angleKeysToDeleteList, channelFrame)
			end
		end	
		
		for a, frame in pairs(angleKeysToDeleteList) do
			angleChannel:DeleteKey(frame)
		end
	end
	
	for p=1, #frameList do
		for i=1, #frameList[p].boneID do
			local bone = skel:Bone(frameList[p].boneID[i])
			local angleChannel = moho:ChannelAsAnimVal(bone.fAnimAngle)
			angleChannel:SetValue(frameList.frame[p], frameList[p].angle[i])
			angleChannel:SetKeyInterp(frameList.frame[p], keyInterp)
		end
	end
	
	if isSoundtrackMuted then
		MOHO.MohoGlobals.MuteSoundtrack = false
	end
	
	MOHO.MohoGlobals.EnableBoneDynamics = isDynamicsOn
		
	moho:SetCurFrame(curFrame)
	self:UpdateWidgets(moho)
	moho:UpdateSelectedChannels()
	moho.view:DrawMe()
	moho:UpdateUI()
	moho.layer:UpdateCurFrame()
end

function MR_BakeBoneDynamics:SelectDynamicsBones(moho)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	skel:SelectNone()
	for i=0, skel:CountBones()-1 do
		local bone = skel:Bone(i)
		local dynamicsChannel = moho:ChannelAsAnimBool(bone.fBoneDynamics)
		if dynamicsChannel:CountKeys() > 1 or dynamicsChannel:GetValue(0) then
			bone.fSelected = true
		end	
	end
end

-- **************************************************
-- Localization
-- **************************************************

function MR_BakeBoneDynamics:Localize(text)
	local phrase = {}

	phrase['Description'] = 'This script allows you to bake the movement of the bones created by bone dynamics'
	phrase['UILabel'] = 'Bake Bone Dynamics'

	phrase['Select Dynamic Bones'] = 'Select dynamic bones'
	phrase['Pre Roll:'] = 'Preroll:'
	phrase['From:'] = 'From:'
	phrase['To:'] = 'To:'
	phrase['Set Project Range'] = 'Set project range'
	phrase['Set Playback Range'] = 'Set playback range'
	phrase['IntervalText'] = 'Interval:'
	phrase['Add Dynamics Keys'] = 'Add dynamics keys'
	phrase['Add Dynamics Keys Tooltip'] = 'Turn off dynamics for baked bones in selected range'
	phrase['Bake'] = 'Bake'

	return phrase[text]
end

Icon
MR Bake Bone Dynamics
Listed

Script type: Tool

Uploaded: Jan 29 2023, 19:40

Script Version: 1.0

This script allows you to bake the movement of the bones created by bone dynamics.
This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 1376