nonogamer9

nono's 3D OBJ viewer for ComputerCraft!

Sep 23rd, 2024 (edited)
336
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