[Free Plugin] MeshToPart

Untitled1

⊱ ─── MeshToPart ─── ⊰


Get it :arrow_double_down: HERE :arrow_double_down: or use the code (below) to save it as a local plugin.

Convert selected SpecialMesh, FileMesh and Part Instances to MeshParts

Simply select one or more SpecialMesh, FileMesh, and/or Part containing either a
SpecialMesh or a PartMesh, and activate the plugin

MeshParts created by converting Parts will also inherit all of the Parts’ properties


:o: Motivation

For various reasons stated elsewhere, the MeshId property of MeshParts cannot be changed by scripts. Converting many Meshes (e.g. imported Accessories) to MeshParts by hand is a very tedious task, especially if you want to preserve more than just the MeshId and TextureId of the original.

Mesh2Part was temporarily unavailable when this plugin was created.


:o: Instructions

A “Mesh2Part_Configuration” Configuration Instance is added to ServerStorage, and you can edit its collisionFidelity, renderFidelity and maxSelectionSize attributes, which will change exactly what you’d expect them to.

The plugin listens for changes in selection, and the toolbar button is enabled or disabled based on the validity of the selection. Here is what makes a selection valid:

  1. The number of selected Instances must not exceed maxSelectionSize
  2. All selected Instances must be either a SpecialMesh, a FileMesh, or a Part that has one of those two as its immediate descendant (child); not all of them have to be the same type, but they all have to be either one of those types
  3. A Part and its SpecialMesh/FileMesh child cannot be selected at the same time (read exception below conditions)
  4. All Meshes that are targeted (whether directly or through a Part) must have the MeshId property set - even if it’s not valid

Regarding (3.), when the plugin looks for a Mesh inside of a Part, it first looks for a SpecialMesh, and then for a FileMesh. As a result, if a SpecialMesh is found, you may select additional SpecialMesh or FileMesh children of the Part (except for that “first” SpecialMesh - you will know which one by checking if the plugin button is enabled or not). Likewise, if a FileMesh is found instead, you may select any other FileMesh childen. In this case, the selected Part and its selected children will be treated separately, that is: the Meshes will be treated as “standalone”, and the Part will convert with inheritance (more on that below).

maxSelectionSize is there simply to protect your device from an accidental conversion of too many Meshes. The default value is 100, and if necessary you may increase or decrease it when you figure out what your device is okay with.


:o: Inheritance

  • MeshParts converted from both Meshes and Parts inherit all properties of those Meshes except for Color/VertexColor
  • MeshParts converted from Parts inherit all properties of that Part (even Mass will be matched if possible), except for Size - the size will match the size the Mesh (visually); Part properties will take precedence over Mesh properties (those that are shared)
  • MeshParts converted from Parts will visually be placed exactly where the Part’s Mesh was, and their pivot will be where the Part’s pivot was, and MeshParts converted from Meshes will be placed in front of the Camera
  • In case of Meshes, their children will be added to the MeshPart, and in case of Parts, both the children of the Part and the children of the Part’s Mesh that was used for the conversion will be added to the MeshPart

Color/VertexColor is not inherited because it makes no difference on a textured MeshPart. If a Mesh has a VertexColor that is not (1, 1, 1), the converted MeshPart will have a “VertexColor” attribute set, and a warning will be shown in the output.

If the Part you are converting is a part of an assembly, has body movers, or any other Instances that may reference other Instances such as Attachments, you should manually set those references again, if they were referencing the original Part or any of its descendants. You will still see the referenced Instances in properties, but those are actually referencing unparented Instances (they are not destroyed so that undoing conversions is possible).

TouchTransmitters (TouchInterests) are not preserved - their Parent property cannot be changed.


:o: The Code

Expand Script
local ChangeHistoryService = game:GetService('ChangeHistoryService')
local InsertService = game:GetService('InsertService')
local ServerStorage = game:GetService('ServerStorage')

local Camera = workspace.CurrentCamera or workspace:WaitForChild('Camera')
local ConvertingCounter = Instance.new('IntValue')

