Parallax waterfall thing

The place for codemasters or beginners to talk about programming any language for the Spectrum.
Post Reply
presh
Manic Miner
Posts: 237
Joined: Tue Feb 25, 2020 8:52 pm
Location: York, UK

Parallax waterfall thing

Post by presh »

Something I've been messing around with over the weekend, inspired by things seen in arcade/16-bit console games back in the day.

Here's an idea for a vertically-scrolling level background, using columns made from just two background tiles (32 x 16 px each) and scrolling them at different rates.

The simplicity of the scene makes it very quick to draw from scratch, taking just under half a frame.

Code: Select all

; Fast Parallax vertical scrolling scene builder.
; ================================================
; HL = display file, points to end of current pixel row. Use  LD SP, HL  then PUSH the values.
; IX = graphics data pointer for rocks. Uses  LD SP, IX  and  POP  to get the values into BC & DE.
; IY = graphics data pointer for water. Also uses SP & POP but to D'E' & H'L' 
; B'C' = used for arithmetic on IX & IY when next graphic required
; BC DE = current rock bitmaps
; D'E' H'L' = current water bitmaps 
; A' = loop counter for display_file blocks
; A  = used for arithmetic

display_file EQU 16384 + 2048   ; to keep things simple for now, try to stay ahead of the beam! ;)

InterruptVector_I EQU $BF       ; doesn't matter... yet! but will when we create the interrupt vector

  ORG 32768
  
  ; ===== Setup ===== ;
  
  ; CBA to set up IM2 yet, so just disable interrupts so we can use IY
  DI
  
  ; --- Draw colour attributes --- ;
  
  ; - Rocks & Water
  
  LD (smc_sp_attr), SP
  
  LD IX, 22528 + 256 + 30     ; 1 block down, rhs
  
  LD H, 6*8 +64  ; bright yellow
  LD L, H
  
  LD D, 5*8      ; cyan 
  LD E, D
  
  LD BC, 32 ; add to IX to get next row
  LD A, 16  ; row counter
  
attr_row:

  LD SP, IX
  
  PUSH HL
  PUSH HL
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH HL
  PUSH HL
  
  ADD IX, BC
  
  DEC A
  JP NZ, attr_row
  
smc_sp_attr EQU $+1
  LD SP, 0
  
  
  ; --- Initialise offsets --- ;
  
  XOR A
  LD (tile1_offset_px), A
  LD (tile2_offset_px), A
  
  
  ; ================= ;
  ; === MAIN LOOP === ;
  ; ================= ;
  
main:

  ; ===== Tile graphics ===== ;
  
  ; Calculate IX & IY based on offset values
  
  ; IX
  LD A, (tile1_offset_px)
  RLCA    ; x2
  RLCA    ; x4  (4 bytes per px height)
  AND 11111100b ; mask off any carried bits 
  LD B, 0
  LD C, A
  LD IX, tile_rock
  ADD IX, BC
  
  ; IY
  LD A, (tile2_offset_px)
  RLCA    ; x2
  RLCA    ; x4  (4 bytes per px height)
  AND 11111100b ; mask off any carried bits 
  ; LD B, 0
  LD C, A
  LD IY, tile_water
  ADD IY, BC
  
  
  ; ===== Build the scene ===== ;

  ; Disable interrupts
  ; DI
  
  ; Store SP
  LD (smc_sp), SP
  
  ; Use I to count number of pixels down
  XOR A
  LD I, A
  
do_gfx_pixel:
  
  ; Calculate display_file destination address
  LD A, I
  CP 8        
  JR NC, l_row_bottom
  
l_row_top:
  LD L, 30   ;  First PUSH should fill in +29 & +28
  JP l_row_done
  
l_row_bottom
  LD L, 32 + 30   ; as above, but a row down
 
l_row_done: 
  ; Amount to add to H (0-7)
  AND 7
  ADD A, display_file / 256
  LD H, A
  ; HL is now correct
  
  ; Get the graphics
  LD SP, IX
  POP BC
  POP DE
  EXX
  LD SP, IY
  POP DE
  POP HL
  ; Advance graphics pointers to next position
  LD BC, 4
  ADD IX, BC
  ADD IY, BC
  EXX 
  
  ; Loop counters
  LD A, 2     ; 2 blocks
  EX AF, AF'  ; store counter
  
do_row:

  ; Get SP into position
  LD SP, HL
  
  ; Rock x4 cells
  PUSH DE
  PUSH BC
  
  ; Water x20 cells
  EXX
  PUSH HL
  PUSH DE
  PUSH HL
  PUSH DE
  PUSH HL
  PUSH DE
  PUSH HL
  PUSH DE
  PUSH HL
  PUSH DE
  
  ; Rock x4 cells
  EXX
  PUSH DE
  PUSH BC

  ; Pixel row done!
  
  ; Move down 2 cells
  LD A, L
  ADD A, 64
  LD L, A
  JP NC, do_row
  ; Check loop counter - any blocks left to do?
  EX AF, AF'
  DEC A
  JR Z, next_pixel_down
  ; Carry occurred, so L is in correct place but H isn't
  EX AF, AF'    ; store loop counter
  LD A, H 
  ADD A, 8
  LD H, A
  JP do_row
  
  
next_pixel_down:
  
  ; Update our HL-adjuster
  LD A, I
  INC A
  OUT (254), A    ; stripes!
  ; Done if we've reached the 16th pixel
  AND 15
  JP Z, finish
  ; Next!
  LD I, A
  JP do_gfx_pixel
  
  
finish:

  ; Restore SP
smc_sp EQU $+1
  LD SP, 0    ; updated via smc
  
  ; IMPORTANT! if using IM2, restore I here!
  LD A, InterruptVector_I
  LD I, A

  
  ; --- Move tiles --- ;
  
  ; Update tile offsets
  
  ; IX
  LD A, (tile1_offset_px)
  DEC A   ; 1px
  AND 15
  LD (tile1_offset_px), A
  
  ; IY
  LD A, (tile2_offset_px)
  SUB 3   ; 3px
  AND 15
  LD (tile2_offset_px), A
  
  
  ; Black border
  XOR A
  OUT (254), A
  
  ; Wait for interrupt, then disable again
  ; IMPORTANT! if using IM0, restore IY first!
  LD IY, $5C3A
  EI
  HALT    ; 50fps
  ; HALT    ; 25fps
  DI
  
  JP main
  
  
; data

tile1_offset_px DB 0 
tile2_offset_px DB 0
 
; graphics
; priority: CHAR_X, CHAR_LINE, CHAR_Y
;
; n.b. each tile graphic is repeated (making it twice as tall) to avoid having to loop back when advancing IX & IY
  
tile_water:
  ; x1
  DEFB	239,185,237,245,182,155,109,254
	DEFB	 84,211,108,166, 93,215,110,179
	DEFB	 84,245, 59,154,246,181,185, 22
	DEFB	163,189,153,151,179, 26,211,213
	DEFB	183,121,209,117, 55,233,243, 53
	DEFB	126,203,187,124,108, 79, 55,238
	DEFB	237,109,126,110,105, 89,110,253
	DEFB	 89,219,206,229,219,249,231,183
	; x2
  DEFB	239,185,237,245,182,155,109,254
	DEFB	 84,211,108,166, 93,215,110,179
	DEFB	 84,245, 59,154,246,181,185, 22
	DEFB	163,189,153,151,179, 26,211,213
	DEFB	183,121,209,117, 55,233,243, 53
	DEFB	126,203,187,124,108, 79, 55,238
	DEFB	237,109,126,110,105, 89,110,253
	DEFB	 89,219,206,229,219,249,231,183
  
tile_rock:
  ; x1
  DEFB	224,192,192, 65,249,128,224,193
	DEFB	207,  1,249,129,130,  1,159,129
	DEFB	130,  1, 12,  1,130,  3, 24,  3
	DEFB	134,  7,248, 15,223, 12, 28,127
	DEFB	243,152, 15,207,224,240, 12,  3
	DEFB	192,192,  8,  1,128, 64, 24,  1
	DEFB	128, 64,126,  7,128, 96,195, 31
	DEFB	192,127,129,243,192, 96,128, 97
	; x2
  DEFB	224,192,192, 65,249,128,224,193
	DEFB	207,  1,249,129,130,  1,159,129
	DEFB	130,  1, 12,  1,130,  3, 24,  3
	DEFB	134,  7,248, 15,223, 12, 28,127
	DEFB	243,152, 15,207,224,240, 12,  3
	DEFB	192,192,  8,  1,128, 64, 24,  1
	DEFB	128, 64,126,  7,128, 96,195, 31
	DEFB	192,127,129,243,192, 96,128, 97
