Get it HERE 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 pluginMeshParts created by converting Parts will also inherit all of the Parts’ properties
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.
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:
-
The number of selected Instances must not exceed
maxSelectionSize
- 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
- A Part and its SpecialMesh/FileMesh child cannot be selected at the same time (read exception below conditions)
- 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.
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.
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 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.
Updates