Learn how to create a stunning 3D starfield flythrough in LÖVE (Lua)

A starfield zoom effect simulates flying through space at high speed. Each star is positioned in 3D space, and we project it onto a 2D screen using perspective math. The closer a star gets to the camera, the larger and faster it appears to move.
Download the full code here: main.lua or follow the steps below.
First, we set up our LÖVE environment and define our variables:
function love.load()
love.window.setTitle("Starfield Zoom")
-- Screen dimensions
width = love.graphics.getWidth()
height = love.graphics.getHeight()
centerX = width / 2
centerY = height / 2
-- Star configuration
numStars = 400
speed = 200
stars = {}
-- Create all stars
for i = 1, numStars do
table.insert(stars, createStar())
end
end
What's happening: We store the screen center (our vanishing point), create an empty table to hold stars, and initialize 400 stars.
Each star needs an X, Y, and Z position. We use polar coordinates to distribute them evenly around the center:
function createStar()
local angle = love.math.random() * math.pi * 2
local distance = love.math.random(1, 500)
return {
x = math.cos(angle) * distance,
y = math.sin(angle) * distance,
z = love.math.random(1, 100)
}
end
angle - Random angle in radians (0 to 2π)x = cos(angle) × distance - Horizontal positiony = sin(angle) × distance - Vertical positionz - Depth (distance from camera, 1-100)Every frame, we move stars closer to the camera by decreasing their Z value:
function love.update(dt)
for i, star in ipairs(stars) do
-- Move star towards camera
star.z = star.z - speed * dt
-- Reset star if it passes the camera
if star.z <= 0 then
local angle = love.math.random() * math.pi * 2
local distance = love.math.random(1, 500)
star.x = math.cos(angle) * distance
star.y = math.sin(angle) * distance
star.z = 100
end
end
end
Key points:
dt (delta time) ensures smooth movement regardless of frame ratez <= 0, the star has passed the camera, so we respawn it far awayThis is where the illusion happens. We convert 3D coordinates to 2D screen positions:
-- Project 3D coordinates to 2D screen
local k = 128 / star.z
local px = star.x * k + centerX
local py = star.y * k + centerY
screenX = (3D_X × focalLength / Z) + centerX
screenY = (3D_Y × focalLength / Z) + centerY
Example: If a star is at x=100, z=50, then px = (100 × 128/50) + centerX = 256 + centerX
We create a trail effect by drawing a line from the previous position to current position:
-- Calculate previous position for trail
local prevZ = star.z + speed * (1/60)
local prevK = 128 / prevZ
local prevPx = star.x * prevK + centerX
local prevPy = star.y * prevK + centerY
-- Star brightness based on distance
local brightness = 1 - (star.z / 100)
local size = brightness * 3
love.graphics.setColor(brightness, brightness, brightness, 1)
love.graphics.setLineWidth(size)
love.graphics.line(prevPx, prevPy, px, py)
love.graphics.circle("fill", px, py, size)
Why this works:
Finally, add keyboard controls to make it interactive:
function love.keypressed(key)
if key == "escape" then
love.event.quit()
elseif key == "space" then
-- Reset all stars
stars = {}
for i = 1, numStars do
table.insert(stars, createStar())
end
elseif key == "up" then
speed = speed + 50
elseif key == "down" then
speed = math.max(50, speed - 50)
end
end
The formula k = focalLength / z is the heart of perspective. As Z increases (object moves away), k decreases, making the projected position closer to the center and smaller.
Multiplying movement by dt ensures consistent speed across different frame rates. Without it, slow computers would make stars move slower!
Instead of creating and destroying stars, we recycle them by resetting their position when they pass the camera. This is more efficient.
Experiment with these values:
numStars - More stars = denser field (try 200-1000)speed - Faster = more dramatic (try 100-500)focal length (128) - Higher = narrower field of viewz range (1-100) - Larger range = longer "tunnel"distance (1-500) - How far stars spawn from centerOnce you understand the basic starfield, try these enhancements: