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

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

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

Post by ReFreezed »

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:

Image

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.

Image

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: 112
Joined: Sat Dec 28, 2019 6:35 pm

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

Post by gcmartijn »

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

    entityManager:add(self)
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
User avatar
ReFreezed
Party member
Posts: 585
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

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

Post by ReFreezed »

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
(13.07 KiB) Downloaded 77 times
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: 112
Joined: Sat Dec 28, 2019 6:35 pm

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

Post by gcmartijn »

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

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

Post by gcmartijn »

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: 112
Joined: Sat Dec 28, 2019 6:35 pm

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

Post by gcmartijn »

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."
User avatar
ReFreezed
Party member
Posts: 585
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

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

Post by ReFreezed »

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: 112
Joined: Sat Dec 28, 2019 6:35 pm

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

Post by gcmartijn »

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.
User avatar
ReFreezed
Party member
Posts: 585
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

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

Post by ReFreezed »

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: 112
Joined: Sat Dec 28, 2019 6:35 pm

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

Post by gcmartijn »

@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.
Post Reply

Who is online

Users browsing this forum: No registered users and 17 guests