## [solved] example request , canvas scale - camera - pixel perfect - simplified

General discussion about LÖVE, Lua, game development, puns, and unicorns.
ReFreezed
Party member
Posts: 570
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

### Re: example request , canvas scale - camera - pixel perfect - simplified

I didn't intend for my example to show how scaled sprites can look better when using subpixels, but sure, you could use subpixels for that purpose I guess. (See Xeodrifter for somewhat interesting usage of scale.) Movement also become smoother, if sprite coordinates aren't rounded when drawn (or are rounded, but to the subpixels instead of the "full" pixels). Here's an example and comparison with parallax layers without and with usage of subpixels:

Notice how movements in the "new" version with subpixels are a bit smoother.

Your image with the "glitchy" text shows the issue with using nearest filtering, i.e. that some pixels look a lot wider and/or taller than their neighbors, which is what the linear+subpixel combination (which make pixel borders slightly blurry) is supposed to fix.

In the upper version the pixel sizes and lines are all wacky, while in the lower version the pixel sizes and lines look more uniform.
Tools: Hot Particles, LuaPreprocess, InputField, (more) Games: Momento Temporis
"If each mistake being made is a new one, then progress is being made."
gcmartijn
Party member
Posts: 110
Joined: Sat Dec 28, 2019 6:35 pm

### Re: example request , canvas scale - camera - pixel perfect - simplified

ReFreezed wrote: Thu Mar 03, 2022 11:42 am I didn't intend for my example to show how scaled sprites can look better when using subpixels, but sure, you could use subpixels for that purpose I guess.
Short story: can you extend the first example with moving camera and screenToWorldMouseXY / worldToScreenMouseXY ?
So I can extract the code and re-use that, I have most of the code now but that part is still not working.

Long story:

After your example and some weeks later... I finally have implemented your code.
Most of the time I had problem with font scaling and get the correct mouse coordinates when scaling the window or using fullscreen.

I don't use a hardware cursor (image), but I draw somethings using the mouse coordinates.
But that is not the problem anymore, that is working.

Now I spend some time creating the moving 'camera' but now i'm confused.
Maybe your can solve this last part for me?

canvas.lua

Code: Select all

function Canvas:new(data)
self.name = data.name
self.x = data.x or 0
self.y = data.y or 0
self.width = data.width or love.graphics.getWidth()
self.height = data.height or love.graphics.getHeight()
self.subpixels = data.subpixels
self.integerScaling = data.integerScaling

-- the real canvas
self.canvas = love.graphics.newCanvas(self.width * self.subpixels, self.height * self.subpixels)
self.canvas:setFilter("nearest", "nearest")

-- calculated
self.scale = 1
self.scaledWidth = nil
self.scaledHeight = nil

end

function Canvas:setSubpixels(s)
self.subpixels = s
end

function Canvas:setX(x)
self.x = x
end

function Canvas:setY(y)
self.y = y
end

function Canvas:getX()
return self.x
end

function Canvas:getY()
return self.y
end

function Canvas:getSubpixels()
return self.subpixels
end

---
-- This is without the subpixels
function Canvas:getHeight()
return self.height
end

---
-- This is without the subpixels
function Canvas:getWidth()
return self.width
end

---
-- This is with the optional subpixels and current render
function Canvas:getRenderHeight()
return self:getCanvas():getHeight()
end

---
-- This is with the optional subpixels and current render
function Canvas:getRenderWidth()
return self:getCanvas():getWidth()
end

---
-- The calculated width scaled
function Canvas:getScaledWidth()
return self.scaledWidth
end

---
-- The calculated height scaled
function Canvas:getScaledHeight()
return self.scaledHeight
end

---
-- The love canvas object
function Canvas:getCanvas()
return self.canvas
end

---
-- The calculated scale factor
function Canvas:getScale()
return self.scale
end

-- return width and height
function Canvas:getDimensions()
return self.canvas:getDimensions()
end

function Canvas:scaleMath()
local _, _, windowWidth, windowHeight = love.window.getSafeArea()
local canvasWidth, canvasHeight = self:getDimensions()

-- Fill as much of the window as possible with the canvas while preserving the aspect ratio.
self.scale = math.min(windowWidth / canvasWidth, windowHeight / canvasHeight)
-- self.scale = windowHeight / canvasHeight -- This would fill the height and possibly cut off the sides.

if self.integerScaling then
self.scale = math.floor(self.scale * self:getSubpixels()) / self:getSubpixels()
self.scale = math.max(self.scale, 1 / self:getSubpixels()) -- Avoid self.scale =0 if the window is tiny!
end

self.scaledWidth = canvasWidth * self.scale
self.scaledHeight = canvasHeight * self.scale

-- center canvas
self:setX(math.floor((windowWidth - self.scaledWidth) / 2))
self:setY(math.floor((windowHeight - self.scaledHeight) / 2))
end

-- function Canvas:release(args)
-- end

