aardappel / lobster

The Lobster Programming Language
http://strlen.com/lobster
2.24k stars 121 forks source link

Font filter mode not settable? #255

Closed morew4rd closed 1 year ago

morew4rd commented 1 year ago

(This is more of a question/feature request so if this doesn't fit here, please feel free to move to the discussions area or close.)

I'm playing with lobster codebase & capabilities, currently trying to see if it works well for "pixel-perfect" style rendering. For pixel 2D games, devs usually use a very small resolution (say 320*200) and small fonts. As an example, one I like is this: https://managore.itch.io/m5x7

So everything gets scaled to be visible with the modern machines and high resolutions and high DPIs.

The problem is with scaling filters. I'd like to be able to use nearest neighbor for font scaling so that font pixels can look like big clean squares. My guess is fonts are currently scaled with linear algorithm.

image

I'm expecting this to look like this:

image

Note: This is also required for image/texture scaling as well. But I haven't played with those parts of the lobster engine yet. I'll amend this issue if I find similar problems there as well.

morew4rd commented 1 year ago

Forgot to say: I'm open to helping out with this and similar issues if you're OK with taking contributions. Thanks.

aardappel commented 1 year ago

See the flags in texture.lobster, you want to set both texture_format_nearest_mag and texture_format_nearest_min when you call gl_load_texture, then when rendering with e.g. gl_rect make sure the screen size in pixels is an integer multiple of the texture res if possible.

aardappel commented 1 year ago

If you're actually using font files.. well, those would contain vector representations of your pixellated font. If you get the linear look, you are using gl_scale on font output instead of correctly setting gl_set_font_size relative to the native pixels of the screen.

morew4rd commented 1 year ago

Re: gl_load_texture sounds good. Looks like the same flags can be set while creating a framebuffer too so I should be OK on that front.

However, with fonts I want to push back a little. You're presumably caching the glyphs in a texture, no? (Otherwise it'd be very expensive to render text.) Would it not be possible to set the min/mag filters to neareset on that texture? See I scaled the font in the OP "incorrectly" and the pixels are still crisp (though probably a bit rectanglish 😄) (*)

image

(*) Done with my own framework (Lyte2D: Lua + C, similar to Lobster in that Lua code is the driver, calling leaf C APIs.) (I'm using a library for font caching)

aardappel commented 1 year ago

Yes, it be possible to specify those flags on the font texture too, but that is typically not what you want, as you'd likely get deformed characters.

The font contains a vector definition, which likely doesn't line up with pixels directly. It will then clamp those to pixels when generating the font texture. Then you scale it as a texture and "nearest" clamps it again. That's 2x clamping that will have side effects if you're not careful. Also, there's not just the texture filtering to take into account, but FreeType's anti-aliasing, depending on how the vectors match the pixels.

Meanwhile, when using gl_set_font_size directly for the right size, you'll get crisp font rendering no matter the discrepancy between how the font was specified and the screen pixels.

If you want to be using nearest filtering directly, you are better off with a font that comes as a texture, giving you full control over pixels.

morew4rd commented 1 year ago

I see, so perhaps the approach I am taking in Lyte2D is wrong. I was taking inspiration from some other frameworks (and using a library), but it makes sense that scaling bitmaps twice can easily create artifacts.

I'll do a bit more experimentation, and will report back if I see any other issues on this area. I think this one can be closed (unless you want to use it to track some issue).

morew4rd commented 1 year ago

Just a FYI. TIL, thanks to you.

I made the double clamping issue visible using Lyte2D. I didn't even think about this until you brought it up. Red is texture scaling and blue is font scaling. Placements are clearly wrong on the red one.

image

I'll fix my samples (And think a design where perhaps my scale and set_font_size/draw_text APIs aware of each other)

aardappel commented 1 year ago

Cool :)

As for them being aware of eachother, I actually have some pretty tricky code in ui_rendertextatlocalsize whose sole purpose is that after a game has applied an arbitrary scale with gl_scale to get back to pixel scale so the font rendering can be 1:1 with what FreeType renders :) So yeah, it's a tricky problem, but worth getting right for accurate rendering.

// helper for rendering text at arbitrary scaled locations with maximum precision
// normal font rendering is specified in pixels, and is only rendered at precise pixel boundaries (which looks best)
// when gl_scale(1) and gl_translate in integer amounts
// if you use arbitrary scaling it will look more blurry than necessary
// using this function, it will find the appropriate font size given a size relative to the current scale (not pixels),
// clamp to pixel boundaries,
// then execute any text rendering contained in f

def ui_rendertextatpixelsize(f):
    // Translate to nearest pixel:
    gl_translate(-fraction(gl_origin() + 0.5) + 0.5):
        f()

def ui_rendertextatlocalsize(localsize:float, f):
    let oldfontsize = gl_get_font_size()
    let oldoutlinesize = gl_get_outline_size()
    let sc = gl_scaling().x
    let localfontsizef = sc * localsize
    let localfontsize = round(localfontsizef)    // font size to nearest pixel
    gl_set_font_size(localfontsize, oldoutlinesize * (localfontsizef / oldfontsize))
    gl_scale xy_1 / gl_scaling():   // get back to pixel scale
        ui_rendertextatpixelsize(f)
    gl_set_font_size(oldfontsize, oldoutlinesize)

def ui_text_pixel(t):
    ui_rendertextatpixelsize():
        gl_text(t)

def ui_text_local(t):
    ui_rendertextatlocalsize():
        gl_text(t)
morew4rd commented 1 year ago

Awesome, and thank you so much for the pointer! I was thinking something similar, but there're some gotchas for sure.

(OpenGL moment: moving by [0.5, 0.5] 😄)