Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flame tiled web rendering vertical lines issue while camera movement #1152

Open
Jcupzz opened this issue Nov 29, 2021 · 47 comments · Fixed by #1866
Open

Flame tiled web rendering vertical lines issue while camera movement #1152

Jcupzz opened this issue Nov 29, 2021 · 47 comments · Fixed by #1866
Labels
bug Flutter issue This problem is likely caused by the Flutter framework

Comments

@Jcupzz
Copy link

Jcupzz commented Nov 29, 2021

Current bug behaviour

Expected behaviour

Vertical lines issue in web whenever camera moves, in android as APK there is no issue. Those vertical lines exist only for the web.

2021-11-29_12-45-49.mp4

Steps to reproduce

https://beach-website.web.app/#/
use arrow keys to move the player

repo: https://github.com/Jcupzz/beach_hack_website_2022

Flutter doctor output

Output of: flutter doctor -v

More environment information

flame: ^1.0.0-releasecandidate.16
flame_tiled: ^1.0.0-releasecandidate.15

Log information

Enter log information in this code block

More information

flutter/flutter#14288

@Jcupzz Jcupzz added the bug label Nov 29, 2021
@phyohtetarkar
Copy link

I think it is texture bleeding. May be you can try adding padding to your texture when packing.

@Jcupzz
Copy link
Author

Jcupzz commented Dec 10, 2021

I think it is texture bleeding. May be you can try adding padding to your texture when packing.

I tried adding extrusion of 1 pixel and 2 pixel to each tiled sprite using texturepacker...it doesn't work. I think the reason is due to drawAtlas method which doesn't work for flutter web. Flutter doesn't support drawAtlas method for web. The site works perfectly for android but doesn't work for web

@spydon
Copy link
Member

spydon commented Dec 10, 2021

@Jcupzz have you tried this with 1.0.0? The fallback was removed in the stable release, but that said, you can get those artifacts with drawAtlas too. See #1153.

@Jcupzz
Copy link
Author

Jcupzz commented Dec 18, 2021

@Jcupzz have you tried this with 1.0.0? The fallback was removed in the stable release, but that said, you can get those artifacts with drawAtlas too. See #1153.

@spydon Yes, I have tried with 1.0.0 still the problem persists. Well, in android the same code is working perfectly there are no vertical lines issue. So I think with drawAtlas I don't get those artifacts

@leeflix
Copy link
Contributor

leeflix commented Dec 29, 2021

I also discovered this bug some time ago... I wanted to make a tile based game with Flutter / Flame, but can't do it because of this. I opened issues on this in the Flutter (flutter/flutter#74127) and Flame #637 repo some time ago with no success. There is also a big issue (flutter/flutter#14288) on this in the flutter repo which was opened in 2018 but is not resolved yet...

I think it hast to do with the methods canvas.scale and canvas.translate. Every plattform even Android is affected (just scale the canvas by something like 1.32). It just happens that certain screen sizes with the right scaling dont produce this bug. For example when I move Mario in the game of @Jcupzz I have no problems only when i resize the browser windows from full screen. (I have a 4k monitor)

It would be cool, if we find a fix / workaround for this, because a friend of mine an me really want to make a tile based game with Flame.

