- 566
- Posts
- 5
- Years
- Seen yesterday
Many people nowadays play Gen 1 and Gen 2 pokemon games on modern backlit LCD screens: hd monitors, smartphones, tablets, the AGS-101 GBA SP, even game boys with an IPS screen mod. Playing GBC games this way will have you notice that the colors are extremely saturated. This isn't a bug, it's a feature! The transflective screens of the old gameboys wash out the colors, so developers made sure the color levels were set right to compensate. Unless you have some option on your play device to tweak the gamma and color level you just have to live with it.
But one day I had an idea. Would it be possible to do color-correction in the game rom itself using a hard-coded shader that does basic color-mixing and gamma enhancement? Can the GBC's processor even handle that? The answer to both is yes!
This report will mainly focus on my hack, Shin Pokemon, which backports the GBC color engine from pokemon Yellow. Naturally you will be able to apply this to pokeyellow disassembly without much fuss. Though a little more difficult, you can also apply this to the Gen 2 disassemblies or any other GBC disassembly.
First is the setup. Make a new asm file that will have all the code for your gamma shader and INCLUDE it in your project. For pokered projects, that's main.asm. Put it in a rom bank that has plenty of space (you're going to need it). This is my file, and I'm placing it in bank $2E.
Now to grab some unused addresses in hram that will be used to temporarily store RGB values. Pokered uses hram.asm for this. Here's I'm equating address $FFFB to the constant hRGB and stating that 3 bytes are used.
FFFB will hold red, FFFC will hold green, and FFFD will hold blue.
Pokered has predefs coded in order to easily jump to functions across rom banks without clobbering your registers. I want a predef for my main shader function called GBCGamma. Go to engine/predefs.asm and add it at the bottom.
Now comes the most important part of setting up. The GBC uses 5-bit RGB values so all three colors fit into a single 'word' (2 bytes). You want to find a good spot to intercept these RGB words. You will grab it, store it, jump to the GBCGamma function, do all the stuff, store it back, jump back to where you left off, and load the modified RGB values instead. Shin Pokemon and Pokeyellow have a function in engine/palettes.asm called DMGPalToGBCPal. This is a function that converts Super gameboy palettes into a GBC-readable format. It's the perfect place to hijack RGB words. Scroll down to the following code of the function:
Those last four lines are the target. The RGB word is stored at the consecutive addresses given by the HL register. The A register is used to transfer the two bytes of the RGB word. I have changed it to the following:
The DE is preserved using a simple push/pop, and I've decided that it will store the RGB word. After predef GBCGamma is finished doing everything, the modified RGB word in DE is loaded into its originally-intended location.
Now, a note about RGB words. GBC games assign 5 bits to each color so that they fit into a 2-byte word. The format is as follows and reads right to left:
Now for the meat of the project. Time to code everything up in the custom_functions/func_gamma.asm files that I created. The primary GBCGamma serves as an outline for the whole process. GetPredefRegisters is a pokered predef function that gets back the register values from before the predef jump. Registers BC and HL are preserved via push/pop (we're going to use these a lot). DE serves as the input/output for the RGB word in out little system. The RGB values are going to be parsed out into three separate bytes and stored into hram, then they will be color-mixed, then a gamma enhance is applied, and finally the RGB values are translated back into a word in DE. Always do gamma after mixing. Mixing darkens the colors while gamma-adjustment lightens them. Doing it in reverse will cause the colors to be dimmed instead of softened (which I guess could be used to create a nighttime or shadowed effect).
Remember that hram constant made during setup? Now it's going to get used. GetRGB takes the word-encoded RGB values, parses them into separate bytes, and stores them in hRGB, hRGB+1, and hRGB+2.
WriteRGB does the opposite of GetRGB. Nail down these two functions before anything else. Run the game with just these two implemented (comment out the gamma or matrix mix function calls). All your colors should be exactly as they would be normally. If something is wonky, you made a mistake somewhere. You have to make sure you can parse and unparse the color values correctly between hram and DE or else all is for naught.
Let's talk about this one fist because it's easier on the post formatting. GammaConv takes those parsed 5-bit colors (r, g, and b separately) and applies a gamma function to them. The gamma equation for a 5-bit value is 31*[ (value/31)^(1/gamma)], and we will use gamma=2.0 for lightening things up. Yes, you must calculate a gamma-root. No, you are not going to be doing root estimations on the GBC's processor or else the game will lag terribly. Instead, I have pre-calculated a lookup table in the form of GammaList. Much, much faster for the low cost of 32 bytes.
Okay, now for the final function. MixColorMatrix does color mixing, and it's a fun one because it involves matrix math. It's a slight modification of RiskyJump's GLSL port of the color correction option on Gambatte emulator. You have to do the following matrix multiply:
Protip: Do not try to do straight matrix multiplication as it makes this very laggy (I found out the hard way). Instead, takes as many shortcuts as possible. No need to multiply by 0 or 1 since the answer is zero or the multiplicand respectively. Multiplying by 2 or 3 is done faster by adding the multiplicand to itself once or twice. Use a lookup table for multiplying by 13. Multiplying by 14 is just the same lookup table but add the multiplicand one time after. I use HL for summations and push/pop to save results because the matrix elements can exceed 1 byte and you can do 16-bit additions with HL.
And there you have it. See attached images for a before and after comparison.
But one day I had an idea. Would it be possible to do color-correction in the game rom itself using a hard-coded shader that does basic color-mixing and gamma enhancement? Can the GBC's processor even handle that? The answer to both is yes!
This report will mainly focus on my hack, Shin Pokemon, which backports the GBC color engine from pokemon Yellow. Naturally you will be able to apply this to pokeyellow disassembly without much fuss. Though a little more difficult, you can also apply this to the Gen 2 disassemblies or any other GBC disassembly.
First is the setup. Make a new asm file that will have all the code for your gamma shader and INCLUDE it in your project. For pokered projects, that's main.asm. Put it in a rom bank that has plenty of space (you're going to need it). This is my file, and I'm placing it in bank $2E.
Code:
INCLUDE "custom_functions/func_gamma.asm"
Code:
hRGB EQU $FFFB ;3 bytes ;joenote - used to store color RGB color values for color correction
Pokered has predefs coded in order to easily jump to functions across rom banks without clobbering your registers. I want a predef for my main shader function called GBCGamma. Go to engine/predefs.asm and add it at the bottom.
Code:
add_predef GBCGamma
Now comes the most important part of setting up. The GBC uses 5-bit RGB values so all three colors fit into a single 'word' (2 bytes). You want to find a good spot to intercept these RGB words. You will grab it, store it, jump to the GBCGamma function, do all the stuff, store it back, jump back to where you left off, and load the modified RGB values instead. Shin Pokemon and Pokeyellow have a function in engine/palettes.asm called DMGPalToGBCPal. This is a function that converts Super gameboy palettes into a GBC-readable format. It's the perfect place to hijack RGB words. Scroll down to the following code of the function:
Code:
.convert
;"A" now holds the palette data
color_index = 0
REPT NUM_COLORS
ld b, a ;"B" now holds the palette data
and %11 ;"A" now has just the value for the shade of palette color 0
call .GetColorAddress
;now load the value that HL points to into wGBCPal offset by the loop
ld a, [hli]
ld [wGBCPal + color_index * 2], a
ld a, [hl]
ld [wGBCPal + color_index * 2 + 1], a
Code:
.convert
;"A" now holds the palette data
color_index = 0
REPT NUM_COLORS
ld b, a ;"B" now holds the palette data
and %11 ;"A" now has just the value for the shade of palette color 0
call .GetColorAddress
push de
;get the palett color value in de
ld a, [hli]
ld e, a
ld a, [hl]
ld d, a
predef GBCGamma
;now load the value that HL points to into wGBCPal offset by the loop
ld a, e
ld [wGBCPal + color_index * 2], a
ld a, d
ld [wGBCPal + color_index * 2 + 1], a
pop de
Now, a note about RGB words. GBC games assign 5 bits to each color so that they fit into a 2-byte word. The format is as follows and reads right to left:
The red value is bits 0 to 5, green is bits 5 to 9 (yes it's split between two bytes), and blue is bits 10 to 14. Bit-15 is unused and should be kept at 0. Red, green, and blue can each have a value from 0 (darkest) to 31 (brightest).|byte1 | byte0|
|x B B B B B G G | G G G R R R R R|
|15 14 13 12 11 10 9 8 | 7 6 5 4 3 2 1 0|
Now for the meat of the project. Time to code everything up in the custom_functions/func_gamma.asm files that I created. The primary GBCGamma serves as an outline for the whole process. GetPredefRegisters is a pokered predef function that gets back the register values from before the predef jump. Registers BC and HL are preserved via push/pop (we're going to use these a lot). DE serves as the input/output for the RGB word in out little system. The RGB values are going to be parsed out into three separate bytes and stored into hram, then they will be color-mixed, then a gamma enhance is applied, and finally the RGB values are translated back into a word in DE. Always do gamma after mixing. Mixing darkens the colors while gamma-adjustment lightens them. Doing it in reverse will cause the colors to be dimmed instead of softened (which I guess could be used to create a nighttime or shadowed effect).
Code:
;This function tries to apply gamma correction to a GBC palette color
;de holds pointer to the color
;returns value in de
GBCGamma:
call GetPredefRegisters ;restores the BC, DE, and HL registers
push hl
push bc
call GetRGB ;store the RGB values at hRGB
call MixColorMatrix
call GammaConv
call WriteRGB
pop bc
pop hl
ret
Remember that hram constant made during setup? Now it's going to get used. GetRGB takes the word-encoded RGB values, parses them into separate bytes, and stores them in hRGB, hRGB+1, and hRGB+2.
Code:
;get the RGB values out of color in de into a spots pointed to by hRGB
GetRGB:
;GetRed:
;red bits in e are %00011111
ld a, e
and %00011111 ;mask to get just the color value
ld [hRGB + 0], a
;GetGreen:
;green bits in de are %00000011 11100000
ld a, e
and %11100000
;a is now xxx00000
ld b, a
srl b
srl b
srl b
srl b
srl b
;b is now 00000xxx
ld a, d
and %00000011
sla a
sla a
sla a
;a is now 000xx000
or b
;a is now 000xxxxx
ld [hRGB + 1], a
;GetBlue:
;blue bits in d are %01111100
ld a, d
rra
rra
and %00011111 ;mask to get just the color value
ld [hRGB + 2], a
ret
WriteRGB does the opposite of GetRGB. Nail down these two functions before anything else. Run the game with just these two implemented (comment out the gamma or matrix mix function calls). All your colors should be exactly as they would be normally. If something is wonky, you made a mistake somewhere. You have to make sure you can parse and unparse the color values correctly between hram and DE or else all is for naught.
Code:
;write a colors at hRGB into their proper bit placement in de
WriteRGB:
;writeRed:
ld a, [hRGB + 0]
ld b, a
ld a, e
and %11100000
or b
ld e, a
;writeGreen:
ld a, [hRGB + 1]
; d e
;green bits are 00000011 11100000
;bits in a are 00011111
rrca
rrca
rrca
ld b, a
;bits in b are 11100011
;now load into d
and %00000011
ld c, a
;bits in c are 00000011
ld a, d
and %11111100
or c
ld d, a
;bits in b are still 11100011
;now load into e
ld a, b
and %11100000
ld c, a
;bits in c are 11100000
ld a, e
and %00011111
or c
ld e, a
;writeBlue:
ld a, [hRGB + 2]
ld b, a ;blue bits are 00011111
ld a, d
and %10000011
sla b ;blue bits are 00111110
sla b ;blue bits are 01111100
or b
ld d, a
ret
Let's talk about this one fist because it's easier on the post formatting. GammaConv takes those parsed 5-bit colors (r, g, and b separately) and applies a gamma function to them. The gamma equation for a 5-bit value is 31*[ (value/31)^(1/gamma)], and we will use gamma=2.0 for lightening things up. Yes, you must calculate a gamma-root. No, you are not going to be doing root estimations on the GBC's processor or else the game will lag terribly. Instead, I have pre-calculated a lookup table in the form of GammaList. Much, much faster for the low cost of 32 bytes.
Code:
;This is does gamma conversions of hRGB color values via lookup list.
GammaConv:
ld hl, hRGB
ld c, 3
.loop
ld a, [hl]
push hl
ld hl, GammaList
push bc
ld b, $00
ld c, a
add hl, bc
pop bc
ld a, [hl]
pop hl
ld [hli], a
dec c
jr nz, .loop
ret
GammaList: ;gamma=2 conversion
db 0 ; color value 0
db 6 ; color value 1
db 8 ; color value 2
db 10 ; color value 3
db 11 ; color value 4
db 12 ; color value 5
db 14 ; color value 6
db 15 ; color value 7
db 16 ; color value 8
db 17 ; color value 9
db 18 ; color value 10
db 18 ; color value 11
db 19 ; color value 12
db 20 ; color value 13
db 21 ; color value 14
db 22 ; color value 15
db 22 ; color value 16
db 23 ; color value 17
db 24 ; color value 18
db 24 ; color value 19
db 25 ; color value 20
db 26 ; color value 21
db 26 ; color value 22
db 27 ; color value 23
db 27 ; color value 24
db 28 ; color value 25
db 28 ; color value 26
db 29 ; color value 27
db 29 ; color value 28
db 30 ; color value 29
db 30 ; color value 30
db 31 ; color value 31
Okay, now for the final function. MixColorMatrix does color mixing, and it's a fun one because it involves matrix math. It's a slight modification of RiskyJump's GLSL port of the color correction option on Gambatte emulator. You have to do the following matrix multiply:
Which will give you:|13 2 1| |r value|
|0 3 1| |g value|
|0 2 14| |b value|
Then you need to bit-shift each row result to the right several times:Rx = 13*r + 2*g + 1*b
Gx = 0*r + 3*g + 1*b
Bx = 0*r + 14*g + 1*b
Ry, Gy, and By are now your mixed color values and can be stored. They should all be values between 0 and 31.Ry = Rx / 16 ;equivalent to 4 right shifts
Gy = Gx / 4 ;equivalent to 2 right shifts
By = Bx / 16 ;equivalent to 4 right shifts
Protip: Do not try to do straight matrix multiplication as it makes this very laggy (I found out the hard way). Instead, takes as many shortcuts as possible. No need to multiply by 0 or 1 since the answer is zero or the multiplicand respectively. Multiplying by 2 or 3 is done faster by adding the multiplicand to itself once or twice. Use a lookup table for multiplying by 13. Multiplying by 14 is just the same lookup table but add the multiplicand one time after. I use HL for summations and push/pop to save results because the matrix elements can exceed 1 byte and you can do 16-bit additions with HL.
Code:
;Applies the color-mixing matrix to colors at hRGB
;Doing as few calculations as possible to increase speed because a matrix multiply causes lag
MixColorMatrix:
;calculate red row and store it
ld hl, $0000
;multiply red value by 13 and add to hl
ld a, [hRGB + 0]
ld b, 0
ld c, a
push hl
ld hl, Table5Bx13
add hl, bc
add hl, bc
ld a, [hli]
ld c, a
ld a, [hl]
ld b, a
pop hl
add hl, bc
;multiply green value by 2 and add to hl
ld a, [hRGB + 1]
ld b, 0
ld c, a
add hl, bc
add hl, bc
;multiply blue value by 1 and add to hl
ld a, [hRGB + 2]
ld b, 0
ld c, a
add hl, bc
;shift 4 bits to the right
srl h
rr l
srl h
rr l
srl h
rr l
srl h
rr l
ld a, l
push af
;calculate green row and store it
ld hl, $0000
;multiply red value by 0 and add to hl
;no actions for this
;multiply green value by 3 and add to hl
ld a, [hRGB + 1]
ld b, 0
ld c, a
add hl, bc
add hl, bc
add hl, bc
;multiply blue value by 1 and add to hl
ld a, [hRGB + 2]
ld b, 0
ld c, a
add hl, bc
;shift 2 bits to the right
srl h
rr l
srl h
rr l
ld a, l
push af
;calculate blue row and store it
ld hl, $0000
;multiply red value by 0 and add to hl
;no actions for this
;multiply green value by 2 and add to hl
ld a, [hRGB + 1]
ld b, 0
ld c, a
add hl, bc
add hl, bc
;multiply blue value by 14 and add to hl
ld a, [hRGB + 2]
ld b, 0
ld c, a
add hl, bc
push hl
ld hl, Table5Bx13
add hl, bc
add hl, bc
ld a, [hli]
ld c, a
ld a, [hl]
ld b, a
pop hl
add hl, bc
;shift 4 bits to the right
srl h
rr l
srl h
rr l
srl h
rr l
srl h
rr l
ld a, l
;now store the color-mixed values
ld [hRGB + 2], a
pop af
ld [hRGB + 1], a
pop af
ld [hRGB + 0], a
ret
;lookup table for multiplying a 5-bit number by 13
Table5Bx13:
dw $0000
dw $000D
dw $001A
dw $0027
dw $0034
dw $0041
dw $004E
dw $005B
dw $0068
dw $0075
dw $0082
dw $008F
dw $009C
dw $00A9
dw $00B6
dw $00C3
dw $00D0
dw $00DD
dw $00EA
dw $00F7
dw $0104
dw $0111
dw $011E
dw $012B
dw $0138
dw $0145
dw $0152
dw $015F
dw $016C
dw $0179
dw $0186
dw $0193
And there you have it. See attached images for a before and after comparison.