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

ScriptName = "AE_WaveInbetweener"

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

AE_WaveInbetweener = {}

function AE_WaveInbetweener:Name()
	return 'Wave Inbetweener'
end

function AE_WaveInbetweener:Version()
	return '1.0'
end

function AE_WaveInbetweener:UILabel()
	return 'Wave Inbetweener'
end

function AE_WaveInbetweener:Creator()
	return 'Alexandra Evseeva'
end

function AE_WaveInbetweener:Description()
	return ''
end

function AE_WaveInbetweener:ColorizeIcon()
	return true
end

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

function AE_WaveInbetweener:IsRelevant(moho)
	return true
end

function AE_WaveInbetweener:IsEnabled(moho)
	return true
end

AE_WaveInbetweener.fixStart = true

-- **************************************************
-- The guts of this script
-- **************************************************
function AE_WaveInbetweener:GetLongestChainChild(moho, skel, bone)
	if skel:CountBoneChildren(bone, true) == 0 then return nil, 0 end
	local foundChild, chainLength = nil, -1
	for b = 0, skel:CountBones() - 1 do
		local nextChild = skel:Bone(b)
		if nextChild.fParent == skel:BoneID(bone) then			
			local longestChild, longestLength = self:GetLongestChainChild(moho, skel, nextChild)
			if longestLength > chainLength then
				foundChild = nextChild
				chainLength = longestLength
			end
		end
	end

	AE_Utilities:Log(self.log, 'returning child '.. (foundChild and foundChild:Name() or 'nil')..' with '.. (chainLength+1)..' length chain\n')

	return foundChild, chainLength + 1
end

function AE_WaveInbetweener:DebugExtremums(moho, startMatrix, finMatrix)
	--log found extrems for debug purpoces
	for i = 0, #self.extremums do
		extr = self.extremums[i]
		if extr then
			local startPos = LM.Vector2:new_local()
			local finPos = LM.Vector2:new_local()
			if extr.start then
				startPos:Set(extr.start)
				if startMatrix then startMatrix:Transform(startPos) end
			end
			if extr.fin then 
				finPos:Set(extr.fin)
				if finMatrix then finMatrix:Transform(finPos) end	
			end
			AE_Utilities:Log(self.log, 'extremum no. ', i, ': start ', startPos.x, ', ', startPos.y, ' end: ', finPos.x, ', ', finPos.y, ' dir: ', extr.dir, 
			' boneIDs: start ', extr.startBoneID, ' end ', extr.finBoneID, '\n')
		end
	end
end