presh
Manic Miner
Posts: 237
Joined: Tue Feb 25, 2020 8:52 pm
Location: York, UK

Re: Parallax waterfall thing

Post by presh »

Here's a version with a platform added, for our plucky hero to stand on while fighting off whatever else happens to be going on at the time... don't fall off, now!

From here it's quite easy to imagine overlaying sprites for descending obstacles to be avoided, enemies and so on.

However that's less likely to run at 50z without flicker, so you'll see some commented HALTs in the code which will give an idea of how it runs at 25Hz and 16.66Hz... still very usable in my opinion!


Code: Select all

; Fast Parallax vertical scrolling scene builder.
; ================================================
; HL = display file, points to end of current pixel row. Use  LD SP, HL  then PUSH the values.
; IX = graphics data pointer for rocks. Uses  LD SP, IX  and  POP  to get the values into BC & DE.
; IY = graphics data pointer for water. Also uses SP & POP but to D'E' & H'L' 
; B'C' = used for arithmetic on IX & IY when next graphic required
; BC DE = current rock bitmaps
; D'E' H'L' = current water bitmaps 
; A' = loop counter for display_file blocks
; A  = used for arithmetic

display_file EQU 16384 + 2048   ; to keep things simple for now, start lower down to try & stay ahead of the beam! ;)

InterruptVector_I EQU $BF       ; doesn't matter... yet! but will when we create the interrupt vector

  ORG 32768
  
  ; ===== Setup ===== ;
  
  ; CBA to set up IM2 yet, so just disable interrupts so we can use IY
  DI
  
  ; --- Draw colour attributes --- ;
  
  ; - Rocks & Water
  
  LD (smc_sp_attr), SP
  
  LD IX, 22528 + 256 + 30     ; 1 block down, rhs
  
  LD H, 6*8 +64  ; bright yellow
  LD L, H
  
  LD D, 5*8      ; cyan 
  LD E, D
  
  LD BC, 32 ; add to IX to get next row
  LD A, 16  ; row counter
  
attr_row:

  LD SP, IX
  
  PUSH HL
  PUSH HL
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH HL
  PUSH HL
  
  ADD IX, BC
  
  DEC A
  JP NZ, attr_row
  
smc_sp_attr EQU $+1
  LD SP, 0
  
  
  ; - Platform
  
  LD (smc_sp_platform_attr), SP
  
  LD SP, 22528 + (22*32) + 25 
  
  LD H, 7 +64  ; bright white
  LD L, H
  
  PUSH HL
  PUSH HL
  PUSH HL
  PUSH HL
  PUSH HL
  PUSH HL
  PUSH HL
  PUSH HL
  PUSH HL
  
smc_sp_platform_attr EQU $+1
  LD SP, 0
  
  ; The two supports
  LD A, H
  LD (23264 +  9), A
  LD (23264 + 22), A
  
  
  ; --- Initialise offsets --- ;
  
  XOR A
  LD (tile1_offset_px), A
  LD (tile2_offset_px), A
  
  
  ; ================= ;
  ; === MAIN LOOP === ;
  ; ================= ;
  
main:

  ; ===== Tile graphics ===== ;
  
  ; Calculate IX & IY based on offset values
  
  ; IX
  LD A, (tile1_offset_px)
  RLCA    ; x2
  RLCA    ; x4  (4 bytes per px height)
  AND 11111100b ; mask off any carried bits 
  LD B, 0
  LD C, A
  LD IX, tile_rock
  ADD IX, BC
  
  ; IY
  LD A, (tile2_offset_px)
  RLCA    ; x2
  RLCA    ; x4  (4 bytes per px height)
  AND 11111100b ; mask off any carried bits 
  ; LD B, 0
  LD C, A
  LD IY, tile_water
  ADD IY, BC
  
  
  ; ===== Build the scene ===== ;

  ; Disable interrupts
  ; DI
  
  ; Store SP
  LD (smc_sp), SP
  
  ; Use I to count number of pixels down
  XOR A
  LD I, A
  
do_gfx_pixel:
  
  ; Calculate display_file destination address
  LD A, I
  CP 8        
  JR NC, l_row_bottom
  
l_row_top:
  LD L, 30   ;  First PUSH should fill in +29 & +28
  JP l_row_done
  
l_row_bottom
  LD L, 32 + 30   ; as above, but a row down
 
l_row_done: 
  ; Amount to add to H (0-7)
  AND 7
  ADD A, display_file / 256
  LD H, A
  ; HL is now correct
  
  ; Get the graphics
  LD SP, IX
  POP BC
  POP DE
  EXX
  LD SP, IY
  POP DE
  POP HL
  ; Advance graphics pointers to next position
  LD BC, 4
  ADD IX, BC
  ADD IY, BC
  EXX 
  
  ; Loop counters
  LD A, 2     ; 2 blocks
  EX AF, AF'  ; store counter
  
do_row:

  ; Get SP into position
  LD SP, HL
  
  ; Rock x4 cells
  PUSH DE
  PUSH BC
  
  ; Water x20 cells
  EXX
  PUSH HL
  PUSH DE
  PUSH HL
  PUSH DE
  PUSH HL
  PUSH DE
  PUSH HL
  PUSH DE
  PUSH HL
  PUSH DE
  
  ; Rock x4 cells
  EXX
  PUSH DE
  PUSH BC

  ; Pixel row done!
  
  ; Move down 2 cells
  LD A, L
  ADD A, 64
  LD L, A
  JP NC, do_row
  ; Check loop counter - any blocks left to do?
  EX AF, AF'
  DEC A
  JR Z, next_pixel_down
  ; Carry occurred, so L is in correct place but H isn't
  EX AF, AF'    ; store loop counter
  LD A, H 
  ADD A, 8
  LD H, A
  JP do_row
  
  
next_pixel_down:
  
  ; Update our HL-adjuster
  LD A, I
  INC A
  OUT (254), A    ; stripes!
  ; Done if we've reached the 16th pixel
  AND 15
  JP Z, finish
  ; Next!
  LD I, A
  JP do_gfx_pixel
  
  
finish:

  ; Restore SP
smc_sp EQU $+1
  LD SP, 0    ; updated via smc
  
  ; IMPORTANT! if using IM2, you should restore I here!
  LD A, InterruptVector_I
  LD I, A
  
  
  ; --- Draw the platform --- ;
  
  LD A, 4
  OUT (254), A
  
  ; Store SP
  LD (smc_sp_platform), SP
  
  LD HL, gfx_platform
  LD IX, display_file + 2048 + ( 32 * 6 ) + 25
  
  LD B, 8   ; loop counter
  
platform_loop:

  ; Get the value to push
  LD A, (HL)
  INC HL
  
  ; Put it in the registers to PUSH
  LD D, A
  LD E, A
  
  ; Set up display_file position
  LD SP, IX
  
  ; - PUSH the values!
  
  ; Add right edge
  LD A, D
  AND 11111100b
  OR  00000011b
  LD D, A
  PUSH DE
  LD D, E
  
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  PUSH DE
  
  ; Add Left edge
  LD A, E
  AND 00111111b
  OR  11000000b
  LD E, A
  PUSH DE
  
  ; - Next row
  
  ; Next pixel down 
  INC IXH
  
  ; Loop
  DJNZ platform_loop