The following is a minimal example to reproduce this (resize the window if it doesn't happen):

import 'package:flame/game.dart';
import 'package:flutter/material.dart';

void main() => runApp(GameWidget(game: MyGame()));

class MyGame extends FlameGame {
  Paint p = Paint()..color = Colors.blue;

  @override
  void render(Canvas canvas) {
    super.render(canvas);

    int tilesInX = 16;
    int tilesInY = 16;
    double tileSize = 16;

    canvas.scale(size.x / (tilesInX * tileSize));

    for (int x = 0; x < tilesInX; x++) {
      for (int y = 0; y < tilesInY; y++) {
        canvas.drawRect(
          Rect.fromLTWH(
            x * tileSize,
            y * tileSize,
            tileSize,
            tileSize,
          ),
          p,
        );
      }
    }
  }
}

@spydon
Copy link
Member

spydon commented Dec 29, 2021

@felithium I don't think there is much we can do unfortunately, since it seems to be a Flutter bug.
We can of course try to fix it upstream, but I wouldn't have a clue of where to start even...
Or if there is some kind of work around that we can do maybe, where the tiles are overlapping with one pixel for example?

@leeflix
Copy link
Contributor

leeflix commented Jan 1, 2022

Hey, I might have found a solution. After spending hours trying different solutions to find a workaround (which I actually did aswell) I found out that this problem does not appear when you exactly use canvas.drawRectImage AND set the isAntialias of the Paint instance to false. If you for example use canvas.drawImage it simply does not matter if isAntialias is true or false,
the artifacts will eventually appear. I have no clue why that is...

I fixed the game of @Jcupzz by stepping in the source of Flame where the TiledMap is painted. And it does use canvas.drawRectImage, but isAntialias is not set, so by default true, so I set it to false manually.

This is the file but the latest version is slightly different than the version of @Jcupzz:
https://github.com/flame-engine/flame/blob/main/packages/flame/lib/src/sprite_batch.dart

void render(
    Canvas canvas, {
    BlendMode? blendMode,
    Rect? cullRect,
    Paint? paint,
  }) {
    paint ??= Paint();

    if (kIsWeb) {
      for (final batchItem in _batchItems) {
        paint..blendMode = blendMode ?? paint.blendMode;

        canvas
          ..save()
          ..transform(batchItem.matrix.storage)
          ..drawRect(batchItem.destination, batchItem.paint)
          ..drawImageRect(
            atlas,
            batchItem.source,
            batchItem.destination,
            paint..isAntiAlias = false, // i changed this
          )
          ..restore();
      }
    } else {
      canvas.drawAtlas(
        atlas,
        _transforms,
        _sources,
        _colors,
        blendMode ?? defaultBlendMode,
        cullRect,
        paint,
      );
    }
  }

I couldn't produce the artifacts anymore.

2022-01-01.19-23-46.mp4

Would be cool if someone else could confirm this, because I am not 100% if I am not seeing ghosts after spending to long on this problem.

Hope this helps!

@Jcupzz
Copy link
Author

Jcupzz commented Jan 1, 2022

Hey, I might have found a solution. After spending hours trying different solutions to find a workaround (which I actually did aswell) I found out that this problem does not appear when you exactly use canvas.drawRectImage AND set the isAntialias of the Paint instance to false. If you for example use canvas.drawImage it simply does not matter if isAntialias is true or false, the artifacts will eventually appear. I have no clue why that is...

I fixed the game of @Jcupzz by stepping in the source of Flame where the TiledMap is painted. And it does use canvas.drawRectImage, but isAntialias is not set, so by default true, so I set it to false manually.

This is the file but the latest version is slightly different than the version of @Jcupzz: https://github.com/flame-engine/flame/blob/main/packages/flame/lib/src/sprite_batch.dart

void render(
    Canvas canvas, {
    BlendMode? blendMode,
    Rect? cullRect,
    Paint? paint,
  }) {
    paint ??= Paint();

    if (kIsWeb) {
      for (final batchItem in _batchItems) {
        paint..blendMode = blendMode ?? paint.blendMode;

        canvas
          ..save()
          ..transform(batchItem.matrix.storage)
          ..drawRect(batchItem.destination, batchItem.paint)
          ..drawImageRect(
            atlas,
            batchItem.source,
            batchItem.destination,
            paint..isAntiAlias = false, // i changed this
          )
          ..restore();
      }
    } else {
      canvas.drawAtlas(
        atlas,
        _transforms,
        _sources,
        _colors,
        blendMode ?? defaultBlendMode,
        cullRect,
        paint,
      );
    }
  }

I couldn't produce the artifacts anymore.

2022-01-01.19-23-46.mp4
Would be cool if someone else could confirm this, because I am not 100% if I am not seeing ghosts after spending to long on this problem.

Hope this helps!

I appreciate the efforts you have taken, good job. I tested with paint..isAntiAlias = false it worked in mine too. However, I was working on a latest version of tile image which I made by editing in photoshop which I haven't uploaded to github yet. In that newer version of game even though I made paint..isAntiAlias = false and tested the problem persists, sadly there are white vertical lines.

2022-01-02.01-00-28.mp4

For the above new version I haven't added extrusion to the tile image(tile width is 64).
Well, for the one that you have tested there I think I have added extrusion of 2 or 1 pixels(not sure), because in the tmx file the tile width and height is 66.

So, I think the actual problem is not fully solved. But you have found a work around to solve the problem @felithium 👏👏👏

@Jcupzz
Copy link
Author

Jcupzz commented Jan 1, 2022

Hey, I might have found a solution. After spending hours trying different solutions to find a workaround (which I actually did aswell) I found out that this problem does not appear when you exactly use canvas.drawRectImage AND set the isAntialias of the Paint instance to false. If you for example use canvas.drawImage it simply does not matter if isAntialias is true or false, the artifacts will eventually appear. I have no clue why that is...

I fixed the game of @Jcupzz by stepping in the source of Flame where the TiledMap is painted. And it does use canvas.drawRectImage, but isAntialias is not set, so by default true, so I set it to false manually.

This is the file but the latest version is slightly different than the version of @Jcupzz: https://github.com/flame-engine/flame/blob/main/packages/flame/lib/src/sprite_batch.dart

void render(
    Canvas canvas, {
    BlendMode? blendMode,
    Rect? cullRect,
    Paint? paint,
  }) {
    paint ??= Paint();

    if (kIsWeb) {
      for (final batchItem in _batchItems) {
        paint..blendMode = blendMode ?? paint.blendMode;

        canvas
          ..save()
          ..transform(batchItem.matrix.storage)
          ..drawRect(batchItem.destination, batchItem.paint)
          ..drawImageRect(
            atlas,
            batchItem.source,
            batchItem.destination,
            paint..isAntiAlias = false, // i changed this
          )
          ..restore();
      }
    } else {
      canvas.drawAtlas(
        atlas,
        _transforms,
        _sources,
        _colors,
        blendMode ?? defaultBlendMode,
        cullRect,
        paint,
      );
    }
  }

I couldn't produce the artifacts anymore.

2022-01-01.19-23-46.mp4
Would be cool if someone else could confirm this, because I am not 100% if I am not seeing ghosts after spending to long on this problem.

Hope this helps!

I also discovered this bug some time ago... I wanted to make a tile based game with Flutter / Flame, but can't do it because of this. I opened issues on this in the Flutter (flutter/flutter#74127) and Flame #637 repo some time ago with no success. There is also a big issue (flutter/flutter#14288) on this in the flutter repo which was opened in 2018 but is not resolved yet...

I think it hast to do with the methods canvas.scale and canvas.translate. Every plattform even Android is affected (just scale the canvas by something like 1.32). It just happens that certain screen sizes with the right scaling dont produce this bug. For example when I move Mario in the game of @Jcupzz I have no problems only when i resize the browser windows from full screen. (I have a 4k monitor)

It would be cool, if we find a fix / workaround for this, because a friend of mine an me really want to make a tile based game with Flame.

The following is a minimal example to reproduce this (resize the window if it doesn't happen):

import 'package:flame/game.dart';
import 'package:flutter/material.dart';

void main() => runApp(GameWidget(game: MyGame()));

class MyGame extends FlameGame {
  Paint p = Paint()..color = Colors.blue;

  @override
  void render(Canvas canvas) {
    super.render(canvas);

    int tilesInX = 16;
    int tilesInY = 16;
    double tileSize = 16;

    canvas.scale(size.x / (tilesInX * tileSize));

    for (int x = 0; x < tilesInX; x++) {
      for (int y = 0; y < tilesInY; y++) {
        canvas.drawRect(
          Rect.fromLTWH(
            x * tileSize,
            y * tileSize,
            tileSize,
            tileSize,
          ),
          p,
        );
      }
    }
  }
}

I also tested this with paint..isAntiAlias = false but didn't work. So I think maybe in mario it worked because of the extrusion of 1 or 2 pixels( I don't remember it correctly)

@natebot13
Copy link
Contributor

This could also have to do with rendering pixel art at non integer scales. Tiles that are, for example, 16 pixels wide, aren't going to render very well scaled to 17 pixels wide. This is a common issue with pixel art. It can be somewhat fixed by what's been discussed above, i.e. using antialiasing, but this subtly blends colors together, somewhat ruining the perfectness of pixel art. I've instead fixed it in my game by ensuring everything is positioned and scaled at integer increments. Crucially, I made my own copy of FixedResolutionViewport and modified it to only scale at integer increments. This works for my pixel perfect game but might not be suited for every game.

@st-pasha
Copy link
Contributor

Crucially, I made my own copy of FixedResolutionViewport and modified it to only scale at integer increments.

Interesting, so you restrict the camera zoom level to integers (1, 2, 3, ...)? And the viewfinder's position to integer coordinates as well? And same for all the components?

@natebot13
Copy link
Contributor

Interesting, so you restrict the camera zoom level to integers (1, 2, 3, ...)? And the viewfinder's position to integer coordinates as well? And same for all the components?

Yes, everything is stuck to an integer grid and specifically I ceil the scale of the viewport. This way it's locked to an integer scale slightly larger than the screen to fill it rather than flooring or rounding. This also means I can't take advantage of the built-in camera smoothing since I have to jump from pixel to pixel. I may figure out a way around that but haven't gotten there yet.

As a side note, I'm using flame_oxygen, which means I think I have more control to make my systems operate at integers only.

@st-pasha
Copy link
Contributor

Interesting, what about component rendering -- how do you ensure that a component renders at the integer screen coordinates only?

@natebot13
Copy link
Contributor

I just use a rounded version of the position component before rendering, which locks it to the pixel grid. Even if the viewport is scaled up, the sprites scale and lock to the pixel grid, keeping all the sprites consistent, and the whole scene like a single pixel art image.

@Jcupzz
Copy link
Author

Jcupzz commented May 14, 2022

Crucially, I made my own copy of FixedResolutionViewport and modified it to only scale at integer increments.

Is there any sample code that you can share?

@natebot13
Copy link
Contributor

natebot13 commented May 14, 2022

Sure, it's a small change. I copied the full FixedResolutionViewport class from viewport.dart to a file in my project and in resize added a .ceil().toDouble() to the _scale:

157,160c55,61
<     _scale = math.min(
<       canvasSize!.x / effectiveSize.x,
<       canvasSize!.y / effectiveSize.y,
<     );
---
>     _scale = math
>         .min(
>           canvasSize!.x / effectiveSize.x,
>           canvasSize!.y / effectiveSize.y,
>         )
>         .ceil()
>         .toDouble();

I also round() the _scaledSize in order to ensure the viewport sits on an exact pixel, like this:

*** 165,168 ****
      _resizeOffset
        ..setFrom(canvasSize!)
        ..sub(_scaledSize)
<       ..scale(0.5);
--- 66,70 ----
      _resizeOffset
        ..setFrom(canvasSize!)
        ..sub(_scaledSize)
>       ..scale(0.5)
>       ..round();

You can replace either of those with ceil, floor, or round for your needs, but I chose ceil for _scale so that the scene would scale outside of the window to fill the screen more. Of course that cuts off some of my game world but that's fine for my game. I round _resizeOffset to match all my other entities which are also rounding their coordinates.

Then of course create and apply your custom viewport to the camera. Camera and Viewport

@spydon
Copy link
Member

spydon commented May 14, 2022

Do you want to PR that viewport @natebot13? It seems like it could be useful to others too.

@ryokuchayurai
Copy link

ryokuchayurai commented May 20, 2022

Thank you for the promising solutions.

I added the filterQuality setting in addition to paint..isAntiAlias = false and it improved mine.

void render(
    Canvas canvas, {
    BlendMode? blendMode,
    Rect? cullRect,
    Paint? paint,
  }) {
    paint ??= Paint();

    if (kIsWeb) {
      for (final batchItem in _batchItems) {
        paint..blendMode = blendMode ?? paint.blendMode;

        canvas
          ..save()
          ..transform(batchItem.matrix.storage)
          ..drawRect(batchItem.destination, batchItem.paint)
          ..drawImageRect(
            atlas,
            batchItem.source,
            batchItem.destination,
            paint..isAntiAlias = false
              ..filterQuality = FilterQuality.low, // i changed this
          )
          ..restore();
      }
    } else {
      canvas.drawAtlas(
        atlas,
        _transforms,
        _sources,
        _colors,
        blendMode ?? defaultBlendMode,
        cullRect,
        paint,
      );
    }
  }

@ryokuchayurai
Copy link

I have found that a modification to the RenderableTiledMap improves the situation.

@@ -135,7 +137,7 @@ class RenderableTiledMap {
     await Future.forEach(map.tiledImages(), (TiledImage img) async {
       final src = img.source;
       if (src != null) {
-        result[src] = await SpriteBatch.load(src);
+        result[src] = await SpriteBatch.load(src, useAtlas: kIsWeb ? false: true);
       }
     });
 
@@ -192,10 +194,14 @@ class RenderableTiledMap {
     });
   }
 
+  final Paint _paint = Paint()
+    ..isAntiAlias = false
+    ..filterQuality = FilterQuality.low;
+
   /// Render [batchesByLayer] that compose this tile map.
   void render(Canvas c) {
     batchesByLayer.forEach((batchMap) {
-      batchMap.forEach((_, batch) => batch.render(c));
+      batchMap.forEach((_, batch) => batch.render(c, paint: kIsWeb ? _paint: null));
     });
   }

I do not know if this modification is appropriate....

@natebot13
Copy link
Contributor

I recently changed monitors and this issue cropped up for me again, despite my previous rounding fixes I had made. I then figured out that that this issue is actually a consequence of Flutter's feature of using the devicePixelRatio to keep the size of widgets the same across all devices. This is a problem because even if you round your draw coordinates, where the sprite actually lands on the screen might be between two physical pixels. I'm looking into a solution that takes the pixel ratio into account to hopefully place all the pixels onto physical pixels precisely.

@spydon
Copy link
Member

spydon commented Aug 25, 2022

Not really a solution, but if you run with Impeller this issue shouldn't occur.

@spydon
Copy link
Member

spydon commented Dec 21, 2023

Here's a great article by @erickzanardo that explains how to work around this issue:
https://verygood.ventures/blog/solving-super-dashs-rendering-challenges-eliminating-ghost-lines-for-a-seamless-gaming-experience

@benni-tec
Copy link

@spydon Thank you, for linking the article. I was running into the same problem and will probably endup doing this in the short term. However it seems that is a persisting issue and a Viewport that scales to the device pixel boundaries seems to me like a more sustainable solution.

I looked into implementing #2810 however I was not able to. I managed to get the lines to be static (instead of changing when moving) but I was unable to replicate the effect that @natebot13 was able to achieve.

Is there any documentation on how the Viewport/Viewfinder work together and why these classes all inherit a resolution, and how the ScaleProvider affects the cameras behaviour? Related to this I have also found it difficult to convert screen coordinates into world coordinates! However I could not understand what is actually happening from the source code alone and have not found additonal resources.

I am happy to implement #2810 myself if there is further documentation available, otherwise any help would be greatly appreciated!

@spydon
Copy link
Member

spydon commented Dec 29, 2023

Is there any documentation on how the Viewport/Viewfinder work together

If you've already read the docs about it on our docs page I can recommend reading the dartdocs that accompany the code, they are pretty good.

and why these classes all inherit a resolution, and how the ScaleProvider affects the cameras behaviour?

The scale for the viewfinder is the zoom and the scale for the viewport is used to set up things like the FixedResolutionViewport.

Related to this I have also found it difficult to convert screen coordinates into world coordinates!

Just use globalToLocal and localToGlobal:
https://github.com/flame-engine/flame/blob/main/packages%2Fflame%2Flib%2Fsrc%2Fcamera%2Fcamera_component.dart#L209

@benni-tec
Copy link

If you've already read the docs about it on our docs page I can recommend reading the dartdocs that accompany the code, they are pretty good.

I think I already looked at the code and did not find dartdocs on these classes, however this was a few weeks ago. I will look into it again!

The scale for the viewfinder is the zoom and the scale for the viewport is used to set up things like the FixedResolutionViewport.

That makes sense! Thank you :)

Just use globalToLocal and localToGlobal: https://github.com/flame-engine/flame/blob/main/packages%2Fflame%2Flib%2Fsrc%2Fcamera%2Fcamera_component.dart#L209

I think I did however the values did not match what I expected, I was particulary confused because these can be called on multiple objects. Did I understand correctly that I first need to translate them from screen to global coordinates using the camera and then from global to local using the World component?

@benni-tec
Copy link

Just took a brief look at the code again: While the default viewports do have dartdocs explaining what they do, they do not have dartdocs/comments explaining how they do it.

I tried basing a device-pixel integer viewport on the FixedResolutionViewPort and the old implementation I believe, but did not really understand what is happening, esspecially what the class it self, it's super-class and the ScaleProvider class change regarding the viewports behaviour.

I will look into this further after the holidays 🎅

@spydon
Copy link
Member

spydon commented Dec 29, 2023

I think I did however the values did not match what I expected, I was particulary confused because these can be called on multiple objects. Did I understand correctly that I first need to translate them from screen to global coordinates using the camera and then from global to local using the World component?

No, you only have to do it on the CameraComponent and it will bring it straight to world coordinates, since the world doesn't modify the coordinates, it is only observed through the camera.

@benni-tec
Copy link

No, you only have to do it on the CameraComponent and it will bring it straight to world coordinates, since the world doesn't modify the coordinates, it is only observed through the camera.

Ah just had a quick look at my code, turns out I was only using the viewfinder to convert from canvas to world coordinates, I'm guessing thats where the discrepancy came from. I will test it once I'm back at work.

Thank you for the quick response, eventhough this is off-topic for this issue 😄

@spydon
Copy link
Member

spydon commented Dec 29, 2023

Ah true, dartdocs are for "what it does" not "how it does it".

Thank you for the quick response, eventhough this is off-topic for this issue 😄

Np, I can recommend joining our Discord server if you're not there already btw.

@benni-tec
Copy link

So I tried again to fix this issue, firstly I added margins and spacing. That seemed to make the lines "smoother" or lighter, but they are still there. However they are now static and horizontal!

I tried to implement the DeviceIntegerViewport without much luck:

class DeviceIntegerViewport extends Viewport implements ReadOnlyScaleProvider {
  static FlutterView _firstView() =>
      f.WidgetsBinding.instance.platformDispatcher.views.first;

  bool clipping;

  DeviceIntegerViewport({this.clipping = true});

  double _ratio = 0;

  @override
  Vector2 size = Vector2.zero();
  
  @override
  Vector2 get scale => Vector2.all(1 / _ratio);

  @override
  void onLoad() => _resize();

  @override
  void onViewportResize() => _resize();

  void _resize() {
    final _size = _firstView().physicalSize.toVector2();
    if (_size == size) return;

    size = _size;
    _ratio = _firstView().devicePixelRatio;
    position.round();
  }

  @override
  void clip(Canvas canvas) {
    if (clipping) canvas.clipRect(size.toRect());
  }
}

