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

ScriptName = "LK_Render"

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

LK_Render = {}

function LK_Render:ColorizeIcon()
	return true
end

function LK_Render:Name()
	return "Render Current Shot"
end

function LK_Render:Version()
	return "0.3"
end

function LK_Render:Description()
	return "Render Current Shot"
end

function LK_Render:Creator()
	return "Lukas Krepel, Frame Order"
end

function LK_Render:UILabel()
	return "Render Current Shot"
end

function LK_Render:IsRelevant()
	if MohoMode ~= nil then
		return MohoMode.animation
	end
	return true
end

function LK_Render:IsEnabled()
	return true
end

LK_Render.machine = "locally" -- * "locally" or "gestion"
LK_Render.shotName = ""
LK_Render.start = 1
LK_Render.startTXT = ""
LK_Render.stop = 1
LK_Render.stopTXT = ""
LK_Render.range = 1
LK_Render.split = 0
LK_Render.output = ""
LK_Render.format = "png"
LK_Render.halfResolution = false
LK_Render.singleThread = true
LK_Render.hasComps = false
LK_Render.comps = {}

LK_Render.rendersDirectory = ""
LK_Render.lookForDirectory = "Shots"
LK_Render.createDirectory = "Renders"

-- **************************************************
-- Render Options Dialog
-- **************************************************

local LK_RenderOptionsDialog = {}

LK_RenderOptionsDialog.UPDATE =					MOHO.MSG_BASE
LK_RenderOptionsDialog.LOCAL =					MOHO.MSG_BASE + 1
LK_RenderOptionsDialog.GESTION =				MOHO.MSG_BASE + 2
LK_RenderOptionsDialog.TOGGLE_HALF_RES =		MOHO.MSG_BASE + 3
LK_RenderOptionsDialog.TOGGLE_SINGLETHREAD =	MOHO.MSG_BASE + 4
LK_RenderOptionsDialog.GO_TO_DIRECTORY =		MOHO.MSG_BASE + 5

function LK_RenderOptionsDialog:new(moho)
	local d = LM.GUI.SimpleDialog("Render", LK_RenderOptionsDialog)
	local l = d:GetLayout()
	d.moho = moho

	-- d.docPath = d.moho.document:Path()

	l:Indent()
		l:AddChild(LM.GUI.StaticText("Choose Renderer:"), LM.GUI.ALIGN_LEFT)
		d.gestion = LM.GUI.RadioButton("Add to Gestion queue", self.GESTION)
		-- * Gestion is Frame Order only:
		if LK_RenderStatus ~= nil then
			l:AddChild(d.gestion, LM.GUI.ALIGN_LEFT)
		else
			LK_Render.machine = "locally"
		end
		d.locally = LM.GUI.RadioButton("Local Moho", self.LOCAL)
		l:AddChild(d.locally, LM.GUI.ALIGN_LEFT)
	l:Unindent()

	l:Indent()
		l:AddChild(LM.GUI.StaticText("Options:"), LM.GUI.ALIGN_LEFT)
		d.halfRes = LM.GUI.CheckBox("Half resolution ("..(moho.document:Width()/2).." ✕ "..(moho.document:Height()/2)..") (faster)", self.TOGGLE_HALF_RES)
		l:AddChild(d.halfRes, LM.GUI.ALIGN_LEFT)
		d.singleThreadCheckbox = LM.GUI.CheckBox("Don't multithread (slower)", self.TOGGLE_SINGLETHREAD)
		l:AddChild(d.singleThreadCheckbox, LM.GUI.ALIGN_LEFT)
	l:Unindent()

	l:PushH(LM.GUI.ALIGN_CENTER, 0)
		d.destination = LM.GUI.DynamicText(LK_Render.msg1, 800)
		l:AddChild(d.destination, LM.GUI.ALIGN_LEFT)
		local buttonLabel = "Reveal in Finder"
		if FO_Utilities:getOS() == "win" then
			buttonLabel = "Open location in Explorer"
		end
		d.openDestinationDirectoryButton = LM.GUI.Button(buttonLabel, self.GO_TO_DIRECTORY)
		l:AddChild(d.openDestinationDirectoryButton)
	l:Pop()

	d.compWidgets = {}
	if LK_Render.hasComps then
		for i = 1, #LK_Render.compNames do
			local compName = LK_Render.compNames[i]
			local compDestination = "Layercomp: \'"..LK_Render.sequenceDirectory.."/"..compName.."/"..LK_Render.shotName.."-"..compName.."_"..string.format("%05d", LK_Render.start).."."..LK_Render.format.."\'"
			d.compDestination = LM.GUI.DynamicText(compDestination, 800)
			l:AddChild(d.compDestination, LM.GUI.ALIGN_LEFT)
			table.insert(d.compWidgets, d.compDestination)
		end
	end

	d.frames = LM.GUI.DynamicText(LK_Render.msg2, 800)
	l:AddChild(d.frames, LM.GUI.ALIGN_LEFT)

	d.locally:SetValue(LK_Render.machine == "locally")
	d.gestion:SetValue(LK_Render.machine == "gestion")

	if (LK_Render.machine == "gestion") then
		LK_Render.halfResolution = false
	end
	d.halfRes:SetValue(LK_Render.halfResolution)
	d.singleThreadCheckbox:SetValue(LK_Render.singleThread)

	d.halfRes:Enable(LK_Render.machine == "locally")
	d.singleThreadCheckbox:Enable(LK_Render.machine == "locally")
	return d
