Thursday, May 4, 2017

Fading circumstances

The fade effect seen when a stage starts is accomplished by changing the contents of CRAM every frame. Since Mega Drive colors have only 3 bits per channel, for a total of 8 distinct shades, increasing the brightness on every channel at once would result in a very quick, or if slowed down, choppy-looking fade.

Sonic games work around this by fading each channel individually, extending the total fade length from 7 to 21 frames long. I was curious as how it worked, so I looked up the relevant code in the current S&K disassembly. This is the function responsible for fading a single color:
Pal_AddColor:
        move.w  (a1)+,d2
        move.w  (a0),d3
        cmp.w   d2,d3
        beq.s   loc_3B7C
        move.w  d3,d1
        addi.w  #$200,d1
        cmp.w   d2,d1
        bhi.s   loc_3B6A
        move.w  d1,(a0)+
        rts
; ---------------------------------------------------------------------------

loc_3B6A:
        move.w  d3,d1
        addi.w  #$20,d1
        cmp.w   d2,d1
        bhi.s   loc_3B78
        move.w  d1,(a0)+
        rts
; ---------------------------------------------------------------------------

loc_3B78:
        addq.w  #2,(a0)+
        rts
; ---------------------------------------------------------------------------

loc_3B7C:
        addq.w  #2,a0
        rts
Here's the breakdown:
  • Load the target color from the address stored in a1 into d2, then increment a1. Load the current color from the address stored in a0 into d3.
  • Compare d2 and d3. If they're equal, we're done, so branch to loc_3B7C, increment a0 and return. Both a0 and a1 are now pointing at the next color in line.
  • Copy the current color from d3 to d1 and add $200 to it.
  • If d1 is higher than d2, branch to loc_3B6A. Otherwise write d1 to the current color, increment a0 and return.
  • Copy the current color from d3 to d1 and add $20 to it.
  • If d1 is higher than d2, branch to loc_3B78. Otherwise write d1 to the current color, increment a0 and return.
  • Add 2 to the current color address, increment a0 and return.

In other words, for each color, blue is incremented until it reaches the target blue, then green until it reaches the target green, and finally red until the two colors are equal. A consequence of this algorithm is that brighter colors, and colors with several color components take longer to fade in than darker or primary colors.

For instance, in the example below, the grass fades in almost instantly because it's mostly pure green, despite being fairly bright. On the other hand, the brightest spots of the ground pattern have a lot of different color components to them, so they take quite longer than the surrounding terrain.


Here's the part I didn't expect: using the same algorithm, if we tried to reverse the order and increment red first, and red happened to already be maxed out, the value would actually overflow into green and not be caught by the naïve "higher than" check. So the order they chose to fade the colors might have simply been for the sake of writing simpler code, rather than the aesthetic effect.

Showing the title card during the fade is a bit of a conundrum. We fill CRAM with black to make the screen black, but we also need colors in there to display the title card with! The solution: load Sonic's palette right away, use it to display the title card, and only fade in the stage's colors. The player characters sort of pop into existence, but since they occupy a relatively small part of the screen, this isn't very noticeable.

No comments:

Post a Comment