A side from not showing any visible effect it also does not change size properly but I do not understand why, when resizing the browser window it seems to change size at all (causing clipping when enlarging and not moving the centered player to the center). However as far as I understand this should work, what am I missing?

@benni-tec
Copy link

Correction: There are no lines anymore but I can see the background between them, checking if I correctly added margin and spacing. Still the Viewport I have written does not work/do anything 😢

@benni-tec
Copy link

It appears like the entire tile is shown, so maybe the transparent margin/spacing causes the background to show through?

image

When zooming by 2 using the viewfinder the lines appear all around:
image

Changing the viewfinder zoom to devicePixelRation or its inverse also resulted in only horizontal lines. Therefore I think this is the same bug.

Changing the window size it is possible to find a sweet spot where there are no lines.

This was all tested without a viewport!

@erickzanardo
Copy link
Member

Hey @benni-tec 👋

Have you tried the suggested solution at the Troubleshooting section? It has solved that issue in many of our projects

@benni-tec
Copy link

benni-tec commented Jan 3, 2024

I think I fixed a part of the Viewport, it does properly change size now, but the centered player still does not stay centred (do I need to adjust the position by the difference?).

The lines now allways appear vertically and horizontally and stay consistent when resizing. Basically I'm forcing the game to always use the physical size and then adjust by the inverse of the devicePixelRation to undo the scaling flutter is doing, is this correct?

class DeviceIntegerViewport extends Viewport with f.WidgetsBindingObserver implements ReadOnlyScaleProvider {
  static FlutterView _firstView() => f.WidgetsFlutterBinding.ensureInitialized().platformDispatcher.views.first;

  bool clipping;

  DeviceIntegerViewport({this.clipping = true});

  Vector2 _size = _firstView().physicalSize.toVector2();
  double _ratio = _firstView().devicePixelRatio;

  @override
  void onLoad() {
    f.WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeMetrics() {
    _size = _firstView().physicalSize.toVector2();
    _ratio = _firstView().devicePixelRatio;

    position.round();
  }

  @override
  Vector2 get size => _size;

  @override
  Vector2 get scale => Vector2.all(1 / _ratio);

  @override
  void onViewportResize() => position.round();

  @override
  void clip(Canvas canvas) {
    if (clipping) canvas.clipRect(size.toRect());
  }
}

@benni-tec
Copy link

Hey @benni-tec 👋

Have you tried the suggested solution at the Troubleshooting section? It has solved that issue in many of our projects

Yes I did, however as I mentioned before I do not think that is a sustainable solution. Especially for large tile sets this does get quite tedious and requires up to 4x the space. While your solution does work and is quite smart, I think there should still be an effort to correct this on the "engine" side instead of just working around the bug!

As this was fixed before by adjusting the camera (in the old camera system) I think it should also be possible to do the same with a viewport. Am I wrong in that assumption?

I'm currently trying to figure out how the viewports actually work, since they do not behave as I would expect from reading the documentation/dartdocs, or I'm just missing something big!

PS: Sorry for the excessive posting, my last 3 comments could have been one 😅

@benni-tec
Copy link

benni-tec commented Jan 3, 2024

@spydon @erickzanardo If you prefer I can move this discussion about implementing #2810 into that issue, however I was hoping that participants here would have more insight, since a similar solution for the old API was already implemented in this issue!

@erickzanardo
Copy link
Member

Especially for large tile sets this does get quite tedious and requires up to 4x the space.

I agree with ya, even on small projects, but with relative big tilesets it can get tedious, I remember that took me some time to go through all of the tiles in Super Dash 🙃

As this was fixed before by adjusting the camera (in the old camera system) I think it should also be possible to do the same with a viewport. Am I wrong in that assumption?

From everything that I researched when trying to solve that issue on Super Dash, I think this is not 100% solvable in the camera code. Like I mention on that article, this error happens due to point-float imprecision, and sadly, there is nothing we can do solve that (at least not without trying to figure out how to build a new whole system of storing point-float numbers in computers 😅).

That imprecision gets worse or better depending on which platform is the game running, for example, on web it is quite noticeable, and that is mostly likely because the JS number has a worse accuracy in other platforms, ofc this is not a rule and in some games it may run better on web than elsewhere, but at least in my experience that was on pattern that I noticed.

I don't really believe that we can 100% automatically solve the issue without the user intervention, but I believe that solution presented on the article is the only achievable one (I would love to be proved wrong on this btw), so I think what we should do, is to expand it and make it less tedious, for example, now you have to manually select a lot of tiles, I am pretty sure we can make a smart algorithm that would "auto-patch" tiles, making the process less tedious and more scalable for larger projects.

@benni-tec
Copy link

From everything that I researched when trying to solve that issue on Super Dash, I think this is not 100% solvable in the camera code. Like I mention on that article, this error happens due to point-float imprecision, and sadly, there is nothing we can do solve that (at least not without trying to figure out how to build a new whole system of storing point-float numbers in computers 😅).

My tests (see above) lead me to believe that this should be possible: With the default viewport the lines change when resizing the browser and they even disappear completly at certain sizes. Shouldn't it be possible then to find that scaling (it should be devicePixelRatio right?) and adjust by that in the viewport?

That imprecision gets worse or better depending on which platform is the game running, for example, on web it is quite noticeable, and that is mostly likely because the JS number has a worse accuracy in other platforms, ofc this is not a rule and in some games it may run better on web than elsewhere, but at least in my experience that was on pattern that I noticed.

This is certainly true! When building for windows with the default viewport and no zoom there are no lines at all at least on my laptop and desktop!

I don't really believe that we can 100% automatically solve the issue without the user intervention, but I believe that solution presented on the article is the only achievable one (I would love to be proved wrong on this btw), so I think what we should do, is to expand it and make it less tedious, for example, now you have to manually select a lot of tiles, I am pretty sure we can make a smart algorithm that would "auto-patch" tiles, making the process less tedious and more scalable for larger projects.

Yes a auto-patch algorithm would be an improvement, if a fix in the camera is not possible I will likely implement this. However at that point there would be so many duplicates that I don't really see a difference to just rendering the layers beforehand and importing them as pictures instead of as tiles. If this much preprocessing is required why not assemble all static parts into images, that would be far less computationally expensive for the game and solve this issue for good!

@benni-tec
Copy link

benni-tec commented Jan 3, 2024

I think currently my biggest problem is that I do not understand how the viewport really works, espacially how virtualSize can impact scaling and such. I tried implementing the scaling (instead of just forcing it to use the physical size) analogous to FixedResolutionViewport without much luck, the behaviour did not change at all.

Is there something I'm missing with the viewport API??

class DeviceIntegerViewport extends Viewport with f.WidgetsBindingObserver implements ReadOnlyScaleProvider {
  static FlutterView _firstView() => f.WidgetsFlutterBinding.ensureInitialized().platformDispatcher.views.first;