end

function LK_RenderOptionsDialog:HandleMessage(msg)
	if msg == self.GESTION then
		LK_Render.machine = "gestion"
		LK_Render.halfResolution = false
		LK_Render.singleThread = false
	elseif msg == self.LOCAL then
		LK_Render.machine = "locally"
	elseif msg == self.TOGGLE_HALF_RES then
		LK_Render.halfResolution = not LK_Render.halfResolution
	elseif msg == self.TOGGLE_SINGLETHREAD then
		LK_Render.singleThread = not LK_Render.singleThread
	elseif msg == self.GO_TO_DIRECTORY then
		FO_Utilities:RevealDirectory(LK_Render.revealDirectory)
	end

	-- * Update dialog widgets:
	self.locally:SetValue(LK_Render.machine == "locally")
	self.gestion:SetValue(LK_Render.machine == "gestion")
	self.halfRes:SetValue(LK_Render.halfResolution)
	self.singleThreadCheckbox:SetValue(LK_Render.singleThread)

	self.halfRes:Enable(LK_Render.machine == "locally")
	self.singleThreadCheckbox:Enable(LK_Render.machine == "locally")

	if LK_Render.machine == "locally" then
		LK_Render.sequenceDirectory = LK_Render:FixLocalPath(self.moho.document:Path(), LK_Render.sequenceDirectory)
	else
		LK_Render.sequenceDirectory = LK_Render:RevertToServerPath(moho, LK_Render.sequenceDirectory)
	end
	LK_Render.output = LK_Render.sequenceDirectory.."/"..LK_Render.shotName--LK_Render.sequenceDirectory.."/"..LK_Render.shotName.."/"..LK_Render.shotName
	local destinationMsg = "Destination: \'"..LK_Render.output.."_"..string.format("%05d", LK_Render.start).."."..LK_Render.format.."\'"

	if LK_Render.hasComps then
		for i = 1, #LK_Render.compNames do
			local compName = LK_Render.compNames[i]
			local compDestination = "Layercomp: \'"..LK_Render.sequenceDirectory.."/"..compName.."/"..LK_Render.shotName.."-"..compName.."_"..string.format("%05d", LK_Render.start).."."..LK_Render.format.."\'"
			self.compWidgets[i]:SetValue(compDestination)
		end
	end
	self.destination:SetValue(destinationMsg)
end

function LK_RenderOptionsDialog:OnOK()
	if (self.locally:Value()) then
		LK_Render.machine = "locally"
	end
	if (self.gestion:Value()) then
		LK_Render.machine = "gestion"
	end
	LK_Render.halfResolution = self.halfRes:Value()
end

-- **************************************************
-- The guts of this script
-- **************************************************

function LK_Render:LoadPrefs(prefs)
	LK_Render.machine = prefs:GetString("LK_Render.machine", "locally")	
end

function LK_Render:SavePrefs(prefs)
	prefs:SetString("LK_Render.machine", LK_Render.machine)
end