smc_sp_platform EQU $+1
  LD SP, 0

  ; - Add the two supports underneith

  LD HL, 20713
  LD BC, 1101001111001011b
  
  LD (HL), B
  INC H
  LD (HL), C
  INC H
  LD (HL), B
  INC H
  LD (HL), C
  INC H
  LD (HL), B
  INC H
  LD (HL), C
  INC H
  LD (HL), B
  INC H
  LD (HL), C
  
  LD L, 246
  LD (HL), C
  DEC H
  LD (HL), B
  DEC H
  LD (HL), C
  DEC H
  LD (HL), B
  DEC H
  LD (HL), C
  DEC H
  LD (HL), B
  DEC H
  LD (HL), C
  DEC H
  LD (HL), B
  
  
  
  ; --- Set up for next iteration of main loop --- ;
  
  ; Black border (finished drawing stuff!)
  XOR A
  OUT (254), A
  
  ; Update tile offsets
  
  ; IX
  LD A, (tile1_offset_px)
  DEC A   ; 1px - try changing this (e.g. ADD to change direction)
  AND 15
  LD (tile1_offset_px), A
  
  ; IY
  LD A, (tile2_offset_px)
  SUB 3   ; 3px - try changing this too!
  AND 15
  LD (tile2_offset_px), A
  
  
  
  ; Wait for interrupt, then disable again
  ; IMPORTANT! if using IM0, you should restore IY first!
  LD IY, $5C3A
  EI
  HALT    ; 50fps
  ; HALT    ; 25fps
  ; HALT    ; 16.66fps
  DI
  
  JP main
  
  
; data

tile1_offset_px DB 0 
tile2_offset_px DB 0
 
; graphics

; n.b. the tile graphics needs to be *duplicated vertically* to avoid having to loop back around when updating IX & IY
  
tile_water:
  ; x1
  DEFB	239,185,237,245,182,155,109,254
	DEFB	 84,211,108,166, 93,215,110,179
	DEFB	 84,245, 59,154,246,181,185, 22
	DEFB	163,189,153,151,179, 26,211,213
	DEFB	183,121,209,117, 55,233,243, 53
	DEFB	126,203,187,124,108, 79, 55,238
	DEFB	237,109,126,110,105, 89,110,253
	DEFB	 89,219,206,229,219,249,231,183
	; x2
  DEFB	239,185,237,245,182,155,109,254
	DEFB	 84,211,108,166, 93,215,110,179
	DEFB	 84,245, 59,154,246,181,185, 22
	DEFB	163,189,153,151,179, 26,211,213
	DEFB	183,121,209,117, 55,233,243, 53
	DEFB	126,203,187,124,108, 79, 55,238
	DEFB	237,109,126,110,105, 89,110,253
	DEFB	 89,219,206,229,219,249,231,183
  
tile_rock:
  ; x1
  DEFB	224,192,192, 65,249,128,224,193
	DEFB	207,  1,249,129,130,  1,159,129
	DEFB	130,  1, 12,  1,130,  3, 24,  3
	DEFB	134,  7,248, 15,223, 12, 28,127
	DEFB	243,152, 15,207,224,240, 12,  3
	DEFB	192,192,  8,  1,128, 64, 24,  1
	DEFB	128, 64,126,  7,128, 96,195, 31
	DEFB	192,127,129,243,192, 96,128, 97
	; x2
  DEFB	224,192,192, 65,249,128,224,193
	DEFB	207,  1,249,129,130,  1,159,129
	DEFB	130,  1, 12,  1,130,  3, 24,  3
	DEFB	134,  7,248, 15,223, 12, 28,127
	DEFB	243,152, 15,207,224,240, 12,  3
	DEFB	192,192,  8,  1,128, 64, 24,  1
	DEFB	128, 64,126,  7,128, 96,195, 31
	DEFB	192,127,129,243,192, 96,128, 97
  
gfx_platform:
  DB 255, 255, 0, 10101010b, 01010101b, 0, 255, 255
  
presh
Manic Miner
Posts: 237
Joined: Tue Feb 25, 2020 8:52 pm
Location: York, UK

Re: Parallax waterfall thing

Post by presh »

Since I'm using IX & IY to determine the location of the tile graphics, it becomes easy to add animated tiles with almost zero overhead (68 Ts per frame for the pointer calculation & update, I think?!)... so here's an attempt at a top-down, bubbling lava effect (only let down by my crap graphics!!)*



I did also play around with some further parallax within the "lava" tiles if you look closely... but due to the difference in frame rate between the scrolling and the tile animations, it looks a bit jarring. Probably would have looked neater without.

But if got me thinking... I'd seen tricks used on tile-based systems to create parallax effects by drawing them within the actual tile animations, so I thought I'd give that a go with a simpler chicken-wire design, giving me space "behind" the tile to draw a third layer peeking through... it could even give the impression of an independent horizontal scroll!

It's a bit more of a memory hog (the 16 animated tiles taking up exactly 1KB, given a reduction in width of these tiles from 32px to 16px) but still comes in at around 1400 bytes total despite this.



* I also noticed that YouTube doesn't always do these justice, even on my true-50Hz monitor. If it looks jerky, that's YouTube and/or your device. Rewinding it a bit sometimes fixes it. I'd hate for y'all to be watching it thinking, "nice idea, but it's jerky as" :| I can provide code later if anyone's interested in running it "live", I'd do it now but it's way past my bedtime once again...
EdToo
Manic Miner
Posts: 228
Joined: Thu Nov 03, 2022 4:23 pm

Re: Parallax waterfall thing

Post by EdToo »

That last one looks very impressive.
User avatar
MatGubbins
Dynamite Dan
Posts: 1239
Joined: Mon Nov 13, 2017 11:45 am
Location: Kent, UK

Re: Parallax waterfall thing

Post by MatGubbins »

Very nice indeed.
Those late night coding ideas that won't wait until the morning.
User avatar
Bedazzle
Manic Miner
Posts: 305
Joined: Sun Mar 24, 2019 9:03 am

Re: Parallax waterfall thing

Post by Bedazzle »

presh wrote: Sun Apr 02, 2023 2:53 am * I also noticed that YouTube doesn't always do these justice, even on my true-50Hz monitor. If it looks jerky, that's YouTube and/or your device.
Try to upscale video, and upload in 4K resolution.
Wise guys said, that YT does weird recompression for videos in small resolutions like from our beloved Speccy.
presh
Manic Miner
Posts: 237
Joined: Tue Feb 25, 2020 8:52 pm
Location: York, UK

Re: Parallax waterfall thing

Post by presh »

Bedazzle wrote: Sun Apr 02, 2023 2:39 pm Try to upscale video, and upload in 4K resolution.
Wise guys said, that YT does weird recompression for videos in small resolutions like from our beloved Speccy.
Yeah, I found that out the hard way!

Now I use this ffmpeg command to convert Spectaculator .avi output to 720p@50Hz .mkv with letterbox:

Code: Select all

ffmpeg -i input.avi -c:v libx264 -preset slow -crf 17 -c:a copy -vf "scale=(iw*sar)*min(1280/(iw*sar)\,720/ih):ih*min(1280/(iw*sar)\,720/ih), pad=1280:720:(1280-iw*min(1280/iw\,720/ih))/2:(720-ih*min(1280/iw\,720/ih))/2" output.mkv
User avatar
Joefish
Rick Dangerous
Posts: 2059
Joined: Tue Nov 14, 2017 10:26 am

Re: Parallax waterfall thing

Post by Joefish »

The last one looks good.

The problem with trying to do a waterfall is you need a fair bit more variety in it than just a few repeated tiles or columns. Added to which, water accelerates and spreads out as it falls - most of the good waterfalls in games rely on static animations of a few frames rather than scrolling. So the last one, which is clearly meant to be a regular pattern, looks better.

If you really want this to be a waterfall effect, you could try an expanding scroll, where rows are either duplicated, or spaced out more as you go down the screen.
presh
Manic Miner
Posts: 237
Joined: Tue Feb 25, 2020 8:52 pm
Location: York, UK

Re: Parallax waterfall thing

Post by presh »

Joefish wrote: Wed Apr 05, 2023 4:21 pm The last one looks good.

The problem with trying to do a waterfall is you need a fair bit more variety in it than just a few repeated tiles or columns. Added to which, water accelerates and spreads out as it falls - most of the good waterfalls in games rely on static animations of a few frames rather than scrolling. So the last one, which is clearly meant to be a regular pattern, looks better.

If you really want this to be a waterfall effect, you could try an expanding scroll, where rows are either duplicated, or spaced out more as you go down the screen.
That's a nice waterfall idea, though requires a completely different approach - the purpose of these exercises was to see how fast I could PUSH stuff to the screen (new territory for me!) :)

The idea is also loosely based on how tile-based consoles could create a similar parallax effect, using a single background layer and manipulating the tile graphics (rather than the tile map and/or scroll position)
Post Reply