How to do pixel-perfect rendering in löve?

General discussion about LÖVE, Lua, game development, puns, and unicorns.
PolySaken
Prole
Posts: 4
Joined: Mon Mar 16, 2020 6:20 am

How to do pixel-perfect rendering in löve?

Post by PolySaken »

As the title suggests, I'd like to have my pixel-art game perform pixel perfect rendering. The common way to do this is to render everything at a low resolution and upscale it using nearest-neighbour.

What's the best way to do this in löve?
User avatar
GVovkiv
Party member
Posts: 668
Joined: Fri Jan 15, 2021 7:29 am

Re: How to do pixel-perfect rendering in löve?

Post by GVovkiv »

PolySaken wrote: Sat Aug 14, 2021 7:38 am As the title suggests, I'd like to have my pixel-art game perform pixel perfect rendering. The common way to do this is to render everything at a low resolution and upscale it using nearest-neighbour.

What's the best way to do this in löve?
https://forum.yoyogames.com/index.php?t ... -game.995/
It's for GameMaker studio, but explanation on how it cand be done, can be used in other cases, i guess
If you know game Pixel Dungeon (or Shattered Pixel Dungeon), game uses something like that:
There options menu which allow you to switch in-beetwen integer x2, x3, x4, etc scaling
Maybe the easest solution will be use options menu, so user can choose more preferable option and, maybe, add "Auto" mode where that factor will be calculated automaticaly depending on window width and height, something like:

Code: Select all

scale = math.ceil(windowWidth / 1000)
or more clever way
User avatar
pgimeno
Party member
Posts: 3544
Joined: Sun Oct 18, 2015 2:58 pm

Re: How to do pixel-perfect rendering in löve?

Post by pgimeno »

If by "pixel perfect" you mean each pixel is a square with sharp borders, people usually use something like Push for this. https://love2d.org/forums/viewtopic.php?f=5&t=80738

The idea behind Push is to render to a canvas of a smaller resolution, then render that canvas to the screen appropriately scaled.
User avatar
Gunroar:Cannon()
Party member
Posts: 1085
Joined: Thu Dec 10, 2020 1:57 am

Re: How to do pixel-perfect rendering in löve?

Post by Gunroar:Cannon() »

GVovkiv wrote: Sat Aug 14, 2021 12:47 pm
If you know game Pixel Dungeon (or Shattered Pixel Dungeon),...
Very well made game :awesome:
The risk I took was calculated,
but man, am I bad at math.

-How to be saved and born again :huh:
User avatar
GVovkiv
Party member
Posts: 668
Joined: Fri Jan 15, 2021 7:29 am

Re: How to do pixel-perfect rendering in löve?

Post by GVovkiv »

Gunroar:Cannon() wrote: Sat Aug 14, 2021 3:30 pm
GVovkiv wrote: Sat Aug 14, 2021 12:47 pm
If you know game Pixel Dungeon (or Shattered Pixel Dungeon),...
Very well made game :awesome:
Meh, maybe
User avatar
Gunroar:Cannon()
Party member
Posts: 1085
Joined: Thu Dec 10, 2020 1:57 am

Re: How to do pixel-perfect rendering in löve?

Post by Gunroar:Cannon() »

GVovkiv wrote: Sat Aug 14, 2021 3:53 pm Meh, maybe
I agree that the :awesome: might have been an over exaggeration... But what do you mean by maybe (and the "meh" :rofl: ) I want to see your view.
(I say it's well made, you know, for one guy to make a game that's complete and has a nice ui and clean code, if you look at the code that is. :P)

PS: I'm talking about the original.
The risk I took was calculated,
but man, am I bad at math.

-How to be saved and born again :huh:
User avatar
GVovkiv
Party member
Posts: 668
Joined: Fri Jan 15, 2021 7:29 am

Re: How to do pixel-perfect rendering in löve?

Post by GVovkiv »

Gunroar:Cannon() wrote: Sat Aug 14, 2021 9:15 pm
GVovkiv wrote: Sat Aug 14, 2021 3:53 pm Meh, maybe
I agree that the :awesome: might have been an over exaggeration... But what do you mean by maybe (and the "meh" :rofl: ) I want to see your view.
(I say it's well made, you know, for one guy to make a game that's complete and has a nice ui and clean code, if you look at the code that is. :P)

PS: I'm talking about the original.
TLDR: Original game fells unfair, unfinished and annoing, better play forks like Shattered, YAPD and maybe Remixed
Original game i don't really like because:
1 Random at combat - like, random at level generation and other events it's okay, it makes game fells new every time you play it, but in combat...
It's anoying me, like: you strong, expirienced warriror, you can easly pick up and use heavy (18 strg) weapons, use magic wands...
...but you cant hit rat because random, it really annoy me
Sometimes i lost run because of that
2 There is like 4 quest in game:
kill stink rat and you win some random useless leather armour +0 or shortsword+1 when you have something better
kill fire elemental and win useless sheep wand
mine 15 ore and win free +1 upgrade
kill 8 monks and win yet another wand charger ring when you play purely melee
3 There is not much content at all - there 5 level of deep, but they fells more like copy-paste, level gen not that interesting;most of potions not really helpful, most weapons also not that useful or interesting to play with since they just do little more damage and that it; to win there like 1 valid strat - don't use upgrade scroll until you found some roundom tier 4 weapon at sewers and use all of them in it; there is no optional levels;
4 - swarm of flies; monks; scorpions; i hate them
Like, that okay game, but after you play it more and more, it start fells annoing, repetative and unfair
If you want better Pixel Dungeon expirience, then original game is not that good
User avatar
Gunroar:Cannon()
Party member
Posts: 1085
Joined: Thu Dec 10, 2020 1:57 am