function LK_Render:Run(moho)
	local path = moho.document:Path()
	if not string.match(path, "_v%d%d%d_%u+.moho") then -- * (%d%d%d = 3 digits, %u+ unlimited amount of uppercase-letters)
		FO_Utilities:Alert(	"Please save your file with correct formatting: *_v001_X.moho",
							"For example:",
							"",
							"Whatever_v001_L.moho",
							"EP01_001_Intro_v003_P.moho",
							"Crazy-Guy_v006_M.moho",
							"",
							"This makes it easy to keep track of versions and see who the last animator of a shot was.",
							"",
							"Everything before '_v001_X' will be considered the 'Shotname' of the file.",
							"This means rendering file Hello_v002_X.moho will always overwrite Hello_v001_X.moho",
							"",
							"A 'Renders' directory will be created in the same location as the highest 'Shots' directory in the file's path. If no 'Shots' directory is found, the 'Renders' directory is created in the same location as the moho file.",
							"",
							"Inside the 'Renders' directory another directory will be created with the 'Shotname'.",
							"",
							"If the file contains Layercomps, they will be rendered in subfolders of the sequence folder. The regular sequence will also still be rendered, but without the layers specified in the layercomps. This sequence could be considered the background in most cases."
							)
		return
	end
	self.hasComps = moho.document:CountLayerComps() > 0
	if self.hasComps then
		self.comps = FO_Utilities:AllComps(moho)
		self.compNames = {}
		for i = 1, #self.comps do
			table.insert(self.compNames, self.comps[i]:Name())
		end
	end

	local lookFor = self.lookForDirectory
	local lastShotsPos = (string.lower(path):reverse()):find(string.lower(lookFor):reverse())
	if lastShotsPos ~= nil then
		self.rendersDirectory = string.sub(path, 0, -lastShotsPos-string.len(lookFor)-1).."/"..self.createDirectory
	else
		self.rendersDirectory = FO_Utilities:FileDirectory(path).."/"..self.createDirectory
	end
	-- * Get shotname by stripping "_v000_X.moho" of filename:
	self.shotName = FO_Utilities:ShotName(moho)
	-- * Render location:
	self.sequenceDirectory = self.rendersDirectory.."/"..self.shotName
	-- * Reveal directory:
	self.revealDirectory = self.sequenceDirectory
	-- *
	if self.machine == "locally" then
		self.sequenceDirectory = self:FixLocalPath(moho.document:Path(), self.sequenceDirectory)
		-- * Check if renders location exists:
		FO_Utilities:CreateDirectory(moho, self.rendersDirectory)
		local directoryExists = FO_Utilities:FileExists(moho, self.rendersDirectory)
		if not directoryExists then
			local msg1 = "Render directory does not exist: \n\n\""..self.rendersDirectory.."\""
			local msg2 = "Make sure it exists and try again."
			FO_Utilities:Alert(msg1, msg2)
			return
		end
		FO_Utilities:CreateDirectory(moho, self.sequenceDirectory)
		-- print ("sequenceDirectory = "..self.sequenceDirectory)
	else
		self.sequenceDirectory = LK_Render:RevertToServerPath(moho, self.sequenceDirectory)
	end
	-- * Get frame range:
	self.start = MOHO.MohoGlobals.PlayStart
	self.stop = MOHO.MohoGlobals.PlayEnd
	self.startTXT = "PlayStart"
	self.stopTXT = "PlayEnd"
	if self.start == -1 then
		self.start = moho.document:StartFrame()
		self.startTXT = "DocumentStart"
	end
	if self.stop == -1 then
		self.stop = moho.document:EndFrame()
		self.stopTXT = "DocumentEnd"
	end
	self.range = self.stop-self.start+1
	-- * Set up messages:
	self.output = self.sequenceDirectory.."/"..self.shotName
	self.msg1 = "Destination: \'"..self.output.."_"..string.format("%05d", self.start).."."..self.format.."\'"
	self.msg2 = "Range: "..self.start.." - "..self.stop.." ("..self.range.." Frames)"
	-- ***************
	-- *** OPTIONS ***
	-- ***************
	local optionsDlog = LK_RenderOptionsDialog:new(moho)
	if (optionsDlog:DoModal() == LM.GUI.MSG_CANCEL) then
		return
	end
	
	-- *** DISABLED ***
	-- self:PrepForRendering(moho)
	
	-- * LOCAL:
	if LK_Render.machine == "locally" then
		LK_Render:Local(moho)
	elseif LK_Render.machine == "gestion" then
	-- * GESTION:
		LK_Render:Gestion(moho)
	end
