local plr = game:GetService("Players").LocalPlayer; local rs = game:GetService("ReplicatedStorage"); local buildevt = rs.Remotes.BuildEvent; local objfile = "model.obj"; local mtlfile = "model.mtl"; local voxsize = 0.5; local offset = plr.Character.HumanoidRootPart.Position; local centermodel = true; local createdparts = {}; local waitingforparts = false; buildevt.OnClientEvent:Connect(function(parts) if parts and waitingforparts then for _, part in parts do table.insert(createdparts, part); end end end) local function loadtexture(filename) local filepath = filename .. ".txt"; if not isfile(filepath) then warn("texture file not found:", filepath); return nil; end local data = readfile(filepath); local lines = {}; for line in data:gmatch("[^\r\n]+") do table.insert(lines, line); end local width, height = lines[1]:match("(%d+)%s+(%d+)"); width = tonumber(width); height = tonumber(height); local pixels = {}; for i = 2, #lines do local r, g, b = lines[i]:match("([%d%.]+)%s+([%d%.]+)%s+([%d%.]+)"); if r and g and b then table.insert(pixels, Color3.new(tonumber(r), tonumber(g), tonumber(b))); end end print("loaded texture:", filename, width .. "x" .. height, #pixels, "pixels"); return { width = width, height = height, pixels = pixels }; end local function sampletexture(tex, u, v) if not tex then return Color3.new(0.8, 0.8, 0.8); end u = u % 1; v = v % 1; if u < 0 then u = u + 1; end if v < 0 then v = v + 1; end v = 1 - v; local x = math.floor(u * tex.width); local y = math.floor(v * tex.height); x = math.clamp(x, 0, tex.width - 1); y = math.clamp(y, 0, tex.height - 1); local idx = y * tex.width + x + 1; return tex.pixels[idx] or Color3.new(0.8, 0.8, 0.8); end local function parsemtl(data) local mats = {}; local current; for line in data:gmatch("[^\r\n]+") do local stripped = line:match("^%s*(.-)%s*$"); if stripped:sub(1, 1) ~= "#" and stripped ~= "" then local cmd = stripped:match("^(%S+)"); if cmd == "newmtl" then current = stripped:match("newmtl%s+(.+)"); mats[current] = {color = Color3.new(0.8, 0.8, 0.8), texture = nil}; elseif cmd == "Kd" and current then local r, g, b = stripped:match("Kd%s+([%d%.%-eE]+)%s+([%d%.%-eE]+)%s+([%d%.%-eE]+)"); if r and g and b then mats[current].color = Color3.new(tonumber(r), tonumber(g), tonumber(b)); end elseif cmd == "map_Kd" and current then local texfile = stripped:match("map_Kd%s+(.+)"); if texfile then texfile = texfile:match("([^/\\]+)$"); print("loading texture:", texfile, "for material:", current); mats[current].texture = loadtexture(texfile); end end end end return mats; end local function parseobj(data) local verts = {}; local uvs = {}; local faces = {}; local currentmtl; for line in data:gmatch("[^\r\n]+") do local stripped = line:match("^%s*(.-)%s*$"); if stripped:sub(1, 1) ~= "#" and stripped ~= "" then local cmd = stripped:match("^(%S+)"); if cmd == "v" then local x, y, z = stripped:match("v%s+([%d%.%-eE]+)%s+([%d%.%-eE]+)%s+([%d%.%-eE]+)"); if x and y and z then table.insert(verts, Vector3.new(tonumber(x), tonumber(y), tonumber(z))); end elseif cmd == "vt" then local u, v = stripped:match("vt%s+([%d%.%-eE]+)%s+([%d%.%-eE]+)"); if u and v then table.insert(uvs, Vector2.new(tonumber(u), tonumber(v))); end elseif cmd == "f" then local vindices = {}; local uvindices = {}; for vert in stripped:gmatch("(%S+)") do if vert ~= "f" then local parts = {}; for part in vert:gmatch("[^/]+") do table.insert(parts, part); end if #parts >= 1 then local vidx = tonumber(parts[1]); if vidx < 0 then vidx = #verts + vidx + 1; end table.insert(vindices, vidx); end if #parts >= 2 and parts[2] ~= "" then local uvidx = tonumber(parts[2]); if uvidx < 0 then uvidx = #uvs + uvidx + 1; end table.insert(uvindices, uvidx); end end end if #vindices >= 3 then table.insert(faces, { vindices = vindices, uvindices = #uvindices > 0 and uvindices or nil, mtl = currentmtl }); end elseif cmd == "usemtl" then currentmtl = stripped:match("usemtl%s+(.+)"); end end end print("parsed", #verts, "verts,", #uvs, "uvs,", #faces, "faces"); return verts, uvs, faces; end local function getbounds(verts) local minv = Vector3.new(math.huge, math.huge, math.huge); local maxv = Vector3.new(-math.huge, -math.huge, -math.huge); for _, v in verts do minv = Vector3.new(math.min(minv.X, v.X), math.min(minv.Y, v.Y), math.min(minv.Z, v.Z)); maxv = Vector3.new(math.max(maxv.X, v.X), math.max(maxv.Y, v.Y), math.max(maxv.Z, v.Z)); end return minv, maxv; end local function barycentric(p, a, b, c) local v0 = b - a; local v1 = c - a; local v2 = p - a; local d00 = v0:Dot(v0); local d01 = v0:Dot(v1); local d11 = v1:Dot(v1); local d20 = v2:Dot(v0); local d21 = v2:Dot(v1); local denom = d00 * d11 - d01 * d01; if math.abs(denom) < 0.0001 then return 0.33, 0.33, 0.33; end local v = (d11 * d20 - d01 * d21) / denom; local w = (d00 * d21 - d01 * d20) / denom; local u = 1 - v - w; return u, v, w; end local function triangleboxoverlap(boxcenter, boxhalfsize, v0, v1, v2) local v0c = v0 - boxcenter; local v1c = v1 - boxcenter; local v2c = v2 - boxcenter; local e0 = v1c - v0c; local e1 = v2c - v1c; local e2 = v0c - v2c; local fex = math.abs(e0.X); local fey = math.abs(e0.Y); local fez = math.abs(e0.Z); local p0 = e0.Z * v0c.Y - e0.Y * v0c.Z; local p2 = e0.Z * v2c.Y - e0.Y * v2c.Z; local rad = fez * boxhalfsize.Y + fey * boxhalfsize.Z; if math.min(p0, p2) > rad or math.max(p0, p2) < -rad then return false; end p0 = -e0.Z * v0c.X + e0.X * v0c.Z; p2 = -e0.Z * v2c.X + e0.X * v2c.Z; rad = fez * boxhalfsize.X + fex * boxhalfsize.Z; if math.min(p0, p2) > rad or math.max(p0, p2) < -rad then return false; end p0 = e0.Y * v0c.X - e0.X * v0c.Y; local p1 = e0.Y * v1c.X - e0.X * v1c.Y; rad = fey * boxhalfsize.X + fex * boxhalfsize.Y; if math.min(p0, p1) > rad or math.max(p0, p1) < -rad then return false; end fex = math.abs(e1.X); fey = math.abs(e1.Y); fez = math.abs(e1.Z); p0 = e1.Z * v0c.Y - e1.Y * v0c.Z; p2 = e1.Z * v2c.Y - e1.Y * v2c.Z; rad = fez * boxhalfsize.Y + fey * boxhalfsize.Z; if math.min(p0, p2) > rad or math.max(p0, p2) < -rad then return false; end p0 = -e1.Z * v0c.X + e1.X * v0c.Z; p2 = -e1.Z * v2c.X + e1.X * v2c.Z; rad = fez * boxhalfsize.X + fex * boxhalfsize.Z; if math.min(p0, p2) > rad or math.max(p0, p2) < -rad then return false; end p0 = e1.Y * v0c.X - e1.X * v0c.Y; p1 = e1.Y * v1c.X - e1.X * v1c.Y; rad = fey * boxhalfsize.X + fex * boxhalfsize.Y; if math.min(p0, p1) > rad or math.max(p0, p1) < -rad then return false; end fex = math.abs(e2.X); fey = math.abs(e2.Y); fez = math.abs(e2.Z); p0 = e2.Z * v0c.Y - e2.Y * v0c.Z; p1 = e2.Z * v1c.Y - e2.Y * v1c.Y; rad = fez * boxhalfsize.Y + fey * boxhalfsize.Z; if math.min(p0, p1) > rad or math.max(p0, p1) < -rad then return false; end p0 = -e2.Z * v0c.X + e2.X * v0c.Z; p1 = -e2.Z * v1c.X + e2.X * v1c.Z; rad = fez * boxhalfsize.X + fex * boxhalfsize.Z; if math.min(p0, p1) > rad or math.max(p0, p1) < -rad then return false; end p0 = e2.Y * v0c.X - e2.X * v0c.Y; p2 = e2.Y * v2c.X - e2.X * v2c.Y; rad = fey * boxhalfsize.X + fex * boxhalfsize.Y; if math.min(p0, p2) > rad or math.max(p0, p2) < -rad then return false; end local minc = Vector3.new(math.min(v0c.X, v1c.X, v2c.X), math.min(v0c.Y, v1c.Y, v2c.Y), math.min(v0c.Z, v1c.Z, v2c.Z)); local maxc = Vector3.new(math.max(v0c.X, v1c.X, v2c.X), math.max(v0c.Y, v1c.Y, v2c.Y), math.max(v0c.Z, v1c.Z, v2c.Z)); if minc.X > boxhalfsize.X or maxc.X < -boxhalfsize.X then return false; end if minc.Y > boxhalfsize.Y or maxc.Y < -boxhalfsize.Y then return false; end if minc.Z > boxhalfsize.Z or maxc.Z < -boxhalfsize.Z then return false; end return true; end local function voxelize(verts, uvs, faces, mats, size) local voxmap = {}; local minbound, maxbound = getbounds(verts); if centermodel then local center = (minbound + maxbound) / 2; for i, v in verts do verts[i] = v - center; end minbound, maxbound = getbounds(verts); end local boxhalfsize = Vector3.new(size / 2, size / 2, size / 2); for fidx, face in faces do local vindices = face.vindices; if #vindices >= 3 then local v0 = verts[vindices[1]]; local v1 = verts[vindices[2]]; local v2 = verts[vindices[3]]; if not v0 or not v1 or not v2 then continue; end local mat = face.mtl and mats[face.mtl]; local tex = mat and mat.texture; local uv0, uv1, uv2; if face.uvindices and #face.uvindices >= 3 then uv0 = uvs[face.uvindices[1]]; uv1 = uvs[face.uvindices[2]]; uv2 = uvs[face.uvindices[3]]; end local trimin = Vector3.new( math.min(v0.X, v1.X, v2.X), math.min(v0.Y, v1.Y, v2.Y), math.min(v0.Z, v1.Z, v2.Z) ); local trimax = Vector3.new( math.max(v0.X, v1.X, v2.X), math.max(v0.Y, v1.Y, v2.Y), math.max(v0.Z, v1.Z, v2.Z) ); local startx = math.floor(trimin.X / size) * size; local starty = math.floor(trimin.Y / size) * size; local startz = math.floor(trimin.Z / size) * size; local endx = math.ceil(trimax.X / size) * size; local endy = math.ceil(trimax.Y / size) * size; local endz = math.ceil(trimax.Z / size) * size; local x = startx; while x <= endx do local y = starty; while y <= endy do local z = startz; while z <= endz do local boxcenter = Vector3.new(x + size / 2, y + size / 2, z + size / 2); if triangleboxoverlap(boxcenter, boxhalfsize, v0, v1, v2) then local color = mat and mat.color or Color3.new(0.8, 0.8, 0.8); if tex and uv0 and uv1 and uv2 then local u, v, w = barycentric(boxcenter, v0, v1, v2); local uvcoord = uv0 * u + uv1 * v + uv2 * w; color = sampletexture(tex, uvcoord.X, uvcoord.Y); end local key = string.format("%.3f,%.3f,%.3f", x, y, z); voxmap[key] = {pos = Vector3.new(x, y, z), color = color}; end z = z + size; end y = y + size; end x = x + size; end end if fidx % 100 == 0 then print("voxelizing face", fidx, "/", #faces); task.wait(); end end return voxmap; end local function build() if not isfile(objfile) then warn("obj file not found"); return; end print("parsing obj file..."); local objdata = readfile(objfile); local verts, uvs, faces = parseobj(objdata); local mats = {}; if isfile(mtlfile) then print("parsing mtl file..."); local mtldata = readfile(mtlfile); mats = parsemtl(mtldata); end print("voxelizing mesh..."); local voxmap = voxelize(verts, uvs, faces, mats, voxsize); local voxlist = {}; for _, vox in voxmap do table.insert(voxlist, vox); end print("placing", #voxlist, "voxels"); waitingforparts = true; for i, vox in voxlist do local worldpos = offset + vox.pos; createdparts = {}; buildevt:FireServer("Create", "Part", worldpos); local timeout = 0; while #createdparts == 0 and timeout < 50 do task.wait(0.01); timeout += 1; end if #createdparts > 0 then buildevt:FireServer("Color", {createdparts[1]}, vox.color); end if i % 50 == 0 then print("progress:", i, "/", #voxlist); task.wait(0.1); end end waitingforparts = false; print("build complete!"); end build();