local map = love.graphics.newImage("map.png") -- virtual game world local WORLD_WIDTH = 16 * 30 local WORLD_HEIGHT = 16 * 30 -- camera viewport local VIEW_WIDTH = 16 * 15 local VIEW_HEIGHT = 16 * 15 -- actual window size local SCREEN_WIDTH = 16 * 100 local SCREEN_HEIGHT = 9 * 100 local PLAYER_SIZE = 16 local PLAYER_COLOR = {0.2, 0.6, 1, 1} local player = { x = WORLD_WIDTH / 2 - PLAYER_SIZE / 2, y = WORLD_HEIGHT / 2 - PLAYER_SIZE / 2, speed = 80, } local camera = { x = 0, y = 0, follow_speed = 2, } -- calcualte offset of camera position to the nearest integer pixel (grid space) function camera:getShaderOffset() local sub_x = camera.x - math.floor(camera.x) local sub_y = camera.y - math.floor(camera.y) return {sub_x / VIEW_WIDTH, sub_y / VIEW_HEIGHT} end local canvas -- Toggle: when true, shader adds smooth sub-pixel interpolation; when false, integer-only (choppier) local smooth_shader_enabled = true function love.load() canvas = love.graphics.newCanvas(VIEW_WIDTH, VIEW_HEIGHT) canvas:setFilter("nearest", "nearest") map:setFilter("nearest", "nearest") love.window.setMode(SCREEN_WIDTH, SCREEN_HEIGHT) camera.x = player.x + PLAYER_SIZE / 2 - VIEW_WIDTH / 2 camera.y = player.y + PLAYER_SIZE / 2 - VIEW_HEIGHT / 2 smooth_shader = love.graphics.newShader("smoothCamera.glsl") end function love.keypressed(key) if key == "f1" then smooth_shader_enabled = not smooth_shader_enabled end end function love.update(dt) local dx, dy = 0, 0 if love.keyboard.isDown("w") then dy = dy - 1 end if love.keyboard.isDown("s") then dy = dy + 1 end if love.keyboard.isDown("a") then dx = dx - 1 end if love.keyboard.isDown("d") then dx = dx + 1 end if dx ~= 0 or dy ~= 0 then local len = math.sqrt(dx * dx + dy * dy) dx, dy = dx / len, dy / len player.x = player.x + dx * player.speed * dt player.y = player.y + dy * player.speed * dt end player.x = math.max(0, math.min(WORLD_WIDTH - PLAYER_SIZE, player.x)) player.y = math.max(0, math.min(WORLD_HEIGHT - PLAYER_SIZE, player.y)) local target_x = player.x + PLAYER_SIZE / 2 - VIEW_WIDTH / 2 local target_y = player.y + PLAYER_SIZE / 2 - VIEW_HEIGHT / 2 -- camera follows the target position camera.x = camera.x + (target_x - camera.x) * camera.follow_speed * dt camera.y = camera.y + (target_y - camera.y) * camera.follow_speed * dt -- clamp camera position to the world bounds camera.x = math.max(0, math.min(WORLD_WIDTH - VIEW_WIDTH, camera.x)) camera.y = math.max(0, math.min(WORLD_HEIGHT - VIEW_HEIGHT, camera.y)) end function love.draw() love.graphics.setCanvas(canvas) love.graphics.clear() love.graphics.push() local cam_x = math.floor(camera.x) local cam_y = math.floor(camera.y) -- translate the camera to the nearest integer pixel -- this will allow the shader to work properly by calculating the offset from the nearest integer pixel love.graphics.translate(-cam_x, -cam_y) love.graphics.draw(map, 0, 0, 0, 2, 2) love.graphics.setColor(PLAYER_COLOR) love.graphics.rectangle("fill", player.x, player.y, PLAYER_SIZE, PLAYER_SIZE) love.graphics.setColor(1, 1, 1, 1) love.graphics.pop() love.graphics.setCanvas() -- calculate the scale of the viewport to the screen local scale = math.floor(math.min(SCREEN_WIDTH / VIEW_WIDTH, SCREEN_HEIGHT / VIEW_HEIGHT)) scale = math.max(1, scale) local draw_w = VIEW_WIDTH * scale local draw_h = VIEW_HEIGHT * scale local offset_x = (SCREEN_WIDTH - draw_w) / 2 local offset_y = (SCREEN_HEIGHT - draw_h) / 2 if smooth_shader_enabled then -- calculate the offset of the camera position to the nearest integer pixel local offset = camera:getShaderOffset() -- send the offset to the shader smooth_shader:send("offset", offset) -- set the shader love.graphics.setShader(smooth_shader) end -- print on screen if shader is enabled love.graphics.print("Shader is " .. (smooth_shader_enabled and "enabled" or "disabled"), 10, 10) love.graphics.draw(canvas, offset_x, offset_y, 0, scale, scale) love.graphics.setShader() end