end


-- **************************************************
-- Prepare file for rendering
-- **************************************************
function LK_Render:PrepForRendering(moho)
	-- * Fix image quality:
	self:FixImageQuality(moho)
	LK_ToggleRefLayers:Run(moho)
	self:CleanLayerComps(moho)
	-- * Make sure we're not in design mode:
	if moho.frame == 0 then
		moho:SetCurFrame(1)
	end
	-- * Save file:
	moho:FileSave()
end

-- ****************************************************************************
-- Removes layers from layercomp of which parents are not part of the layercomp
-- ****************************************************************************
function LK_Render:CleanLayerComps(moho)
	local comps = FO_Utilities:AllComps(moho)
	local layers = FO_Utilities:AllLayers(moho)
	for i = 1, #comps do
		local comp = comps[i]
		for i = 1, #layers do
			layer = layers[i]
			if comp:ContainsLayer(layer) then
				local parent = layer:Parent()
				while parent ~= nil do
					if not comp:ContainsLayer(parent) then
						comp:RemoveLayer(layer)
					end
					parent = parent:Parent()
				end
			end
		end
	end
end

-- **************************************************
-- Fix image quality
-- **************************************************
function LK_Render:FixImageQuality(moho)
	local layers = FO_Utilities:AllLayers(moho)
	for i = 1, #layers do
		local layer = layers[i]
		if (layer:LayerType() == MOHO.LT_IMAGE) then
			self:SetHighImageQuality(moho, layer)
		end
	end
end

function LK_Render:SetHighImageQuality(moho, imageLayer)
	imageLayer = moho:LayerAsImage(imageLayer)	
	imageLayer:SetQualityLevel(2)
	-- * Change image sampling mode:
	-- * 0 = SM_NEAREST (Nearest neighbor sampling)
	-- * 1 = SM_BILINEAR (Bilinear sampling)
	imageLayer:SetSamplingMode(0)
end

-- **************************************************
-- Render locally
-- **************************************************
function LK_Render:Local(moho)
	-- * Command-line rendering:
	local mohopath = ""
	local render = moho.document:Path()
	-- * Create shot directory within renders-directory if it doesn't exist:
	-- print ("output = "..self.output)
	FO_Utilities:CreateDirectory(moho, self.output)
	-- * Add extension to output:
	self.output = self.output.."."..self.format
	if FO_Utilities:getOS(moho) == "win" then
		-- ****************
		-- *** Windows: ***
		-- ****************
		mohopath = "C:/Program Files/Moho/Moho.exe"
		mohopath = "\""..string.gsub(mohopath, "/", "\\").."\""
		render = "\""..string.gsub(render, "/", "\\").."\""
		self.output = "\""..string.gsub(self.output, "/", "\\").."\""
	else
		-- **************
		-- *** MacOS: ***
		-- **************
		mohopath = "/Applications/Moho.app/Contents/MacOS/Moho"
		mohopath = string.gsub(mohopath, " ", "\\ ")
		render = string.gsub(render, " ", "\\ ")
		self.output = string.gsub(self.output, " ", "\\ ")
	end
	-- * Extra settings:
	local halfsize = "no"
	if LK_Render.halfResolution then
		halfsize = "yes"
	end
	local multiThread = "no"
	if LK_Render.singleThread then
		multiThread = "yes"
	end
	local command = mohopath.." -r "..render.." -o "..self.output.." -f "..self.format.." -start "..self.start.." -end "..self.stop.." -halfsize "..halfsize.." -multithread "..multiThread.." -extrasmooth yes"
	local command2 = command.." -layercomp AllComps -createfolderforlayercomp yes -addlayercompsuffix yes"

	if FO_Utilities:getOS() == "win" then
		command = ' "start "any title" '..command..' " '
		command2 = ' "start "any title" '..command2..' " '
	end

	-- ***********************
	-- *** EXECUTE COMMAND ***
	-- ***********************
	local startTime = os.time()
	local ask = false
	local feedback = false

	if self.hasComps then
		local layers = FO_Utilities:AllLayers(moho)
		for i = 1, #layers do
			layer = layers[i]
			layer:SetVisible(true)
			for j = 1, #self.comps do
				local comp = self.comps[j]
				if comp:ContainsLayer(layer) then
					layer:SetVisible(false)
					break
				end
			end
		end
		--
		for i = 1, #layers do
			layer = layers[i]
			if layer:IsVisible() then
				self:SetParentVisible(layer)
			end
		end
		-- * Save file:
		moho:FileSave()
	end

	FO_Utilities:Execute(command, ask, feedback)
	if self.hasComps then
		FO_Utilities:Execute(command2, ask, feedback)
	end

	-- * Change reveal path to server:
	FO_Utilities:RevealDirectory(self.revealDirectory)
	-- *
	if self.hasComps then
		local layers = FO_Utilities:AllLayers(moho)
		for i = 1, #layers do
			layer = layers[i]
			layer:SetVisible(true)
		end
		LK_ToggleRefLayers:Run(moho)
	end
