Twinside / Rasterific

A drawing engine in Haskell
BSD 3-Clause "New" or "Revised" License
140 stars 11 forks source link

Pixel perfect image drawing? #22

Open liamoc opened 8 years ago

liamoc commented 8 years ago

Hi,

I was hoping to use this library to render some pixel art for a game I'm making by composing various pre-drawn images and shapes together. The issue I've encountered is that if you use an image it isn't rendered pixel-perfect, even when it is not scaled. Here's an example, where I'm trying to render each letter of a bitmap font, next to each other. Two of the letters are shown below the output of my little program. You can see how the output is "fuzzy", whereas the input is crisp.

example

Twinside commented 8 years ago

Hi, do you have some code?

I already encountered the problem in #15 and the fix were to shift the position by 0.5px (as can be seen in the commit

hsyl20 commented 3 years ago

Hi @Twinside

This topic came up again a few weeks ago in https://groups.google.com/g/diagrams-discuss/c/TRtnvrQaT-k/m/LPq6iqmTBAAJ?pli=1

The following code demonstrates the issue.

import Codec.Picture
import Graphics.Rasterific

main :: IO ()
main = do
    i <- readPng "tiny-rectangle.png"
    case i of
        Left err -> error err
        Right img -> do
            let d = drawImage (convertRGBA8 img) 0.0 (V2 0 0)
            let img2 = renderDrawing 4 5 (PixelRGBA8 0 0 0 0) d
            writePng "tiny-rectangle-out.png" img2

With the following "tiny-rectangle.png" image: tiny-rectangle (a 4x5 image with a red box and a black line through it, it only appears blurry in the browser)

The following patch seems to fix the issue:

diff --git src/Graphics/Rasterific.hs src/Graphics/Rasterific.hs
index 088248d..e60e2ac 100644
--- src/Graphics/Rasterific.hs
+++ src/Graphics/Rasterific.hs
@@ -823,8 +823,8 @@ drawImageAtSize img@Image { imageWidth = w, imageHeight = h } borderSize ip
         stroke (borderSize / 2) (JoinMiter 0)
                (CapStraight 0, CapStraight 0) rect'
         where
-          p = ip ^-^ V2 0.5 0.5
-          rect = rectangle (V2 0 0) rw rh
+          p = ip ^+^ V2 0.5 0.5
+          rect = rectangle (V2 (-0.5) (-0.5)) rw rh
           rect' = rectangle p reqWidth reqHeight

My analysis was:

With the current code, in Graphics.Rasterific.Immediate.fillWithTexture, if we print "els" (the shape of the image we want to render) we get:

[LinePrim Line (V2 (-0.5) (-0.5)) (V2 3.5 (-0.5))
,LinePrim Line (V2 3.5 (-0.5)) (V2 3.5 4.5)
,LinePrim Line (V2 3.5 4.5) (V2 (-0.5) 4.5)
,LinePrim Line (V2 (-0.5) 4.5) (V2 (-0.5) (-0.5))]

Notice the (-0.5,-0.5) translation.

It then gets clipped to:

[LinePrim Line (V2 0.0 0.0) (V2 3.5 0.0)
,LinePrim Line (V2 3.5 0.0) (V2 3.5 0.0)
,LinePrim Line (V2 3.5 0.0) (V2 3.5 0.0)
,LinePrim Line (V2 3.5 0.0) (V2 3.5 0.125)
,LinePrim Line (V2 3.5 0.125) (V2 3.5 0.75)
,LinePrim Line (V2 3.5 0.75) (V2 3.5 2.0)
,LinePrim Line (V2 3.5 2.0) (V2 3.5 4.5)
,LinePrim Line (V2 3.5 4.5) (V2 1.5 4.5)
,LinePrim Line (V2 1.5 4.5) (V2 0.5 4.5)
,LinePrim Line (V2 0.5 4.5) (V2 0.0 4.5)
,LinePrim Line (V2 0.0 4.5) (V2 0.0 4.5)
,LinePrim Line (V2 0.0 4.5) (V2 0.0 0.0)]

My guess is that ^-^ V2 0.5 0.5 in drawImageAtSize was added to compensate for line sampling (hence rectangle sampling) which adds (0.5,0.5) to every sample coordinate (probably to get at the center of the pixels).

Texture sampling seems to use final image coordinates according to the comment in Graphics.Rasterific.Texture. So if we remove the correction:

-          p = ip ^-^ V2 0.5 0.5
+          p = ip

We get the correct shape after clipping:

[LinePrim Line (V2 0.0 0.0) (V2 4.0 0.0)
,LinePrim Line (V2 4.0 0.0) (V2 4.0 5.0)
,LinePrim Line (V2 4.0 5.0) (V2 0.0 5.0)
,LinePrim Line (V2 0.0 5.0) (V2 0.0 0.0)]

But we don't compensate the (0.5,0.5) translation in samples anymore so the output is still bad.

We can also change:

-          rect = rectangle (V2 0 0) rw rh
+          rect = rectangle (V2 (-0.5) (-0.5)) rw rh

to compensate, but then the coordinates are (-0.5,-0.5) off. So finally we also need to do:

-          p = ip ^-^ V2 0.5 0.5
+          p = ip ^+^ V2 0.5 0.5