diff options
| -rw-r--r-- | main.lua | 122 | ||||
| -rw-r--r-- | map.png | bin | 0 -> 4238 bytes | |||
| -rw-r--r-- | smoothCamera.glsl | 17 |
3 files changed, 139 insertions, 0 deletions
diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..087ae61 --- /dev/null +++ b/main.lua @@ -0,0 +1,122 @@ +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
\ No newline at end of file Binary files differdiff --git a/smoothCamera.glsl b/smoothCamera.glsl new file mode 100644 index 0000000..074c52c --- /dev/null +++ b/smoothCamera.glsl @@ -0,0 +1,17 @@ +//[[ +// +// applies sub-pixel offset to the viewport texture for smooth camera movement +// while keeping pixel-perfect rendering. +// - game is rendered with camera snapped to integer pixels (pixel-perfect) +// - this shader samples the texture with a UV offset = (smooth_pos - snapped_pos) +// - the offset creates the illusion of smooth camera movement +// +// offset: (subPixelX, subPixelY) in UV space (0-1 range) +// texture must have 1px padding on each side to avoid black bars when sampling. +//]] + +extern vec2 offset; + +vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { + return Texel(tex, texture_coords + offset); +} |