local newSession = true
local defaultConfiguration = { collisionFidelity = Enum.CollisionFidelity.Default, renderFidelity = Enum.RenderFidelity.Automatic, maxSelectionSize = 100 }
local failedConversions, noHandleYOffset, convertedSelection

local function getIcon()
	local mainBackgroundColor = settings().Studio.Theme:GetColor('MainBackground')
	return mainBackgroundColor.R + mainBackgroundColor.G + mainBackgroundColor.B < 1.5 and 'rbxassetid://15194645325' or 'rbxassetid://15194642755'
end

local toolbar = plugin:CreateToolbar('MeshToPart')
local button = toolbar:CreateButton('Convert Selection', 'Convert selected SpecialMesh, FileMesh and Part Instances to MeshParts', getIcon())

local function getConfiguration(field: string): EnumItem | number
	local Configuration = ServerStorage:FindFirstChild('MeshToPart_Configuration')
	if not Configuration or not Configuration:IsA('Configuration') then
		Configuration = Instance.new('Configuration')
		Configuration.Name = 'MeshToPart_Configuration'
		for attribute, value in defaultConfiguration do
			Configuration:SetAttribute(attribute, value)
		end
		Configuration.Parent = ServerStorage
		if newSession then
			newSession = false
		else
			warn('MeshToPart: A "MeshToPart_Configuration" Configuration was not found in ServerStorage; a new one with default values was created')
		end
		return defaultConfiguration[field]
	end
	if newSession then
		newSession = false
	end
	local value = Configuration:GetAttribute(field)
	if typeof(value) == typeof(defaultConfiguration[field]) and (typeof(value) == 'number' or value.EnumType == defaultConfiguration[field].EnumType) then
		return value
	else
		Configuration:SetAttribute(field, defaultConfiguration[field])
		warn(`MeshToPart: The type of the {field} attribute of MeshToPart_Configuration was not valid; the attribute was reset to the default value`)
		return defaultConfiguration[field]
	end
end

local function isSelectionValid()
	local selection: {Instance?} = game.Selection:Get()
	if #selection == 0 or #selection > getConfiguration('maxSelectionSize') then
		return false
	end
	
	local meshes: {[SpecialMesh | FileMesh]: true?}, selectionValid = {}, true
	for _, Object: Instance in selection do
		local Mesh = (Object:IsA('SpecialMesh') or Object:IsA('FileMesh')) and Object or Object:IsA('Part') and (Object:FindFirstChildWhichIsA('SpecialMesh') or Object:FindFirstChildWhichIsA('FileMesh'))
		if not Mesh or Mesh.MeshId == '' or meshes[Mesh] then
			selectionValid = false
			break
		end
		meshes[Mesh] = true
	end
	
	table.clear(selection)
	table.clear(meshes)
	return selectionValid
end

