Advertisement
nonogamer9

nono's 3D OBJ viewer for ComputerCraft!

Sep 23rd, 2024 (edited)
62
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 8.66 KB | Software | 0 0
  1. if not fs.exists("3d") then fs.makeDir("3d") end
  2.  
  3. local objList = {
  4.     {n="Teapot",u="https://raw.githubusercontent.com/alecjacobson/common-3d-test-models/master/data/teapot.obj"},
  5.     {n="Suzanne",u="https://raw.githubusercontent.com/alecjacobson/common-3d-test-models/master/data/suzanne.obj"},
  6.     {n="Beetle",u="https://raw.githubusercontent.com/alecjacobson/common-3d-test-models/master/data/beetle.obj"}
  7. }
  8.  
  9. local function centerText(text)
  10.     local w, _ = term.getSize()
  11.     local x = math.floor((w - #text) / 2)
  12.     term.setCursorPos(x, term.getCursorPos())
  13.     print(text)
  14. end
  15.  
  16. local function showMenu()
  17.     term.clear()
  18.     centerText("=== nono's 3D OBJ viewer for ComputerCraft ===")
  19.     print("\nSelect an .obj file:")
  20.     for i,obj in ipairs(objList) do
  21.         print(i..". "..obj.n)
  22.     end
  23.     print("Enter choice (1-"..#objList..") or 0 to exit:")
  24.     local c = tonumber(read())
  25.     if c == 0 then return nil end
  26.     return c and c >= 1 and c <= #objList and objList[c] or nil
  27. end
  28.  
  29. local function downloadObj(obj)
  30.     centerText("Downloading "..obj.n.."...")
  31.     local f = "3d/"..obj.n..".obj"
  32.     if fs.exists(f) then
  33.         print("File already exists. Use cached version? (y/n)")
  34.         if read():lower() == "y" then return true end
  35.     end
  36.     local success = shell.run("wget", obj.u, f)
  37.     return success
  38. end
  39.  
  40. local function parseObj(f)
  41.     local v,faces,vn = {},{},{}
  42.     for l in io.lines(f) do
  43.         local p = {}
  44.         for w in l:gmatch("%S+") do table.insert(p,w) end
  45.         if p[1]=="v" then
  46.             table.insert(v,{x=tonumber(p[2]),y=tonumber(p[3]),z=tonumber(p[4])})
  47.         elseif p[1]=="vn" then
  48.             table.insert(vn,{x=tonumber(p[2]),y=tonumber(p[3]),z=tonumber(p[4])})
  49.         elseif p[1]=="f" then
  50.             local face = {}
  51.             for i=2,#p do
  52.                 local indices = {}
  53.                 for index in p[i]:gmatch("(%d+)/?") do
  54.                     table.insert(indices, tonumber(index))
  55.                 end
  56.                 table.insert(face, indices)
  57.             end
  58.             table.insert(faces,face)
  59.         end
  60.     end
  61.     if #faces == 0 then
  62.         for l in io.lines(f) do
  63.             local p = {}
  64.             for w in l:gmatch("%S+") do table.insert(p,w) end
  65.             if p[1]=="f" then
  66.                 local face = {}
  67.                 for i=2,#p do
  68.                     table.insert(face, {tonumber(p[i])})
  69.                 end
  70.                 table.insert(faces,face)
  71.             end
  72.         end
  73.     end
  74.     return v,faces,vn
  75. end
  76.  
  77. local function rotateX(p,a) return {x=p.x,y=p.y*math.cos(a)-p.z*math.sin(a),z=p.y*math.sin(a)+p.z*math.cos(a)} end
  78. local function rotateY(p,a) return {x=p.x*math.cos(a)+p.z*math.sin(a),y=p.y,z=-p.x*math.sin(a)+p.z*math.cos(a)} end
  79. local function rotateZ(p,a) return {x=p.x*math.cos(a)-p.y*math.sin(a),y=p.x*math.sin(a)+p.y*math.cos(a),z=p.z} end
  80. local function translate(p,tx,ty,tz) return {x=p.x+tx, y=p.y+ty, z=p.z+tz} end
  81.  
  82. local function project(p,w,h,zoom,offsetX,offsetY)
  83.     local f = math.min(w, h) * zoom
  84.     local x = (p.x*f)/(p.z+10)+w/2+offsetX
  85.     local y = (p.y*f)/(p.z+10)+h/2+offsetY
  86.     return math.floor(x),math.floor(y)
  87. end
  88.  
  89. local function getAsciiChar(intensity)
  90.     local chars = " .'`^\",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$"
  91.     return chars:sub(math.floor(intensity * #chars) + 1, math.floor(intensity * #chars) + 1)
  92. end
  93.  
  94. local function showControlGuide()
  95.     term.clear()
  96.     centerText("=== Control Guide ===")
  97.     print("\nWASD: Rotate model")
  98.     print("IJKL: Move model")
  99.     print("UO: Move model forward/backward")
  100.     print("Arrow Up/Down: Zoom in/out")
  101.     print("X: Exit viewer")
  102.     print("\nPress any key to continue...")
  103.     os.pullEvent("key")
  104. end
  105.  
  106. local function render(v,faces,vn)
  107.     local w,h = term.getSize()
  108.     local ax,ay,az,zoom = 0,0,0,0.5
  109.     local tx,ty,tz = 0,0,0
  110.     local running = true
  111.     local buffer = {}
  112.     local lastTime = os.clock()
  113.     local frameCount = 0
  114.     local fps = 0
  115.  
  116.     for y=1,h do
  117.         buffer[y] = {}
  118.         for x=1,w do
  119.             buffer[y][x] = {char=" ", depth=math.huge}
  120.         end
  121.     end
  122.  
  123.     while running do
  124.         local startTime = os.clock()
  125.  
  126.         for y=1,h do
  127.             for x=1,w do
  128.                 buffer[y][x] = {char=" ", depth=math.huge}
  129.             end
  130.         end
  131.  
  132.         local p2d = {}
  133.         for i,vert in ipairs(v) do
  134.             local rotated = rotateZ(rotateY(rotateX(vert,ax),ay),az)
  135.             local translated = translate(rotated,tx,ty,tz)
  136.             p2d[i] = {project(translated,w,h,zoom,0,0)}
  137.         end
  138.  
  139.         for _,face in ipairs(faces) do
  140.             local v1,v2,v3 = v[face[1][1]], v[face[2][1]], v[face[3][1]]
  141.             local normal = {
  142.                 x=(v2.y-v1.y)*(v3.z-v1.z)-(v2.z-v1.z)*(v3.y-v1.y),
  143.                 y=(v2.z-v1.z)*(v3.x-v1.x)-(v2.x-v1.x)*(v3.z-v1.z),
  144.                 z=(v2.x-v1.x)*(v3.y-v1.y)-(v2.y-v1.y)*(v3.x-v1.x)
  145.             }
  146.  
  147.             local mag = math.sqrt(normal.x^2 + normal.y^2 + normal.z^2)
  148.             if mag > 0 then
  149.                 normal.x, normal.y, normal.z = normal.x/mag, normal.y/mag, normal.z/mag
  150.             else
  151.                 normal.x, normal.y, normal.z = 0, 0, 1
  152.             end
  153.  
  154.             local intensity = math.max(0.1, normal.x*0.5 + normal.y*0.5 + normal.z*0.5)
  155.  
  156.             for i=1,#face do
  157.                 local x1,y1 = p2d[face[i][1]][1],p2d[face[i][1]][2]
  158.                 local x2,y2 = p2d[face[i%#face+1][1]][1],p2d[face[i%#face+1][1]][2]
  159.                 local depth = (v[face[i][1]].z + v[face[i%#face+1][1]].z) / 2
  160.  
  161.                 local dx,dy = math.abs(x2-x1), math.abs(y2-y1)
  162.                 local sx,sy = x1 < x2 and 1 or -1, y1 < y2 and 1 or -1
  163.                 local err = dx-dy
  164.  
  165.                 while true do
  166.                     if x1 > 0 and x1 <= w and y1 > 0 and y1 <= h then
  167.                         if depth < buffer[y1][x1].depth then
  168.                             buffer[y1][x1] = {char=getAsciiChar(intensity), depth=depth}
  169.                         end
  170.                     end
  171.  
  172.                     if x1 == x2 and y1 == y2 then break end
  173.                     local e2 = 2*err
  174.                     if e2 > -dy then err = err - dy; x1 = x1 + sx end
  175.                     if e2 < dx then err = err + dx; y1 = y1 + sy end
  176.                 end
  177.             end
  178.         end
  179.  
  180.         term.clear()
  181.         for y=1,h do
  182.             for x=1,w do
  183.                 term.setCursorPos(x,y)
  184.                 term.write(buffer[y][x].char)
  185.             end
  186.         end
  187.  
  188.         frameCount = frameCount + 1
  189.         if os.clock() - lastTime >= 1 then
  190.             fps = frameCount
  191.             frameCount = 0
  192.             lastTime = os.clock()
  193.         end
  194.  
  195.         term.setCursorPos(1,1)
  196.         term.write("FPS: " .. fps .. " | Zoom: "..string.format("%.2f", zoom).." | Pos: "..string.format("%.2f", tx)..", "..string.format("%.2f", ty)..", "..string.format("%.2f", tz))
  197.         term.setCursorPos(1,2)
  198.         term.write("Rot: "..string.format("%.2f", ax)..", "..string.format("%.2f", ay)..", "..string.format("%.2f", az))
  199.         term.setCursorPos(1,3)
  200.         term.write("WASD: rotate, IJKL: move, UO: forward/back, Arrows: zoom, X: exit")
  201.  
  202.         local event, key = os.pullEvent("key")
  203.         if key == keys.w then ax = ax + 0.1
  204.         elseif key == keys.s then ax = ax - 0.1
  205.         elseif key == keys.a then ay = ay - 0.1
  206.         elseif key == keys.d then ay = ay + 0.1
  207.         elseif key == keys.q then az = az + 0.1
  208.         elseif key == keys.e then az = az - 0.1
  209.         elseif key == keys.i then ty = ty + 0.1
  210.         elseif key == keys.k then ty = ty - 0.1
  211.         elseif key == keys.j then tx = tx - 0.1
  212.         elseif key == keys.l then tx = tx + 0.1
  213.         elseif key == keys.u then tz = tz + 0.1
  214.         elseif key == keys.o then tz = tz - 0.1
  215.         elseif key == keys.up then zoom = zoom * 1.1
  216.         elseif key == keys.down then zoom = zoom / 1.1
  217.         elseif key == keys.x then running = false
  218.         end
  219.  
  220.         local endTime = os.clock()
  221.         local frameTime = endTime - startTime
  222.         if frameTime < 0.05 then
  223.             os.sleep(0.05 - frameTime)
  224.         end
  225.     end
  226. end
  227.  
  228. while true do
  229.     showControlGuide()
  230.     local obj = showMenu()
  231.     if obj then
  232.         if downloadObj(obj) then
  233.             local path = "3d/"..obj.n..".obj"
  234.             local v,f,vn = parseObj(path)
  235.             if v and f then
  236.                 print("Rendering " .. obj.n .. " model...")
  237.                 os.sleep(1)
  238.                 render(v,f,vn)
  239.             else
  240.                 print("Failed to parse .obj file.")
  241.             end
  242.         end
  243.     else
  244.         print("Exiting...")
  245.         break
  246.     end
  247. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement