Removing the Water Surface Object in Sonic 1

Discussion in 'Tutorials' started by ProjectFM, Jun 5, 2020.

  1. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    854
    Location:
    Portland, Maine
    Context

    In short, the water surface object (obj1B) is a necessity for covering up artifacts which occur as a result of the mid-screen color swapping which creates the water’s transparent effect.* Sonic 3 incorporates an alternative method in which it only copies three colors per line which keeps most artifacts off screen, removing the necessity of the water surface object. This gives the water a more natural, less cartoony look and prevents sprite flicker when several sprites are on the same horizontal lines as the water’s surface. In addition, if chosen well enough, the method of changing three colors per line can give a sense of depth to the water’s surface.

    *Most emulators don’t recreate the artifacts found on real hardware. The ones that I know of that do are Exodus and BlastEm. The screenshots I’ve taken are from BlastEm.

    Part 1: Remove obj1B
    These lines in “Level_ChkWater” spawn the object and set its initial state, so just delete them or comment them out.
    Code:
            cmpi.b    #1,($FFFFFE10).w ; is level LZ?
            bne.s    Level_LoadObj    ; if not, branch
            move.b    #$1B,($FFFFD780).w ; load water    surface    object
            move.w    #$60,($FFFFD788).w
            move.b    #$1B,($FFFFD7C0).w
            move.w    #$120,($FFFFD7C8).w
    Part 2: Replacing PalToCRAM
    PalToCRAM is the code Sonic 1 calls during a horizontal interrupt, which happens during every HBlank, which is the period between the drawing of scanlines. It is here where the game switches colors to create the water effect, but it can also be used to switch out information in order to make 2 player possible or adjust the VSRAM (Vertical Scrolling RAM) to create vertical scaling effects. Here, it mainly just changes the CRAM (Color RAM) to the water palette at the correct spot. Replace everything from “PalToCRAM:” to before “locret_119C” with this code taken from Sonic 3:
    Code:
    PalToCRAM:
            tst.w    ($FFFFF644).w
            beq.s    locret_119C
            move.w    #0,($FFFFF644).w
            movem.l    d0-d1/a0-a2,-(sp)
     
            lea    ($C00000).l,a1
            move.w    #$8ADF,4(a1)        ; Reset HInt timing
            move.w  #$100,($A11100).l ; stop the Z80
    @z80loop:
            btst    #0,($A11100).l
            bne.s   @z80loop ; loop until it says it's stopped
            movea.l    ($FFFFF610).w,a2
            moveq    #$F,d0        ; adjust to push artifacts off screen
    @loop:
            dbf    d0,@loop    ; waste a few cycles here
    
            move.w    (a2)+,d1
            move.b    ($FFFFFE07).w,d0
            subi.b    #200,d0    ; is H-int occuring below line 200?
            bcs.s    @transferColors    ; if it is, branch
            sub.b    d0,d1
            bcs.s    @skipTransfer
    
    @transferColors:
            move.w    (a2)+,d0
            lea    ($FFFFFA80).w,a0
            adda.w    d0,a0
            addi.w    #$C000,d0
            swap    d0
            move.l    d0,4(a1)    ; write to CRAM at appropriate address
            move.l    (a0)+,(a1)    ; transfer two colors
            move.w    (a0)+,(a1)    ; transfer the third color
            nop
            nop
            moveq    #$24,d0
    
    @wasteSomeCycles:
            dbf    d0,@wasteSomeCycles
            dbf    d1,@transferColors    ; repeat for number of colors
    
    @skipTransfer:
            move.w  #0,($A11100).l    ; start the Z80
            movem.l    (sp)+,d0-d1/a0-a2
            tst.b    ($FFFFF64F).w
            bne.s    loc_119E
    $FFFFF610 is an unused longword variable in Sonic 1, but any mentioning of this variable can be replaced with any free longword variable. The same goes for $FFFFFE07, which is byte length. This code will read a table pointed to by $FFFFF610 and write to CRAM the 3 consecutive colors the table points to that corresponds with the scanline the interrupt is at at that moment. $FFFFFE07 is a replacement for $FFFFF625, which is the position of the water's surface on screen. Read MarkeyJester's post to understand why $FFFFF625 is inaccurate to what is represented on screen. To update $FFFFFE07, at loc_C22, loc_CD4, loc ED8, and loc_F9A, underneath "move.w ($FFFFF624).w,(a5)", add this line:
    Code:
            move.b    ($FFFFF625).w,($FFFFFE07).w
    Part 3: Creating The Table
    In “Level_ClrVars3”, underneath the line “bne.s Level_LoadPal”, add this:
    Code:
            move.l    #WaterTransition_LZ,($FFFFF610).w
    Finally, the table can be created. Here is an example which works fairly well with Labyrinth Zone. I recommend putting it before LZWaterEffects.
    Code:
    WaterTransition_LZ:    dc.w $13    ; # of entries - 1
            dc.w $62
            dc.w $68
            dc.w $7A
            dc.w $6E
            dc.w $74
            dc.w $42
            dc.w $48
            dc.w $4E
            dc.w $54
            dc.w $5A
            dc.w 2
            dc.w 8
            dc.w $E
            dc.w $14
            dc.w $1A
            dc.w $34
            dc.w $22
            dc.w $3A
            dc.w $2E
            dc.w $28
    The first entry of the table is the header, which is the number entries after it minus 1. The value of each entry corresponds to a location in “pallet/lz_uw.bin” and is the first of six consecutive bytes that will be written to CRAM during its corresponding horizontal interrupt. The first entry is read on the same line that is generally used as the surface of the water, and each subsequent entry is read on the next line underneath. This table in particular will switch every color color except the background color at $40.
    blastem_20200604_205751.png

    Part 4: Taking Advantage of the Effect
    That’s it for coding, but here is part where you can get creative with it. If you look at Angel Island Zone, which uses this technique, the location at which each set of colors is loaded at is chosen carefully so that things which can be interpreted as being the closest to the camera have their water colors switched at lower scanlines than things that are more towards the back, creating a sense of depth. In fact, at first I thought this was intentional and not a workaround for the limitations of the console. This works nicely because darker shades often represent areas further away because shadows tend to form in those areas.
    blastem_20200604_205816.png
    In Labyrinth Zone, the indented parts of the face tiles are darker, creating this effect. Of course, there are places where this is not the case and the illusion can falter, such as how some thin pillars have priority over Sonic, yet are covered by a higher level of water than Sonic. Fortunately, the effect is subtle enough that a few inconsistencies won’t ruin it.
    blastem_20200604_205843.png
    Generally, I recommend loading background colors first, then the foreground colors, then the colors of Sonic and other objects you’d find near the surface, and finally anything that you would rarely see near the surface, such as colors used by the enemies. Any colors that remain unchanged in the water can be left off the table. If you want to go above and beyond to ensure the best effect, then you can arrange the palette so that every three colors that must be on the same line are together and loaded at the same time, you can reload the same background colors before loading the rest of the colors so that the background looks more distant, you can reload two old colors and one new color several times so that each darker shade even further away, or you can draw your level art with this effect in mind by using colors in groups of three, only putting shadows in areas that should be further away, and reserving colors specifically for things that are further away.

    Part 5: Bugs
    There are two bugs I've found:
    1. W̶h̶e̶n̶ ̶t̶h̶e̶ ̶s̶c̶r̶e̶e̶n̶ ̶s̶w̶i̶t̶c̶h̶e̶s̶ ̶f̶r̶o̶m̶ ̶m̶a̶i̶n̶l̶y̶ ̶u̶s̶i̶n̶g̶ ̶t̶h̶e̶ ̶n̶o̶r̶m̶a̶l̶ ̶p̶a̶l̶e̶t̶t̶e̶ ̶t̶o̶ ̶t̶h̶e̶ ̶u̶n̶d̶e̶r̶w̶a̶t̶e̶r̶ ̶p̶a̶l̶e̶t̶t̶e̶,̶ ̶t̶h̶e̶ ̶n̶o̶r̶m̶a̶l̶ ̶p̶a̶l̶e̶t̶t̶e̶ ̶s̶t̶a̶y̶s̶ ̶f̶o̶r̶ ̶a̶ ̶f̶r̶a̶m̶e̶ ̶l̶o̶n̶g̶e̶r̶ ̶t̶h̶a̶n̶ ̶i̶t̶ ̶s̶h̶o̶u̶l̶d̶,̶ ̶r̶e̶s̶u̶l̶t̶i̶n̶g̶ ̶i̶n̶ ̶a̶ ̶f̶l̶a̶s̶h̶.̶ ̶I̶ ̶h̶a̶v̶e̶ ̶s̶p̶e̶n̶t̶ ̶h̶o̶u̶r̶s̶ ̶c̶o̶m̶p̶a̶r̶i̶n̶g̶ ̶c̶o̶d̶e̶ ̶a̶n̶d̶ ̶c̶o̶u̶l̶d̶ ̶n̶o̶t̶ ̶f̶i̶n̶d̶ ̶a̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶ ̶t̶o̶ ̶w̶h̶y̶ ̶t̶h̶i̶s̶ ̶h̶a̶p̶p̶e̶n̶s̶ ̶i̶n̶ ̶S̶o̶n̶i̶c̶ ̶1̶,̶ ̶b̶u̶t̶ ̶n̶o̶t̶ ̶S̶o̶n̶i̶c̶ ̶3̶K̶.̶ ̶I̶ ̶s̶u̶s̶p̶e̶c̶t̶ ̶t̶h̶a̶t̶ ̶t̶h̶e̶r̶e̶ ̶i̶s̶ ̶s̶o̶m̶e̶ ̶d̶e̶l̶a̶y̶ ̶i̶n̶ ̶t̶h̶e̶ ̶c̶o̶d̶e̶ ̶t̶h̶a̶t̶ ̶w̶r̶i̶t̶e̶s̶ ̶t̶o̶ ̶C̶R̶A̶M̶ ̶o̶r̶ ̶t̶h̶e̶ ̶e̶v̶e̶n̶t̶s̶ ̶l̶e̶a̶d̶i̶n̶g̶ ̶u̶p̶ ̶t̶o̶ ̶w̶r̶i̶t̶i̶n̶g̶ ̶t̶h̶e̶ ̶u̶n̶d̶e̶r̶w̶a̶t̶e̶r̶ ̶p̶a̶l̶e̶t̶t̶e̶ ̶t̶o̶ ̶C̶R̶A̶M̶ ̶a̶r̶e̶ ̶h̶a̶p̶p̶e̶n̶i̶n̶g̶ ̶o̶u̶t̶ ̶o̶f̶ ̶o̶r̶d̶e̶r̶.̶ ̶I̶’̶d̶ ̶a̶p̶p̶r̶e̶c̶i̶a̶t̶e̶ ̶a̶n̶y̶ ̶h̶e̶l̶p̶ ̶w̶i̶t̶h̶ ̶f̶i̶g̶u̶r̶i̶n̶g̶ ̶t̶h̶i̶s̶ ̶o̶u̶t̶.̶

    2. Because there is no scanline above the top scanline of the screen, the game switches from using the normal palette to using the underwater palette once the surface of the water goes above the top of the screen. The scanlines underneath which have a mix of normal and underwater colors will turn to their underwater colors, which can be jarring to look at. As far as I can tell, Sonic 3K has that same issue, but manages to keep most things that would most noticeably change color close enough to the water’s surface so that it isn’t noticeable. I recommend doing the same.
     
    Last edited: Jun 5, 2020
  2. ralakimus

    ralakimus pretty much a dead account Member

    Joined:
    Aug 26, 2013
    Messages:
    1,069
    EDIT: Trash, thought I had found a solution, turns out I'm just as stumped.
     
    Last edited: Jun 5, 2020
  3. MarkeyJester

    MarkeyJester ♡ ! Member

    Joined:
    Jun 27, 2009
    Messages:
    2,863
    The cause of the first bug is due to the H-blank interrupt line variable in 68k RAM ($FFFFF624-5) vs what's in the VDP register 8A.

    There is a clash involving the unsigned comparison of 200 (C8) or higher, this comparison is to ensure that if the water line is near the bottom of the screen, then the number of entries in the table is reduced (the closer to the bottom the line is, the less scanlines/colours it needs to swap out since it'll reach the end of the screen, and any swapping at that point is useless). When the water is shifting above the screen, the 68k variable is forced to DF and this happens during display, later during V-blank, this variable is sent to the VDP register for the next frame. The problem is the current frame H-blank register is still set to trigger near the top of the screen from being set by the previous frame. But since the LZ water subroutine is one of the first subroutines to be called during the main loop, it is able to successfully change the 68k variable before H-blank gets to it. The previous frame H-blank was probably set to say 02, but the subroutine has changed the 68k variable to DF, so the H-blank routine is interrupting on scanline 02, but thinks its DF because of the LZ water subroutine, and so only a partial transfer of the colours occur.

    The solution is simple, during V-blank, wherever the word $FFFFF624 is sent to the VDP, right under it, copy the byte in $FFFFF625 to some other unused RAM byte space, then get H-blank to read that unused RAM space instead, that way, 68k will be reading only what the VDP was last set to, I'd recommend $FFFFFE07 for this.

    As for the second issue, this is more to do with the LZ subroutine which is forcing the H-blank line immediately to DF if it goes above 00, and the H-blank routine isn't helping either. The solution in my opinion; it really needs a proper careful rewrite, no bashing with a rock or hacking it in blindly like Sonic Team did.
     
    TheInvisibleSun and ProjectFM like this.
  4. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    854
    Location:
    Portland, Maine
    Thank you so much, Markey! I guess I didn't take into account how long it takes the screen to update in comparison to how fast the code runs. Next time I have a similar out-of-sync issue, I'll keep this in mind.

    I updated the guide to fix the issue.
     
    Last edited: Jun 5, 2020