function AE_WaveInbetweener:Run(moho)
	moho.document:SetDirty()
	moho.document:PrepUndo(nil)
	--self.log = "P:/test/moho.log"
	AE_Utilities:Log(self.log, "starting\n")
	
	-- get selected bone and find its chain, find start and finish frames
	local skel = moho:Skeleton()
	if not skel or moho:CountSelectedBones()~=1 then return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Select bone layer and one root bone") end
	self.boneChain = {}
	local rootBone = skel:Bone(skel:SelectedBoneID())
	local nextBone = rootBone
	while nextBone do
		table.insert(self.boneChain, nextBone)
		AE_Utilities:Log(self.log, "ready to call for "..nextBone:Name().."\n")
		nextBone = self:GetLongestChainChild(moho, skel, nextBone)
		AE_Utilities:Log(self.log, "now nextBone is "..(nextBone and nextBone:Name() or 'nil').."\n")
	end 
	if #self.boneChain < 4 then return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Need chain of at least 4 bones, only " .. #self.boneChain .. " found") end
	local finBone = self.boneChain[#self.boneChain]
	local startID = rootBone.fAnimAngle:GetClosestKeyID(moho.layerFrame)
	local startFrame = rootBone.fAnimAngle:GetKeyWhen(startID)
	local finFrame = rootBone.fAnimAngle:GetKeyWhen(startID + 1)
	if startFrame < 1 or not finFrame or finFrame <= startFrame then return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Set current frame between two keys of selected bone rotation") end

	-- create line matrix
	local startA = AE_Utilities:GetGlobalBonePos(moho, skel, rootBone, startFrame)
	local startB = LM.Vector2:new_local()
	startB:Set(finBone.fLength, 0)
	local startBoneMatrix = AE_Utilities:GetGlobalBoneMatrix(moho, skel, finBone, startFrame)
	startBoneMatrix:Transform(startB)
	local finA = AE_Utilities:GetGlobalBonePos(moho, skel, rootBone, finFrame)
	local finB = LM.Vector2:new_local()
	finB:Set(finBone.fLength, 0)
	local finBoneMatrix = AE_Utilities:GetGlobalBoneMatrix(moho, skel, finBone, finFrame)
	finBoneMatrix:Transform(finB)	
	
	local startMatrix = LM.Matrix:new_local()
	startMatrix:Identity()
	startMatrix:Translate(startA.x, startA.y, 0)
	local startAB = startB - startA
	startMatrix:Rotate(LM.Z_AXIS, math.atan2(startAB.y, startAB.x))
	startMatrix:Scale(startAB:Mag(), 1, 1)
	local inverseStartMatrix = LM.Matrix:new_local()
	inverseStartMatrix:Set(startMatrix)
	inverseStartMatrix:Invert()
	
	local finMatrix = LM.Matrix:new_local()
	finMatrix:Identity()
	finMatrix:Translate(finA.x, finA.y, 0)
	local finAB = finB - finA
	finMatrix:Rotate(LM.Z_AXIS, math.atan2(finAB.y, finAB.x))
	finMatrix:Scale(finAB:Mag(), 1, 1)	
	local inverseFinMatrix = LM.Matrix:new_local()
	inverseFinMatrix:Set(finMatrix)
	inverseFinMatrix:Invert()	
	AE_Utilities:Log(self.log, "matrices created\n")
	-- for frame start: get each chain projection, find max abs Y ones (from start to end) and put them into table
	
	self.extremums = {}
	local prePos = LM.Vector2:new_local()
	local curDir = 0
	for i, bone in pairs(self.boneChain) do
		if bone == rootBone then
			local nextPos = LM.Vector2:new_local()
			table.insert(self.extremums, {start=nextPos, dir=curDir, startBoneID=1})
		else
			local pos = AE_Utilities:GetGlobalBonePos(moho, skel, bone, startFrame)
			inverseStartMatrix:Transform(pos)
			local distY = pos.y - prePos.y
			if curDir ~= 0 then
				if (distY < 0 and curDir > 0) or (distY > 0 and curDir < 0) then
					local nextPos = LM.Vector2:new_local()
					nextPos:Set(prePos)
					table.insert(self.extremums, {start=nextPos, dir=curDir, startBoneID=(i-1)})
				end
			end		
			prePos:Set(pos)
			curDir = (distY > 0) and 1 or -1
			if self.extremums[1].dir == 0 then self.extremums[1].dir = - curDir end
			if i == #self.boneChain then
				local nextPos = LM.Vector2:new_local()
				nextPos:Set(1,0)
				table.insert(self.extremums, {start=nextPos, dir=curDir, startBoneID=i+1})
			end
		end
	end
	self.numExtremums = #self.extremums
	
	AE_Utilities:Log(self.log, "start extrems found\n")
	-- for frame end: find max abs Y projections and put them to the same table (every finish must be greater x then start)
	
	prePos:Set(0,0)
	curDir = 0	
	local lastFoundIndex = -1
	for i, bone in pairs(self.boneChain) do
		if bone == rootBone then
			local nextPos = LM.Vector2:new_local()
			self.extremums[0] = {fin=nextPos, dir=curDir, finBoneID=1}
		else
			local pos = AE_Utilities:GetGlobalBonePos(moho, skel, bone, finFrame)
			inverseFinMatrix:Transform(pos)
			if i == #self.boneChain then pos:Set(1,0) end
			local distY = pos.y - prePos.y
			if curDir ~= 0 then
				if (distY < 0 and curDir > 0) or (distY > 0 and curDir < 0) then
					-- find in self.extremums a suitable member: the last with x < prePos.x and dir==curDir
					local foundIndex = 0
					for j=0, #self.extremums do
						local extr = self.extremums[j]
						if extr and extr.start then
							if extr.start.x <= prePos.x and extr.dir == curDir then
								foundIndex = j
							end
							if extr.start.x > prePos.x then break end
						end
					end
					local nextPos = LM.Vector2:new_local()
					nextPos:Set(prePos)
					AE_Utilities:Log(self.log, "found index ", foundIndex, " for ", bone:Name(), "\n")
					if foundIndex == 0 and not self.extremums[0] then 
						self.extremums[0] = {fin=nextPos, dir=curDir, startBoneID=1, finBoneID=i}
					else
						if lastFoundIndex > 0 and (foundIndex - lastFoundIndex) > 1 then foundIndex = lastFoundIndex + 1 end
						while self.extremums[foundIndex].fin do foundIndex = foundIndex + 1 end
						AE_Utilities:Log(self.log, "now found index is ", foundIndex, "\n")
						self.extremums[foundIndex].fin = nextPos
						self.extremums[foundIndex].finBoneID = i
					end	
					lastFoundIndex = foundIndex
				end
			end
			if i == #self.boneChain then
				local nextPos = LM.Vector2:new_local()
				nextPos:Set(1,0)	
				for j=0, #self.extremums do
					local extr = self.extremums[j]
					if extr then
						if not extr.fin then
							extr.fin = nextPos
							extr.finBoneID = i
							break
						end
					end
				end
			end
			prePos:Set(pos)
			curDir = (distY > 0) and 1 or -1	
			if self.extremums[0].dir == 0 then self.extremums[0].dir = - curDir end
		end
	end
	AE_Utilities:Log(self.log, "fin extrems found\n")
	
	self:DebugExtremums(moho)
	AE_Utilities:Log(self.log, "\n")	
	--  set extra start and end if needed
	
	if self.extremums[0] then
		self.numExtremums = self.numExtremums + 1
		if not self.extremums[0].start then 
			local extraStart = LM.Vector2:new_local()
			extraStart:Set(-self.extremums[2].start.x, - self.extremums[2].start.y)
			self.extremums[0].start = extraStart
			self.extremums[0].startBoneID = - self.extremums[1].startBoneID
		end
	end

	if not self.extremums[#self.extremums].fin then 
		local extraFin = LM.Vector2:new_local()
		extraFin:Set( 2 - self.extremums[#self.extremums-2].fin.x, - self.extremums[#self.extremums - 2].fin.y)
		self.extremums[#self.extremums].fin = extraFin
		self.extremums[#self.extremums].finBoneID = #self.boneChain + #self.boneChain - self.extremums[#self.extremums - 1].finBoneID
	end
	AE_Utilities:Log(self.log, "extra extrems found\n")
	
	self:DebugExtremums(moho)
	AE_Utilities:Log(self.log, "\n")
	self:DebugExtremums(moho, startMatrix, finMatrix)
	
	
	--TODO: create mesh and a curve for start extremums
	--local preview = MOHO.MeshPreview(200, 200)
	local currentLayer = moho.layer
	local newLayer = moho:CreateNewLayer(MOHO.LT_VECTOR, true)
	newLayer:SetTimingOffset(currentLayer:TotalTimingOffset())
	newLayer = moho:LayerAsVector(newLayer)
	local previewMesh = newLayer:Mesh()
	
	local p = 0
	for i = 0, #self.extremums do
		extr = self.extremums[i]
		if extr then
			local startPos = LM.Vector2:new_local()
			startPos:Set(extr.start)
			startMatrix:Transform(startPos)
			if p == 0 then 
				previewMesh:AddLonePoint(startPos, 0)
			else 
				previewMesh:AppendPoint(startPos,0)
			end
			p = p + 1
		end
	end
	local curve = previewMesh:Curve(0)
	local interp = MOHO.InterpSetting:new_local()
	interp:Reset()
	interp.interpMode = MOHO.INTERP_LINEAR
	p = 0
	for i = 0, #self.extremums do
		extr = self.extremums[i]
		if extr then
			local point = curve:Point(p)
			point.fAnimPos:AddKey(startFrame)
			point.fAnimPos:SetKeyInterp(startFrame, interp)
			local finPos = LM.Vector2:new_local()
			finPos:Set(extr.fin)
			finMatrix:Transform(finPos)
			point.fAnimPos:SetValue(finFrame, finPos)
			p = p + 1
		end
	end
	
	--TODO: Find bezier curvature size for each extremum 
	
	
	-- find segment borders for start and finish frames (part of curve filled with real bones)
	local startSegmentA, startSegmentB, finSegmentA, finSegmentB = 1, #self.extremums, 0, #self.extremums - 1
	for j = 0, #self.extremums do
		local extr = self.extremums[j]
		if extr then
			local i = j
			if not self.extremums[0] then i = j - 1 end
			if extr.start.x == 0 then startSegmentA = i end
			if extr.start.x == 1 then startSegmentB = i end
			if extr.fin.x == 0 then finSegmentA = i end
			if extr.fin.x == 1 then finSegmentB = i end		
		end
	end
	
	AE_Utilities:Log(self.log, "found segments from ", startSegmentA, " to ", startSegmentB, " and from ", finSegmentA, " to ", finSegmentB, " \n")

	local returnFrame = moho.frame
	local boneAngles = {}
	for i,bone in pairs(self.boneChain) do table.insert(boneAngles,{}) end
	for f = startFrame, finFrame do
		moho:SetCurFrame(f)
		-- get a part of curve
		local k = (f - startFrame)/(finFrame - startFrame)
		local segmentStart = startSegmentA + k * (finSegmentA - startSegmentA)
		local segmentFin = startSegmentB + k * (finSegmentB - startSegmentB)
		-- get start and finish of curve segment
		local percentStart, fake1, fake2, percentFin = 0, 0, 0, 1
		percentStart, fake1 = curve:GetSegmentRange(math.floor(segmentStart), percentStart, fake1 )
		percentStart = percentStart + (fake1 - percentStart) * (segmentStart - math.floor(segmentStart))
		local endSegment = math.floor(segmentFin)
		local endSegmentCrop = segmentFin - math.floor(segmentFin)
		if endSegment >= curve:CountSegments() then 
			endSegment = curve:CountSegments() - 1 
			if endSegmentCrop == 0 then endSegmentCrop = 1 end
		end
		fake2, percentFin = curve:GetSegmentRange(endSegment, fake2, percentFin )
		percentFin = fake2 + (percentFin - fake2) * endSegmentCrop
		-- get total length of bones (multiplied with scales)
		local boneTotalLength = 0
		for i, bone in pairs(self.boneChain) do
			boneTotalLength = boneTotalLength + bone.fLength * bone.fScale
		end
		local boneCurrentLength = 0
		for i, bone in pairs(self.boneChain) do
			-- find start end end positions for every bone
			local boneLength = bone.fScale * bone.fLength
			local startBonePercent = boneCurrentLength
			local finBonePercent = boneCurrentLength + (boneLength/boneTotalLength)
			local startPos = curve:GetPercentLocation(percentStart + startBonePercent * (percentFin - percentStart))
			local finPos = curve:GetPercentLocation(percentStart + finBonePercent * (percentFin - percentStart))
			-- find global angle 
			local boneVector = finPos - startPos
			local globalAngle = math.atan2(boneVector.y, boneVector.x)
			-- convert global angle to bone local angle, then apply new value and update bone matrix
			local pos, angle = AE_Utilities:GetGlobalBonePRS(moho, skel, bone, moho.layerFrame)
			local angleDif = globalAngle - angle
			angleDif = AE_Utilities:CropAngle(angleDif)
			local newAngle = bone.fAnimAngle:GetValue(moho.layerFrame) + angleDif
			if f ~= startFrame and f ~= finFrame then
				bone.fAnimAngle:SetValue(moho.layerFrame, newAngle )
				skel:UpdateBoneMatrix(skel:BoneID(bone))
			end
			boneCurrentLength = finBonePercent
			boneAngles[i][f]= newAngle
		end
	end
	
	--TODO: cleanup rotation keyframes (try various methods)
	--TODO: and also remove strong changes near the first and last frames (have to calculate (but not apply) angles for first and last frames for it)
	-- now turned off because does not work as expected
	-- the only valid way to reduce keys is to set interpolation of extremly valued keys to follow existing motion curve  
	
	for i, bone in pairs(self.boneChain) do
		local keys2delete = {}
		for f = startFrame + 1, finFrame - 1 do
			local average = (boneAngles[i][f-1] + boneAngles[i][f+1])/2
			if math.abs(boneAngles[i][f] - average) < 0.001 then table.insert(keys2delete, f) end
		end
		for j, f in pairs(keys2delete) do
			--bone.fAnimAngle:DeleteKey(f)
		end
	end
	
	self.extremums = nil
	self.boneChain = nil
	moho:DeleteLayer(newLayer)
	moho:SetSelLayer(currentLayer)
	moho:SetCurFrame(returnFrame)
	AE_Utilities:Log(self.log, "now finish\n")
end

Wave Inbetweener
Listed

Script type: Button/Menu

Uploaded: Apr 19 2022, 00:03

Calculates frames between two keys of bone chain wavely motion
Selet a root bone of the chain and place current frame between two (rotation) keys -- start and end frames of motion segment. Keys must exist both in start and end frames for every bone of the chain.

Installation Options:

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: 659