drawpile / Drawpile

A collaborative drawing program
http://drawpile.net/
GNU General Public License v3.0
1.06k stars 133 forks source link

Canvas view zoom out aliasing #1389

Open cromachina opened 1 month ago

cromachina commented 1 month ago

Drawpile version 2.2.1, Linux

Problem

When looking at a large canvas while zoomed out (for example, 4000x4000 on a 1080p screen), the canvas view looks extremely aliased, as if using nearest neighbor downscaling. I checked in the Preferences -> General -> Canvas View: Interpolate when view is zoomed or rotated, and it only seems to apply when zoomed in, not zoomed out.

Potential solution?

I'm not quite sure how it works in Drawpile, but I'm guessing it's because of the limited resampling options provided for QPixmaps, Qt::SmoothTransformation (bilinear, probably) and Qt::FastTransformation (nearest neighbor), both of which tend to look pretty bad when downsampling large images. For my own paint tool project, I hacked around this by using OpenCV::resize to downscale the composited canvas view with INTER_AREA interpolation: https://github.com/cromachina/crowpainter/blob/main/src/crowpainter/main.py#L269

OpenCV::resize seems to be really well optimized and parallelized, but you could also compute a zoom level pyramid or mipmap off of the main thread and update the view later, async (kind of like how Paint Tool SAI does it), if there are performance or library usage concerns.

Aliased canvas example in Drawpile: image

Smooth canvas example using my own tool: image

askmeaboutlo0m commented 1 month ago

The behavior is correct, the analysis not quite. It's not using nearest-neighbor interpolation, you just run into the limits of bilinear interpolation at those kinds of zooms and end up with not very smooth results.

There's currently three different canvas renderers: one based on QGraphicsView that'll hopefully get thrown out at some point, a "software" renderer using QPainter directly and a hardware renderer that uses OpenGL (on Windows, it's Direct3D under the hood via ANGLE.) They all work a bit differently.

In the first two cases, there's the QGraphicsView CanvasItem paint function and the software renderer paint function. Since you can't control how QPainter does its thing, you'd probably have to generate LOD instances of the canvas to feed to it.

I can't quite tell, but Krita seems to solve this via "prescaling", chasing through what kis_qpainter_canvas.cpp does. I guess it just interpolates the image twice. But the performance of the software canvas in Krita is pants, so maybe not the place to look for inspiration.

Adding OpenCV for this is also not tenable maintenance-wise, if you can even manage to get it to build on Android, the web browser and Windows. In the browser, doing the scaling on another thread is also not an option due to overhead. Although Android and browser should really always be using the hardware canvas.

OpenGL is implemented in glcanvas.cpp. It just uses linear interpolation currently, presumably you can convince the GPU to do better by using mipmaps or something. Krita has stuff they call "high-quality filtering" in KisOpenGLCanvasRenderer.cpp. That's all done GPU-side, which means it shouldn't cause any performance issues, given what little it has to do to render at most a few quads.

Doing the OpenGL implementation should presumably be reasonably straight-forward. The software scaling part looks like a pretty large amount of work with huge negative performance implications, so it would need to be handled with more care. If it's even worth bothering with at all, given the limited breadth of devices that can't deal with the hardware renderer.

cromachina commented 1 month ago

Thank you for the informative response!

Krita's GPU canvas view does indeed look pretty good when zoomed out and GPU definitely does seem like the most reasonable choice, given all of the constraining requirements for the different target platforms.

MorrowShore commented 1 month ago

Is it possible to just change the interpolation method? Rather than doing two passes or adding extra steps.

askmeaboutlo0m commented 1 month ago

In software mode, it's down to what QPainter provides, which is either nearest-neighbor or bilinear scaling. There's no other options.

OpenGL ES 2.0 provides nearest-neighbor and bilinear scaling, as well as mipmaps, which are also a form of pre-scaling. But you can manually do multiple samples in the fragment shader, so you can implement arbitrary other interpolation that way.

cromachina commented 1 month ago

A small side note on the GPU canvas: Krita on Linux sometimes encounters issues with drivers corrupting or not preserving buffer data after waking up from system sleep (canvas becomes covered in random noise). Just some funny edge case to look out for in your own implementation.