Page 1 of 1

Pixel Perfect Rendering

Posted: Fri Feb 19, 2016 7:26 am
by monkeyman
Hi, I'm new to Love and programming in general, and I've run into a problem that I hope you guys can help me out on. I'm trying to render scaled pixel art sprites so that they align with the grid of pixels they would be drawn at, had they not been scaled. If I use delta time to move a character, the character will move in increments smaller than the scaled pixel size so that they move out of alignment with the surrounding pixels.

I searched this topic before making this post and saw that some members recommended drawing everything to a canvas, then scaling that canvas to the desired resolution. However, I had problems with this method as I don't really understand how drawing to a canvas works, especially when the amount of items start to add up. If anyone could help me with this problem, or offer a different solution, it would be much appreciated.

Here's an example to demonstrate my problem, hopefully I attached it correctly. If you move the character slightly off screen you'll notice you can move the character so that a column or row of it's pixels can be split in an unappealing way, this effect would become more obvious with lots of objects on the screen. Use the arrow keys to move.

Re: Pixel Perfect Rendering

Posted: Fri Feb 19, 2016 10:06 am
by ivan
One hacky solution could be:

Code: Select all

local _floor = math.floor
function love.draw()
	love.graphics.scale(windowScaleX, windowScaleY)

	love.graphics.setColor(255, 255, 255)
	love.graphics.rectangle("fill", 0, 0, 320, 180)
	-- round
	local x = _floor(player.x + 0.5)
	local y = _floor(player.y + 0.5)
	love.graphics.draw(player.image, x, y)
end
The point being that your simulation uses floating point numbers
but when rendering we round the position to the nearest integer.
This may produce strange jitters when moving diagonally.

A better option for old-school games
could be to use integers for the simulation with a fixed time step.
In which case you can ignore 'dt' and you don't have to round positions.

Code: Select all

local accum = 0 -- accumulator
local interval = 1/30 -- 30 FPS (fixed interval step)
function love.update(dt)
  accum = accum + dt
  local skip = 10 -- maximum frameskip
  while accum >= interval and skip > 0 do
    skip = skip - 1
    accum = accum - interval
    
    -- input
    -- 1 pixel per frame or 30 pixels per second
    if love.keyboard.isDown("left") then
      player.x = player.x - 1
    end
    if love.keyboard.isDown("right") then
      player.x = player.x + 1
    end
    if love.keyboard.isDown("up") then
      player.y = player.y - 1
    end
    if love.keyboard.isDown("down") then
      player.y = player.y + 1
    end
    -- game logic
  end
end

Re: Pixel Perfect Rendering

Posted: Fri Feb 19, 2016 10:15 am
by monkeyman
Thanks ivan! That works perfectly. I didn't anticipate the solution being so simple.

Re: Pixel Perfect Rendering

Posted: Fri Feb 19, 2016 10:32 am
by marco.lizza
You can scale the object coordinates, as well. That is, you work (for example) with coordinates in the range [0, 100] then, prior blitting, truncate and scale the position by 3 and here you are.

Re: Pixel Perfect Rendering

Posted: Mon Feb 22, 2016 4:47 pm
by shru
Drawing everything to a canvas and then scaling that canvas afterwards is definitely the better solution. I don't recommend what ivan suggested because (a) it forces everything to move in exact integer magnitudes (b) it ties your game logic to a specific framerate.

EDIT: I've actually had a look around and have seen some reasonable arguments of why it may be a good idea to use a fixed framerate (or tickrate, more specifically) so point "b" may not be such a big deal afterall. However, I still believe that drawing to a canvas, then drawing that canvas scaled at the end is correct.

Something very close to this should work:

Code: Select all

local canvas = love.graphics.newCanvas(320, 240)
local scale = 3

function love.draw()
    love.graphics.setCanvas(canvas)

    -- Do all drawing here as usual.
    -- You should round all love.graphics.draw coordinates or you will get visual bugs around the edges of your sprites.
    -- eg. love.graphics.draw(player.image, math.floor(x + 0.5), math.floor(y + 0.5))

    love.graphics.setCanvas()
    love.graphics.draw(canvas, 0, 0, 0, scale, scale);
end