How to do Integer scaling in new Window class

Community Forums/Monkey2 Talk/How to do Integer scaling in new Window class

Leo Santos(Posted 2016) [#1]
Hi,

I'm digging the new Window class and its layout options. However, If you want to do pixel art, one option is missing: "integer scaling", which is similar to "letterbox", but pixels are only scaled in integer values (which may cause borders around the frame, depending on your resolution).

I know how to set the virtual resolution using OnMeasure(), but to scale it like I want I'd have to control how that image is drawn into the window canvas.

I know I could simply create a new image canvas and draw it on the Window canvas in "fill" layout and manually control the texture size, but then I feel like I'd be reinventing the wheel and I'd end up having to do a lot of the convenient things already in the window class, like how the mouse coordinates respect the layout, etc.

Any suggestions?


Leo Santos(Posted 2016) [#2]
...and relevant to this, a question to Mark:
Could private fields like _canvas in the Window class be Protected instead? I feel like it would make extending that class a bit easier.

Thanks!


marksibly(Posted 2016) [#3]
Don't quite follow with integer scaling thing. Wont it be wildy out if the virtual size is 'nearly' the actual size?

Or are you talking about a 'zoom' style feature? There's already an 'Offset' in there that could be complemented with a 'Zoom'...

If you can describe what you're after a bit more clearly I can probably come up with something.

> Could private fields like _canvas in the Window class be Protected instead?

Nope, not yet, and probably not ever. It's early days and things are still very volatile so I'm being super conservative in what I make public so that I have the flexiblity to change things in the future.

Also, I really, really don't like the idea of protected vars. Perhaps in hugely speed-critical cases, but in general I'd much rather add a property if access to an internal var is really required - even a public property is better IMO. This way, I get to prevent 'writes' to the var if I want, or invalidate state when writes happen, or validate state when the var is read, or create the var value on the fly etc.

Making _canvas protected is incredibly easy, but it means I can no longer 'trust' it's state and, worse, I'm stuck with having to make sure it behaves the same way, for ever and ever, taking into account all the weird and wonderful ways people would inevitably end up using it!

A bit of a rant, and we've covered this ground before more or less in the Great Virtual vs Final Debate, but I just want to make sure everyone knows where I'm coming from and that requests to make vars protected are unlikely to be successful.

Basically, if you want acess to something that's private, I suggest first telling me why you want access to it - there may be a better/alternate way to achieve what you want to do. Failing that, it might be time to add a new property. Failing THAT, yer forked!


Danilo(Posted 2016) [#4]
Good.


therevills(Posted 2016) [#5]
I think Leo is talking about disabling the filtering when scaling, say when you have your game resolution at 640x480 but scale it up to 1280x768 he wants to see sharp pixels instead of the filtered ones.


Leo Santos(Posted 2016) [#6]
he wants to see sharp pixels instead of the filtered ones.

I know how to get that! I can disable the flag for filtering in the texture flags, that's working fine.

What I want is a bit...pickier... :-)
Consider this example:

1. I want to do all the rendering at a virtual resolution of 320x240
2. The window resolution will be 1920x1080, and the 320x240 image will be letterboxed.

So far so good. The current Window class can do it, and it looks like this:

Virtual resolution 320x240, with a "checkerboard" pixel pattern that's useful for finding scaling issues.


Letterboxed window at 1920x1080 (The Forum scales images, you may need to open the image in a new tab to see it 1:1)


If you look closely, the pixels are scaled unevenly because the scaling factor is 4.5 and the filtering is off. We end up with some pixels scaled by 4, and some scaled by 5, like this:


So in this proposed new layout, the original virtual resolution is only scaled by 4, and space is left blank around it. Not only you preserve the aspect, you also scale all pixels evenly, like this:


It's actually kinda subtle with a factor of 4.5, but this can lead to really bad, visible artifacts if the factor is smaller, between 1 and 3. This kind of option is very useful for low-res games. Emulators tend to have this feature as an option.

A fancier option would be to pre-scale the virtual res by 4, unfiltered, then scale it by 1.125 to fill the frame with filtering on. This still prevents artifacts, but avoids the empty area at the top and bottom.

Nope, not yet, and probably not ever

No problem, I figured that was the case, but I thought I should ask anyway since things are changing so quickly at this point.

Thanks!


marksibly(Posted 2016) [#7]
Ok, this can easily be done via "letterbox-int" Layout mode or something - probably the most convenient way for now.

What's the best thing to do with scales <1 though? Clamp to 1 or allow to go fractional?

Incidentally, how are you disabling filtering? via Texture.Load/New Image? I think this is the only way right now...

I've never really found a way I'm happy with to 'turn off' filtering. I usually end up adding a global 'DefaultTextureFlags' style var, but that's a bit ugly, esp. if it's 'too late', eg: for fonts.

It's kind of tricky 'coz opengl stores the filtering state with the texture 'binding', so something like a Canvas.TextureFiltering:Bool() property get messy (canvas needs to 'track' all bound textures etc) but perhaps it's worth the mess?

> A fancier option would be to pre-scale the virtual res by 4, unfiltered, then scale it by 1.125 to fill the frame with filtering on

This would seriously mess with mouse coords though?

And don't forget you can always draw to an image canvas then just 'DrawImage' the result inside OnRender.


Leo Santos(Posted 2016) [#8]
What's the best thing to do with scales <1

Yes, you can go fractional below 1.

Incidentally, how are you disabling filtering? via Texture.Load/New Image? I think this is the only way right now...

I made a function that loads a texture with the flags I want, then generates a new image from that. I feel like filtering is by far the most used flag, and could be an optional argument in Image.Load(). It would be great if you could load a font using (optional) flags as well!

And don't forget you can always draw to an image canvas then just 'DrawImage' the result inside OnRender.

Yeah, that's how I was doing in Monkey1 with mojo2. I wanted to check if there was an 'official" way to do this before making my own again, since I always prefer adopting the "official" way. You can see it working in this little Monkey1 test, just scale the canvas around: https://dl.dropboxusercontent.com/u/446189/cave/index.html

Thanks!


marksibly(Posted 2016) [#9]
> I feel like filtering is by far the most used flag, and could be an optional argument in Image.Load().

I've had a quick look at adding a canvas.TextureFilteringEnabled property and I don't think it'd be too hard - I can just rebind materials etc when it changes, which wont be often.

Wouldn't this be kind of the 'ultimate' solution for pixelart games? How do other engines do it?

I can - and probably will - add 'flags' to Image.Load, but this approach does end up kind of 'leaking' out, ie: anything that depends on texture.Load (like image and maybe material) needs a 'flags' param, and then so does anything that relies on image (like font or your own image wrapper classes) etc.

I dig pixel art games and they don't seem to be getting any less popular so I think they're worth supporting as well as I can.


ImmutableOctet(SKNG)(Posted 2016) [#10]
Isn't the point of mipmapping to mitigate this issue?


marksibly(Posted 2016) [#11]
Mipmaps are kind of the opposite - they're smaller 'precalced' versions of textures that are used when texels are smaller than 1 pixel.


Leo Santos(Posted 2016) [#12]
<deleted, duplicated post>


Leo Santos(Posted 2016) [#13]
Isn't the point of mipmapping to mitigate this issue?

I don't think mipmapping has any effect in this case. Mipmapping "kicks in" when you scale textures down - the GPU renders the pre-computed smaller texture instead of the original one - and in this case we're scaling things up. The issue here is that when you have interpolation off, any fractional scaling will result in artifacts, noticeably "pixel crawling" in side scrollers, which is when the same pixel gets scaled to different sizes at different frames.

This issue does not apply to high definition textures! You should always keep mipmapping and filtering on for those! ;-)

How do other engines do it?

I think all the required building blocks are already in Mojo, the only missing piece for me is the integer canvas scaling.
The Pixel Perfect checklist is:
- Filtering off.
- No fractional coordinates (I think turning off filtering takes care of that - even if the drawing happens at a fractional coordinate, the texture ignores it?)
- No rotation.
- Only integer scaling, either on the sprites or the canvas itself.
- Rendering to a texture and then scaling the texture to the entire window is usually the preferred method, but some games (Kero Blaster, for instance, from the same guy who made Cave Story) use a canvas that is larger than the perceived resolution and scale the sprites instead. In Kero Blaster's case (and, I think, the original Cave Story as well), sprites are at scale=2, so when the camera moves around the movement appears smoother. It is still pixel perfect, though, since no fractional scaling, fractional coordinates or rotations are used, but has to be used carefully if you're going for "retro".
- Applying a shader (like scan lines) happens at the Window canvas resolution, so the virtual resolution canvas can still be pixel perfect!

Thanks!


marksibly(Posted 2016) [#14]
> - No fractional coordinates (I think turning off filtering takes care of that - even if the drawing happens at a fractional coordinate, the texture ignores it?)

Actually, I think you still have to do this or pixels will still be drawn at sub-pixel positions.

For example, if you set an extreme virtual res of 2x2 via OnMeasure, it'll still be possible to 'pan' a huge pixel around smoothly.