Improve the fade in\fade out progression routines in Sonic 2
From Sonic Retro
(Original guide by MarkeyJester)
Updated by User:WBilgini
Updated again by User:CosmotheFoxxo
The palette fading routines in Sonic 1 and Sonic 2 (and possibly to a degree Sonic 3 & Knuckles) known as "Pal_FadeFromBlack" and "Pal_FadeToBlack", are not working exactly well. Here is a screenshot of Sonic 2's title screen 6 frames during fade in:
From Sonic Team's point of view, it may not be incorrect and is possibly intentional, however, from a logical point of view, this is incorrect fading, what happens here is it fades the blue parts in first, then fades the green parts in, and then finally fades the red parts in, the same goes for fading out, red first, then green, finally blue. Strictly speaking for a the nearest possible perfect fade, all colours need to be fading in and out simultaneously (At the correct timing of course), so for example the colour 06E4 should fade in the pattern of:
0020 | 0040 | 0060 | 0080 | 02A0 | 04C2 | 06E4 |
And fade out in the pattern of:
04C2 | 02A0 | 0080 | 0060 | 0040 | 0020 | 0000 |
To make the above function for Sonic 2, there are two main places to look at:
The Main Routine
The main palette fading routines are known as "Pal_FadeFromBlack", "Pal_FadeToBlack","Pal_FadeFromWhite", and "Pal_FadeToWhite". You'll need to go to the routine "Pal_FadeFromBlack" and replace everything from there down to the end of "Pal_FadeToWhite" with this:
; ---------------------------------------------------------------------------
; Subroutine to fade in from black
; ---------------------------------------------------------------------------
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; sub_23C6: Pal_FadeTo:
Pal_FadeFromBlack:
move.w #$3F,(Palette_fade_range).w
moveq #0,d0
lea (Normal_palette).w,a0
move.b (Palette_fade_start).w,d0
adda.w d0,a0
moveq #0,d1
move.b (Palette_fade_length).w,d0
; loc_23DE: Pal_ToBlack:
.palettewrite:
move.w d1,(a0)+
dbf d0,.palettewrite ; fill palette with $000 (black)
moveq #$0E,d4 ; MJ: prepare maximum colour check
moveq #$00,d6 ; MJ: clear d6
.nextframe:
bsr.w RunPLC_RAM
move.b #$12,(Vint_routine).w
bsr.w WaitForVint
bchg #$00,d6 ; MJ: change delay counter
beq .nextframe ; MJ: if null, delay a frame
bsr.s .UpdateAllColours
subq.b #$02,d4 ; MJ: decrease colour check
bne .nextframe ; MJ: if it has not reached null, branch
move.b #$12,(Vint_routine).w ; MJ: wait for V-blank again (so colours transfer)
bra WaitForVint ; MJ: ''
; End of function Pal_FadeTo
; ---------------------------------------------------------------------------
; Subroutine to update all colours once
; ---------------------------------------------------------------------------
; sub_23FE: Pal_FadeIn:
.UpdateAllColours:
moveq #0,d0
lea (Normal_palette).w,a0
lea (Target_palette).w,a1
move.b (Palette_fade_start).w,d0
adda.w d0,a0
adda.w d0,a1
move.b (Palette_fade_length).w,d0
.nextcolor
bsr.s .UpdateColour
dbf d0,.nextcolor
tst.b (Water_flag).w
beq.s .skipunderwater
moveq #0,d0
lea (Underwater_palette).w,a0
lea (Underwater_target_palette).w,a1
move.b (Palette_fade_start).w,d0
adda.w d0,a0
adda.w d0,a1
move.b (Palette_fade_length).w,d0
.nextcolor2
bsr.s .UpdateColour
dbf d0,.nextcolor2
.skipunderwater:
rts
; ---------------------------------------------------------------------------
; Subroutine to update a single colour once
; ---------------------------------------------------------------------------
; sub_243E: Pal_AddColor:
.UpdateColour:
move.b (a1),d5 ; MJ: load blue
move.w (a1)+,d1 ; MJ: load green and red
move.b d1,d2 ; MJ: load red
lsr.b #$04,d1 ; MJ: get only green
andi.b #$0E,d2 ; MJ: get only red
move.w (a0),d3 ; MJ: load current colour in buffer
cmp.b d5,d4 ; MJ: is it time for blue to fade?
bhi FCI_NoBlue ; MJ: if not, branch
addi.w #$0200,d3 ; MJ: increase blue
FCI_NoBlue:
cmp.b d1,d4 ; MJ: is it time for green to fade?
bhi FCI_NoGreen ; MJ: if not, branch
addi.b #$20,d3 ; MJ: increase green
FCI_NoGreen:
cmp.b d2,d4 ; MJ: is it time for red to fade?
bhi FCI_NoRed ; MJ: if not, branch
addq.b #$02,d3 ; MJ: increase red
FCI_NoRed:
move.w d3,(a0)+ ; MJ: save colour
rts ; MJ: return
; ---------------------------------------------------------------------------
; Subroutine to fade out to black
; ---------------------------------------------------------------------------
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; sub_246A: Pal_FadeFrom:
Pal_FadeToBlack:
move.w #$3F,(Palette_fade_range).w
moveq #$07,d4 ; MJ: set repeat times
moveq #$00,d6 ; MJ: clear d6
.nextbframe
bsr.w RunPLC_RAM
move.b #$12,(Vint_routine).w
bsr.w WaitForVint
bchg #$00,d6 ; MJ: change delay counter
beq .nextbframe ; MJ: if null, delay a frame
bsr.s .UpdateAllColours
dbf d4,.nextbframe
rts
; End of function Pal_FadeToBlack
; ---------------------------------------------------------------------------
; Subroutine to update all colours once
; ---------------------------------------------------------------------------
; sub_248A: Pal_FadeOut:
.UpdateAllColours:
moveq #0,d0
lea (Normal_palette).w,a0
move.b (Palette_fade_start).w,d0
adda.w d0,a0
move.b (Palette_fade_length).w,d0
.Nextcolour
bsr.s .UpdateColour
dbf d0,.Nextcolour
moveq #0,d0
lea (Underwater_palette).w,a0
move.b (Palette_fade_start).w,d0
adda.w d0,a0
move.b (Palette_fade_length).w,d0
.Nextcolour2
bsr.s .UpdateColour
dbf d0,.Nextcolour2
rts
; ---------------------------------------------------------------------------
; Subroutine to update a single colour once
; ---------------------------------------------------------------------------
; sub_24B8: Pal_DecColor:
.UpdateColour:
move.w (a0),d5 ; MJ: load colour
move.w d5,d1 ; MJ: copy to d1
move.b d1,d2 ; MJ: load green and red
move.b d1,d3 ; MJ: load red
andi.w #$0E00,d1 ; MJ: get only blue
beq FCO_NoBlue ; MJ: if blue is finished, branch
subi.w #$0200,d5 ; MJ: decrease blue
FCO_NoBlue:
andi.b #$E0,d2 ; MJ: get only green
beq FCO_NoGreen ; MJ: if green is finished, branch
subi.b #$20,d5 ; MJ: decrease green
FCO_NoGreen:
andi.b #$0E,d3 ; MJ: get only red
beq FCO_NoRed ; MJ: if red is finished, branch
subq.b #$02,d5 ; MJ: decrease red
FCO_NoRed:
move.w d5,(a0)+ ; MJ: save new colour
rts ; MJ: return
; End of function Pal_FadeToBlack
; Everything after this point was implemented by CosmotheFoxxo
; ---------------------------------------------------------------------------
; Subroutine to update all colours once
; ---------------------------------------------------------------------------
; sub_248A: Pal_FadeOut:
.UpdateAllColours:
; Update above-water palette
moveq #0,d0
lea (Normal_palette).w,a0
move.b (Palette_fade_start).w,d0
adda.w d0,a0
move.b (Palette_fade_length).w,d0
.nextcolour:
bsr.s .UpdateColour
dbf d0,.nextcolour
; Notice how this one lacks a check for
; if Water_flag is set, unlike Pal_FadeFromBlack?
; Update underwater palette
moveq #0,d0
lea (Underwater_palette).w,a0
move.b (Palette_fade_start).w,d0
adda.w d0,a0
move.b (Palette_fade_length).w,d0
.nextcolour2:
bsr.s .UpdateColour
dbf d0,.nextcolour2
rts
; ---------------------------------------------------------------------------
; Subroutine to update a single colour once
; ---------------------------------------------------------------------------
; sub_24B8: Pal_DecColor:
.UpdateColour:
move.w (a0),d5
move.w d5,d1
move.b d1,d2
move.b d1,d3
andi.b #$0E,d3
beq .updategreen
subq.b #$02,d5
; loc_24C8: Pal_DecGreen:
.updategreen:
andi.b #$E0,d2
beq .updateblue
subi.b #$20,d5
; loc_24D6: Pal_DecBlue:
.updateblue:
andi.w #$0E00,d1
beq .updatenone
subi.w #$0200,d5
; loc_24E4: Pal_DecNone:
.updatenone:
move.w d5,(a0)+
rts
; ---------------------------------------------------------------------------
; Subroutine to fade in from white
; ---------------------------------------------------------------------------
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; sub_24E8: Pal_MakeWhite:
Pal_FadeFromWhite:
move.w #$3F,(Palette_fade_range).w
moveq #0,d0
lea (Normal_palette).w,a0
move.b (Palette_fade_start).w,d0
adda.w d0,a0
move.w #$EEE,d1
move.b (Palette_fade_length).w,d0
.palettewrite:
move.w d1,(a0)+
dbf d0,.palettewrite
moveq #$0E,d4
moveq #$00,d6
.nextframe:
move.b #VintID_Fade,(Vint_routine).w
bsr.w WaitForVint
bchg #$00,d6
beq .nextframe
bsr.s .UpdateAllColours
subq.b #$02,d4
bne .nextframe
move.b #$12,(Vint_routine).w
bra WaitForVint
; ---------------------------------------------------------------------------
; Subroutine to update all colours once
; ---------------------------------------------------------------------------
; sub_2522: Pal_WhiteToBlack:
.UpdateAllColours:
; Update above-water palette
moveq #0,d0
lea (Normal_palette).w,a0
lea (Target_palette).w,a1
move.b (Palette_fade_start).w,d0
adda.w d0,a0
adda.w d0,a1
move.b (Palette_fade_length).w,d0
.nextcolour:
bsr.s .UpdateColour
dbf d0,.nextcolour
tst.b (Water_flag).w
beq.s .skipunderwater
; Update underwater palette
moveq #0,d0
lea (Underwater_palette).w,a0
lea (Underwater_target_palette).w,a1
move.b (Palette_fade_start).w,d0
adda.w d0,a0
adda.w d0,a1
move.b (Palette_fade_length).w,d0
.nextcolour2:
bsr.s .UpdateColour
dbf d0,.nextcolour2
.skipunderwater:
rts
; ---------------------------------------------------------------------------
; Subroutine to update a single colour once
; ---------------------------------------------------------------------------
; sub_2562: Pal_DecColor2:
.UpdateColour:
move.b (a1),d5
move.w (a1)+,d1
move.b d1,d2
lsr.b #$04,d1
andi.b #$0E,d2
move.w (a0),d3
cmp.b d5,d4
bls A_NoBlue
subi.w #$0200,d3
A_NoBlue:
cmp.b d1,d4
bls A_NoGreen
subi.b #$20,d3
A_NoGreen:
cmp.b d2,d4
bls A_NoRed
subq.b #$02,d3
A_NoRed:
move.w d3,(a0)+
rts
; ---------------------------------------------------------------------------
; Subroutine to fade out to white (used when you enter a special stage)
; ---------------------------------------------------------------------------
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; sub_2592: Pal_MakeFlash:
Pal_FadeToWhite:
move.w #$3F,(Palette_fade_range).w
moveq #$07,d4 ; MJ: set repeat times
moveq #$00,d6 ; MJ: clear d6
.nextcframe:
move.b #VintID_Fade,(Vint_routine).w
bsr.w WaitForVint
bchg #$00,d6
beq .nextcframe
bsr.s .UpdateAllColours
dbf d4,.nextcframe
rts
; End of function Pal_FadeToWhite
; ---------------------------------------------------------------------------
; Subroutine to update all colours once
; ---------------------------------------------------------------------------
; sub_25B2: Pal_ToWhite:
.UpdateAllColours:
; Update above-water palette
moveq #0,d0
lea (Normal_palette).w,a0
move.b (Palette_fade_start).w,d0
adda.w d0,a0
move.b (Palette_fade_length).w,d0
.nextcolour:
bsr.s .UpdateColour
dbf d0,.nextcolour
; Notice how this one lacks a check for
; if Water_flag is set, unlike Pal_FadeFromWhite?
; Update underwater palette
moveq #0,d0
lea (Underwater_palette).w,a0
move.b (Palette_fade_start).w,d0
adda.w d0,a0
move.b (Palette_fade_length).w,d0
.nextcolour2:
bsr.s .UpdateColour
dbf d0,.nextcolour2
rts
; ---------------------------------------------------------------------------
; Subroutine to update a single colour once
; ---------------------------------------------------------------------------
; sub_25E0: Pal_AddColor2:
.UpdateColour:
move.w (a0),d5
move.w d5,d1
move.b d1,d2
move.b d1,d3
andi.w #$0E00,d1
cmpi.w #$0E00,d1
beq a_NoBlue
addi.w #$0200,d5
a_NoBlue:
andi.b #$E0,d2
cmpi.b #$E0,d2
beq a_NoGreen
addi.b #$20,d5
a_NoGreen:
andi.b #$0E,d3
cmpi.b #$0E,d3
beq a_NoRed
addq.b #$02,d5
a_NoRed:
move.w d5,(a0)+
rts
; End of function Pal_AddColor2
Title Screen Specific Routine
Unfortunately, because of how unique the title screen is, the above change will not apply very well to it, hence, we have to change the specific routines responsible for its special fading/control.
At routine "ObjC9", you'll find this code below:
ObjC9:
moveq #0,d0
move.b routine(a0),d0
move.w ObjC9_Index(pc,d0.w),d1
jmp ObjC9_Index(pc,d1.w)
; ===========================================================================
ObjC9_Index: offsetTable
offsetTableEntry.w ObjC9_Init ; 0
offsetTableEntry.w ObjC9_Main ; 2
; ===========================================================================
ObjC9_Init:
addq.b #2,routine(a0)
moveq #0,d0
move.b subtype(a0),d0
lea (PaletteChangerDataIndex).l,a1
adda.w (a1,d0.w),a1
move.l (a1)+,ttlscrpalchanger_codeptr(a0)
movea.l (a1)+,a2
move.b (a1)+,d0
move.w d0,ttlscrpalchanger_start_offset(a0)
lea (Target_palette).w,a3
adda.w d0,a3
move.b (a1)+,d0
move.w d0,ttlscrpalchanger_length(a0)
- move.w (a2)+,(a3)+
dbf d0,-
move.b (a1)+,d0
move.b d0,ttlscrpalchanger_fadein_time_left(a0)
move.b d0,ttlscrpalchanger_fadein_time(a0)
move.b (a1)+,ttlscrpalchanger_fadein_amount(a0)
rts
; ===========================================================================
ObjC9_Main:
subq.b #1,ttlscrpalchanger_fadein_time_left(a0)
bpl.s +
move.b ttlscrpalchanger_fadein_time(a0),ttlscrpalchanger_fadein_time_left(a0)
subq.b #1,ttlscrpalchanger_fadein_amount(a0)
bmi.w DeleteObject
movea.l ttlscrpalchanger_codeptr(a0),a2
movea.l a0,a3
move.w ttlscrpalchanger_length(a0),d0
move.w ttlscrpalchanger_start_offset(a0),d1
lea (Normal_palette).w,a0
adda.w d1,a0
lea (Target_palette).w,a1
adda.w d1,a1
- jsr (a2) ; dynamic call! to Pal_FadeFromBlack.UpdateColour, loc_1344C, or loc_1348A, assuming the PaletteChangerData pointers haven't been changed
dbf d0,-
movea.l a3,a0
+
rts
Here are the necessary changes required for the fading to perform correctly:
ObjC9:
moveq #0,d0
move.b routine(a0),d0
move.w ObjC9_Index(pc,d0.w),d1
jmp ObjC9_Index(pc,d1.w)
; ===========================================================================
ObjC9_Index: offsetTable
offsetTableEntry.w ObjC9_Init ; 0
offsetTableEntry.w ObjC9_Main ; 2
; ===========================================================================
ObjC9_Init:
addq.b #2,routine(a0)
moveq #0,d0
move.b subtype(a0),d0
lea (PaletteChangerDataIndex).l,a1
adda.w (a1,d0.w),a1
move.l (a1)+,ttlscrpalchanger_codeptr(a0)
movea.l (a1)+,a2
move.b (a1)+,d0
move.w d0,ttlscrpalchanger_start_offset(a0)
lea (Target_palette).w,a3
adda.w d0,a3
move.b (a1)+,d0
move.w d0,ttlscrpalchanger_length(a0)
move.b #$0E,objoff_33(a0) ; MJ: set fade counter
- move.w (a2)+,(a3)+
dbf d0,-
move.b (a1)+,d0
move.b d0,ttlscrpalchanger_fadein_time_left(a0)
move.b d0,ttlscrpalchanger_fadein_time(a0)
move.b (a1)+,ttlscrpalchanger_fadein_amount(a0)
rts
; ===========================================================================
ObjC9_Main:
subq.b #1,ttlscrpalchanger_fadein_time_left(a0)
bpl.s +
move.b ttlscrpalchanger_fadein_time(a0),ttlscrpalchanger_fadein_time_left(a0)
subq.b #1,ttlscrpalchanger_fadein_amount(a0)
bmi.w DeleteObject
moveq #$00,d4 ; MJ: clear d4
move.b objoff_33(a0),d4 ; MJ: load fade counter
subq.b #$02,objoff_33(a0) ; MJ: decrease fade counter
movea.l ttlscrpalchanger_codeptr(a0),a2 ; load routine to run
movea.l a0,a3 ; store current object
move.w ttlscrpalchanger_length(a0),d0 ; load length
move.w ttlscrpalchanger_start_offset(a0),d1 ; load loadtooffset
movea.l ttlscrpalchanger_codeptr(a0),a2
movea.l a0,a3
move.w ttlscrpalchanger_length(a0),d0
move.w ttlscrpalchanger_start_offset(a0),d1
lea (Normal_palette).w,a0
adda.w d1,a0
lea (Target_palette).w,a1
adda.w d1,a1
- jsr (a2) ; dynamic call! to Pal_FadeFromBlack.UpdateColour, loc_1344C, or loc_1348A, assuming the PaletteChangerData pointers haven't been changed
dbf d0,-
movea.l a3,a0
+
rts
This will ensure the object uses our new fading routines correctly, the next thing to do is at:
off_1338C: C9PalInfo Pal_FadeFromBlack.UpdateColour, Pal_1342C, $60, $F,2,$15
Because of the nature of the new fade in routine being faster in the transition, we need to change the end value $15 to something smaller, say $07:
off_1338C: C9PalInfo Pal_FadeFromBlack.UpdateColour, Pal_1342C, $60, $F,2,$07 ; MJ: 15 to 7
This ensures the fading in routine is used correctly by the object, now there's one more fix to look at, if the player presses start to skip the transition halfway through fading in, the object continues fading in even though the entire full palette is loaded, to fix this goto "TitleScreen_SetFinalState":
- move.l (a1)+,(a2)+
dbf d6,-
tst.b objoff_30(a0)
bne.s + ; rts
moveq #signextendB(MusID_Title),d0 ; title music
jsrto (PlayMusic).l, JmpTo4_PlayMusic
+
rts
And place this instruction just after the "dbf d6,-" instruction:
sf.b (Object_RAM+($100+$32)).w ; set fade counter to 00 (finish)
This will set the palette fading objects fade counter to 0 and set the object to delete itself, thus preventing it from messing with the palette if the transition was skipped.
If the above instructions have been followed carefully, then this will ensure that the colour fading scheme fades normally and smoothly, here is the new fading routine in action, 6 frames after fading in: