ZX Spectrum Raytracer (gabrielgambetta.com)
Inspired by a recent article in HN about raytracing on some ancient architecture, I realized that I've implemented many a raytracer in my life, but never one for my first love, the ZX Spectrum. So I fixed the glitch.
Trigger warning: the source code includes GO TO statements. Because that's how we did it in the 80s! Enter at your own peril.
82 comments
[ 4.8 ms ] story [ 169 ms ] threadThis was almost my first computer (almost because it was actually a TK-90) so it's nice to see it here, I guess it was a common starting point if you were in a family with a computer in the 80s in UY :)
While I know it's not the same (plus I'm a backend guy so I know nothing about graphics, a fact that may change in the near future as my daughter is set to study animation & videogames after she's done with high school next year) but your last version made me think of Batman for the Spectrum. I remember thinking that game's graphics were magical compared with most of the other games for that computer at the time.
And the funny thing is, I like the spectrum renders better than the "perfect" ones from a modern computer :-)
Now that's impressive.
It wasn't the best game around, hopefully the loader made up for that!!
https://youtu.be/8e_IkqfMeD4?t=290
(Fighting Warrior by Melbourne House / Studio B)
It's very similar to Polaroid's colouring. I always thought it looked great.
In my defense I was 11/12 when I got mine.
Also, not exactly the same type or rendering, but basically word for word what you wrote: the full-screen fully animated parts appear to be "a set of frames rendered out and stitched together to make an animated sequence"
https://www.youtube.com/watch?v=o59LrpzGUaE
I generated a couple of dozen frames of a Julia set distorting, in monochrome, saved them to the RAMdisk... which took a couple of hours or something... and then loaded them back in sequence from the RAMdisk, which gave me a few frames-per-second "video".
It worked and it fit into 20kB or less of code, and about 90kB of RAMdisk.
Here is a page with some info and a video of the animation: http://www.randelshofer.ch/animations/anims/eric_graham/Jugg...
RDX yes, and I did that (and a lot more) in the later iterations where performance started to become an issue.
If you can find optimizations that don't obliterate code readability for the fifth iteration (https://gabrielgambetta.com/zx-raytracer-5-src.html), I'm all ears :)
DZ is always one, you can just skip it.
1212 is the same for all pixels, all are constants, you could do it once per sphere.
2100 A stays the same.
(did a 2d morphing animator and a simple cad tool back then. also straight forward base algs that you can keep adding to, adding some more, and some more...)
Great write-up. Thanks. Love the Spectrum and its direct token input despite never owning one.
https://worldofspectrum.org/software?id=0008953
That's not entirely true. If you modify the colour attributes between the drawing of each row, you can get eight separate 2-colour palettes in an 8x8 block. I believe I saw somewhere that people had managed this horizontally as well somehow.
The frame timing was based on one single interrupt at the top of each frame and you had to count down using CPU cycles to trigger the change. This meant no variable path code.
Secondly it needed doing every frame so a lot of those precious CPU cycles were taken up with the fancy display so less available for the game logic.
You make a good point about both the simplicity and immediacy of the environment, which made it so easy to just start coding.
Actually, it's much, much slower than that.
The Z80 takes at least one clock cycle to add or subtract an 8-bit number (sometimes several clock cycles, depending on addressing mode). For a 32-bit number those clock cycles add up quickly.
In contrast, a modern 3.5GHz CPU can add or subtract several 64-bit numbers every clock cycle
And don't get me started on multiplication or floating-point arithmetic. A modern CPU would be thousands times faster, even if it was running at the same clock speed.
Thus, I think that the feat is even more impressive than presented!
The fastest Z80 instructions are 4 clock cycles, at least 7 clock cycles if they also need to load or store an 8-bit value from/to memory, and at least 10 clock cycles for instructions that load or store a 16-bit value.
I did a little deep dive into Z80 timings a little while ago using the Z80 netlist simulation from visual6502.org:
https://floooh.github.io/2021/12/06/z80-instruction-timing.h...
But for BASIC programs those timings don't matter much, since BASIC on 8-bit home computers was easily 100x times slower than assembly code doing the same thing (the difference is probably less though when floating point math is involved, since this also needs to be 'software-emulated' in assembly).
...except that the BBC BASIC was giving me Heisenbugs, so I had to give up on it. So I'm on Forth now, and immediately found that Forth is a more productive environment than an early BASIC anyway. Linenums add a killer amount of bookkeeping.
BASIC on ZX Spectrum was meant for learning, but no serious software of the era relied on it for anything that needed to run fast.
I suspect this code could be 10x faster if written in assembly or a compiled language (a handful existed for the platform).
https://spectrumcomputing.co.uk/entry/8249/ZX-Spectrum/HiSof...
The manual:
http://hayne.net/Spectrum/HiSoftBASIC/manual.html
And we'll waste all these cycles on JavaScript.
* colored spheres (like you did)
* (one) checkerboard surface
* self-shadowing (like you did)
* cast shadows (like you did)
* partial or total specular reflection!
See https://www.cpc-power.com/index.php?page=detail&num=19283
The webpage displays the color (left) and green (right) versions on hovering your pointer over the ribbon (above). Image 1 and 2 are okay in both cases, image 3 is clearly tuned for color screen, image 4 for green screen.
Or direct links to the relevant variants:
https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol...
https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol...
https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol...
https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol...
https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol...
https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol...
Well, you impressed me (and others) with your simple bet that "hey, what if I simply choose the two most popular colors" and just code that in simple BASIC. The same could be done with the shading. It would probably pick a good compromise when bright yellow meets dark red: by choosing to have the few should-be-red pixels become yellow, you avoid a bigger obviously-square-grid-based clash of colors.
Next step: error diffusion dithering would IMHO be a good bet. But maybe that becomes really to much for Basic.
I've been considering doing Raytracing on the Amstrad CPC, but in C. Perhaps using integers only, for speed.
Done that instead: https://cpcitor.itch.io/just-get-9
Hey, found this https://bbcmic.ro/?t=9ctpk via "BBC BASIC raytracer in 432 characters (mastodon.me.uk)" https://news.ycombinator.com/item?id=39023056#39023768
I'm not sure, but is this statement implying that `REM` is a British invention? The original Darthmouth BASIC had a REM statement. https://en.wikipedia.org/wiki/Dartmouth_BASIC#First_Edition
* REM for Remark is 100% standard usage.
This is true in all BASIC dialects I know of and in DOS batch files and so on.
* Line numbers are not for GOTO and GOSUB.
Line numbers are an elegant abstraction for a teaching language, and for machines with no directly-addressible non-volatile storage.
Type in some valid BASIC, and the computer does it now. Prefix it with a line number, and it _doesn't_ do it now, it remembers it for later. Sequence is controlled by the numbers, so flow of control is explicit, not implicit.
This is much simpler than learning multiple Unix-like abstractions such as "this is a 'file', files have 'file names', to edit the code you must 'load' a 'file' into another separate program called an 'editor', then you 'save' the file to 'disk' and pass its 'file name' to the 'interpreter'."
All that stuff is 1970s minicomputer legacy nonsense, that we have fossilised into our computing culture. Smalltalk banished all this legacy baggage in 1980 but Unix was so dominant that we learned nothing from it.
Kids should not have to know about ludicrous 1970s abstractions such as "files", "programs", "editors", "interpreters", "compilers" and all that DEC PDP-7 nonsense. The computer should come to us, not us to its concepts.
* No ENDIF? No, almost no early-1980s BASIC had ENDIF. BBC BASIC on the BBC Micro is the _only_ one I can think of, and it took 2x the ROM space of Sinclair BASIC -- and that was on top of a separate 16kB ROM chip for the BBC Micro OS, or MOS. Full 50% of the BBC Micro's memory space was consumed by 32kB of ROM. The Spectrum had 50% less ROM and so you got 50% more usable memory.
* The increment of 10 is nothing to do with the Sinclair editor. All BASICs did that.
* "there are no function calls" -- yes there are. DEF FN() worked, as in more or less all 8-bit BASICs.
* "It’s also interpreted, so super slow." -- again, they all were.
These are cavils. It's a great tech demo and I enjoyed reading it!
I guess you were inspired by the recent Acorn Electron demo, as used by the BBC Micro Bot on Mastodon?
https://mastodon.me.uk/@coprolite9000/111762131524979212
However, using line numbers for editors wasn't AFAIK unusual at the time and I think making the semantics depend on them was unfortunate. However 20/20 hindsight etc.