return Canvas

config.lua (snippet)

Code: Select all

    self.canvas = {}
self.canvas.width = 320
self.canvas.height = 180
self.canvas.integerScaling = true
self.canvas.subpixels = 4

conf.lua

Code: Select all

    -- 1x 320x180
-- 2x 640x360
-- 4x 1280x720 -- < minimal target height
-- 6x 1920x1080 (fullHD)
-- 12x 3840x2160 (4K)
t.window.width = 1280 -- The window width (number)
t.window.height = 720 -- The window height (number)

camera.lua (snippet)
I was trying to implement a follow player x,y like https://github.com/a327ex/STALKER-X
But then with less code, without all the functions, to understand what is happening, and I only need the follow player

Code: Select all

function Camera:follow(x, y)
self.targetX, self.targetY = x, y
end

function Camera:setBounds(x, y, w, h)
self.bound = true
self.bounds_min_x = x
self.bounds_min_y = y
self.bounds_max_x = x + w
self.bounds_max_y = y + h
end

function Camera:attach()
-- extend inside the main.lua below the canvas things
love.graphics.translate(math.floor(self.width / 2), math.floor(self.height / 2))
love.graphics.translate(-math.floor(self.x), -math.floor(self.y))
end

function Camera:update(dt)
if self.targetX == nil or self.targetY == nil then
return
end

self.x, self.y = self.targetX, self.targetY

-- if self.bound then
--     self.x = math.min(math.max(self.x, self.bounds_min_x + self.width / 2), self.bounds_max_x - self.width / 2)
--     self.y = math.min(math.max(self.y, self.bounds_min_y + self.height / 2), self.bounds_max_y - self.height / 2)
-- end
end

main.lua (snippet)

Code: Select all

function draw()
self.canvas:scaleMath()

-- Draw to the canvas
love.graphics.push("all")
love.graphics.setCanvas(self.canvas:getCanvas())
love.graphics.clear()
love.graphics.scale(self.canvas:getSubpixels())

self.camera:attach() -- at the moment without push/pop

self.world:draw()

love.graphics.pop()
love.graphics.clear(0, 0, 0)
love.graphics.draw(self.canvas:getCanvas(), self.canvas:getX(), self.canvas:getY(), 0, self.canvas:getScale())
end

function love.mousepressed(xOrg, yOrg, button, istouch, presses)
local x, y = xOrg, yOrg
if self.cursor then
x, y = self.cursor:getScreenToWorldXY(x, y)
end
self.world:mousePressedEvent(
{
x = x,
y = y,
button = button,
istouch = istouch,
presses = presses
}
)
end

function love.resize(w, h)
-- calculate so we have the new variables
self.canvas:scaleMath()

-- forward to other entities
self.world:windowResizeEvent(
{
w = w,
h = h
}
)
end

cursor.lua (snippet)

Code: Select all

function Cursor:getScreenToWorldXY(x, y)
return self:getScreenToWorldX(x), self:getScreenToWorldY(y)
end

function Cursor:getScreenToWorldX(x)
x = x or love.mouse.getX()
return ((x - self.gameCanvas:getX()) / self.gameCanvas:getSubpixels()) / self.gameCanvas:getScale()

-- I have to do something here with the self.camera.x
end

function Cursor:getScreenToWorldY(y)
y = y or love.mouse.getY()
return ((y - self.gameCanvas:getY()) / self.gameCanvas:getSubpixels()) / self.gameCanvas:getScale()

-- I have to do something here with the self.camera.y
end

Inside cursor.lua I have to include the camera x and y. I try several things but this is what is happening right now:
- I click at the right side
- Player is moving to the right side (camera moves to the left side, looks good for now)
- The cursor position is not correct anymore

I do mean local x, y = self:getScreenToWorldXY() is not correct.
First I thought , just do things like this but it don't work.

Code: Select all

function Cursor:getScreenToWorldX(x)
x = x or love.mouse.getX()
x = ((x - self.gameCanvas:getX()) / self.gameCanvas:getSubpixels()) / self.gameCanvas:getScale()
if self.camera then
x = x - self.camera.x
end
end

ReFreezed
Party member
Posts: 570
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

### Re: example request , canvas scale - camera - pixel perfect - simplified

You're probably not too far off.

I've extended my initial example program with the concept of a world coordinate system, a movable player, and a camera that follows the player (in love.update). Also, screenToWorld and worldToScreen functions.
Attachments
PixelArtRendering.20220328.love
Tools: Hot Particles, LuaPreprocess, InputField, (more) Games: Momento Temporis
"If each mistake being made is a new one, then progress is being made."
gcmartijn
Party member
Posts: 110
Joined: Sat Dec 28, 2019 6:35 pm

### Re: example request , canvas scale - camera - pixel perfect - simplified

Thanks very much! give me some time to check and process this
gcmartijn
Party member
Posts: 110
Joined: Sat Dec 28, 2019 6:35 pm