end

function LK_Render:SetParentVisible(layer)
	local parent = layer:Parent()
	if parent ~= nil then
		if not parent:IsVisible() then
			parent:SetVisible(true)
		else
			return self:SetParentVisible(parent)
		end
	else
		return false
	end
end

-- **************************************************
-- Gestion: Add to Render Queue
-- **************************************************  
function LK_Render:Gestion(moho)
	local python = FO_Utilities:Python(moho, "addtoqueue.py") -- * (Returns string "python addtoqueue.py" correctly for either Windows or macOS)
	-- * Build command:
	local currentFile = 	" --file ".."\""..moho.document:Path().."\""
	local startFrame = 		" --start "..tostring(LK_Render.start)
	local endFrame = 		" --end "..tostring(LK_Render.stop)
	local split = 		" --split "..tostring(LK_Render.split)
	local project = 		" --project Baboo"
	-- * Full command:
	local command =	python..currentFile..startFrame..endFrame..split..project
	-- * Execute:
	local ask = false
	local feedback = false
	if FO_Utilities:getOS() == "unix" then
		ask = true
		feedback = true
	end
	local output = FO_Utilities:Execute(command, ask, feedback)
end

--[[
****************************
*** COMMAND LINE OPTIONS ***
****************************

********************
*** MAIN OPTIONS ***
********************
-r <moho project file> or -render <moho project file>
This is the only required option. It tells Moho to render the given project file and then quit. The Moho project file can be a .moho project or one of the legacy document formats (.anime, .anme, or .moho) back to version 4 of the format.

-f <format>
This tells Moho what format to render the output as. Valid formats are any of the presets specified in the Export Animation dialog. Here are the some examples that work on both Mac and Windows:
• JPEG
• TGA
• BMP
• PNG
• PSD
• QT
• MP4 (H.264-AAC)
• Animated GIF
• SWF (Flash)
The video, Animated GIF, and SWF formats generate a single output file, while the other formats create numbered sequences of still images. If you don’t specify a format on the command-line, the default format used will be “MP4 (H.264-AAC)”.

-o <output> or -output <output>
Specifies an output file or folder. The output file should include the file extension of the desired output format. If the output format is PNG the output should be named something like MyScene.png.
If you skip this option, the output file will automatically have the same name as the input file, but with an extension matching the format you choose. For example, if you are rendering a file named MyScene.moho to a format of “MP4 (H.264-AAC)”, the output will be placed in the same folder as the document file with the name MyScene.mp4.
If a folder is specified instead of a file name, the output will be placed within the folder with a name matching the document file and the appropriate output extension.

-start <frame number>
Specifies the starting frame to render. If omitted, Moho will start rendering at the document’s start frame (usually 1).

-end <frame number>
Specifies the last frame to render. If omitted, Moho will render up through the last frame of the document.

-v or -verbose
Runs the rendering job in “verbose” mode. In this mode, Moho will print out the render option values along with messages about its status. This includes an estimate of the time remaining in the rendering job.

-q or -quiet
Runs the renderer in “quiet” mode. “quiet” mode disables all command-line output, both information and errors. “quiet” mode overrides “verbose” mode. This option was introduced in Anime Studio Pro 9.5.

-log <logfile>
Specifies a log file for the render. Information and errors that are normally shown as command-line output are instead logged to the log file. This option was introduced in Anime Studio Pro 9.5.

**********************
*** RENDER OPTIONS ***
**********************
The following options control rendering options, just like you see in the Export Animation or Moho Exporter dialog in Moho Pro. Most of the options can be turned on or off with a value of yes or no. If an option is not specified, the render will perform the default.

-multithread <yes|no> or -multithreaded <yes|no>
Used to turn on or off multithreaded rendering. If multi-threaded rendering is on, the render will use up to 5 threads (4 image frame threads + 1 movie encoding thread) to render your output files. Set this option to no if you wish to increase the system’s performance while a render is taking place. The default is yes.

-halfsize <yes|no>
Render the output at half size. If the project dimensions of the document is set to 720p (1280x720), the output dimensions of the render would be 640x360 when using this option. This option is useful if you want to get a quick overview of your scene as it will dramatically reduce the time it takes to render. The default is no.

-halffps <yes|no>
Render at half frame rate. If the project frame rate is set to 30 FPS, the output format would be 15 FPS when using this option. The default is no.

-shapefx <yes|no>
Render shape effects. The default is yes.

-layerfx <yes|no>
Render layer effects. The default is yes.

-fewparticles <yes|no>
Use reduced particles. The default is no.

-layercomp <comp name>
Render a specific layer comp defined within the document.
If AllComps or AllLayerComps is specified as the comp name, all layer comps will be exported during one run of the renderer. It is highly recommended that you use either -addlayercompsuffix OR -createfolderforlayercomp when specifying these comp names, otherwise multiple layer comp exports could overwrite each other.
The recommended way to render all layer comps in Moho 13.5 is AllComps or AllLayerComps).
This option was introduced in Anime Studio Pro 10.0.

-aa <yes|no>
Antialiased edges. The default is yes.

-extrasmooth <yes|no>
Extra-smooth images. The default is no.

-premultiply <yes|no>
Premultiply alpha. The default is yes.

-ntscsafe
-ntscsafe <yes|no>
NTSC safe colors. The default is no.

-variablewidths <yes|no>
Variable line widths (only applies to SWF format). The default is no.

*********************************
*** (INFO FROM MOHO HELP PDF) ***
*********************************
--]]