local function convert(Object: SpecialMesh | FileMesh | Part, collisionFidelity: Enum.CollisionFidelity, renderFidelity: Enum.RenderFidelity)
	local Mesh: SpecialMesh | FileMesh = Object:IsA('Part') and (Object:FindFirstChildWhichIsA('SpecialMesh') or Object:FindFirstChildWhichIsA('FileMesh')) or Object
	local Handle: Part? = Object:IsA('Part') and Object
	local Parent = Handle and Handle.Parent or Mesh.Parent
	
	local success, response = pcall(InsertService.CreateMeshPartAsync, InsertService, Mesh.MeshId, collisionFidelity, renderFidelity)
	if not success then
		warn(`MeshToPart: Conversion of {Handle and Handle:GetFullName() or Mesh:GetFullName()} failed - {response}`)
		table.insert(convertedSelection, Handle or Mesh)
		failedConversions += 1
		ConvertingCounter.Value -= 1
		return
	end
	
	local MeshPart: MeshPart = response
	MeshPart.TextureID = Mesh.TextureId
	MeshPart.Size *= Mesh.Scale
	if Handle then
		MeshPart.CastShadow = Handle.CastShadow
		MeshPart.Color = Handle.Color
		MeshPart.Material = Handle.Material
		MeshPart.MaterialVariant = Handle.MaterialVariant
		MeshPart.Reflectance = Handle.Reflectance
		MeshPart.Transparency = Handle.Transparency
		MeshPart.Archivable = Handle.Archivable
		MeshPart.Locked = Handle.Locked
		MeshPart.Name = Handle.Name
		MeshPart.CFrame = Handle.CFrame * CFrame.new(Mesh.Offset)
		MeshPart.PivotOffset = CFrame.new(-Mesh.Offset) * Handle.PivotOffset
		MeshPart.CanCollide = Handle.CanCollide
		MeshPart.CanTouch = Handle.CanTouch
		MeshPart.CollisionGroup = Handle.CollisionGroup
		MeshPart.Anchored = Handle.Anchored
		local physicalProperties = Handle.CustomPhysicalProperties or PhysicalProperties.new(Handle.Material)
		MeshPart.CustomPhysicalProperties = physicalProperties
		MeshPart.CustomPhysicalProperties = PhysicalProperties.new(physicalProperties.Density * Handle.Mass / MeshPart.Mass, physicalProperties.Friction, physicalProperties.Elasticity, physicalProperties.FrictionWeight, physicalProperties.ElasticityWeight)
		MeshPart.Massless = Handle.Massless
		MeshPart.RootPriority = Handle.RootPriority
		MeshPart.AssemblyLinearVelocity = Handle.AssemblyLinearVelocity
		MeshPart.AssemblyAngularVelocity = Handle.AssemblyAngularVelocity
		Handle.Parent = nil
		for _, Child in Handle:GetChildren() do
			if Child ~= Mesh and not Child:IsA('TouchTransmitter') then
				Child:Clone().Parent = MeshPart
			end
		end
	else
		MeshPart.Archivable = Mesh.Archivable
		MeshPart.Name = Mesh.Name
		MeshPart.CFrame = Camera.CFrame * CFrame.Angles(0, math.pi, 0) * CFrame.new(0, 0, 1024 / workspace.Camera.FieldOfView) * CFrame.new(0, noHandleYOffset, 0)
		noHandleYOffset += 4
		Mesh.Parent = nil
	end
	if Mesh.VertexColor ~= Vector3.one then
		warn(`MeshToPart: {Mesh:GetFullName()} had a ({Mesh.VertexColor}) VertexColor; `
			.. 'the converted MeshPart will not inherit that Color, but will have a "VertexColor" Attribute set')
		MeshPart:SetAttribute('VertexColor', Mesh.VertexColor)
	end
	for _, Child in Mesh:GetChildren() do
		if not Child:IsA('TouchTransmitter') then
			Child:Clone().Parent = MeshPart
		end
	end
	
	MeshPart.Parent = Parent
	table.insert(convertedSelection, MeshPart)
	ConvertingCounter.Value -= 1
end

local function onSelectionChanged()
	if convertedSelection then
		return
	end
	button.Enabled = isSelectionValid()
end

