Skipping a slow grind on mobile is way easier when you use a solid Obby Creator script to speed up your building sessions.
You get to make an obby, showcase or cart ride, build alone or with friends, earn likes to climb the leaderboards, and play millions of user-created obbies while using InsertService to place parts so it is 100 percent guaranteed to work also works with meshes kinda but very good it does material color and shape and only works with 3d assets from roblox.
Check out the working code below and test it out in your next build session.
Obby Creator Script – MODEL IMPORTER KIDNA WORKS WITH MESHES & More
--[[
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)
endCopy the code above into a compatible Roblox script executor if you choose to test it.
Obby Creator Script Status
Roblox and game updates can affect compatibility, so the script’s status should be treated as temporary rather than guaranteed.
Frequently Asked Questions
What is a Obby Creator script?
A Obby Creator script is third-party Lua code intended to run alongside the Roblox game through a compatible script executor.
What features does this Obby Creator script include?
Different scripts contain different code. This version includes: MODEL IMPORTER KIDNA WORKS WITH MESHES.
How do you use the Obby Creator script?
Pick a compatible executor such as Hydrogen, MacSploit, AWP.GG, Delta, Fluxus, Codex, launch Roblox and join the game, copy the complete Lua code from the script card above, paste it into the executor’s editor, and click Run or Execute. Compatibility changes over time, so a name here doesn’t guarantee current support.
Can a Obby Creator script get your Roblox account banned?
Yes. Third-party scripts can violate Roblox rules and may put an account at risk. Only use code from a source you trust, never enter your Roblox password into a script or executor, and test on an alternate account first.
Does the Obby Creator script work on mobile?
Check whether both the executor and the script support your device and the current Roblox version. Mobile executor support does not guarantee that every script will work.