  DeviceIntegerViewport();

  Vector2 _physicalSize = _firstView().physicalSize.toVector2();
  late double _ratio = math.min(size.x / _physicalSize.x, size.y / _physicalSize.y);

  @override
  void onMount() {
    f.WidgetsBinding.instance.addObserver(this);
    super.onMount();
  }
  
  @override
  void onRemove() {
    super.onRemove();
    f.WidgetsBinding.instance.removeObserver(this);
  }

  @override
  void didChangeMetrics() {
    _physicalSize = _firstView().physicalSize.toVector2();
    onViewportResize();
  }

  @override
  void onViewportResize() {
    final scaleX = size.x / _physicalSize.x;
    final scaleY = size.y / _physicalSize.y;
    _ratio = math.min(scaleX, scaleY);

    position.round();
  }

  @override
  Vector2 get scale => Vector2.all(_ratio);

  @override
  Vector2 get virtualSize => _physicalSize;

  @override
  void clip(Canvas canvas) {}
}

@erickzanardo
Copy link
Member

My tests (see above) lead me to believe that this should be possible: With the default viewport the lines change when resizing the browser and they even disappear completly at certain sizes. Shouldn't it be possible then to find that scaling (it should be devicePixelRatio right?) and adjust by that in the viewport?

They disappear because the size of the canvas changes and they probably get closer to a full integer, or to even numbers, which are mostly are likely to cause imprecision erros, when I tested such approach, it reduced the frequency of the issue, but it didn't solved for good. But like I mentioned, I would love to be proved wrong on this hahahah, so if you feeling motivated on digging more on that approach, that would be awesome, even if it doesn't get 100% solved, but improves, that is great already!

@benni-tec
Copy link

benni-tec commented Jan 3, 2024

They disappear because the size of the canvas changes and they probably get closer to a full integer, or to even numbers, which are mostly are likely to cause imprecision erros, when I tested such approach, it reduced the frequency of the issue, but it didn't solved for good. But like I mentioned, I would love to be proved wrong on this hahahah, so if you feeling motivated on digging more on that approach, that would be awesome, even if it doesn't get 100% solved, but improves, that is great already!

Yes I do agree on why this changes, still I think it should be possible. As far as I know the devicePixelRatio is only dependent on the display so it should not change when resizing the window.

I was able to get the lines to be static both using a viewport and by limiting the size of the game-widget with the default viewport using constraints to be a multiple of the devicePixelRatio or its inverse. I think I maximised the lines by doing this. Why shouldn't this also work in reverse, so minimizing the lines?

@benni-tec
Copy link

Whelp now I'm offically confused, as it turns out the devicePixelRatio is hard-coded to 1 in browsers, so nothing of this should realy matter...

How did any of the previous solution involving devicePixelRatio work (in the browser) at all?

I can't spend anymore time on this today I will look into this issue again later, maybe the solution lies in what sizes result in no lines 🤔

@benni-tec
Copy link

benni-tec commented Jan 8, 2024

So I just had another go at it and fixed the problem in like 5 mintues 🤦

Looking at the sizes that did not cause lines I noticed they were all multiples of 4, so I tried forcing the game widget's size (using a SizedBox) to be a multiple of 4, and it worked!

Actually it also works with a multiple of 2, but not 1 so I'm not realy sure what the 2 actually does but it does work! Huzza
I also used a commit that does not have marings/spacings in the tilesets yet!

I also wrote a small Viewport in order to prevent edges, which works just fine as far as I can tell!

class MultipleViewport extends MaxViewport {
  final int multipleOf;

