OBJ To Build
Key Required

OBJ To Build

[SKY ASSASSIN] Jujutsu Shenanigansxenon9012xenon9012 1.9K views 60 executesUpdated Jan 29, 2026
1.7/ 5(3 votes)
1233% Positive
Lua Script•428 lines (14352 chars)
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();

Features & Information

take any 3d model and put it in ur exploits workspace and rename it to "model.obj" aswell as the mtl. From my discord server of unfinished projects. I won't update this.

Community Comments (5)

Nig*67Nig*67Jan 29, 2026

bro put it where

Replying to @Nig*67
xenon9012xenon9012Jan 29, 2026

@Nig67 figure it out. It's an old project.

Replying to @xenon9012
Nig*67Nig*67Jan 29, 2026

@Syneergee prob a click bait

Replying to @Nig*67
xenon9012xenon9012Jan 29, 2026

@Nig67 the source is right there, how can u say its click bait

Replying to @xenon9012
Nig*67Nig*67Jan 29, 2026

@Syneergee spill the beans dude

More Scripts for [SKY ASSASSIN] Jujutsu Shenanigans