function LK_Render:FixLocalPath(docPath, path)
	if LK_ChangeServerPaths ~= nil then
		if LK_ChangeServerPaths.choice ~= 0 and LK_ChangeServerPaths.choice ~= 1 then
			path = LK_ChangeServerPaths:FixPath(path, nil, docPath)
		end
	end
	return path
end

function LK_Render:RevertToServerPath(moho, path)
	if LK_ChangeServerPaths ~= nil then
		path = LK_ChangeServerPaths:FixPath(path, LK_ChangeServerPaths.serverPaths[1]) -- * Revert to studio paths
	end
	return path
end

Icon
LK_Render
Listed

Author: Lukas View Script
Script type: Button/Menu

Uploaded: Apr 19 2022, 07:12

Last modified: Jun 29 2022, 03:50

Render and overwrite .PNG sequences
Please save your file with correct formatting: *_v001_X.moho
For example:

Whatever_v001_L.moho
EP01_001_Intro_v003_P.moho
Crazy-Guy_v006_M.moho

This makes it easy to keep track of versions and see who the last animator of a shot was.

Everything before _v001_X will be considered the Shotname of the file.
This means rendering file Hello_v002_X.moho will always overwrite the render of Hello_v001_X.moho

A Renders directory will be created in the same location as the highest Shots directory in the file's path. If no Shots directory is found the Renders directory is created in the same location as the moho file.

Inside the Renders directory another directory will be created with the Shotname.

If the file contains Layercomps they will be rendered in subfolders of the sequence folder. The regular sequence will also still be rendered but without the layers specified in the layercomps. This sequence could be considered the background in most cases.

This script has some stuff in it that sends a render to our studio's render server, which is of course disabled, but could be modified to use your own system. That's why I left it in. Feel free to modify it to your specific needs.

The script will assume your Moho install is located at:

Windows: C:/Program Files/Moho/Moho.exe
macOS: /Applications/Moho.app/Contents/MacOS/Moho

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