Re: How to do pixel-perfect rendering in löve?

Post by Gunroar:Cannon() »

Okay, I see your points...:3
GVovkiv wrote: Sat Aug 14, 2021 9:41 pm TLDR: Original game fells unfair, unfinished and annoing, better play forks like Shattered, YAPD and maybe Remixed
unfair, I guess. Unfinished...It kind of lacks bugs and has some polish to it. YAPD is...better I guess (I actually managed to finish that one one easy mode :P :rofl:). Remixed adds a lot of more content but is a little too hard where getting killed in floor one by a rat is an easy possibility. But it added saves(save scumming :? ) and I guess they know it's harder. Original to me just seems...simply complete. Like "hey, dude, make a semi-decent roguelike on android"(yes, I'm talking about android version. Comparing it to PC roguelikes is a no-go) and then he makes pixel dungeon. Which is like the first good roguelike on Android with a nice retro vibe. :?
Original game i don't really like because:
1 Random at combat - like, random at level generation and other events it's okay, it makes game fells new every time you play it, but in combat...
It's anoying me, like: you strong, expirienced warriror, you can easly pick up and use heavy (18 strg) weapons, use magic wands...
...but you cant hit rat because random, it really annoy me
Hey! That's balanced. In brogue and others you can wield a heavy item but because it's too heavy for you your character keeps missing.
2 There is like 4 quest in game:
kill stink rat and you win some random useless leather armour +0 or shortsword+1 when you have something better
kill fire elemental and win useless sheep wand
mine 15 ore and win free +1 upgrade
kill 8 monks and win yet another wand charger ring when you play purely melee
The quests do give pretty weak stuff :rofl: . But the sheep wand has saved me countless times. Helps to make an escape ;)
3 There is not much content at all - there 5 level of deep, but they fells more like copy-paste, level gen not that interesting;most of potions not really helpful, most weapons also not that useful or interesting to play with since they just do little more damage and that it; to win there like 1 valid strat - don't use upgrade scroll until you found some roundom tier 4 weapon at sewers and use all of them in it; there is no optional levels;
(You mean 25 levels. But I guess you meant like levels (separating on boss fights) and not floors :) )

Hmm...I guess it's true. But I think the boss battles are nice and varied. And there's some hidden stuff (like use scroll of mapping in level 5 , goo boss, and find a door where theres loot(not a lot though) and a rat king. Just a fun thing to find. Have you seen it? ;) )
4 - swarm of flies; monks; scorpions; i hate them
Like, that okay game, but after you play it more and more, it start fells annoing, repetative and unfair
If you want better Pixel Dungeon expirience, then original game is not that good
Swarm of flies are the beeeeeessssst(best)! Go to a corridor and start killing them. Whatever potion they drop is sure to be a healing potion. And there's like a 60% chance they drop one. :ultrashocked:
I guess all games get repetitive eventually. Some just get there faster than others :)
The risk I took was calculated,
but man, am I bad at math.

-How to be saved and born again :huh:
User avatar
Xii
Party member
Posts: 137
Joined: Thu Aug 13, 2020 9:09 pm
Contact:

Re: How to do pixel-perfect rendering in löve?

Post by Xii »

Instead of pre-deciding on a fixed resolution with black bars around, I adapt to any screen size and orientation. Scaling only by integer multiples (for square pixels), but drawing the upscaled canvas such that the edgemost 1 pixel on all sides "bleeds" outside the screen. That is, the view fills the whole screen, always, but the edgemost pixels are cut off.

You set the minimum view size you want, on the shorter axis of the screen. The resolution is scaled up as much as possible, but not beyond your minimum size. The calculations then give you the actual view size you get.

