• Just a reminder that providing specifics on, sharing links to, or naming websites where ROMs can be accessed is against the rules. If your post has any of this information it will be removed.
  • Our friends from the Johto Times are hosting a favorite Pokémon poll - and we'd love for you to participate! Click here for information on how to vote for your favorites!
  • Dawn, Serena, Hilda, Wes - which Pokémon protagonist is your favorite? Let us know by voting in our poll!
  • Welcome to PokéCommunity! Register now and join one of the best fan communities on the 'net to talk Pokémon and more! We are not affiliated with The Pokémon Company or Nintendo.

[Graphics] Built-in Color Correction for GBC games

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.
Code:
INCLUDE "custom_functions/func_gamma.asm"
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.
Code:
hRGB EQU $FFFB ;3 bytes ;joenote - used to store color RGB color values for color correction
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.
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
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:
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
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:
|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|
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).

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:
|13 2 1| |r value|
|0 3 1| |g value|
|0 2 14| |b value|
Which will give you:
Rx = 13*r + 2*g + 1*b
Gx = 0*r + 3*g + 1*b
Bx = 0*r + 14*g + 1*b
Then you need to bit-shift each row result to the right several times:
Ry = Rx / 16 ;equivalent to 4 right shifts
Gy = Gx / 4 ;equivalent to 2 right shifts
By = Bx / 16 ;equivalent to 4 right shifts
Ry, Gy, and By are now your mixed color values and can be stored. They should all be values between 0 and 31.
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.
 

Attachments

  • [PokeCommunity.com] Built-in Color Correction for GBC games
    bgb00029_before.png
    2.1 KB · Views: 38
  • [PokeCommunity.com] Built-in Color Correction for GBC games
    bgb00030_after.png
    2.1 KB · Views: 37
Back
Top