
Obby Creator Script – MODEL IMPORTER KIDNA WORKS WITH MESHES
--[[
Obby Creator Model Importer
Paste a Roblox model ID -> model ghosts on your mouse -> click to place ->
builds it brick by brick (place -> color -> material) using Obby Creator remotes + uncap method.
Standalone. Uses InsertService to load models.
]]
-- ============================================================
-- ERROR HANDLER
-- ============================================================
local _ok, _err = xpcall(function()
-- ============================================================
-- SERVICES
-- ============================================================
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local CoreGui = game:GetService("CoreGui")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local InsertService = game:GetService("InsertService")
local plr = Players.LocalPlayer
local mouse = plr:GetMouse()
-- ============================================================
-- COLOR THEME (dark cyan, matches glitched parts panel)
-- ============================================================
local C = {
bg = Color3.fromRGB(8, 12, 16),
panel = Color3.fromRGB(12, 18, 24),
card = Color3.fromRGB(18, 26, 34),
cardHi = Color3.fromRGB(26, 36, 48),
border = Color3.fromRGB(20, 30, 42),
accent = Color3.fromRGB(0, 229, 255),
accentD = Color3.fromRGB(0, 140, 160),
success = Color3.fromRGB(0, 255, 157),
danger = Color3.fromRGB(255, 61, 90),
warn = Color3.fromRGB(255, 180, 0),
text = Color3.fromRGB(224, 231, 236),
textDim = Color3.fromRGB(122, 136, 150),
}
-- ============================================================
-- BYPASS HELPERS (mini version — just what we need for color/material)
-- ============================================================
local propMap = {
["Material"]="Mtl",["Reflectance"]="Rf",["Transparency"]="Tr",
["Color"]="C",["BrickColor"]="BC",
}
local function findValueObjectDeep(parent, targetName)
local result = nil
pcall(function()
for _, child in ipairs(parent:GetChildren()) do
if child:IsA("ValueBase") and child.Name == targetName then
result = child
return
end
local isContainer = false
pcall(function()
if child:IsA("Folder") or child:IsA("Configuration") or child:IsA("Model") then
isContainer = true
end
end)
if isContainer and not result then
result = findValueObjectDeep(child, targetName)
if result then return end
end
end
end)
return result
end
-- ============================================================
-- LOG
-- ============================================================
local logLines = {}
local LogTextBox = nil
local function log(msg, col)
local text = tostring(msg)
table.insert(logLines, text)
if #logLines > 100 then table.remove(logLines, 1) end
pcall(function() print("[IMPORTER] " .. text) end)
if LogTextBox and LogTextBox.Parent then
pcall(function()
LogTextBox.Text = table.concat(logLines, "\n")
LogTextBox.CursorPosition = #LogTextBox.Text + 1
end)
end
end
-- ============================================================
-- STATE
-- ============================================================
local loadedModel = nil -- the InsertService model (stored in workspace, hidden)
local loadedModelPivot = nil -- the model's ORIGINAL pivot (before hiding)
local origPartData = {} -- snapshots each part's original properties before hiding
local ghostModel = nil -- clone that follows mouse
local ghostConn = nil -- RenderStepped conn for mouse follow
local placingMode = false -- true while ghost is following mouse
local ghostRotation = CFrame.new() -- accumulated rotation offset (R key rotates 90° on Y)
local building = false -- true during build sequence
local cancelBuild = false -- flag to cancel mid-build
local buildSpeed = 1 -- seconds between placement operations
local partType = "Part" -- AddObject type (Part, Star, etc.)
-- Mesh surface conversion settings
local convertMeshes = true
local meshVoxelSize = 0.4 -- smaller = more detail, but creates more parts
local meshMaxParts = 1200 -- safety limit per MeshPart
local meshSurfaceCache = {}
-- ============================================================
-- UI HELPERS
-- ============================================================
local function corner(parent, r)
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, r or 4)
c.Parent = parent
return c
end
local function stroke(parent, col, thick)
local s = Instance.new("UIStroke")
s.Color = col or C.border
s.Thickness = thick or 1
s.Parent = parent
return s
end
-- ============================================================
-- SCREENGUI + PANEL
-- ============================================================
local gui = Instance.new("ScreenGui")
gui.Name = "ModelImporter"
gui.ResetOnSpawn = false
gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
gui.DisplayOrder = 99997
gui.IgnoreGuiInset = true
pcall(function() gui.Parent = CoreGui end)
if not gui.Parent then gui.Parent = plr:WaitForChild("PlayerGui") end
local panel = Instance.new("Frame")
panel.Size = UDim2.new(0, 340, 0, 420)
panel.Position = UDim2.new(0, 20, 0.5, -210)
panel.BackgroundColor3 = C.bg
panel.BorderSizePixel = 0
panel.Parent = gui
corner(panel, 8)
stroke(panel, C.accentD, 1)
-- ===== HEADER =====
local header = Instance.new("Frame")
header.Size = UDim2.new(1, 0, 0, 32)
header.BackgroundColor3 = C.panel
header.BorderSizePixel = 0
header.Parent = panel
corner(header, 8)
local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, -60, 1, 0)
title.Position = UDim2.new(0, 10, 0, 0)
title.BackgroundTransparency = 1
title.Text = "MODEL IMPORTER"
title.TextColor3 = C.accent
title.Font = Enum.Font.GothamBold
title.TextSize = 13
title.TextXAlignment = Enum.TextXAlignment.Left
title.Parent = header
local closeBtn = Instance.new("TextButton")
closeBtn.Size = UDim2.new(0, 22, 0, 22)
closeBtn.Position = UDim2.new(1, -26, 0, 5)
closeBtn.BackgroundColor3 = C.danger
closeBtn.Text = "X"
closeBtn.TextColor3 = Color3.new(1, 1, 1)
closeBtn.Font = Enum.Font.GothamBold
closeBtn.TextSize = 11
closeBtn.Parent = header
corner(closeBtn, 4)
closeBtn.MouseButton1Click:Connect(function()
if ghostConn then ghostConn:Disconnect() end
if ghostModel then ghostModel:Destroy() end
if loadedModel then loadedModel:Destroy() end
gui:Destroy()
end)
-- Drag
local dragging, dragStart, startPos
header.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
dragStart = input.Position
startPos = panel.Position
end
end)
UserInputService.InputChanged:Connect(function(input)
if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
local delta = input.Position - dragStart
panel.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y)
end
end)
UserInputService.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
dragging = false
end
end)
-- ===== CONTENT =====
local content = Instance.new("Frame")
content.Size = UDim2.new(1, -16, 1, -44)
content.Position = UDim2.new(0, 8, 0, 40)
content.BackgroundTransparency = 1
content.Parent = panel
local layout = Instance.new("UIListLayout")
layout.Padding = UDim.new(0, 6)
layout.Parent = content
-- Helper: section label
local function sectionLabel(text)
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(1, 0, 0, 16)
lbl.BackgroundTransparency = 1
lbl.Text = text
lbl.TextColor3 = C.accent
lbl.Font = Enum.Font.GothamBold
lbl.TextSize = 10
lbl.TextXAlignment = Enum.TextXAlignment.Left
lbl.Parent = content
return lbl
end
-- Helper: textbox
local function mkTextBox(parent, placeholder)
local t = Instance.new("TextBox")
t.Size = UDim2.new(1, 0, 0, 24)
t.BackgroundColor3 = C.card
t.TextColor3 = C.text
t.PlaceholderText = placeholder or ""
t.PlaceholderColor3 = C.textDim
t.Text = ""
t.Font = Enum.Font.Gotham
t.TextSize = 10
t.ClearTextOnFocus = false
t.Parent = parent
corner(t, 4)
stroke(t, C.border, 1)
return t
end
-- Helper: button
local function mkButton(parent, text, bgColor, textColor, height)
local b = Instance.new("TextButton")
b.Size = UDim2.new(1, 0, 0, height or 26)
b.BackgroundColor3 = bgColor or C.card
b.Text = text
b.TextColor3 = textColor or C.text
b.Font = Enum.Font.GothamBold
b.TextSize = 10
b.Parent = parent
corner(b, 4)
return b
end
-- ============================================================
-- SECTION 1: MODEL ID INPUT
-- ============================================================
sectionLabel("=== MODEL ID ===")
local idBox = mkTextBox(content, "Enter model ID (e.g. 1234567890)")
local loadBtnRow = Instance.new("Frame")
loadBtnRow.Size = UDim2.new(1, 0, 0, 28)
loadBtnRow.BackgroundTransparency = 1
loadBtnRow.Parent = content
local loadBtnRowLay = Instance.new("UIListLayout")
loadBtnRowLay.FillDirection = Enum.FillDirection.Horizontal
loadBtnRowLay.Padding = UDim.new(0, 4)
loadBtnRowLay.Parent = loadBtnRow
local loadBtn = mkButton(loadBtnRow, "LOAD MODEL", C.accent, C.bg, 28)
loadBtn.Size = UDim2.new(0.65, -2, 1, 0)
loadBtn.TextSize = 11
local forceLoadBtn = mkButton(loadBtnRow, "FORCE LOAD", C.warn, C.bg, 28)
forceLoadBtn.Size = UDim2.new(0.35, -2, 1, 0)
forceLoadBtn.TextSize = 9
local loadStatus = Instance.new("TextLabel")
loadStatus.Size = UDim2.new(1, 0, 0, 16)
loadStatus.BackgroundTransparency = 1
loadStatus.Text = "No model loaded"
loadStatus.TextColor3 = C.textDim
loadStatus.Font = Enum.Font.Gotham
loadStatus.TextSize = 9
loadStatus.TextXAlignment = Enum.TextXAlignment.Left
loadStatus.Parent = content
-- ============================================================
-- SECTION 2: PART TYPE + SPEED
-- ============================================================
sectionLabel("=== SETTINGS ===")
-- Part type dropdown (all Obby Creator part types)
local partTypes = {
"Part", "Ball", "Wedge", "CornerWedge", "Cylinder", "Truss",
"3 Point Pyramid", "Cone", "Half Ball", "Half Cylinder",
"Half Hollow Cylinder", "Head", "Hole",
-- Extra part types
"Hollow Cylinder", "Pyramid", "Ramp",
}
local ptButtons = {}
-- Dropdown button (shows current selection)
local ptRow = Instance.new("Frame")
ptRow.Size = UDim2.new(1, 0, 0, 24)
ptRow.BackgroundTransparency = 1
ptRow.Parent = content
local ptLbl = Instance.new("TextLabel")
ptLbl.Size = UDim2.new(0, 80, 1, 0)
ptLbl.BackgroundTransparency = 1
ptLbl.Text = "Part Type:"
ptLbl.TextColor3 = C.text
ptLbl.Font = Enum.Font.Gotham
ptLbl.TextSize = 10
ptLbl.TextXAlignment = Enum.TextXAlignment.Left
ptLbl.Parent = ptRow
local ptDdBtn = Instance.new("TextButton")
ptDdBtn.Size = UDim2.new(1, -80, 0, 22)
ptDdBtn.Position = UDim2.new(0, 80, 0, 1)
ptDdBtn.BackgroundColor3 = C.card
ptDdBtn.Text = partType
ptDdBtn.TextColor3 = C.text
ptDdBtn.Font = Enum.Font.GothamBold
ptDdBtn.TextSize = 10
ptDdBtn.TextXAlignment = Enum.TextXAlignment.Left
ptDdBtn.Parent = ptRow
corner(ptDdBtn, 4)
stroke(ptDdBtn, C.border, 1)
-- Dropdown list (hidden by default)
local ptDdList = Instance.new("ScrollingFrame")
ptDdList.Size = UDim2.new(1, -80, 0, 0)
ptDdList.Position = UDim2.new(0, 80, 0, 25)
ptDdList.BackgroundColor3 = C.card
ptDdList.BorderSizePixel = 0
ptDdList.Visible = false
ptDdList.ZIndex = 20
ptDdList.ScrollBarThickness = 4
ptDdList.Parent = ptRow
corner(ptDdList, 4)
stroke(ptDdList, C.accentD, 1)
local ptDdLay = Instance.new("UIListLayout")
ptDdLay.Padding = UDim.new(0, 1)
ptDdLay.Parent = ptDdList
local ptDropdownOpen = false
local function rebuildPTDropdown()
for _, ch in ipairs(ptDdList:GetChildren()) do
if ch:IsA("TextButton") then ch:Destroy() end
end
for _, t in ipairs(partTypes) do
local b = Instance.new("TextButton")
b.Size = UDim2.new(1, 0, 0, 20)
b.BackgroundColor3 = (t == partType) and C.cardHi or C.card
b.Text = t
b.TextColor3 = (t == partType) and C.accent or C.text
b.Font = Enum.Font.Gotham
b.TextSize = 9
b.TextXAlignment = Enum.TextXAlignment.Left
b.ZIndex = 21
b.Parent = ptDdList
b.MouseButton1Click:Connect(function()
partType = t
ptDdBtn.Text = t
ptDropdownOpen = false
ptDdList.Visible = false
rebuildPTDropdown()
log("Part type: " .. t, C.accent)
end)
end
ptDdList.CanvasSize = UDim2.new(0, 0, 0, #partTypes * 21)
end
ptDdBtn.MouseButton1Click:Connect(function()
ptDropdownOpen = not ptDropdownOpen
ptDdList.Visible = ptDropdownOpen
if ptDropdownOpen then
ptDdList.Size = UDim2.new(1, -80, 0, math.min(#partTypes * 21, 140))
end
end)
rebuildPTDropdown()
-- Build speed slider (simple +/- buttons + label)
local speedRow = Instance.new("Frame")
speedRow.Size = UDim2.new(1, 0, 0, 22)
speedRow.BackgroundTransparency = 1
speedRow.Parent = content
local speedLbl = Instance.new("TextLabel")
speedLbl.Size = UDim2.new(0, 80, 1, 0)
speedLbl.BackgroundTransparency = 1
speedLbl.Text = "Speed:"
speedLbl.TextColor3 = C.text
speedLbl.Font = Enum.Font.Gotham
speedLbl.TextSize = 10
speedLbl.TextXAlignment = Enum.TextXAlignment.Left
speedLbl.Parent = speedRow
local speedValLbl = Instance.new("TextLabel")
speedValLbl.Size = UDim2.new(1, -80, 1, 0)
speedValLbl.Position = UDim2.new(0, 80, 0, 0)
speedValLbl.BackgroundTransparency = 1
speedValLbl.Text = string.format("%.2fs/brick", buildSpeed)
speedValLbl.TextColor3 = C.accent
speedValLbl.Font = Enum.Font.GothamBold
speedValLbl.TextSize = 10
speedValLbl.TextXAlignment = Enum.TextXAlignment.Left
speedValLbl.Parent = speedRow
-- Speed is fixed at 1 second per brick — no +/- buttons needed.
-- ============================================================
-- SECTION 3: BUILD CONTROLS
-- ============================================================
sectionLabel("=== BUILD OPTIONS ===")
-- Color toggle
local colorToggleBtn = mkButton(content, "Colors: ON", C.accent, C.bg, 22)
local applyColors = true
colorToggleBtn.MouseButton1Click:Connect(function()
applyColors = not applyColors
if applyColors then
colorToggleBtn.Text = "Colors: ON"
colorToggleBtn.BackgroundColor3 = C.accent
colorToggleBtn.TextColor3 = C.bg
else
colorToggleBtn.Text = "Colors: OFF"
colorToggleBtn.BackgroundColor3 = C.card
colorToggleBtn.TextColor3 = C.textDim
end
end)
-- Material toggle
local materialToggleBtn = mkButton(content, "Materials: ON", C.accent, C.bg, 22)
local applyMaterials = true
materialToggleBtn.MouseButton1Click:Connect(function()
applyMaterials = not applyMaterials
if applyMaterials then
materialToggleBtn.Text = "Materials: ON"
materialToggleBtn.BackgroundColor3 = C.accent
materialToggleBtn.TextColor3 = C.bg
else
materialToggleBtn.Text = "Materials: OFF"
materialToggleBtn.BackgroundColor3 = C.card
materialToggleBtn.TextColor3 = C.textDim
end
end)
sectionLabel("=== BUILD ===")
local placeBtn = mkButton(content, "PLACE (click to set position)", C.warn, C.bg, 28)
placeBtn.TextSize = 11
local buildBtn = mkButton(content, "BUILD MODEL", C.success, C.bg, 30)
buildBtn.TextSize = 12
stroke(buildBtn, C.success, 2)
local cancelBtn = mkButton(content, "CANCEL BUILD", C.danger, Color3.new(1,1,1), 22)
cancelBtn.TextSize = 10
local progressLbl = Instance.new("TextLabel")
progressLbl.Size = UDim2.new(1, 0, 0, 16)
progressLbl.BackgroundTransparency = 1
progressLbl.Text = "Ready"
progressLbl.TextColor3 = C.textDim
progressLbl.Font = Enum.Font.Gotham
progressLbl.TextSize = 9
progressLbl.TextXAlignment = Enum.TextXAlignment.Left
progressLbl.Parent = content
-- Progress bar
local progressBg = Instance.new("Frame")
progressBg.Size = UDim2.new(1, 0, 0, 8)
progressBg.BackgroundColor3 = C.card
progressBg.Parent = content
corner(progressBg, 2)
local progressFill = Instance.new("Frame")
progressFill.Size = UDim2.new(0, 0, 1, 0)
progressFill.BackgroundColor3 = C.accent
progressFill.Parent = progressBg
corner(progressFill, 2)
local function setProgress(pct, text)
progressFill.Size = UDim2.new(pct, 0, 1, 0)
if text then progressLbl.Text = text end
end
-- ============================================================
-- SECTION 4: LOG
-- ============================================================
sectionLabel("=== LOG ===")
LogTextBox = Instance.new("TextBox")
LogTextBox.Size = UDim2.new(1, 0, 0, 70)
LogTextBox.BackgroundColor3 = C.card
LogTextBox.TextColor3 = C.text
LogTextBox.Text = ""
LogTextBox.Font = Enum.Font.RobotoMono
LogTextBox.TextSize = 7
LogTextBox.TextWrapped = true
LogTextBox.TextXAlignment = Enum.TextXAlignment.Left
LogTextBox.TextYAlignment = Enum.TextYAlignment.Top
LogTextBox.MultiLine = true
LogTextBox.ClearTextOnFocus = false
LogTextBox.TextEditable = false -- can't type into it (read-only)
LogTextBox.Active = false -- can't be focused at all
LogTextBox.Parent = content
corner(LogTextBox, 4)
stroke(LogTextBox, C.border, 1)
-- ============================================================
-- GHOST MODEL SYSTEM (follows mouse)
-- ============================================================
local function clearGhost()
if ghostConn then ghostConn:Disconnect() ghostConn = nil end
if ghostModel then ghostModel:Destroy() ghostModel = nil end
placingMode = false
placeBtn.Text = "PLACE (click to set position)"
placeBtn.BackgroundColor3 = C.warn
placeBtn.TextColor3 = C.bg
end
local function startGhost()
if not loadedModel then
log("Load a model first!", C.danger)
return
end
clearGhost()
ghostRotation = CFrame.new() -- reset rotation
ghostModel = loadedModel:Clone()
-- Make all parts semi-transparent neon for ghost appearance
for _, p in ipairs(ghostModel:GetDescendants()) do
if p:IsA("BasePart") then
p.Transparency = 0.5
p.Material = Enum.Material.Neon
p.CanCollide = false
p.CanQuery = false
p.CanTouch = false
p.Anchored = true
end
end
ghostModel.Parent = workspace
placingMode = true
placeBtn.Text = "CLICK IN WORLD TO PLACE... (R to rotate)"
placeBtn.BackgroundColor3 = C.accent
placeBtn.TextColor3 = C.bg
ghostConn = RunService.RenderStepped:Connect(function()
if not ghostModel or not ghostModel.Parent then
clearGhost()
return
end
-- Follow mouse hit position + apply accumulated rotation
local mousePos = mouse.Hit.Position
ghostModel:PivotTo(CFrame.new(mousePos) * ghostRotation)
end)
log("Ghost active — click in the world to place. Press R to rotate 90°.", C.accent)
end
placeBtn.MouseButton1Click:Connect(function()
if building then
log("Build in progress — cancel first", C.warn)
return
end
if placingMode then
-- Cancel placing
clearGhost()
log("Placing cancelled", C.warn)
else
startGhost()
end
end)
-- Click in world to place + R key to rotate
UserInputService.InputBegan:Connect(function(input, gameProcessed)
-- ===== R KEY: rotate ghost model 90° on Y axis =====
if input.KeyCode == Enum.KeyCode.R then
-- Don't rotate if user is typing in a textbox (ID box, etc.)
if UserInputService:GetFocusedTextBox() then return end
if not placingMode then return end
-- Rotate 90 degrees on the Y axis
ghostRotation = ghostRotation * CFrame.Angles(0, math.rad(90), 0)
log("Rotated 90° (Y axis)", C.accent)
return
end
-- ===== MOUSE CLICK: place the ghost =====
if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then
return
end
-- Placement mode owns the world click, even if Roblox reports the
-- input as processed by another UI layer.
if not placingMode then return end
if building then return end
-- Place the ghost at current mouse position (includes rotation)
local placeCFrame = ghostModel:GetPivot()
clearGhost()
log("Placed at " .. tostring(placeCFrame.Position), C.success)
-- Start building
startBuild(placeCFrame)
end)
-- ============================================================
-- MESH SURFACE CONVERTER
-- ============================================================
-- Roblox does not expose arbitrary MeshPart triangle/vertex data to a
-- normal script, so this samples the real MeshPart with raycasts and
-- converts the detected surface into small normal Parts.
local function voxelizeMeshPart(meshPart, pivot)
if not convertMeshes or not meshPart:IsA("MeshPart") then
return nil
end
local data = origPartData[meshPart]
if not data then
return nil
end
local step = math.max(0.25, meshVoxelSize)
local maxParts = math.max(1, meshMaxParts)
local size = meshPart.Size
local half = size * 0.5
local margin = math.max(0.25, step * 0.5)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Include
params.FilterDescendantsInstances = {meshPart}
params.IgnoreWater = true
local samples = {}
local seen = {}
-- how similar two surface normals must be to merge
local NORMAL_TOLERANCE = 0.965
-- maximum number of tiles that can be merged together
local MAX_MERGE_LENGTH = 5
local function makeSurfaceCFrame(position, normal)
normal = normal.Unit
local reference = Vector3.yAxis
if math.abs(normal:Dot(reference)) > 0.95 then
reference = Vector3.xAxis
end
local right = reference:Cross(normal)
if right.Magnitude < 0.001 then
reference = Vector3.zAxis
right = reference:Cross(normal)
end
right = right.Unit
local forward = right:Cross(normal).Unit
return CFrame.fromMatrix(
position,
right,
normal,
forward
)
end
local function addHit(hit)
if not hit then
return
end
local localPos =
meshPart.CFrame:PointToObjectSpace(hit.Position)
local qx = math.floor(localPos.X / step + 0.5)
local qy = math.floor(localPos.Y / step + 0.5)
local qz = math.floor(localPos.Z / step + 0.5)
local key =
qx .. "|" ..
qy .. "|" ..
qz
if seen[key] then
return
end
seen[key] = true
table.insert(samples, {
position = hit.Position,
normal = hit.Normal.Unit,
localPos = localPos,
x = qx,
y = qy,
z = qz,
})
end
local function rayLocal(a, b)
local wa = meshPart.CFrame:PointToWorldSpace(a)
local wb = meshPart.CFrame:PointToWorldSpace(b)
local hit = workspace:Raycast(
wa,
wb - wa,
params
)
addHit(hit)
end
local xCount =
math.max(1, math.ceil(size.X / step))
local yCount =
math.max(1, math.ceil(size.Y / step))
local zCount =
math.max(1, math.ceil(size.Z / step))
------------------------------------------------------------
-- SURFACE SAMPLING
------------------------------------------------------------
-- top / bottom
for xi = 0, xCount do
local x =
-half.X +
size.X * xi / xCount
for zi = 0, zCount do
local z =
-half.Z +
size.Z * zi / zCount
rayLocal(
Vector3.new(
x,
half.Y + margin,
z
),
Vector3.new(
x,
-half.Y - margin,
z
)
)
rayLocal(
Vector3.new(
x,
-half.Y - margin,
z
),
Vector3.new(
x,
half.Y + margin,
z
)
)
end
end
-- left / right
for yi = 0, yCount do
local y =
-half.Y +
size.Y * yi / yCount
for zi = 0, zCount do
local z =
-half.Z +
size.Z * zi / zCount
rayLocal(
Vector3.new(
half.X + margin,
y,
z
),
Vector3.new(
-half.X - margin,
y,
z
)
)
rayLocal(
Vector3.new(
-half.X - margin,
y,
z
),
Vector3.new(
half.X + margin,
y,
z
)
)
end
end
-- front / back
for xi = 0, xCount do
local x =
-half.X +
size.X * xi / xCount
for yi = 0, yCount do
local y =
-half.Y +
size.Y * yi / yCount
rayLocal(
Vector3.new(
x,
y,
half.Z + margin
),
Vector3.new(
x,
y,
-half.Z - margin
)
)
rayLocal(
Vector3.new(
x,
y,
-half.Z - margin
),
Vector3.new(
x,
y,
half.Z + margin
)
)
end
end
if #samples == 0 then
return {}
end
------------------------------------------------------------
-- BUILD NEIGHBOR MAP
------------------------------------------------------------
local grid = {}
local function gridKey(x, y, z)
return x .. "|" .. y .. "|" .. z
end
for _, sample in ipairs(samples) do
grid[
gridKey(
sample.x,
sample.y,
sample.z
)
] = sample
end
------------------------------------------------------------
-- MERGE SURFACE SAMPLES
------------------------------------------------------------
local used = {}
local result = {}
local function similarNormal(a, b)
return a:Dot(b) >= NORMAL_TOLERANCE
end
local function makeTile(tileSamples)
if #result >= maxParts then
return
end
if #tileSamples == 0 then
return
end
local center = Vector3.zero
local normal = Vector3.zero
for _, sample in ipairs(tileSamples) do
center += sample.position
normal += sample.normal
end
center /= #tileSamples
normal = normal.Unit
local surfaceCFrame =
makeSurfaceCFrame(center, normal)
--------------------------------------------------------
-- determine tile dimensions
--------------------------------------------------------
local minX = math.huge
local maxX = -math.huge
local minZ = math.huge
local maxZ = -math.huge
for _, sample in ipairs(tileSamples) do
local relative =
surfaceCFrame:PointToObjectSpace(
sample.position
)
minX = math.min(minX, relative.X)
maxX = math.max(maxX, relative.X)
minZ = math.min(minZ, relative.Z)
maxZ = math.max(maxZ, relative.Z)
end
local tileX =
math.max(step, maxX - minX + step)
local tileZ =
math.max(step, maxZ - minZ + step)
-- very thin surface piece
local thickness =
math.max(0.12, step * 0.22)
table.insert(result, {
part = meshPart,
relCFrame =
pivot:ToObjectSpace(
surfaceCFrame
),
size = Vector3.new(
tileX,
thickness,
tileZ
),
color = data.color,
material = data.material,
transparency = data.transparency,
reflectance = data.reflectance,
name = data.name .. " [mesh]",
className = "Part",
obbyType = "Part",
effects = {},
spinProps = nil,
})
end
------------------------------------------------------------
-- GREEDY 2D SURFACE MERGING
------------------------------------------------------------
for _, base in ipairs(samples) do
if #result >= maxParts then
break
end
local baseKey =
gridKey(
base.x,
base.y,
base.z
)
if not used[baseKey] then
local group = {}
local minX = base.x
local maxX = base.x
local minY = base.y
local maxY = base.y
local minZ = base.z
local maxZ = base.z
table.insert(group, base)
used[baseKey] = true
----------------------------------------------------
-- expand along X
----------------------------------------------------
for direction = 1, 2 do
local sign =
direction == 1 and 1 or -1
for distance = 1, MAX_MERGE_LENGTH do
local nx =
base.x +
sign * distance
local candidate =
grid[
gridKey(
nx,
base.y,
base.z
)
]
if not candidate then
break
end
local candidateKey =
gridKey(
candidate.x,
candidate.y,
candidate.z
)
if used[candidateKey] then
break
end
if not similarNormal(
base.normal,
candidate.normal
) then
break
end
table.insert(
group,
candidate
)
used[candidateKey] = true
minX =
math.min(
minX,
candidate.x
)
maxX =
math.max(
maxX,
candidate.x
)
end
end
----------------------------------------------------
-- expand along Z
----------------------------------------------------
for direction = 1, 2 do
local sign =
direction == 1 and 1 or -1
for distance = 1, MAX_MERGE_LENGTH do
local nz =
base.z +
sign * distance
local candidate =
grid[
gridKey(
base.x,
base.y,
nz
)
]
if not candidate then
break
end
local candidateKey =
gridKey(
candidate.x,
candidate.y,
candidate.z
)
if used[candidateKey] then
break
end
if not similarNormal(
base.normal,
candidate.normal
) then
break
end
table.insert(
group,
candidate
)
used[candidateKey] = true
minZ =
math.min(
minZ,
candidate.z
)
maxZ =
math.max(
maxZ,
candidate.z
)
end
end
makeTile(group)
end
end
return result
end
local function buildMeshCache(model, pivot)
meshSurfaceCache = {}
if not convertMeshes then return end
local meshCount = 0
local generatedCount = 0
local meshObjects = {}
-- include the root object if it is itself a MeshPart
if model:IsA("MeshPart") then
table.insert(meshObjects, model)
end
-- include MeshParts inside the model
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("MeshPart") then
table.insert(meshObjects, d)
end
end
for _, p in ipairs(meshObjects) do
meshCount = meshCount + 1
local ok, result = pcall(function()
return voxelizeMeshPart(p, pivot)
end)
if ok and result and #result > 0 then
meshSurfaceCache[p] = result
generatedCount = generatedCount + #result
log("Mesh '" .. p.Name .. "': generated " .. #result .. " surface parts", C.success)
else
meshSurfaceCache[p] = {}
log("Mesh '" .. p.Name .. "': surface scan failed", C.warn)
end
end
if meshCount > 0 then
log("Mesh conversion: " .. meshCount .. " MeshParts -> " ..
generatedCount .. " surface parts", C.accent)
end
end
-- ============================================================
-- BUILD SEQUENCE
-- ============================================================
-- Collect all BaseParts from the loaded model with their relative CFrames
local function collectBricks()
if not loadedModel then return {} end
local bricks = {}
-- Use the ORIGINAL pivot (captured before the model was hidden)
local pivot = loadedModelPivot or loadedModel:GetPivot()
local skipped = 0
local totalParts = 0
-- ===== AUTO-DETECT Obby Creator part type from Roblox part shape =====
-- Uses STORED original data (the live parts are now hidden/modified)
local function detectObbyType(p, data)
-- WedgePart -> Wedge
if p:IsA("WedgePart") then return "Wedge" end
if p:IsA("CornerWedgePart") then return "CornerWedge" end
if p:IsA("TrussPart") then return "Truss" end
-- Part with Shape enum -> Ball / Cylinder / Part
if p:IsA("Part") then
if data.shape == Enum.PartType.Ball then return "Ball" end
if data.shape == Enum.PartType.Cylinder then return "Cylinder" end
return "Part"
end
-- MeshPart / UnionOperation -> check for SpecialMesh child
if p:IsA("MeshPart") or p:IsA("UnionOperation") then
for _, child in ipairs(p:GetChildren()) do
if child:IsA("SpecialMesh") then
local mt = child.MeshType
if mt == Enum.MeshType.Sphere then return "Ball" end
if mt == Enum.MeshType.Cylinder then return "Cylinder" end
if mt == Enum.MeshType.FileMesh then return "Part" end
end
end
return "Part"
end
return "Part"
end
local collectParts = {}
if loadedModel:IsA("BasePart") then
table.insert(collectParts, loadedModel)
end
for _, d in ipairs(loadedModel:GetDescendants()) do
if d:IsA("BasePart") then
table.insert(collectParts, d)
end
end
for _, p in ipairs(collectParts) do
if p:IsA("BasePart") then
totalParts = totalParts + 1
-- Get STORED original data (the live parts are now invisible/moved)
local data = origPartData[p]
if not data then
-- Fallback: read from live part (shouldn't happen, but just in case)
data = {
transparency = p.Transparency,
canCollide = p.CanCollide,
size = p.Size,
color = p.Color,
material = p.Material,
reflectance = p.Reflectance,
cframe = p.CFrame,
name = p.Name,
className = p.ClassName,
shape = p:IsA("Part") and p.Shape or nil,
}
end
-- MeshParts are represented by the generated surface blocks.
local meshSurface = meshSurfaceCache[p]
if convertMeshes and p:IsA("MeshPart") and meshSurface then
for _, meshBrick in ipairs(meshSurface) do
table.insert(bricks, meshBrick)
end
continue
end
local obbyType = detectObbyType(p, data)
-- ===== SCAN FOR EFFECTS (Fire, SurfaceLight, Texture, Decal, Particles, etc.) =====
-- Use GetDescendants to find effects at ANY depth (attachments, folders, etc.)
local effects = {}
for _, child in ipairs(p:GetDescendants()) do
if child:IsA("Fire") then
table.insert(effects, {
type = "fire",
props = {
{"Heat", child.Heat},
{"Size", child.Size},
{"Color", child.Color},
{"SecondaryColor", child.SecondaryColor},
},
})
elseif child:IsA("SurfaceLight") then
table.insert(effects, {
type = "surfacelight",
props = {
{"Face", tostring(child.Face.Name)},
{"Brightness", child.Brightness},
{"Range", child.Range},
{"Color", child.Color},
{"Angle", child.Angle},
},
})
elseif child:IsA("Texture") then
-- Extract numeric ID from "rbxassetid://12345" format
local texId = child.Texture
local numId = texId:match("%d+")
table.insert(effects, {
type = "texture",
props = {
{"Face", tostring(child.Face.Name)}, -- Face FIRST
{"Texture", numId or texId}, -- then texture ID
{"StudsPerTileU", child.StudsPerTileU},
{"StudsPerTileV", child.StudsPerTileV},
{"Transparency", child.Transparency},
},
})
elseif child:IsA("Decal") then
-- Decals use the same "texture" effect type in Obby Creator
local texId = child.Texture
local numId = texId:match("%d+")
table.insert(effects, {
type = "texture",
props = {
{"Face", tostring(child.Face.Name)}, -- Face FIRST
{"Texture", numId or texId}, -- then texture ID
{"Transparency", child.Transparency},
},
})
elseif child:IsA("ParticleEmitter") then
-- ParticleEmitter -> "particles" effect in Obby Creator
-- Extract numeric texture ID from "rbxassetid://12345" format
local pTexId = child.Texture
local pNumId = pTexId and tostring(pTexId):match("%d+") or nil
table.insert(effects, {
type = "particles",
props = {
{"Texture", pNumId or tostring(pTexId)},
{"Transparency", child.Transparency},
{"Speed", child.Speed},
{"SpreadAngle", child.SpreadAngle},
{"Rate", child.Rate},
{"Lifetime", child.Lifetime},
{"Size", child.Size},
{"LightEmission", child.LightEmission},
{"LightInfluence", child.LightInfluence},
{"Squash", child.Squash},
{"Acceleration", child.Acceleration},
{"Rotation", child.Rotation},
{"RotSpeed", child.RotSpeed},
{"EmissionDirection", tostring(child.EmissionDirection.Name)},
{"Color", child.Color}, -- ColorSequence for particles
},
})
elseif child:IsA("PointLight") then
-- PointLight -> "light" effect (NOT surfacelight)
table.insert(effects, {
type = "light",
props = {
{"Color", child.Color},
{"Range", child.Range},
{"Brightness", child.Brightness},
{"Shadows", child.Shadows},
},
})
elseif child:IsA("Smoke") then
table.insert(effects, {
type = "smoke",
props = {
{"Opacity", child.Opacity},
{"RiseVelocity", child.RiseVelocity},
{"Size", child.Size},
{"Color", child.Color},
},
})
elseif child:IsA("Sparkles") then
table.insert(effects, {
type = "sparkles",
props = {
{"SparkleColor", child.SparkleColor},
},
})
end
end
-- ===== CHECK PARENT MODEL FOR HIGHLIGHT (Outline) =====
-- Highlights are usually on the Model, not the part itself.
-- Walk up the ancestors looking for a Highlight.
local ancestor = p.Parent
while ancestor and ancestor ~= workspace do
for _, child in ipairs(ancestor:GetChildren()) do
if child:IsA("Highlight") then
table.insert(effects, {
type = "outline",
props = {
{"Color3", child.FillColor},
{"LineThickness", 0.03}, -- fixed reasonable thickness
},
})
break -- only take the first Highlight found
end
end
if #effects > 0 and effects[#effects].type == "outline" then break end
ancestor = ancestor.Parent
end
-- Also check the part's own children for Highlight (less common)
for _, child in ipairs(p:GetChildren()) do
if child:IsA("Highlight") then
table.insert(effects, {
type = "outline",
props = {
{"Color3", child.FillColor},
{"LineThickness", 0.03},
},
})
break
end
end
-- ===== DETECT SPIN PROPERTIES =====
-- Obby Creator spin parts use specific attributes or child value objects.
-- We check for common spin properties to determine if this is a spin part.
local spinProps = nil
-- Check for attributes (common in custom spin scripts)
local hasSpinAttr = p:GetAttribute("SpinTime") or p:GetAttribute("sT") or
p:GetAttribute("SpinSpeed") or p:GetAttribute("SpinOffset")
-- Check for child ValueObjects named like spin properties
local spinTimeVal = findValueObjectDeep(p, "SpinTime") or findValueObjectDeep(p, "sT")
local spinOffsetVal = findValueObjectDeep(p, "SpinOffset") or findValueObjectDeep(p, "sO")
local spinDistVal = findValueObjectDeep(p, "SpinDistance") or findValueObjectDeep(p, "sD")
if hasSpinAttr or spinTimeVal or spinOffsetVal then
spinProps = {
sO = (spinOffsetVal and spinOffsetVal.Value) or p:GetAttribute("SpinOffset") or p:GetAttribute("sO") or 1,
sT = (spinTimeVal and spinTimeVal.Value) or p:GetAttribute("SpinTime") or p:GetAttribute("sT") or 2,
sd = (spinDistVal and spinDistVal.Value) or p:GetAttribute("SpinDistance") or p:GetAttribute("sD") or 0,
sA = p:GetAttribute("SpinAngle") or p:GetAttribute("sA") or 0,
A = p:GetAttribute("SpinAxis") or p:GetAttribute("A") or "Y",
E = p:GetAttribute("EasingStyle") or p:GetAttribute("E") or "Sine",
Ed = p:GetAttribute("EasingDirection") or p:GetAttribute("Ed") or "InOut",
mR = p:GetAttribute("Reverse") or p:GetAttribute("mR") or false,
}
end
-- ===== SPLIT PARTICLES AND TEXTURES FROM OTHER EFFECTS =====
-- Multiple ParticleEmitters → clone part per particle
-- Multiple Textures/Decals on different faces → clone part per face
local particleEffects = {}
local textureEffects = {}
local otherEffects = {}
for _, e in ipairs(effects) do
if e.type == "particles" then
table.insert(particleEffects, e)
elseif e.type == "texture" then
table.insert(textureEffects, e)
else
table.insert(otherEffects, e)
end
end
-- Helper: create a brick entry
local function addBrick(effectsList)
table.insert(bricks, {
part = p,
relCFrame = pivot:ToObjectSpace(data.cframe),
size = data.size,
color = data.color,
material = data.material,
transparency = data.transparency,
reflectance = data.reflectance,
name = data.name,
className = data.className,
obbyType = obbyType,
effects = effectsList,
spinProps = spinProps, -- spin properties (nil if not a spin part)
})
end
-- Determine if we need to clone (multiple particles OR multiple textures)
local needClone = #particleEffects > 1 or #textureEffects > 1
if not needClone then
-- Normal: single brick with all effects
addBrick(effects)
else
-- Clone mode: first copy gets other effects + first texture + first particle
local firstEffects = {}
for _, e in ipairs(otherEffects) do
table.insert(firstEffects, e)
end
if #textureEffects > 0 then
table.insert(firstEffects, textureEffects[1])
end
if #particleEffects > 0 then
table.insert(firstEffects, particleEffects[1])
end
addBrick(firstEffects)
-- Additional texture copies (one per extra texture/decal face)
for i = 2, #textureEffects do
addBrick({textureEffects[i]})
end
-- Additional particle copies (one per extra particle emitter)
for i = 2, #particleEffects do
addBrick({particleEffects[i]})
end
local cloneReason = ""
if #textureEffects > 1 then cloneReason = cloneReason .. #textureEffects .. " textures " end
if #particleEffects > 1 then cloneReason = cloneReason .. #particleEffects .. " particles" end
log("Part '" .. (p.Name or "?") .. "' has " .. cloneReason ..
"— cloned into " .. (1 + math.max(0, #textureEffects - 1) + math.max(0, #particleEffects - 1)) .. " parts", C.warn)
end
end
end
-- Log a breakdown of detected types
local typeCounts = {}
for _, b in ipairs(bricks) do
typeCounts[b.obbyType] = (typeCounts[b.obbyType] or 0) + 1
end
local breakdown = {}
for t, c in pairs(typeCounts) do
table.insert(breakdown, t .. "=" .. c)
end
table.sort(breakdown)
log("Model scan: " .. totalParts .. " BaseParts, " .. skipped .. " filtered, " .. #bricks .. " to build", C.accent)
log("Type breakdown: " .. table.concat(breakdown, ", "), C.text)
-- Count effects found
local effectCount = 0
local effectTypes = {}
for _, b in ipairs(bricks) do
if b.effects then
for _, e in ipairs(b.effects) do
effectCount = effectCount + 1
effectTypes[e.type] = (effectTypes[e.type] or 0) + 1
end
end
end
if effectCount > 0 then
local eBreakdown = {}
for t, c in pairs(effectTypes) do table.insert(eBreakdown, t .. "=" .. c) end
table.sort(eBreakdown)
log("Effects found: " .. effectCount .. " (" .. table.concat(eBreakdown, ", ") .. ")", C.success)
else
log("No effects found in model", C.textDim)
end
return bricks
end
-- Apply color to a part via PaintObject + uncap
local function applyColorToPart(part, color)
local evts = ReplicatedStorage:FindFirstChild("Events")
if not evts then return end
spawn(function()
pcall(function()
if evts:FindFirstChild("PaintObject") then
evts.PaintObject:InvokeServer({part}, "Color", color)
end
if evts:FindFirstChild("BehaviourObject") then
evts.BehaviourObject:InvokeServer({part}, "C", color)
end
end)
end)
end
-- Apply material to a part via multiple bypass methods (UNCAP)
local function applyMaterialToPart(part, material)
local evts = ReplicatedStorage:FindFirstChild("Events")
if not evts then return end
local matName = material.Name -- e.g. "Neon", "Plastic", "Metal", "Wood", etc.
-- Method 1: BehaviourObject with various codes
spawn(function()
pcall(function()
if evts:FindFirstChild("BehaviourObject") then
evts.BehaviourObject:InvokeServer({part}, "Mtl", matName)
evts.BehaviourObject:InvokeServer({part}, "Material", matName)
evts.BehaviourObject:InvokeServer({part}, "MaterialName", matName)
-- Also try with the material enum value (number)
evts.BehaviourObject:InvokeServer({part}, "Mtl", material)
evts.BehaviourObject:InvokeServer({part}, "Material", material)
end
end)
end)
-- Method 2: PaintObject (sometimes handles materials too)
spawn(function()
pcall(function()
if evts:FindFirstChild("PaintObject") then
evts.PaintObject:InvokeServer({part}, "Material", matName)
evts.PaintObject:InvokeServer({part}, "Material", material)
end
end)
end)
-- Method 3: Find the Material ValueObject on the part and set it directly (uncap)
spawn(function()
pcall(function()
local matValObj = findValueObjectDeep(part, "Material")
if matValObj then
if matValObj:IsA("StringValue") then
matValObj.Value = matName
elseif matValObj:IsA("NumberValue") or matValObj:IsA("IntValue") then
matValObj.Value = material.Value -- Enum.Material numeric value
end
end
-- Also try "Mtl" value object
local mtlValObj = findValueObjectDeep(part, "Mtl")
if mtlValObj then
if mtlValObj:IsA("StringValue") then
mtlValObj.Value = matName
elseif mtlValObj:IsA("NumberValue") or mtlValObj:IsA("IntValue") then
mtlValObj.Value = material.Value
end
end
end)
end)
end
-- Apply reflectance via multiple bypass methods (UNCAP)
local function applyReflectanceToPart(part, reflectance)
local evts = ReplicatedStorage:FindFirstChild("Events")
if not evts then return end
-- Method 1: BehaviourObject with multiple codes
spawn(function()
pcall(function()
if evts:FindFirstChild("BehaviourObject") then
evts.BehaviourObject:InvokeServer({part}, "Rf", reflectance)
evts.BehaviourObject:InvokeServer({part}, "Reflectance", reflectance)
end
end)
end)
-- Method 2: PaintObject
spawn(function()
pcall(function()
if evts:FindFirstChild("PaintObject") then
evts.PaintObject:InvokeServer({part}, "Reflectance", reflectance)
end
end)
end)
-- Method 3: Direct ValueObject edit
spawn(function()
pcall(function()
local valObj = findValueObjectDeep(part, "Reflectance") or findValueObjectDeep(part, "Rf")
if valObj and (valObj:IsA("NumberValue") or valObj:IsA("IntValue") or valObj:IsA("FloatValue")) then
valObj.Value = reflectance
end
end)
end)
end
-- Apply transparency via multiple bypass methods (UNCAP)
local function applyTransparencyToPart(part, transparency)
local evts = ReplicatedStorage:FindFirstChild("Events")
if not evts then return end
-- Method 1: BehaviourObject with multiple codes
spawn(function()
pcall(function()
if evts:FindFirstChild("BehaviourObject") then
evts.BehaviourObject:InvokeServer({part}, "Tr", transparency)
evts.BehaviourObject:InvokeServer({part}, "Transparency", transparency)
end
end)
end)
-- Method 2: PaintObject
spawn(function()
pcall(function()
if evts:FindFirstChild("PaintObject") then
evts.PaintObject:InvokeServer({part}, "Transparency", transparency)
end
end)
end)
-- Method 3: Direct ValueObject edit
spawn(function()
pcall(function()
local valObj = findValueObjectDeep(part, "Transparency") or findValueObjectDeep(part, "Tr")
if valObj and (valObj:IsA("NumberValue") or valObj:IsA("IntValue") or valObj:IsA("FloatValue")) then
valObj.Value = transparency
end
end)
end)
end
function startBuild(originCFrame)
if building then return end
local bricks = collectBricks()
if #bricks == 0 then
log("No bricks found in model!", C.danger)
return
end
building = true
cancelBuild = false
buildBtn.Text = "BUILDING..."
buildBtn.BackgroundColor3 = C.cardHi
log("Starting build: " .. #bricks .. " bricks", C.accent)
spawn(function()
local evts = ReplicatedStorage:FindFirstChild("Events")
if not evts then
log("No Events folder!", C.danger)
building = false
buildBtn.Text = "BUILD MODEL"
buildBtn.BackgroundColor3 = C.success
return
end
-- ===== PHASE 1: PLACE ALL BRICKS =====
log("Phase 1: Placing bricks...", C.warn)
setProgress(0, "Phase 1/4: Placing 0/" .. #bricks)
local placedParts = {} -- {brick = brick, part = createdPart}
-- Helper: get the player's Obby.Items.Parts folder
local function getPartsFolder()
local obby = workspace:FindFirstChild("Obbies")
if not obby then return nil end
obby = obby:FindFirstChild(plr.Name)
if not obby then return nil end
if not obby:FindFirstChild("Items") then return nil end
return obby.Items:FindFirstChild("Parts")
end
for i, brick in ipairs(bricks) do
if cancelBuild then
log("Build cancelled", C.danger)
building = false
buildBtn.Text = "BUILD MODEL"
buildBtn.BackgroundColor3 = C.success
return
end
-- Calculate world CFrame for this brick (includes rotation)
local worldCFrame = originCFrame:ToWorldSpace(brick.relCFrame)
-- Get the parts folder
local partsFolder = getPartsFolder()
if not partsFolder then
log("Could not find Obby.Items.Parts folder!", C.danger)
building = false
buildBtn.Text = "BUILD MODEL"
buildBtn.BackgroundColor3 = C.success
return
end
-- ===== SNAPSHOT existing parts BEFORE AddObject =====
-- This lets us reliably find the NEW part after AddObject fires.
local existingParts = {}
for _, ch in ipairs(partsFolder:GetChildren()) do
existingParts[ch] = true
end
-- Set up ChildAdded listener BEFORE AddObject so we don't miss the event
local newPartFound = nil
local childAddedConn = partsFolder.ChildAdded:Connect(function(child)
if not existingParts[child] then
newPartFound = child
end
end)
-- 1) AddObject — create the part using the AUTO-DETECTED type for this brick
local useType = brick.obbyType or partType -- fallback to global if detection failed
local ok, err = pcall(function()
evts.AddObject:InvokeServer(useType, worldCFrame)
end)
if not ok then
log("AddObject failed: " .. tostring(err), C.danger)
end
-- 2) Find the NEW part — check snapshot first, then wait for ChildAdded
local newPart = nil
-- Check snapshot immediately
for _, ch in ipairs(partsFolder:GetChildren()) do
if not existingParts[ch] then
newPart = ch
break
end
end
-- If not found immediately, wait for ChildAdded (up to 3 seconds)
if not newPart then
local waited = 0
while not newPartFound and not newPart and waited < 3 do
-- Also re-check snapshot in case we missed it
for _, ch in ipairs(partsFolder:GetChildren()) do
if not existingParts[ch] then
newPart = ch
break
end
end
if not newPart and not newPartFound then
wait(0.05)
waited = waited + 0.05
end
end
-- Use ChildAdded result if snapshot didn't find it
if not newPart and newPartFound then
newPart = newPartFound
end
end
childAddedConn:Disconnect()
if newPart then
-- 3) MoveObject — set size + CFrame (preserves rotation)
pcall(function()
evts.MoveObject:InvokeServer({
{
[1] = newPart,
[2] = worldCFrame,
[3] = brick.size,
}
})
end)
table.insert(placedParts, {brick = brick, part = newPart})
log("Placed " .. i .. "/" .. #bricks .. ": " .. (newPart.Name or "?") .. " [" .. useType .. "]", C.text)
else
log("Could not find new part after AddObject (brick " .. i .. ")", C.danger)
end
setProgress(i / #bricks * 0.25, "Phase 1/4: Placing " .. i .. "/" .. #bricks)
wait(buildSpeed)
end
log("Phase 1 done: " .. #placedParts .. " parts placed", C.success)
-- ===== PHASE 2: APPLY COLORS + TRANSPARENCY =====
if applyColors then
log("Phase 2: Applying colors + transparency...", C.warn)
for i, entry in ipairs(placedParts) do
if cancelBuild then
log("Build cancelled during coloring", C.danger)
building = false
buildBtn.Text = "BUILD MODEL"
buildBtn.BackgroundColor3 = C.success
return
end
if entry.part and entry.part.Parent then
applyColorToPart(entry.part, entry.brick.color)
-- ALWAYS apply transparency (even if 0, to reset any default)
applyTransparencyToPart(entry.part, entry.brick.transparency)
log("Color " .. i .. "/" .. #placedParts .. ": " .. (entry.brick.name or "?") ..
" -> " .. tostring(entry.brick.color) .. " (transparency=" .. entry.brick.transparency .. ")", C.text)
end
setProgress(0.25 + i / #placedParts * 0.25, "Phase 2/4: Coloring " .. i .. "/" .. #placedParts)
wait(buildSpeed)
end
log("Phase 2 done: colors + transparency applied", C.success)
else
log("Phase 2 skipped (Colors toggle OFF)", C.textDim)
end
-- ===== PHASE 3: APPLY MATERIALS + REFLECTANCE =====
if applyMaterials then
log("Phase 3: Applying materials + reflectance...", C.warn)
for i, entry in ipairs(placedParts) do
if cancelBuild then
log("Build cancelled during materials", C.danger)
building = false
buildBtn.Text = "BUILD MODEL"
buildBtn.BackgroundColor3 = C.success
return
end
if entry.part and entry.part.Parent then
applyMaterialToPart(entry.part, entry.brick.material)
-- ALWAYS apply reflectance (even if 0, to reset any default)
applyReflectanceToPart(entry.part, entry.brick.reflectance)
log("Material " .. i .. "/" .. #placedParts .. ": " .. (entry.brick.name or "?") ..
" -> " .. entry.brick.material.Name .. " (reflectance=" .. entry.brick.reflectance .. ")", C.text)
end
setProgress(0.50 + i / #placedParts * 0.25, "Phase 3/4: Material " .. i .. "/" .. #placedParts)
wait(buildSpeed)
end
log("Phase 3 done: materials + reflectance applied", C.success)
else
log("Phase 3 skipped (Materials toggle OFF)", C.textDim)
end
-- ===== PHASE 4: APPLY EFFECTS (Fire, SurfaceLight, Texture, Decal) =====
local totalEffects = 0
for _, entry in ipairs(placedParts) do
if entry.brick.effects then
totalEffects = totalEffects + #entry.brick.effects
end
end
if totalEffects > 0 then
log("Phase 4: Applying " .. totalEffects .. " effects...", C.warn)
local effectIdx = 0
for i, entry in ipairs(placedParts) do
if cancelBuild then
log("Build cancelled during effects", C.danger)
building = false
buildBtn.Text = "BUILD MODEL"
buildBtn.BackgroundColor3 = C.success
return
end
if entry.part and entry.part.Parent and entry.brick.effects then
for _, effect in ipairs(entry.brick.effects) do
-- 1) Add the effect: EffectObject:InvokeServer({part}, type, "Default")
local addOk, addErr = pcall(function()
evts.EffectObject:InvokeServer({entry.part}, effect.type, "Default")
end)
if not addOk then
log(" Effect add FAILED: " .. tostring(addErr), C.danger)
end
wait(0.5) -- wait longer for server to create the effect
-- 2) Set each property: EffectObject:InvokeServer({part}, type, property, value)
for _, propData in ipairs(effect.props) do
local propName, propValue = propData[1], propData[2]
local propOk, propErr = pcall(function()
evts.EffectObject:InvokeServer({entry.part}, effect.type, propName, propValue)
end)
if not propOk then
log(" Property '" .. propName .. "' FAILED: " .. tostring(propErr), C.danger)
end
wait(0.1) -- small delay between each property
end
effectIdx = effectIdx + 1
-- Build a property summary for the log
local propSummary = {}
for _, pd in ipairs(effect.props) do
table.insert(propSummary, pd[1] .. "=" .. tostring(pd[2]))
end
log("Effect " .. effectIdx .. "/" .. totalEffects .. ": " ..
(entry.brick.name or "?") .. " + " .. effect.type ..
" (" .. table.concat(propSummary, ", ") .. ")", C.text)
end
end
setProgress(0.75 + i / #placedParts * 0.25, "Phase 4/5: Effects " .. i .. "/" .. #placedParts)
wait(buildSpeed)
end
log("Phase 4 done: " .. totalEffects .. " effects applied", C.success)
else
log("Phase 4: No effects found in model, skipping", C.textDim)
end
-- ===== PHASE 5: CONVERT SPIN PARTS =====
-- For any part that had spin properties detected, we create a "Spin Part"
-- at the same position, transfer all properties, then delete the original.
local spinCount = 0
for _, entry in ipairs(placedParts) do
if entry.brick.spinProps then spinCount = spinCount + 1 end
end
if spinCount > 0 then
log("Phase 5: Converting " .. spinCount .. " spin parts...", C.warn)
local spinIdx = 0
for i, entry in ipairs(placedParts) do
if cancelBuild then
log("Build cancelled during spin conversion", C.danger)
building = false
buildBtn.Text = "BUILD MODEL"
buildBtn.BackgroundColor3 = C.success
return
end
if entry.part and entry.part.Parent and entry.brick.spinProps then
local sp = entry.brick.spinProps
local origPart = entry.part
local origCFrame = origPart.CFrame
local origSize = origPart.Size
local origColor = origPart.Color
local origMaterial = origPart.Material
local origTransparency = origPart.Transparency
local origReflectance = origPart.Reflectance
-- Get the Spin Parts folder BEFORE creating
local obby = workspace:FindFirstChild("Obbies")
if obby then obby = obby:FindFirstChild(plr.Name) end
local spinFolder = obby and obby:FindFirstChild("Items") and obby.Items:FindFirstChild("Spin Parts")
if not spinFolder then
log("Could not find Spin Parts folder! Creating spin part anyway...", C.warn)
end
-- SNAPSHOT existing spin parts BEFORE AddObject
local existingSpinParts = {}
if spinFolder then
for _, ch in ipairs(spinFolder:GetChildren()) do
existingSpinParts[ch] = true
end
end
-- 1) Create the Spin Part
log("Creating Spin Part at " .. tostring(origCFrame.Position), C.accent)
local addOk, addErr = pcall(function()
evts.AddObject:InvokeServer("Spin Part", origCFrame)
end)
if not addOk then
log("Spin Part AddObject FAILED: " .. tostring(addErr), C.danger)
end
wait(0.5)
-- 2) Find the NEW Spin Part (the one not in our snapshot)
local newSpin = nil
if spinFolder then
local waited = 0
while not newSpin and waited < 3 do
for _, ch in ipairs(spinFolder:GetChildren()) do
if not existingSpinParts[ch] then
newSpin = ch
break
end
end
if not newSpin then
wait(0.1)
waited = waited + 0.1
end
end
end
if not newSpin then
log("Could not find new Spin Part! Skipping.", C.danger)
else
log("Found new Spin Part: " .. (newSpin.Name or "?"), C.accent)
-- 3) Set size + position via MoveObject
pcall(function()
evts.MoveObject:InvokeServer({
{
[1] = newSpin,
[2] = origCFrame,
[3] = origSize,
}
})
end)
wait(0.1)
-- 4) Apply color + material to the spin part
if applyColors then
applyColorToPart(newSpin, origColor)
applyTransparencyToPart(newSpin, origTransparency)
end
if applyMaterials then
applyMaterialToPart(newSpin, origMaterial)
applyReflectanceToPart(newSpin, origReflectance)
end
-- 5) Apply spin properties via BehaviourObject
local bhvr = evts:FindFirstChild("BehaviourObject")
if bhvr then
pcall(function() bhvr:InvokeServer({newSpin}, "sO", sp.sO) end)
pcall(function() bhvr:InvokeServer({newSpin}, "sT", sp.sT) end)
pcall(function() bhvr:InvokeServer({newSpin}, "sd", sp.sd) end)
pcall(function() bhvr:InvokeServer({newSpin}, "sA", sp.sA) end)
pcall(function() bhvr:InvokeServer({newSpin}, "A", sp.A) end)
pcall(function() bhvr:InvokeServer({newSpin}, "mR", sp.mR) end)
pcall(function() bhvr:InvokeServer({newSpin}, "Ed", sp.Ed) end)
-- NEVER send "Line" as easing style — crashes the server
if sp.E ~= "Line" then
pcall(function() bhvr:InvokeServer({newSpin}, "E", sp.E) end)
else
pcall(function() bhvr:InvokeServer({newSpin}, "E", "Sine") end)
end
end
-- 6) Delete the original part
pcall(function()
evts.DeleteObject:InvokeServer({origPart})
end)
spinIdx = spinIdx + 1
log("Spin " .. spinIdx .. "/" .. spinCount .. ": " .. (entry.brick.name or "?") ..
" (axis=" .. sp.A .. ", time=" .. sp.sT .. ", offset=" .. sp.sO .. ")", C.text)
end
end
setProgress(0.80 + i / #placedParts * 0.20, "Phase 5/5: Spin Parts " .. i .. "/" .. #placedParts)
wait(buildSpeed)
end
log("Phase 5 done: " .. spinCount .. " spin parts converted", C.success)
else
log("Phase 5: No spin parts found, skipping", C.textDim)
end
setProgress(1, "Build complete! " .. #placedParts .. " bricks")
log("=== BUILD COMPLETE ===", C.success)
building = false
buildBtn.Text = "BUILD MODEL"
buildBtn.BackgroundColor3 = C.success
end)
end
buildBtn.MouseButton1Click:Connect(function()
if building then
log("Already building — cancel first", C.warn)
return
end
if not loadedModel then
log("Load a model first!", C.danger)
return
end
if not placingMode and not ghostModel then
-- Auto-start ghost mode
startGhost()
log("Click in world to place, then build starts", C.accent)
return
end
end)
cancelBtn.MouseButton1Click:Connect(function()
if building then
cancelBuild = true
log("Cancelling...", C.warn)
else
clearGhost()
log("Nothing to cancel", C.textDim)
end
end)
-- ============================================================
-- LOAD MODEL
-- ============================================================
-- ===== LOAD MODEL FUNCTION (shared by LOAD and FORCE LOAD buttons) =====
local function loadModel(id, forceMode)
loadStatus.Text = "Loading model " .. id .. (forceMode and " (FORCE)..." or "...")
loadStatus.TextColor3 = C.warn
log("Loading model ID " .. id .. (forceMode and " (FORCE MODE)" or ""), C.accent)
-- Clean up previous model
if loadedModel then loadedModel:Destroy() end
clearGhost()
spawn(function()
local model = nil
local loadErr = nil
-- Timeout helper: runs a function and returns nil if it takes too long
local function loadWithTimeout(func, timeoutSec)
local result = nil
local done = false
local co = coroutine.create(function()
local ok, res = pcall(func)
result = {ok = ok, res = res}
done = true
end)
coroutine.resume(co)
local waited = 0
while not done and waited < timeoutSec do
wait(0.1)
waited = waited + 0.1
end
if not done then
log("Load method timed out after " .. timeoutSec .. "s", C.warn)
return nil, "timeout"
end
if not result.ok then
return nil, tostring(result.res)
end
return result.res, nil
end
-- Method 1: game:GetObjects (executor function — preferred) — 10s timeout
local result, err = loadWithTimeout(function()
return game:GetObjects("rbxassetid://" .. id)
end, 10)
if result then
if type(result) == "table" then
if #result > 0 then
model = result[1]
log("Loaded via game:GetObjects (table)", C.success)
else
loadErr = "game:GetObjects returned empty"
log(loadErr, C.warn)
end
elseif typeof(result) == "Instance" then
model = result
log("Loaded via game:GetObjects (instance)", C.success)
else
loadErr = "game:GetObjects returned: " .. typeof(result)
log(loadErr, C.warn)
end
else
loadErr = err or "unknown error"
log("game:GetObjects failed: " .. loadErr, C.warn)
end
-- Method 2: InsertService fallback — 10s timeout
if not model then
result, err = loadWithTimeout(function()
return InsertService:LoadAsset(id)
end, 10)
if result then
model = result
if model:IsA("Model") then
local inner = model:FindFirstChildOfClass("Model")
if inner then model = inner end
end
log("Loaded via InsertService", C.success)
else
loadErr = err or "unknown error"
end
end
-- Method 3 (FORCE MODE only): try alternatives with timeouts
if not model and forceMode then
log("FORCE MODE: trying alternative load methods...", C.warn)
-- Try without rbxassetid prefix — 10s timeout
result, err = loadWithTimeout(function()
return game:GetObjects(id)
end, 10)
if result then
if type(result) == "table" and #result > 0 then
model = result[1]
log("Loaded via game:GetObjects (raw ID)", C.success)
elseif typeof(result) == "Instance" then
model = result
log("Loaded via game:GetObjects (raw ID, instance)", C.success)
end
end
-- Try with https URL — 10s timeout
if not model then
result, err = loadWithTimeout(function()
return game:GetObjects("https://www.roblox.com/item?id=" .. id)
end, 10)
if result then
if type(result) == "table" and #result > 0 then
model = result[1]
log("Loaded via game:GetObjects (https URL)", C.success)
elseif typeof(result) == "Instance" then
model = result
log("Loaded via game:GetObjects (https URL, instance)", C.success)
end
end
end
-- Try LoadAsset one more time — 10s timeout
if not model then
result, err = loadWithTimeout(function()
return InsertService:LoadAsset(id)
end, 10)
if result then
model = result
if model:IsA("Model") then
local inner = model:FindFirstChildOfClass("Model")
if inner then model = inner end
end
log("Loaded via InsertService (retry)", C.success)
else
loadErr = err or "unknown error"
end
end
end
if not model then
loadStatus.Text = "Failed to load"
loadStatus.TextColor3 = C.danger
log("Failed to load model: " .. tostring(loadErr), C.danger)
if not forceMode then
log("Try FORCE LOAD button — it tries alternative methods.", C.warn)
else
log("All load methods failed. This model may be a package or copylocked.", C.warn)
end
return
end
-- ===== FIND THE RICHEST MODEL (most BaseParts) =====
local function countBaseParts(obj)
local count = 0
if obj:IsA("BasePart") then count = 1 end
for _, d in ipairs(obj:GetDescendants()) do
if d:IsA("BasePart") then count = count + 1 end
end
return count
end
local bestModel = model
local bestCount = countBaseParts(model)
log("Initial object: " .. model.ClassName .. " '" .. (model.Name or "?") .. "' with " .. bestCount .. " BaseParts", C.text)
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("Model") or d:IsA("Folder") or d:IsA("Tool") or d:IsA("Accoutrement") then
local c = countBaseParts(d)
if c > bestCount then
bestCount = c
bestModel = d
log("Found richer model: " .. d.ClassName .. " '" .. (d.Name or "?") .. "' with " .. c .. " BaseParts", C.text)
end
end
end
model = bestModel
local brickCount = bestCount
if brickCount == 0 then
loadStatus.Text = "No parts in model"
loadStatus.TextColor3 = C.danger
log("Model has no BaseParts", C.danger)
return
end
-- Log model structure breakdown
local meshParts = 0
local regularParts = 0
local unions = 0
local other = 0
local breakdownParts = {}
if model:IsA("BasePart") then
table.insert(breakdownParts, model)
end
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") then
table.insert(breakdownParts, d)
end
end
for _, d in ipairs(breakdownParts) do
if d:IsA("MeshPart") then
meshParts = meshParts + 1
elseif d:IsA("UnionOperation") then
unions = unions + 1
elseif d:IsA("Part") then
regularParts = regularParts + 1
else
other = other + 1
end
end
log("Breakdown: " .. regularParts .. " Parts, " .. meshParts .. " MeshParts, " .. unions .. " Unions, " .. other .. " other", C.accent)
-- Store the model (hide it — make all parts invisible + move far below)
loadedModel = model
-- Capture the ORIGINAL pivot BEFORE moving
loadedModelPivot = model:GetPivot()
-- ===== CAPTURE ORIGINAL PART DATA BEFORE HIDING =====
-- collectBricks will use this snapshot instead of reading from the
-- (now-modified) live parts. This prevents the "511 filtered, 0 to build" bug.
origPartData = {}
local snapshotParts = {}
-- include the root if the loaded object itself is a BasePart
if model:IsA("BasePart") then
table.insert(snapshotParts, model)
end
-- include any BaseParts inside the model
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") then
table.insert(snapshotParts, d)
end
end
for _, p in ipairs(snapshotParts) do
origPartData[p] = {
transparency = p.Transparency,
canCollide = p.CanCollide,
size = p.Size,
color = p.Color,
material = p.Material,
reflectance = p.Reflectance,
cframe = p.CFrame,
name = p.Name,
className = p.ClassName,
shape = p:IsA("Part") and p.Shape or nil,
}
end
log("Captured " .. #model:GetDescendants() .. " descendants (" .. brickCount .. " BaseParts) before hiding", C.text)
-- Scan meshes while they are still queryable at their original positions.
model.Parent = workspace
buildMeshCache(model, loadedModelPivot)
model:PivotTo(CFrame.new(0, -1000, 0))
for _, p in ipairs(model:GetDescendants()) do
if p:IsA("BasePart") then
p.Transparency = 1
p.CanCollide = false
p.CanQuery = false
p.CanTouch = false
end
end
loadStatus.Text = "Loaded: " .. brickCount .. " bricks"
loadStatus.TextColor3 = C.success
log("Model loaded: " .. brickCount .. " bricks. Click PLACE to start.", C.success)
end)
end
loadBtn.MouseButton1Click:Connect(function()
local idText = idBox.Text
local id = tonumber(idText:match("%d+"))
if not id then
loadStatus.Text = "Invalid ID"
loadStatus.TextColor3 = C.danger
log("Invalid model ID: " .. idText, C.danger)
return
end
loadModel(id, false)
end)
forceLoadBtn.MouseButton1Click:Connect(function()
local idText = idBox.Text
local id = tonumber(idText:match("%d+"))
if not id then
loadStatus.Text = "Invalid ID"
loadStatus.TextColor3 = C.danger
log("Invalid model ID: " .. idText, C.danger)
return
end
loadModel(id, true)
end)
-- ============================================================
-- INIT
-- ============================================================
log("=== Model Importer ===", C.accent)
log("1. Enter model ID -> click LOAD", C.text)
log("2. Click PLACE -> ghost follows mouse", C.text)
log("3. Click in world -> build starts", C.text)
log("Phases: place all -> color all -> material all", C.text)
log("Mesh conversion: " .. (convertMeshes and "ON" or "OFF") ..
" | voxel size=" .. tostring(meshVoxelSize) ..
" | max/mesh=" .. tostring(meshMaxParts), C.text)
end, function(err)
local msg = tostring(err) .. "\n" .. debug.traceback()
pcall(function() warn("[IMPORTER ERROR] " .. msg) end)
pcall(function()
local sg = Instance.new("ScreenGui")
sg.Name = "ImporterError"
sg.Parent = game:GetService("CoreGui")
local tf = Instance.new("TextLabel")
tf.Size = UDim2.new(0.6, 0, 0.6, 0)
tf.Position = UDim2.new(0.2, 0, 0.2, 0)
tf.BackgroundColor3 = Color3.fromRGB(20, 0, 0)
tf.TextColor3 = Color3.new(1, 1, 1)
tf.Text = "[IMPORTER ERROR]\n" .. msg
tf.Font = Enum.Font.Gotham
tf.TextSize = 12
tf.TextWrapped = true
tf.TextXAlignment = Enum.TextXAlignment.Left
tf.TextYAlignment = Enum.TextYAlignment.Top
tf.Parent = sg
end)
return msg
end)
if not _ok then
pcall(function() print("[IMPORTER] Script failed: " .. tostring(_err)) end)
endHow to Execute Obby Creator Script – MODEL IMPORTER KIDNA WORKS WITH MESHES
Step 1: Copy Code: Click the Copy Code button in the viewer above to grab the clean Lua loadstring.
Step 2: Launch Executor: Open a compatible, unpatched executor. If you need one, check our Roblox Executors Directory.
Step 3: Join Experience: Launch Roblox and join Obby Creator.
Step 4: Attach & Run: Attach/inject your executor into the Roblox process, paste the code into the script tab, and press Execute.
Created By
View Author ProfileFeatures & Information
Script Changelog (1)
Community Comments (1)
Frequently Asked Questions
How do I execute Obby Creator Script – MODEL IMPORTER KIDNA WORKS WITH MESHES in Roblox?
To run Obby Creator Script – MODEL IMPORTER KIDNA WORKS WITH MESHES, copy the Lua loadstring from the code viewer above, launch Roblox and join Obby Creator, attach a working executor (such as Potassium, Solara, or Delta from our Executors directory), paste the code into your executor console, and click Execute.
Is Obby Creator Script – MODEL IMPORTER KIDNA WORKS WITH MESHES keyless or does it require an access key?
Obby Creator Script – MODEL IMPORTER KIDNA WORKS WITH MESHES requires an access key. You can retrieve the official key directly using the Get Script Key link provided above.
Is Obby Creator Script – MODEL IMPORTER KIDNA WORKS WITH MESHES currently safe and working?
This script is active and operational. Always test scripts using a secondary (alt) Roblox account to safeguard your main profile against anti-cheat bans.





great