This code conceptually splits your screen into two parts: A square that fills your screen - this is your "main view" or "safe drawing area" that is the same for any screen orientation; And the rectangle that is left over, which can be used for a GUI (but isn't guaranteed to have any minimum size). The main view square is centered and the side view rectangle split into two on very wide screens.

Code: Select all

function love.draw()
	-- DIMENSIONAL MATH:
	
	screen_x, screen_y, screen_w, screen_h = love.window.getSafeArea()
	short_s = math.min(screen_w, screen_h)
	
	if force_resolution >= 1 then
		target_resolution = force_resolution
	elseif target_view_s then
		target_resolution = math.max(1, math.ceil(short_s / (target_view_s * 2 -1)))
	else
		target_resolution = 1
	end
	if resolution ~= target_resolution then
		resolution = target_resolution
		view_w = math.ceil(screen_w / resolution)
		view_h = math.ceil(screen_h / resolution)
		view = love.graphics.newCanvas(view_w, view_h)
		view:setFilter("nearest", "nearest")
	end
	
	short_s = math.ceil(short_s / resolution)
	main_x, main_y, main_w, main_h = 0, 0, short_s, short_s
	if view_w > view_h then -- wide
		side_x, side_y = main_x + main_w, 0
		side_w, side_h = view_w - main_w, view_h
	else -- tall
		side_x, side_y = 0, main_y + main_h
		side_w, side_h = view_w, view_h - main_h
	end
	
	split = false
	if force_split == true or (force_split ~= false and math.min(side_w, side_h) > (short_s / 2)) then
		split = true
	end
	if split then
		if view_w > view_h then
			-- wide
			side_w = math.floor(side_w / 2)
			main_x = main_x + side_w
			side_x = side_x + side_w
			-- fix odd
			if (side_w * 2 + main_w) < view_w then
				main_w = main_w + 1
				side_x = side_x + 1
			end
		else
			-- tall
			side_h = math.floor(side_h / 2)
			main_y = main_y + side_h
			side_y = side_y + side_h
			-- fix odd
			if (side_h * 2 + main_h) < view_h then
				main_h = main_h + 1
				side_y = side_y + 1
			end
		end
	end
	
	view_offs_x = math.floor((screen_w-(view_w*resolution))/2)
	view_offs_y = math.floor((screen_h-(view_h*resolution))/2)
	
	mouse_x, mouse_y = love.mouse.getPosition()
	mouse_x = (mouse_x - view_offs_x) / resolution
	mouse_y = (mouse_y - view_offs_y) / resolution
	
	-- GAME DRAWING:
	-- note: the edgemost 1 pixel border on all sides may be partially outside visible screen.
	
	-- game
	love.graphics.setCanvas(view)
	love.graphics.origin()
	love.graphics.setColor(1, 1, 1, 1)
	love.graphics.setBlendMode("alpha")
	love.graphics.setShader()
	draw_game()
	-- blit
	love.graphics.setCanvas()
	love.graphics.origin()
	love.graphics.setColor(1, 1, 1, 1)
	love.graphics.setBlendMode("alpha")
	love.graphics.setShader()
	love.graphics.draw(view, view_offs_x,view_offs_y, 0, resolution, resolution)
end

function love.resize(w, h)
	resolution = 0 -- force canvas reset
end
The key input variables here are:
  • target_view_s - This is your minimum view size in pixels on the shorter axis of the screen.
  • force_resolution - Integer multiple pixel size. If not nil, overrides adaptive resolution. E.g. set to 1 for actual pixels. Can be changed over time to create dynamic pixelation effects.
  • force_split - If true or false, forces the side view to either split or not split, respectively.
And the output variables you'll use:
  • view, view_w, view_h - The canvas you draw to and its size. At least target_view_s in both dimensions. May be smaller than actual screen because of scaling.
  • main_x, main_y, main_w, main_h - Position and size of main view square on the canvas. This is where you draw your game world.
  • side_x, side_y, side_w, side_h - Position and size of side view rectangle on the canvas. Draw GUI here.
  • split - Whether the side view is wide enough to be split into two.
  • mouse_x, mouse_y - Mouse coordinates on the canvas.
  • resolution - Integer multiple pixel size. That is, this is how big your pixels are. The scale of the view canvas.
  • draw_game() - The function you write that does the actual drawing.
To recap: Set target_view_s to your minimum view size. 'view' is the canvas you draw to in draw_game(). Use 'main' and 'side' coordinates to position your elements on the screen. Note that they change size and position to adapt to any screen size and orientation. The edgemost 1 pixel border may be partially outside screen and thus cut off.

For example, if I set my target_view_s to 480, and my screen is 1920x1080, I'll get an effective resolution of 2x, with a main view of 1080x1080, and a side panel of 840x1080, which is split into two 420x1080 panels around the centered main square.

If the screen is taller than wide (portrait orientation, i.e. probably smart phone), then the side panel is horizontal and below the main square (for good thumb access).
PolySaken
Prole
Posts: 4
Joined: Mon Mar 16, 2020 6:20 am

Re: How to do pixel-perfect rendering in löve?

Post by PolySaken »

pgimeno wrote: Sat Aug 14, 2021 1:38 pm If by "pixel perfect" you mean each pixel is a square with sharp borders, people usually use something like Push for this. https://love2d.org/forums/viewtopic.php?f=5&t=80738

The idea behind Push is to render to a canvas of a smaller resolution, then render that canvas to the screen appropriately scaled.
I mean I want what is usually referred to as pixel perfect rendering, where the pixels of one texture will always align with the pixels of any other, even though those pixels may not actually be a 1x1 pixel size. Basically the opposite of how most 'pixel' games look, where sprites may be offset by less than a pixel.
Post Reply

Who is online

Users browsing this forum: No registered users and 57 guests