Code: Select all
local vec3 = require("math/vec3")
local mat4 = require("math/mat4")
local bmpParser = require("bmp_parser")
local objLoader = require("obj_loader/obj_loader")
-- Window dimensions
local WINDOW_WIDTH, WINDOW_HEIGHT = 640, 360
-- Import commonly used math functions
local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil
local bor, band, lshift, rshift = bit.bor, bit.band, bit.lshift, bit.rshift
local rad = math.rad
-- Load the FFI library to work with C-style data structures
local ffi = require("ffi")
-- Define the framebuffer and depthbuffer data structures
ffi.cdef[[
typedef struct {
uint16_t* pixels;
} FramebufferData;
typedef struct {
float* depth;
} DepthbufferData;
]]
-- Create a table to store the framebuffer, depthbuffer, and texture data
local textureData = {
framebuffer = love.image.newImageData(WINDOW_WIDTH, WINDOW_HEIGHT, "rgb565"),
depthbuffer = love.image.newImageData(WINDOW_WIDTH, WINDOW_HEIGHT, "r32f"),
textureData = bmpParser.parse("gimp.bmp")
}
-- Get pointers to the pixel data using FFI
local framebufferPtr = ffi.cast("uint16_t*", textureData.framebuffer:getFFIPointer())
local depthbufferPtr = ffi.cast("float*", textureData.depthbuffer:getFFIPointer())
local texturePtr = ffi.cast("uint16_t*", textureData.textureData:getFFIPointer())
-- Set the default filter for the framebuffer image
love.graphics.setDefaultFilter("nearest", "nearest", 0)
-- Create the framebuffer Image object
local framebufferImage = love.graphics.newImage(textureData.framebuffer)
-- Clear the framebuffer and depthbuffer with a specific color and depth value
local function clearFramebuffer(color)
for i = 0, WINDOW_WIDTH * WINDOW_HEIGHT - 1 do
framebufferPtr[i] = color
depthbufferPtr[i] = -1e6
end
end
-- Refresh the framebuffer Image with the latest pixel data
local function refreshFramebuffer()
framebufferImage:replacePixels(textureData.framebuffer)
end
-- Calculate the bounding box of a triangle
local function computeBoundingBox(x1, y1, x2, y2, x3, y3, width, height)
local minX = min(x1, x2, x3)
local minY = min(y1, y2, y3)
local maxX = max(x1, x2, x3)
local maxY = max(y1, y2, y3)
-- Ensure the bounding box is within the screen bounds
minX = max(minX, 0)
minY = max(minY, 0)
maxX = min(maxX, width - 1)
maxY = min(maxY, height - 1)
return floor(minX), floor(minY), floor(maxX), floor(maxY)
end
-- Rasterize a triangle and update the framebuffer and depthbuffer
local function rasterizeTriangle(x1, y1, z1, u1, v1, x2, y2, z2, u2, v2, x3, y3, z3, u3, v3, texturePtr, framebufferPtr, depthbufferPtr, width, height)
local minX, minY, maxX, maxY = computeBoundingBox(x1, y1, x2, y2, x3, y3, width, height)
local recip_z1 = 1 / z1
local recip_z2 = 1 / z2
local recip_z3 = 1 / z3
for y = minY, maxY, 1 do
for x = minX, maxX do
local denom = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3)
local w1 = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / denom
local w2 = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / denom
local w3 = 1.0 - w1 - w2
-- Check if the pixel is inside the triangle
if w1 >= 0 and w2 >= 0 and w3 >= 0 then
-- Calculate the depth of the current pixel
local depth = w1 * recip_z1 + w2 * recip_z2 + w3 * recip_z3
local index = y * width + x
-- Check if the current pixel is closer than the one in the Z-buffer
if depth > depthbufferPtr[index] then
-- Calculate UV coordinates
local u = max(1, min(128, floor((w1 * u1 + w2 * u2 + w3 * u3) % 128)))
local v = max(1, min(128, floor((w1 * v1 + w2 * v2 + w3 * v3) % 128)))
-- Update the framebuffer and the Z-buffer
framebufferPtr[index] = texturePtr[bor(lshift(v, 7), u)]
depthbufferPtr[index] = depth
end
end
end
end
end
-- Perform backface culling to skip triangles facing away from the camera
local function isBackfaceCulled(x1, y1, z1, x2, y2, z2, x3, y3, z3)
-- Calculate the triangle normal
local edge1_x = x2 - x1
local edge1_y = y2 - y1
local edge2_x = x3 - x1
local edge2_y = y3 - y1
-- Calculate the normal vector
local normalZ = edge1_x * edge2_y - edge1_y * edge2_x
-- Check if the triangle is facing away from the camera
if normalZ < 0 then
return true -- Backface, skip the triangle
end
return false -- Triangle is visible, process it
end
-- Transform a vertex using the final transformation matrix
local function transformVertex(finalMatrix, vertex)
local x, y, z = mat4_multiplyVector(finalMatrix, vertex.x, vertex.y, vertex.z)
return x, y, z
end
-- Load the 3D model
local model = objLoader.load("test.obj")
-- Set up the view and projection matrices
local NEAR, FAR = 0.1, 1000
local aspect = WINDOW_WIDTH / WINDOW_HEIGHT
local viewMatrix, projectionMatrix, modelMatrix, finalMatrix = mat4_new(), mat4_new(), mat4_new(), mat4_new()
-- Set up the view matrix
mat4_pointAt(0, -0.5, -2, 0, 0, 0, 0, 1, 0, viewMatrix)
-- Set up the projection matrix
mat4_makeProjection(rad(60), aspect, NEAR, FAR, projectionMatrix)
local rotation = 0
-- Update function called every frame
function love.update(dt)
-- Clear the framebuffer
clearFramebuffer(0x008F)
-- Iterate through the triangles of the 3D model
for i = 0, model.numFaces - 1, 1 do
-- Rotate the model
rotation = rotation + dt * 0.01
mat4_makeRotationY(rad(rotation), modelMatrix)
-- Combine the projection, view, and model matrices
mat4_multiply(projectionMatrix, viewMatrix, finalMatrix)
mat4_multiply(finalMatrix, modelMatrix, finalMatrix)
-- Transform the vertices using the final matrix
local x1, y1, z1 = transformVertex(finalMatrix, model.vertices[model.faces[i].v1])
local x2, y2, z2 = transformVertex(finalMatrix, model.vertices[model.faces[i].v2])
local x3, y3, z3 = transformVertex(finalMatrix, model.vertices[model.faces[i].v3])
-- Perform backface culling
if not isBackfaceCulled(x1, y1, z1, x2, y2, z2, x3, y3, z3) then
-- Rasterize the triangle
rasterizeTriangle(
(x1 + 1) * 0.5 * (WINDOW_WIDTH - 1), (y1 + 1) * 0.5 * (WINDOW_HEIGHT - 1), z1,
model.textureCoords[model.faces[i].vt1].u * 128, model.textureCoords[model.faces[i].vt1].v * 128,
(x2 + 1) * 0.5 * (WINDOW_WIDTH - 1), (y2 + 1) * 0.5 * (WINDOW_HEIGHT - 1), z2,
model.textureCoords[model.faces[i].vt2].u * 128, model.textureCoords[model.faces[i].vt2].v * 128,
(x3 + 1) * 0.5 * (WINDOW_WIDTH - 1), (y3 + 1) * 0.5 * (WINDOW_HEIGHT - 1), z3,
model.textureCoords[model.faces[i].vt3].u * 128, model.textureCoords[model.faces[i].vt3].v * 128,
texturePtr, framebufferPtr, depthbufferPtr, WINDOW_WIDTH, WINDOW_HEIGHT
)
end
end
-- Refresh the framebuffer Image
refreshFramebuffer()
-- Set the window title to display the current FPS
love.window.setTitle("FPS : " .. love.timer.getFPS())
end
-- Draw the framebuffer Image to the screen
function love.draw()
love.graphics.draw(framebufferImage, 0, 0)
end