2015/08/12

Emulating the GBA's Display with Gamma Correction

When I was researching the sound technologies of the GBA and SNES for my current project, I started by just searching for YouTube videos comparing SNES games to their GBA ports. In the comments, there were two common complaints about the remakes: The awful audio quality, which I came to witness myself, and the "washed out" palettes.
One or two people responded to the latter complaints: It looks fine on the actual hardware, the GBA didn't have a backlit screen, the palettes had to be remade with that in mind. That's true and all, but it still bothered me; why do none of the popular emulators accomodate this?

I don't maintain an emulator project myself, and I don't want to make yet another fork of Visual Boy Advance. Even if I did find my way around the code base to make the patch, noone would want to maintain any C code I could write. But at least I could do the leg work and figure out how to even emulate the display properly (Welp, looks like it ain't happening in VBA-M), and that's what I did:

Show Your Work!

I busted out my trusty white Game Boy Advance and the copy of Pokémon Sapphire I got for christmas so many years ago, booted it up and found myself in a Poké Center. "That'll do", I thought, and took this picture with my phone:

I already do programming, game design and pixel art.
You can't expect me to be a photographer as well!
I took quite a few more, but this is the one I ended up using. I fetched this picture from Bulbapedia for comparison and got to work. Now, when you're working with a picture of a screen, there are a few things you need to watch out for.
One is light pollution - I took this picture in my bedroom, at midday, with the blinders down and no artificial light. This makes for a mostly neutral lighting environment.
Next is reflections. Obviously, I couldn't use most of this photo because the reflection of my skin and hair tinted the image. Similarly, the reflection of the white ceiling in my room desaturated or completely obscured some sections. The lower right quarter is fine, though - that's the black casing of my phone, making no real reflection.
Then there is the limits of technology: Pixels are not magic, and you can see obvious artifacts from their boundaries on the photographed screen. As with any photo, there's image noise, and because I can't tell my phone not to compress the image, there may be JPEG artifacts.

With all that in mind, I started taking samples of pixels that should be white: The highlights on the table and chairs, the tiles in the middle of the room. From fifteen samples, I determined the median values of the R, G and B channels independently, hopefully minimizing the effects of noise and human error, arriving at (41 50 47) for "white".
I repeated that for two more colors - I got a garbage value for R the first time - and arrived at (15 31 29) for (144 176 112). If you think that looks wrong, don't worry; so did I, until I saw the results.

I decided to try gamma correction before any other more complicated methods, and just needed to calculate the gamma values for the three channels, which seem to have different curves.
For those of you who don't know what gamma correction is, this video does a fine job explaining the concept in a mere four minutes, though it only refers to the term gamma in a foot note on the screen.
Anyway, to calculate the gamma values, we should first make the measured values a little less unwieldy by linearizing them.
The samples from the JPEG could range from 0 to 255, so we first divide by 255. To linearize the result, we then need to take the value 2.2, the gamma value used by most photo cameras.
The samples from the bulbapedia picture range from 0 to 248, a result of the way the image was converted from the GBA's 15 bit color space. The reason they look washed out is, that they probably already are linearized, so we just divide these by 248.

This is the resulting set of equations:
r(0)=0    r(0.58)=0.002   r(1)=0.018
g(0)=0    g(0.71)=0.010   g(1)=0.028
b(0)=0    b(0.45)=0.008   b(1)=0.024

However, a gamma curve is of the form f(x)=x^gamma. This means that f(1)=1, so we'll scale the right side of all equations accordingly and multiply the resulting color by (48 48 48). Note that I determined that value empirically, because the "white" (41 50 47) didn't seem to work that well for this.

The equations now:
r(x)=x^R    r(0.58)=0.1111
g(x)=x^G    g(0.71)=0.3571
b(x)=x^B    b(0.45)=0.3333

Solving this with logarithms is trivial and gives us roughly R=4.0, G=3.0, B=1.4 for our gamma values.

The Results

With all that said, let's look at some screenshots!
The original image.
It seems to work perfectly for GBA games!
GBC games were also playable on GBA.



It does NOT work for SNES games, which were intended for well lit CTR screens.

These are real fucking dark. Reminds me of shifting in my seat just so I could see what I was even doing. If we want a compromise between usability and authenticity, maybe customization is in order;
Same gamma values, different multipliers. From left to right:
(255 255 255), (096 096 096), (048 048 048)

Recap

Most emulators do not emulate the display that came with the emulated device. To convert a color c from 15 bit RGB to C in 24 bit RGB, most use C=c*8, leading to inaccurate color representation.
To emulate the non-backlit display of the GBA, gamma correction and color multiplication should be used, with different gamma values per channel. To make the effect more practical, customization should be offered.

No comments:

Post a Comment