• Our software update is now concluded. You will need to reset your password to log in. In order to do this, you will have to click "Log in" in the top right corner and then "Forgot your password?".
  • 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

536
Posts
4
Years
    • Seen May 8, 2024
    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

    • bgb00029_before.png
      bgb00029_before.png
      2.1 KB · Views: 35
    • bgb00030_after.png
      bgb00030_after.png
      2.1 KB · Views: 34
    Back
    Top