; 3D particle system in a 512-byte boot sector ("fountain of sparks" demo) ; Written for #asm compo #8 by Rusty Wagner ; To run: ; 1. Write fountain.bin to the boot sector of a floppy disk. This can be done with rawrite.exe or debug.com with ; fountain.bin as input. (For debug.com, use the command "w 100 0 0 1" to write to A:) ; 2. Boot your system with the floppy in the drive (be sure to set your BIOS to boot A: before the hard disks). ; Note: If you have a USB keyboard, legacy keyboard support needs to be enabled. Without this, DOS and boot sector ; code won't be able to read from your keyboard. ; Note: This code will not run on Bochs (it has a bug in the pusha instruction). ; Assembled using NASM bits 16 org 0x7c00 ; boot sector is loaded at 0x0000:0x7c000 particlecount EQU 256 ; currently must be multiple of 256 particle_display EQU 0 particle_pos_x EQU 2 particle_pos_y EQU 4 particle_pos_z EQU 6 particle_vel_x EQU 8 particle_vel_y EQU 10 particle_vel_z EQU 12 particle_bright EQU 14 particle_structsize EQU 16 ; Select address of particles such that the end of the array is 0xa000 (segment for video memory) particles EQU 0xa000 - particle_structsize * particlecount ; particles = 0x9000 for 256 particles fademask EQU 0x4001 ; mask for optimizing fade loop, no init required diffuse_val EQU particles - 2 ; storage for error diffusion dithering (no initialization required) backbuffer_seg EQU particles ; backbuffer_seg = particles allows for some extra optimizations start: ; Set up a stack in a known location (highest part of segment 0x0000) push cs pop ss ; ss = 0 xor sp, sp cld ; direction = forward push cs pop ds ; ds = 0 init: ; Switch to 320x200x8 mode mov ax, 0x13 int 0x10 ; Clear interrupts, everything onward will be done using direct hardware access. ; Allows us to not have to worry about what conventional memory we have available ; (some BIOSes and extensions like to place things at the high end of memory ; in segment 0x9000, which is needed for the back buffer). cli mov dx, 0x3c8 xor ax, ax out dx, al ; palette write index = 0 inc dx ; Use 0x3c9 as the initial random seed. The random algorithm used just needs any ; nonzero value to work. mov fs, dx ; Palette is set up to produce 16 shades of orange and 16 shades of gray, with all ; combinations of the two. For color indicies, the high nibble represents the ; brightness of the orange component, and the low nibble represents the brightness ; of the gray component. The gray "floor" will be static in the low nibble of the ; back buffer, with the orange high nibble representing the animated particles. mov cx, 16 xor bx, bx genpalmainloop: pusha mov cl, 16 genpaloutloop: mov al, bh call outpalentry mov al, bh sub al, ah call outpalentry mov al, bl out dx, al inc bx inc bx loop genpaloutloop popa add bh, 4 inc ah loop genpalmainloop push cs pop es ; es = 0 ; Generate entries for all of the particles. All particles are initially invisible, ; but will be regenerated at different intervals to create a continous effect. ; cl = 0 mov ch, particlecount / 256 mov di, particles push di setupparticlesloop: mov bh, cl ; brightness = time to live = loop counter mov al, 0 ; not visible call genparticle loop setupparticlesloop ; di = 0xa000 (from end of setup loop) ; Clear the upper part of the back buffer to black. The rest will be overwritten ; by the floor generation code. pop es ; es = backbuffer_seg xor ax, ax ; ax = 0 xchg ax, di ; di = 0, al = 0 (since di was 0xa000) mov ch, (320 * 136) / 256 rep stosb ; Floor generator. The floor is generated using 3D lighting calculations with ; one static point light (at the center of the fountain) to produce a floor that ; appears to be lit by the fountain particles. mov bx, 36 ; bx = screenpos.y genfloorloop: mov ax, -160 ; ax = screenpos.x mov cx, 320 genfloorrowloop: pusha ; y = z / screenpos.y xchg ax, cx mov dx, 0x10 xor ax, ax idiv bx mov bx, ax ; x = y * screenpos.x cwd imul cx ; The (x, y, z) position of the pixel on the floor we are generating has been found, ; calculate the light amount imul dx, dx sar bx, 8 sub bx, byte 0x4c imul bx, bx add bx, dx ; bx = x^2 + y^2 call rand add bx, ax ; add in a random factor to give it a little bit of texture mov ah, 0x5f ; ax ~= 0x5f00 cwd idiv bx ; light amount = 0x5f00 / (x^2 + y^2 + rand) sub ax, 12 ; subtract out some light to make it pure black a bit away from the light source jc neglight ; write out black if the light value goes negative mov bx, ax shr ax, 4 ; divide light amount by 16 to get a good light radius ; Error diffusion dithering to make things look smooth shl bx, 12 add [diffuse_val], bx adc ax, 0 cmp al, 15 ; clamp light amount to 15 (only have 16 shades of gray in the palette) jbe notcenter mov al, 15 stosb neglight: xor al, al notcenter: stosb popa inc di inc ax loop genfloorrowloop inc bx cmp bl, 100 jb genfloorloop ; Start of main render loop render: ; es = backbuffer_seg ; cx = 0 push es ; cl = 0 mov ch, particlecount / 256 pop si ; si = particles push si renderloop: push cx mov di, si lodsw ; ax = particle.display test al, al lodsw xchg ax, bx ; bx = particle.pos.x lodsw xchg ax, dx ; dx = particle.pos.y lodsw xchg ax, cx ; cx = particle.pos.z jz nodisplay pusha ; Render particle mov bp, [si+particle_bright-particle_vel_x] call displayparticle ; Render particle reflection shr bp, 1 mov di, bp shr di, 1 sub bp, di neg dx add dh, 0x20 call displayparticle popa nodisplay: lodsw ; ax = particle.velocity.x add [si+particle_pos_x-particle_vel_y], ax ; particle.pos.x += particle.velocity.x lodsw ; ax = particle.velocity.y add dx, ax ; particle.pos.y += particle.velocity.y cmp dh, 0x10 ; if particle.pos.y >= 0x1000, the particle has hit the floor jl nobounce ; Bounce the particle off the floor (divide speed by 4, negate velocity.y) mov dx, 0x1000 mov cl, 2 sar ax, cl sar word [si+particle_vel_x-particle_vel_z], cl sar word [si+particle_vel_z-particle_vel_z], cl neg ax nobounce: inc ax ; particle.velocity.y += 2 (gravity) inc ax mov [si+particle_pos_y-particle_vel_z], dx mov [si-2], ax lodsw ; ax = particle.velocity.z add [si+particle_pos_z-particle_bright], ax ; particle.pos.z += particle.velocity.z sub word [si], 0xa0 ; particle.brightness -= 0xa0 lodsw jnc noregen ; Renegerate particle if brightness goes negative push es push cs pop es mov al, 1 mov bh, 0xff call genparticle pop es noregen: pop cx loop renderloop ; Wait for VSYNC, locks framerate to 60 FPS and prevents artifacts mov dx, 0x3da vsyncwait1: in al, dx test al, 8 jnz vsyncwait1 vsyncwait2: in al, dx test al, 8 jz vsyncwait2 ; Copy back buffer to video memory ; cx = 0 ; si = 0xa000 push es pop ds ; ds = backbuffer_seg mov es, si ; es = 0xa000 xor di, di xor si, si mov ch, 0x40 mov bp, cx ; bp = fademask - 1 rep movsd ; si = 0 ; cl = 0 ; Fade out the existing orange component of the back buffer instead of clearing to create a motion blur effect mov ch, 0x10 dec si ; si = -1 mainfadeloop: inc bp inc bp cmp [bp+si], si jnz fadenoskip lea di, [di+16] jmp short fadeloopend fadenoskip: mov dx, 1 fadeloop: mov al, [di] sub al, 0x10 jnc fadenotblack or [bp+si], dx add al, 0x10 fadenotblack: mov [di], al inc di add dx, dx jnz fadeloop fadeloopend: loop mainfadeloop push cs pop ds pop es ; es = backbuffer_seg ; Check for keypress in al, 0x64 shr al, 1 jnc render exit: ; Key pressed, exit by rebooting (won't work on Bochs, but will on anything else...) ; si = -1 ; cx = 0 push si push cx retf ; jmp 0xffff:0x0000 ; Input: ; al = base color ; bl = added color ; Output: ; al = final color outpalentry: add al, bl cmp al, 63 jb entryok mov al, 63 entryok: out dx, al ret ; Input: ; es:di = particle to generate ; al = display particle? ; bx = time to display ; Output: ; ax = ???? ; bx = ???? ; es:di = next particle genparticle: stosw ; write particle.display xor ax, ax stosw ; particle.pos.x = 0 mov ah, 0x10 stosw ; particle.pos.y = 0x1000 mov ah, 0x4c ; particle.pos.z = 0x4c00 call gendir ; particle.velocity.x = random number between -32 and 31 mov ax, -0xc4 ; particle.velocity.y = -0xc4 call gendir ; particle.velocity.z = random number between -32 and 31 xchg ax, bx stosw ; write particle.brightness (also gives time to live) ret ; Writes ax to es:di, random number between -32 and 31 to the next word at es:di+2 gendir: stosw call rand sub ax, 0x20 stosw ret ; Output ; ax = Random number in range 0-63 rand: ; Get next random number mov ax, fs imul ax, byte 13 xchg ah, al mov fs, ax and ax, 0x3f ret ; bx = x ; dx = y ; cx = z ; bp = brightness displayparticle: pusha xchg ax, dx mov di, 100 call project ; screenpos.y = (y / z) + 100 xchg ax, bx mov di, 160 call project ; screenpos.x = (x / z) + 160 imul bx, di ; bx = screenpos.y * 320 add bx, ax ; bx = (screenpos.y * 320) + screenpos.x (offset of pixel in back buffer) ; Draw the 4x3 pixel particle using the intensity array (holds count to shift the brightness to the right) mov si, intensity - 1 mov ax, 4 xchg ax, bp disploop: mov ch, 3 disprowloop: push ax push bx mov cl, [si] shr ah, cl and ah, 0xf0 ; AND color so that it only affects the orange component of the color mov al, [es:bx] ; al = color currently in back buffer cmp al, ah ; if al > color to write, don't add any more brightness (makes motion blur look better) jae notmax add al, ah ; Add color to the one currently on the screen jnc notmax ; Check for overflow or al, 0xf0 ; Color overflowed, so max out the orange component notmax: mov [es:bx], al ; Write to back buffer shr bx, 3 mov byte [bx+fademask], 0 ; Update fade mask so that the pixel will fade to black gradually pop bx pop ax inc si inc bx dec ch jnz disprowloop lea bx, [bx+di-3] dec bp jnz disploop popa ret intensity: db 1, 3 ; First entry (3) comes from the ret instruction (0xc3), as a shift count of 0xc3 is interpreted as 3 db 1, 0, 1 db 1, 1, 1 db 3, 2, 3 ; Input: ; ax = coord ; cx = z coord ; di = max screen offset / 2 ; Output: ; ax = screen offset ; dx = ???? ; di = max screen offset project: cwd xchg al, dl xchg ah, dl ; dx:ax = coord * 256 (gives some extra precision to the projection) idiv cx ; ax = offset from screen center = (coord * 256) / z add ax, di ; ax = screen pos (0 = top left) add di, di ; di = max screen offset ; Check to see if pixel within the bounds of the screen cmp ax, di jb dispnotoutofrange dispoutofrange: ; Pixel out of range, need to return from displayparticle (the only function project is called from) pop ax ; scrap return value popa dispnotoutofrange: ret ; Required data for a valid boot sector times 510-($-$$) db 0 dw 0xaa55