### Re: example request , canvas scale - camera - pixel perfect - simplified

Yes everything is working now, it fits perfectly.
Going too refactor now.

Only added some extra things for the camera because it's something like a point and click. The player is nog in the middle.
Using the camera example code from someone else, with your code gives me this fix:

Code: Select all

function Camera:update(dt)
if self.targetX == nil or self.targetY == nil then
return
end

self.x = damp(self.x, self.targetX, self.followSpeed, dt) -- Move towards player smoothly.
self.y = damp(self.y, self.targetY, self.followSpeed, dt) -- Do camera.x=player.x etc. for instant snap.

if self.bound then
self.x = math.min(math.max(self.x, self.bounds_min_x + self.width / 2), self.bounds_max_x - self.width / 2)
self.y = math.min(math.max(self.y, self.bounds_min_y + self.height / 2), self.bounds_max_y - self.height / 2)
end
end

function Camera:setBounds(x, y, w, h)
self.bound = true
self.bounds_min_x = x
self.bounds_min_y = y
self.bounds_max_x = x + w
self.bounds_max_y = y + h
end

function Camera:follow(x, y)
self.targetX, self.targetY = x, y
end


snippet init

Code: Select all

    Camera(
{
x = 0,
y = 0,
width = config.canvas.width,
height = config.canvas.height
}
)

world snippet

Code: Select all

camera:setBounds(0, 0, scene.width / 4, self.canvas.height) -- 180 height

player snippet

Code: Select all

camera.follow(player.x,player.y)

So now the camera stops when the scene width is end.

Going to refactor now, before I forget things
Really cool !
gcmartijn
Party member
Posts: 110
Joined: Sat Dec 28, 2019 6:35 pm

### Re: example request , canvas scale - camera - pixel perfect - simplified

ReFreezed wrote: Mon Mar 28, 2022 5:00 pm You're probably not too far off.

@ReFreezed
I did some other work, and now i'm at a point that the player is moving to the right/left and the camera is following.
Everything works, but what I see is a little jitter.

When I download your example again and use the same settings as I use inside the pixel art game

Code: Select all

integerScaling = true
subpixels = 4
cameraspeed = 5

And move mega man to the right en left, then I can see a very small jitter in the background image.

Inside my game I use a static framerate with this code
https://github.com/bjornbytes/tick

Code: Select all

frameTick.framerate = 60
frameTick.rate = 1 / 60


Is it possible that you code always give some jitter?
Can it be optimised because I'm using the tick code above?

Or will there alway be some images that jitter (like your example code).
I already use (as far as I know, need to check this again) on each drawing

Code: Select all

function floor(n, subpixels)
return math.floor(n * (subpixels or 1) + .5) / (subpixels or 1)
end

But maybe you say, "ow if you use that frameTick framerate settings, then do this inside the camera code, to make it more smooth."
ReFreezed
Party member
Posts: 570
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

### Re: [solved] example request , canvas scale - camera - pixel perfect - simplified

I don't see any jitter in the background in my example with the settings you posted, but jitter can be caused by numerous things. (See my reply in that other thread.) If you force dt to be 1/60 in love.update, does it make movement smoother (in the example)?
Tools: Hot Particles, LuaPreprocess, InputField, (more) Games: Momento Temporis
"If each mistake being made is a new one, then progress is being made."
gcmartijn
Party member
Posts: 110
Joined: Sat Dec 28, 2019 6:35 pm

### Re: [solved] example request , canvas scale - camera - pixel perfect - simplified

Nope that don't fix it.

I 'fix' it for the moment by using the following things
- don't update the camera.y, because i'm only follow moving objects from left <> right
- don't use the damp function

With this it more smooth.
Going to work with this for now, and test it later on other hardware.
ReFreezed
Party member
Posts: 570
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

### Re: [solved] example request , canvas scale - camera - pixel perfect - simplified

You'll have to explain what exactly you mean by jitter, or show a video or something of what's happening, before I can provide any better help.

Ditching the damp function is probably a good idea as it's not very clever about the movement. In my games I've always just fixed the camera on the player, so there's never any "jitter" specifically for the player sprite on the screen.
Tools: Hot Particles, LuaPreprocess, InputField, (more) Games: Momento Temporis
"If each mistake being made is a new one, then progress is being made."
gcmartijn
Party member
Posts: 110
Joined: Sat Dec 28, 2019 6:35 pm

### Re: [solved] example request , canvas scale - camera - pixel perfect - simplified

@ReFreezed
Can you pm me your mailadres ? I can't PM the game, there is no upload option, and I don't want to upload the game here.
Or maybe I can upload it somewhere like wetransfer and I PM you the link. Is that oke ?

Maybe you can see the problem, I did change the code, so the camera is moving now from left to the right +1 px and every time it stutters at some point.

### Who is online

Users browsing this forum: Google [Bot] and 13 guests