local function convertSelection()
	if convertedSelection then
		return
	end
	button.Enabled = false
	print('MeshToPart: Converting selection...')
	local selection = game.Selection:Get()
	ConvertingCounter.Value, failedConversions, noHandleYOffset, convertedSelection = #selection, 0, 0, {}
	local collisionFidelity, renderFidelity = getConfiguration('collisionFidelity'), getConfiguration('renderFidelity')
	local recordingId = ChangeHistoryService:TryBeginRecording('MeshToPart Conversion', 'MeshToPart Conversion')
	game.Selection:Set({})
	task.wait()
		
	for _, Mesh in selection do
		task.spawn(convert, Mesh, collisionFidelity, renderFidelity)
	end
	while ConvertingCounter.Value ~= 0 do
		ConvertingCounter.Changed:Wait()
	end
	game.Selection:Set(convertedSelection)
	ChangeHistoryService:FinishRecording(recordingId, Enum.FinishRecordingOperation.Commit)
		
	print(`MeshToPart: Conversion{#selection ~= 1 and 's' or ''} completed - {#selection == 1 and (failedConversions == 0 and 'successful' or 'failed') or `{#selection - failedConversions} successful, {failedConversions} failed`}`)
	table.clear(selection)
	table.clear(convertedSelection)
	convertedSelection = nil
	onSelectionChanged()
end

settings().Studio.ThemeChanged:Connect(function()
	button.Icon = getIcon()
end)

game.Selection.SelectionChanged:Connect(onSelectionChanged)
onSelectionChanged()

button.Click:Connect(convertSelection)

Feel free to modify it to your needs, and please report any :beetle:s you run into.

One word of caution: as mentioned before, to preserve the expected undo behavior, no Instances are destroyed after being converted, only their Parent is set to nil, and children of Meshes and Parts are not reparented, they are cloned. This shouldn’t cause an issue for most, but if you find yourself in a situation where you’re doing thousands of conversions, and you hear you device starting to struggle a bit, you should probably close the place and open it again to free up that memory.

Also, keep in mind that the selection is checked every time it changes. As you can see from the code, this operation isn’t computationally heavy and it shouldn’t affect performance most of the time, but if your device is on the weaker end or you plan to select many Instances, you may want to disable the plugin entirely if you don’t intend to use it anytime soon.


:o: Updates

13 Likes

:hammer: :arrow_up: UPDATE 1 - 1 Fix, 1 Upgrade

FIX: The toolbar button was not disabled once the plugin starts converting Meshes; activating the plugin again after it started running would’ve caused issues. Now the button gets disabled, and in case it somehow does get activated again, an additional check was added, to see if the plugin is already running. The same check was also added to the selection checking logic, to prevent a check if the plugin is running.

UPGRADE: Previously, the collision fidelity and render fidelity settings could not be changed; they were always Default and Automatic, respectively. A “MeshToPart_Configuration” Configuration Instance has now been added to ServerStorage, where these two can be changed, as well as the maximum selection size, above which the toolbar button will get disabled:

Untitled

maxSelectionSize is there simply to protect your device from an accidental conversion of too many Meshes. The default value is 100, and if necessary you may increase or decrease it when you figure out what your device is okay with.

For the sake of simplicity and code readability, the Configuration Instance is not observed for changes. Instead, the script looks for a “MeshToPart_Configuration” Configuration in ServerStorage whenever it is needed (when the selection changes or a conversion is initiated). Warnings are shown if it was not found (a new one with default values is added in that case), or the type of one ore more of the attributes was not valid (only those attributes are set back to their default values in that case).

The original post was edited to reflect these changes
1 Like

Hi,
Offhand, what are the benefits or reasons to convert it to s meshpart?

For example, what are you doing with the meshpart, that you could not do otherwise?

Thanks

1 Like

Hi, primarily for collisions. With SpecialMesh/FileMesh you get the collision box of the Parent Part, not in the shape Mesh. The drawbacks are not being able to change the color of the texture (as you would with VertexColor), and not being able to change the MeshId so easily.

And with the collision box comes accurate click/aim or touch detection. Still, it’s something most people would need for only one or two projects, if ever. But for certain genres like matching games and prop hunts, anything where you’d need a larger amount of items - if you want to make use of Roblox items instead of paying for custom ones, it can save a lot of time.

1 Like

Couldn’t this technically be used to, for example, create an asset (using regular parts), convert the parts to MeshParts, and apply surface appearance to each part (for realism)?

1 Like

Not sure I understand what you mean. If you’re asking if the conversions also apply surface appearance, then yes, all children of the Mesh are cloned, and the clones are added to the final MeshPart. If the Mesh was inside of a Part, all children of the Part (except the converted SpecialMesh/FileMesh) get the same treatment as well.

So, yes, SurfaceAppearance Instances will be moved to the converted MeshParts, if that’s what you meant.

1 Like