How to deal with seams on tiled objects?

I’m creating parallax scrolling backgrounds by creating ‘panel’ objects whose sprite components can be tiled in the scrolling direction. An issue I’m encountering is that when I’m scrolling the background, there are certain points where the seam between tiled ‘panels’ is broken, it looks very ugly:


The background collections are setup like this:
image
This is the script for the layer:

require 'true2tile.globals'
local v3 = v3 -- vector3 shortcuts

go.property('vertical', false)
go.property('tiling_interval', 0)
go.property('radius', 0)

function init(self)
    self.world_position = go.get_world_position()
    self.offset = go.get_position().y

    self.panels = {}
    local center_index = self.radius + 1
    self.panels[center_index] = factory.create('#factory', v3.up(self.offset))
    for i = 1, self.radius do
        self.panels[center_index - i] = factory.create('#factory', v3.left(self.tiling_interval * i) + v3.up(self.offset))
        self.panels[center_index + i] = factory.create('#factory', v3.right(self.tiling_interval * i) + v3.up(self.offset))
    end

    for _, id in ipairs(self.panels) do
        go.set_parent(id, go.get_id(), false)
    end
end

function update(self, dt)
    local panel_first = go.get_position(self.panels[1])
    local panel_last = go.get_position(self.panels[#self.panels])

    if panel_first.x < -self.tiling_interval*(self.radius+1) then
        local new_position = go.get_position(self.panels[#self.panels]) + v3.right(self.tiling_interval)
        go.set_position(new_position, self.panels[1])
        local p = self.panels[1]
        table.remove(self.panels, 1)
        table.insert(self.panels, #self.panels + 1, p)
    end

    if panel_last.x > self.tiling_interval*(self.radius+1) then
        local new_position = go.get_position(self.panels[1]) + v3.left(self.tiling_interval)
        go.set_position(new_position, self.panels[#self.panels])
        local p = self.panels[#self.panels]
        table.remove(self.panels, #self.panels)
        table.insert(self.panels, 1, p)
    end

    local panel_center = go.get_position(self.panels[self.radius+1])
    
    do -- realign the panels relative to each other
        local center_index = self.radius + 1
        for i = 1, self.radius do
            go.set_position(v3.left(self.tiling_interval * i) + panel_center, self.panels[center_index - i])
            go.set_position(v3.right(self.tiling_interval * i) + panel_center, self.panels[center_index + i])
        end
    end
end

And the script for the panels themselves:

local camera = require 'orthographic.camera'

go.property('scroll_speed_power_x',0)
go.property('scroll_speed_power_y',0)

function update(self, dt)
    self.velocity = vmath.vector3()
    self.velocity.x = camera.get_position_delta().x * (self.scroll_speed_power_x) * (-0.01)
    self.velocity.y = camera.get_position_delta().y * (self.scroll_speed_power_y) * (-0.01)
    local new_position = go.get_position() + self.velocity
    
    go.set_position(new_position)
end

The script for the parent ‘parallax’ GO just sets the position of that object to match the camera’s position.

My current theory for this behaviour is that because I’m using a reduced size viewport but still lerping with higher than per-pixel precision, something with how the exact value of a decimal number needing to be represented as an integer to represent the correct pixel is causing this (would this be considered something like a subpixel?)

How can I get around this?

What if you move the layers (layer1, layer2, layer3) instead of the individual panels and make sure the panels are parented to their respective layer?

Scratch that, it looks like this is what you are doing. Have you checked Sub Pixels option in the Sprite section of game.project?

I wasn’t aware of this, but even upon checking it the problem persists. After checking, the seam breakage is smaller but it looks like the behaviour of the actual breaking is the same.

Observed in the rearmost layer:

What’s interesting is that changing the speed of the scrolling doesn’t seem to affect the issue either. If I make it slow scrolling and walk the same number of tiles on that layer, the break still occurs → which leads me to believe maybe it doesn’t have something to do with the precision? In which case I’m totally lost :sweat:

Have you checked the positions of the panels? Are they positioned on integer positions? Also, what is the overall position of the camera? How far are you from origin when this happens?

1 Like

I zoomed out the camera and tinted the sprites according to the table index of their GO id’s from factory.create(), and it appears that the breakage is only occuring with a specific panel…


If I limit the framerate, the true nature of the problem becomes more apparent although I’m still not sure what’s going on.

They aren’t probably due to how I’m lerping the position of the parent object using this implementation:

function lerp_with_dt(t, dt, v1, v2)
	if dt == 0 then return vmath.lerp(t, v1, v2) end
	local rate = UPDATE_FREQUENCY * math.log10(1 - t)
	return vmath.lerp(1 - math.pow(10, rate * dt), v1, v2)
	--return vmath.lerp(t, v1, v2)
end

Pretty sure I took the snippet from your orthographic camera, haha! I did make sure to do a little reading on it as well though. This is the readout on their X positions for a frame where the break is exaggerated in the frame-limited case:

{ --[[00000161E9323CC0]]
  1 = -334.73712158203,
  2 = -206.73710632324,
  3 = -78.703956604004,
  4 = 49.262886047363,
  5 = 177.26289367676,
  6 = 305.26287841797,
  7 = 433.26287841797
}

But I’m not sure that’s even the issue, as the behaviour on the frame-limited case and the fact that it’s only happening to one panel would rule that out right?

1 Like

I think the second video, showing the problem for a single panel is interesting.
I would have expected the problem to show occasionally for all panels.

This suggests that this particular panel i s bit special, are you perhaps moving it more than once?

I was primarily thinking of how the panels inside each layer is placed? Those are on nice even pixels right? There must be something with the placement or size of the panels that causes this.

That’s what I was thinking too, but I’ve triple checked and the only source of movement is being childed to the ‘parallax’ GO in the collection and a velocity from scaling the camera position’s per-frame delta which (done in each panel’s own script). There’s nothing that I can see in my code that would make this panel somehow special, but the behaviour suggests otherwise.

I think so? Still not sure I quite understand what you mean to ask. The tiling_interval property is what determines the spacing of the panels when the layers are constructed, and I set that equal to the width of my sprite. The pixels are 1 to 1 in terms of the position units, I’m not doing any scaling.

I tried tracing out the positions of each panel, and I’m not sure if it provides any insight aside from confirming that the affected panel is indeed special somehow. I’m super stumped.

The panel’s scrolling movement is dependent on the camera’s position delta, and there’s nothing different between the affected panel and the panels that were fine → so I thought maybe the issue was that this panel was on the other side of the camera delta update separate from the other panels.

So my hypothesis was → per frame:

  1. Affected panel moves based on the current stored delta
  2. Camera updates this value
  3. The rest of the panels move, but using the updated value

And then the next frame, the cycle would repeat, the affected panel being ‘desynced’ from the rest.

So I added a delay to the layers init(), making the factory.create() calls happen later, and it seems to have solved the issue, which I think supports my theory → looks like it had to do with the order of update() per object?

Is there a good way to enforce / manage this order explicitly?

Yeah, sorry for being unclear. This was exactly what I was wondering. Thanks.

I don’t understand this really. The panels are children of the layers (layer1, layer2, layer3) right? They are not moved in your code? It is the layer or parent go that is moving?

All transforms are updated once and then everything is rendered. There is no in-between thing that could cause this kind of issue.

Good that you solved it but I’m not sure how :slight_smile:

1 Like

Well, both are moving actually. The panels actually do the scrolling themselves, and the layer simply ‘catches’ them and puts them at the opposite side if they move too far. There are technically 3 parts to the panel movement:

  • The parent of the layers follows the camera, making sure of their relative alignment without scripting aside from childing the layers to the parent, and childing the panels to the layer.

  • The panel themselves move by scaling the X and Y values of the camera’s per-frame movement by constants set by properties in their own script.

  • The layers keeps track of the panels’ position for the first and last instances and checks whether they have exceeded a distance equal to a specified number of tiles (the ‘radius’) in either direction. If it has, it moves the panel to the opposite end of the layer and reorders the table it uses to determine which panels to check.