  MultipleViewport({this.multipleOf = 2, super.children});

  @override
  void onGameResize(Vector2 size) {
    super.onGameResize(size);
    size.x = size.x.nearestMultiple(multipleOf).toDouble();
    size.y = size.y.nearestMultiple(multipleOf).toDouble();
  }

  @override
  bool containsLocalPoint(Vector2 point) => true;

  @override
  void clip(Canvas canvas) {}

  @override
  void onViewportResize() {}
}

extension NearestMultiple on num {
  int nearestMultiple(int of) => this ~/ of * of;
}

@erickzanardo Could you test this viewport as well and see if it works for you? You are using flame in the browser right?
I'm guessing this would not work if devicePixelRatios != 1, however I did not test this yet!

@spydon
Copy link
Member

spydon commented Jan 8, 2024

@benni-tec Interesting, I don't understand how this could work for the general case.
Are all your components placed in relation to the size of the viewport maybe?

@benni-tec
Copy link

benni-tec commented Jan 8, 2024

No, I'm just using flame_tiled to load the map, it is therefore pixel-aligned.

Just tried it on my laptop, it does not work there because it turns out that flutter now does seem to set devicePixelRatio != 1 in browsers...

Still mutliple-of-2 did fix the issue for devicePixelRatio == 1. Do you have an idea how I could combine these two in a sensible manner within a viewport, maybe via a virtualSize wich is dependent on the devicepixleratio and always a multiple of 2?

@benni-tec
Copy link

benni-tec commented Jan 8, 2024

So I found a widget in this issue flutter/flutter#32115 which can scale to imitate a devicePixelRatio of 1 by @ardera, the widget can also be found in https://gist.github.com/ardera/25a8c81a54fb37b0dc750d383caac5d9

By using my MultipleViewport (with 2 as a multiple) and wrapping the GameWidget in ardera's FakeDevicePixelRatio widget set to 1, I was able to get rid of the lines on my laptop as well!

I will try this later on some more displays, but I think this works now. Still this definetly isn't the most elegant solution especially since it needs a wrapper widget. However this eliminates the need to manually correct the tiles!

It also does not work with zoom (due to double imprecission even integer zoom does not work), as the viewfinder seems to be applied after the viewport. This again could be solved by using a viewport with virtualSize I think. If I'm correct with this assumption @spydon I will try to implement this later :)

Due to time constraints on my part I will probably stick with it for now!

@DanielLukic
Copy link

DanielLukic commented May 24, 2024

for what it's worth, @ryokuchayurai 's solution from above (#1152 (comment)) seems to fix the issue for me.

i'm merely prototyping a tower defense game with tiled background. not a serious issue. so i needed a quick solution. padding tiles feels like the wrong solution. and FilterQuality.low actually sounds like the right solution for a pixel art game. in fact, FilterQuality.none would be the ideal choice I guess.. 🙃

weirdly, ofc because why not, the FilterQuality.low fix breaks desktop rendering while fixing web rendering.. 🤣 but a !kIsWeb for the paint part works in this case.

using FilterQuality.none, however, seems to work just fine on my tested platforms. that's what i'm using now.

fyi:

    final map = await TiledComponent.load(
      '$basename.tmx',
      Vector2(16.0, 16.0),
      useAtlas: !kIsWeb,
      layerPaintFactory: (it) => _layerPaint(),
    );

  Paint _layerPaint() {
    return Paint()
      ..isAntiAlias = false
      ..filterQuality = FilterQuality.none;
  }

update: had to re-read everything above. i noticed a few glitched tiles when resizing. even with the 'integer viewport' from above. but overall this is acceptable in my case. but definitely a strange api on flutter level... 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Flutter issue This problem is likely caused by the Flutter framework
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants