Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- os.loadAPI(shell.resolve(".").."/matrix")
- os.loadAPI(shell.resolve(".").."/graphics")
- os.loadAPI(shell.resolve(".").."/vertex")
- local w,h = term.getSize()
- --[[
- 3D Render
- Display objects (and eventually textures) mapped in 3D coordinate space
- Makes use of the graphics API for projection and matrix API for transformations
- By Nitrogen Fingers
- Feel free to play with this code as much as you want!
- ]]--
- local verbose = false
- camera = {
- position = vertex.new(0,0,0);
- rX = 0; rY = 0; rZ = 0;
- }
- tetrahedron = {
- name = "Tetrahedron";
- vertices = {
- [1] = vertex.new(0,-3,0);
- [2] = vertex.new(0,3,4.5);
- [3] = matrix.createRotationY((math.pi*2)/3):apply(vertex.new(0,3,4.5));
- [4] = matrix.createRotationY((math.pi*4)/3):apply(vertex.new(0,3,4.5));
- };
- colors = {
- [1] = colors.red;
- [2] = colors.yellow;
- [3] = colors.lime;
- [4] = colors.lightBlue;
- };
- indices = {
- 1,3,2, 3,1,4, 4,1,2, 2,3,4
- };
- scale = 1;
- translate = vector.new(0,0,5);
- rotateX = 0; rotateY = 0; rotateZ = 0;
- }
- cube = {
- name = "Cube";
- vertices = {
- [1] = vertex.new(3,3,-3),
- [2] = vertex.new(3,-3,-3),
- [3] = vertex.new(-3,-3,-3),
- [4] = vertex.new(-3,3,-3),
- [5] = vertex.new(3,3,3),
- [6] = vertex.new(3,-3,3),
- [7] = vertex.new(-3,-3,3),
- [8] = vertex.new(-3,3,3);
- };
- colors = {
- [1] = colors.red;
- [2] = colors.blue;
- [3] = colors.lime;
- [4] = colors.yellow;
- [5] = colors.orange;
- [6] = colors.white;
- [7] = colours.lightBlue;
- [8] = colours.purple;
- };
- --Triangle list: that is each 3 indices make up 1 face (12 total for this model)
- --Must be CLOCKWISE to avoid culling
- indices = {
- 1,2,3, 1,3,4, 7,6,8, 8,6,5, 3,7,4, 4,7,8,
- 5,6,2, 2,1,5, 5,1,4, 4,8,5, 2,6,3, 6,7,3
- };
- scale = 1;
- translate = vector.new(0,0,5);
- rotateX = 0; rotateY = 0; rotateZ = 0;
- }
- hourglass = {
- name = "Hourglass";
- vertices = {
- --Top
- [1] = vertex.new(3,-3,3);
- [2] = vertex.new(3,-3,-3);
- [3] = vertex.new(-3,-3,-3);
- [4] = vertex.new(-3,-3,3);
- --Centre
- [5] = vertex.new(0,0,0);
- --Bottom
- [6] = vertex.new(3,3,3);
- [7] = vertex.new(3,3,-3);
- [8] = vertex.new(-3,3,-3);
- [9] = vertex.new(-3,3,3);
- };
- colors = {
- [1] = colours.red;
- [2] = colours.orange;
- [3] = colours.yellow;
- [4] = colours.white;
- [5] = colours.black; --should never be used
- [6] = colours.blue;
- [7] = colours.lightGrey;
- [8] = colours.lightBlue;
- [9] = colours.cyan;
- };
- indices = {
- --Upper hourglass --Upper Base
- 1,2,5, 2,3,5, 3,4,5, 4,1,5, 1,3,2, 1,4,3,
- --Lower hourglass --Lower Base
- 6,5,7, 7,5,8, 8,5,9, 9,5,6, 6,7,8, 6,8,9
- };
- scale = 1;
- translate = vector.new(0,0,5);
- rotateX = 0; rotateY = 0; rotateZ = 0;
- }
- --This thing is a freaking monster.
- tiefighter = {
- name = "TIE Fighter";
- vertices = {
- --Left wing- centre then ring around
- [1] = vertex.new(-3.5,0,0);
- [2] = vertex.new(-3.5,-4,-1.5);
- [3] = vertex.new(-3.5,0,-2.5);
- [4] = vertex.new(-3.5,4,-1.5);
- [5] = vertex.new(-3.5,4,1.5);
- [6] = vertex.new(-3.5,0,2.5);
- [7] = vertex.new(-3.5,-4,1.5);
- --Right wing- centre then ring around
- [8] = vertex.new(3.5,0,0);
- [9] = vertex.new(3.5,-4,-1.5);
- [10] = vertex.new(3.5,0,-2.5);
- [11] = vertex.new(3.5,4,-1.5);
- [12] = vertex.new(3.5,4,1.5);
- [13] = vertex.new(3.5,0,2.5);
- [14] = vertex.new(3.5,-4,1.5);
- --Left Connector (what a waste of computation...)
- [15] = vertex.new(-3,-0.25,0);
- [16] = vertex.new(-1.5,-0.25,0);
- [17] = vertex.new(-1.5,0.25,0);
- [18] = vertex.new(-3,0.25,0);
- --Right connector
- [19] = vertex.new(3,-0.25,0);
- [20] = vertex.new(1.5,-0.25,0);
- [21] = vertex.new(1.5,0.25,0);
- [22] = vertex.new(3,0.25,0);
- --Cockpit
- [23] = vertex.new(1,-1,-1);
- [24] = vertex.new(1,-1,1);
- [25] = vertex.new(-1,-1,1);
- [26] = vertex.new(-1,-1,-1);
- [27] = vertex.new(1.5,0,-1.5);
- [28] = vertex.new(1.5,0,1.5);
- [29] = vertex.new(-1.5,0,1.5);
- [30] = vertex.new(-1.5,0,-1.5);
- [31] = vertex.new(1,1,-1);
- [32] = vertex.new(1,1,1);
- [33] = vertex.new(-1,1,1);
- [34] = vertex.new(-1,1,-1);
- };
- --Far fewer colors. We're being sneaky with our indices to make this easier.
- colors = {
- [1] = colours.grey;
- [8] = colours.grey;
- [15] = colours.lightGrey;
- [19] = colours.lightGrey;
- [23] = colours.lightGrey;
- [24] = colours.grey;
- [25] = colours.lightGrey;
- [26] = colours.grey;
- [31] = colours.lightGrey;
- [32] = colours.lightGrey;
- [33] = colours.lightGrey;
- [34] = colours.lightGrey;
- };
- --So there are some duplicates, name for the wings and connectors as we need them to display
- --on both sides (making them polygons is just too expensive).
- indices = {
- --Left Wing clockwise
- 1,2,3, 1,3,4, 1,4,5, 1,5,6, 1,6,7, 1,7,2,
- --counter-clockwise
- 1,2,7, 1,7,6, 1,6,5, 1,5,4, 1,4,3, 1,3,2,
- --Right Wing clockwise
- 8,9,10, 8,10,11, 8,11,12, 8,12,13, 8,13,14, 8,14,9,
- --counter-clockwise
- 8,9,14, 8,14,13, 8,13,12, 8,12,11, 8,11,10, 8,10,9,
- --Left connector, clockwise and counter
- 15,16,17, 15,17,18, 15,17,16, 15,18,17,
- --Right connector, clockwise and counter
- 19,20,21, 19,21,22, 19,21,20, 19,22,21,
- --Cockpit, base and roof
- 23,24,25, 23,25,26, 31,33,32, 31,34,33,
- --Cockpit, sides
- 23,27,28, 23,24,28, 24,28,29, 24,29,25, 25,29,30, 25,30,26, 26,30,27, 26,27,23,
- 31,32,27, 31,28,27, 32,33,28, 32,28,29, 33,34,29, 33,29,30, 34,31,30, 34,30,27
- };
- --A little thing I threw in- makes it a bit nicer to look at
- wfcol = colours.lightGrey;
- scale = 1;
- translate = vector.new(0,0,5);
- rotateX = 0; rotateY = 0; rotateZ = 0;
- }
- local modelList = {tetrahedron, cube, hourglass, tiefighter}
- local msel = 1
- local wireframe = false
- function minPoint(v1,v2)
- if v1.y < v2.y then return v1
- elseif v1.y > v2.y then return v2
- elseif v1.x < v2.x then return v1
- else return v2 end
- end
- function maxPoint(v1,v2)
- if v1.y < v2.y then return v2
- elseif v1.y > v2.y then return v1
- elseif v1.x < v2.x then return v2
- else return v1 end
- end
- --Ordering has to be precisely as follows
- -- V1 is the topmost and leftmost of the three vertices
- -- V3 is the bottommost and rightmost
- -- V2 is what's left over
- --It's very inefficient but it saves computational expense in the longrun.
- --NOTE: We now also floor the
- function orderPoints(v1,v2,v3)
- local mini = minPoint(minPoint(v1,v2),v3)
- mini.x = math.floor(mini.x) mini.y = math.floor(mini.y)
- local maxi = maxPoint(maxPoint(v1,v2),v3)
- --This seems to make clipping worse, so I've left it out. True also for midi
- --maxi.x = math.floor(maxi.x) maxi.y = math.floor(maxi.y)
- local midi = nil
- if v1 ~= mini and v1 ~= maxi then return mini,v1,maxi
- elseif v2 ~= mini and v2 ~= maxi then return mini,v2,maxi
- else return mini,v3,maxi end
- end
- --The 'illusion of depth' function, really just for debugging and demo work.
- function drawPointVert(vert)
- term.setCursorPos(vert.x, vert.y)
- if vert.dist < 5 then
- term.setBackgroundColour(vert.color)
- term.write(" ")
- term.setBackgroundColour(colours.black)
- elseif vert.dist < 10 then
- term.setTextColour(vert.color)
- term.write("0")
- elseif vert.dist < 15 then
- term.setTextColour(vert.color)
- term.write("*")
- else
- term.setTextColour(vert.color)
- term.write(".")
- end
- end
- function clearScreen()
- term.setBackgroundColour(colours.black)
- term.clear()
- end
- --Breshman's Implementation
- local function drawLine(x1, y1, x2, y2, colour)
- x1 = math.floor(x1)
- x2 = math.floor(x2)
- y1 = math.floor(y1)
- y2 = math.floor(y2)
- term.setBackgroundColour(colour)
- local steep = math.abs(x2 - x1) < math.abs(y2 - y1)
- if steep then
- local cup = x1
- x1 = y1
- y1 = cup
- cup = x2
- x2 = y2
- y2 = cup
- end
- local dx,dy = math.abs(x2 - x1), math.abs(y2 - y1)
- local err = dx/2
- local ystep,y = 0,y1
- local inc = 0
- if x2 < x1 then inc = -1 else inc = 1 end
- if y2 < y1 then ystep = -1 else ystep = 1 end
- for x = x1, x2, inc do
- if steep then
- term.setCursorPos(y, x)
- else term.setCursorPos(x, y) end
- term.write(" ")
- err = err - dy
- if err < 0 then
- y = y + ystep
- err = err + dx
- end
- end
- end
- --Assuming that V1 is the top of the triangle, and V2 is the left edge of the bottom
- function fillBottomTriangle(v1, v2, v3, colour)
- local leftSlope = (v2.x - v1.x) / (v2.y - v1.y)
- local rightSlope = (v3.x - v1.x) / (v3.y - v1.y)
- leftx = v1.x
- rightx = v1.x
- for y = v1.y, v2.y do
- drawLine(leftx, y, rightx, y, colour)
- leftx = leftx + leftSlope
- rightx = rightx + rightSlope
- end
- end
- --Assuming V3 is bottom of triangle and V1 is left edge of top
- function fillTopTriangle(v1, v2, v3, colour)
- local leftSlope = (v1.x - v3.x) / (v1.y - v3.y)
- local rightSlope = (v2.x - v3.x) / (v2.y - v3.y)
- leftx = v3.x
- rightx = v3.x
- for y = v3.y, v1.y, -1 do
- drawLine(leftx, y, rightx, y, colour)
- leftx = leftx - leftSlope
- rightx = rightx - rightSlope
- end
- end
- --Fills a flat triangle. It uses top and bottom, individually if the triangle is flat on one
- --side, otherwise splits the triangle in two and draws both sides.
- function fillFlatTriangle(p1, p2, p3, colour)
- local v1,v2,v3 = orderPoints(p1,p2,p3)
- term.setCursorPos(1,h-1)
- term.setBackgroundColour(colour)
- if v2.y == v3.y then
- fillBottomTriangle(v1,v2,v3, colour)
- elseif v1.y == v2.y then
- fillTopTriangle(v1,v2,v3, colour)
- else
- v4 = { x = v1.x + ((v2.y - v1.y) / (v3.y - v1.y)) * (v3.x - v1.x), y = v2.y }
- fillBottomTriangle(v1, v2, v4, colour)
- fillTopTriangle(v2, v4, v3, colour)
- end
- end
- --Draws our 3D models. Right now it only draws the vertices, not faces or lines.
- function renderModel(model)
- world = {}
- --Note: Matrix operations, especially rotations are expensive. Rather than re-creating
- --matrices over and over as done here, it is much safer to initialize them at the start
- --of a program, and only update when necessary. If something is beyond the view frustrum,
- --don't render it!!!
- --Creating the world space
- global = matrix.identity()
- global = global * matrix.createScale(model.scale)
- global = global * matrix.createRotationX(model.rotateX)
- global = global * matrix.createRotationY(model.rotateY)
- global = global * matrix.createRotationZ(model.rotateZ)
- global = global * matrix.createTranslation(model.translate)
- distances = {}
- for i,vertex in ipairs(model.vertices) do
- table.insert(world,global:apply(vertex))
- table.insert(distances, math.sqrt(math.pow(camera.position.x - world[i].x, 2) +
- math.pow(camera.position.y - world[i].y, 2) +
- math.pow(camera.position.z - world[i].z, 2)))
- end
- --Converting from world space to camera space
- global = matrix.identity()
- global = global * matrix.createTranslation(-camera.position)
- global = global * matrix.createRotationX(-camera.rX)
- for i,vertex in ipairs(world) do
- --Applying camera space
- world[i] = global:apply(vertex)
- end
- local projection = {}
- --Project camera space into screen space
- for i=1,#world do
- local col = world[i].color
- projection[i] = graphics.project(world[i])
- --Here until we sort out depth buffering
- projection[i].color = model.colors[i]
- projection[i].dist = distances[i]
- end
- --For culling, we first find the direction the camera is facing.
- local cameraFacing = (matrix.createRotationX(camera.rX)):apply(vector.new(0, 0, -1))
- --Preparing our faces to be drawn, depth buffering and so on...
- local faces = {}
- for i=1,#model.indices, 3 do
- --We compute the face normal first (a line perpendicular to that face)
- local v1,v2,v3 = world[model.indices[i]], world[model.indices[i+1]], world[model.indices[i+2]]
- --An average centre (less precise but more reliable than a centroid for right-angles)
- local vc = vector.new((v1.x + v2.x + v3.x)/3, (v1.y + v2.y + v3.y)/3, (v1.z + v2.z + v3.z)/3)
- --Only opposite facing triangles are draw- we cull same-side triangles
- --(who knew it would be that easy?)
- local faceNormal = (v1 - v2):cross(v3 - v2)
- --We don't draw faces behind the camera either
- local cameraPF = matrix.createRotationX(camera.rX):apply(vector.new(0,0,-1)) + camera.position
- if faceNormal:dot(cameraFacing) <= 0 and cameraPF:dot(vc) < 0 then
- local face = {
- p1 = projection[model.indices[i]],
- p2 = projection[model.indices[i+1]],
- p3 = projection[model.indices[i+2]],
- --Placeholder. Will be coloured by a different system soon...
- colour = model.colors[model.indices[i]],
- distance = (camera.position - vc):length()
- }
- local prefSide = 1
- for i=1,#faces do
- if faces[i].distance < face.distance then break
- else prefSide = prefSide+1 end
- end
- table.insert(faces, prefSide, face)
- end
- end
- term.clear()
- --Preparing to draw faces
- for _,face in pairs(faces) do
- fillFlatTriangle(face.p1, face.p2, face.p3, face.colour)
- --This isn't very expensive, and it removes artefacts around edges, which is nice.
- local linecol = model.wfcol or face.colour
- drawLine(face.p1.x, face.p1.y, face.p2.x, face.p2.y, linecol)
- drawLine(face.p2.x, face.p2.y, face.p3.x, face.p3.y, linecol)
- drawLine(face.p1.x, face.p1.y, face.p3.x, face.p3.y, linecol)
- end
- end
- --Again, a little demo function to show off the operations
- function printHUD()
- term.setBackgroundColour(colours.black)
- term.setTextColour(colours.white)
- term.setCursorPos(1,h-1)
- term.write("Drawing "..modelList[msel].name.." +/- to switch")
- term.setCursorPos(1,h)
- term.write("Arrows move, WSAD QE rotate, RF scale. Enter quits.")
- end
- --Lets you play with the 3D model a bit.
- function run()
- quit = false
- repeat
- clearScreen()
- renderModel(modelList[msel])
- printHUD()
- local _,key = os.pullEvent("key")
- --Arrow keys translate
- if key == keys.up then
- modelList[msel].translate.z = modelList[msel].translate.z + 1
- elseif key == keys.down then
- modelList[msel].translate.z = modelList[msel].translate.z - 1
- elseif key == keys.left then
- modelList[msel].translate.x = modelList[msel].translate.x - 1
- elseif key == keys.right then
- modelList[msel].translate.x = cube.translate.x + 1
- --WSAD QE keys rotate
- elseif key == keys.w then
- modelList[msel].rotateX = modelList[msel].rotateX - (math.pi/16)
- elseif key == keys.s then
- modelList[msel].rotateX = modelList[msel].rotateX + (math.pi/16)
- elseif key == keys.q then
- modelList[msel].rotateZ = modelList[msel].rotateZ - (math.pi/16)
- elseif key == keys.e then
- modelList[msel].rotateZ = modelList[msel].rotateZ + (math.pi/16)
- elseif key == keys.a then
- modelList[msel].rotateY = modelList[msel].rotateY + (math.pi/16)
- elseif key == keys.d then
- modelList[msel].rotateY = modelList[msel].rotateY - (math.pi/16)
- --RF scales
- elseif key == keys.r then
- modelList[msel].scale = modelList[msel].scale * 1.1
- elseif key == keys.f then
- modelList[msel].scale = modelList[msel].scale / 1.1
- elseif key == keys.v then
- verbose = not verbose
- -- +/- switches model
- elseif key == keys.minus then
- msel = msel-1 if msel == 0 then msel = #modelList end
- elseif key == keys.equals then
- msel = (msel % #modelList) + 1
- --Enter quits
- elseif key == keys.enter then
- quit = true
- end
- until quit == true
- end
- run()
- shell.run("clear")
Advertisement
Add Comment
Please, Sign In to add comment