12 ms·
ZX Spectrum Raytracer
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.
- fipar 3y agoVery nice! This 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.
- ggambetta 3y agoBatman is a good example of avoiding attribute clashing by going monochrome :) But I have the same memories, huge sprites and smooth animation, it was pretty great!
- AndrewStephens 3y agoI loved that isometric Batman game. I spent an enjoyable 3 weeks of after school hours beating it on the Amstrad. It was just so … weird.
- kinard 3y agoAmazing stuff, love coming back to speccy code.
- lowbloodsugar 3y agoNutter. Love it.
- foobarian 3y agoThe thing I loved about Spectrum was game loading graphics from tape. Spectrum had a very strange video memory layout, so the bitmap would load first into different thirds of the screen (but still all monochrome). And then at the end the attributes (which were much smaller due to the 8x8 size) would load almost instantly, "painting" the monochrome image in a final splash! So this is very nostalgic for sure. And the funny thing is, I like the spectrum renders better than the "perfect" ones from a modern computer :-)
- praptak 3y agoI also remember that there existed cool loaders that were able to paint the image in arbitrary order. It was pretty impressive because it required doing calculations in the tight loop which calculated the delay between slopes of the signal which came from tape.
- Steve44 3y agoBelow is a link to one I did. It loads in various patters, including writing, and from what I remember was spread out over the time it took to load the game so there was always something going on. It wasn't the best game around, hopefully the loader made up for that!! https://youtu.be/8e_IkqfMeD4?t=290 https://youtu.be/8e_IkqfMeD4?t=290 (Fighting Warrior by Melbourne House / Studio B)
- ggambetta 3y ago
- stevekemp 3y agoThere _is_ the possibility to define functions, via "DEF FN": 10 DEF FN s(x)=x*x: 20 PRINT FN s(3) Of course it is limited, as ZX Spectrum BASIC is limited, but it is an option.
- ggambetta 3y agoSure. I meant functions in the modern programming sense of the word, not in the mathematical sense.
- cglace 3y agoMy favorite class in college was building a raytracer from scratch and slowly adding features and optimizing. I always wanted to keep adding on to the raytracer I made but never found the time.
- Keyframe 3y agoHence Ray Tracing in One Weekend by raytracing royalty prof. Shirley himself https://raytracing.github.io/ https://raytracing.github.io/
- roskelld 3y agoIt would be fun to see a set of frames rendered out and stitched together to make an animated sequence. 17 hours per frame. It'll be like Pixar where you'd need a render farm of Speccy's to knock it out.
- MenhirMike 3y agoOne could "cheat" by running this in an emulator of a massively overclocked ZX Spectrum to cut down on the render time and still get an authentic end result.
- giantrobot 3y agoOr fan out the frames to a bunch of emulator instances.
- ClassyJacket 3y agoWhy not add some extra instructions to that emulator too to speed things up? And increase the word length of the CPU by 56 bits or so?!
- s_gourichon 3y agoYou mean, like this? https://www.youtube.com/watch?v=SJ79CdUMfVA https://www.youtube.com/watch?v=SJ79CdUMfVA 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 https://www.youtube.com/watch?v=o59LrpzGUaE
- qingcharles 3y agoI used to do this as a kid with Povray.. one frame every night, overnight...
- lproven 3y agoI did some extremely rudimentary animations on my Spectrum 128 in Beta BASIC this way. 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.
- Anotheroneagain 3y agoIDK, but it seems super wasteful: Why not compute RDX and RDX*RDX outside the inner loop? Why not precompute COX, COY, COZ and EQC once per scene?
- ggambetta 3y ago(COX, COY and COZ) vary per pixel and per sphere, hence so does EQC, so can't really compute them once per scene, unless I'm missing something? RDX yes, and I did that (and a lot more) in the later iterations where performance started to become an issue.
- Anotheroneagain 3y agoI don't see why they would vary? They must remain constant for each object.
- ggambetta 3y agoIf you're talking about the first iteration, you're right (ROX, ROY, ROZ) is hardcoded to (0, 0, 0), but I decided to implement a general algorithm anyway, because performance wasn't a problem at that point. If you can find optimizations that don't obliterate code readability for the fifth iteration (https://gabrielgambetta.com/zx-raytracer-5-src.html https://gabrielgambetta.com/zx-raytracer-5-src.html), I'm all ears :)
- Anotheroneagain 3y agoI mean still there are obvious calculations that stay the same. 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.
- ggambetta 3y agoThese are good ones, thanks!
- hoc 3y agoWaiting on the port for HP calculators in RPL... (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.
- jng 3y agoBeautiful, I love it, congratulations! I started my programming journey with Basic on a ZX Spectrum +, then Z80 assembly language, then 8086 and the rest from there... 40 years of programming and my appreciation for my original platform is still there. Thank you for the tribute.
- ggambetta 3y agoSimilar story here. The Spectrum holds a very special place in my heart :)
- qingcharles 3y agoHa! We're all in the same club :)
- stevekemp 3y agoWe are. I guess given the number of machines sold there are lots of us!
- rahimnathwani 3y agoThe penultimate screenshot looks at least as good as the output from VU-3D, a commercial raytracer for the ZX Spectrum. Pretty impressive! https://worldofspectrum.org/software?id=0008953 https://worldofspectrum.org/software?id=0008953
- qingcharles 3y agoI'd forgotten about VU-3D! Thanks for the memory :)
- Joeboy 3y ago> each 8x8-pixel block can show one or two different colors, but never three or more 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.
- Steve44 3y agoFrom what I remember there were two major challenges with tricks like that. 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.
- varjag 3y agoThough it is naturally out of realm of Spectrum BASIC the author had this exercise with.
- Joeboy 3y agoIndeed, but they mention the possibility of redoing it in assembly.
- shever73 3y agoThis is glorious, thank you for sharing. I also started coding on the ZX Spectrum and can still remember the Z80 assembler opcodes. You make a good point about both the simplicity and immediacy of the environment, which made it so easy to just start coding.
- mbitsnbites 3y ago> The ZX Spectrum has a 3.5 MHz Z80 processor (1,000 times slower than current computers) 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!
- HackerThemAll 3y ago> A modern CPU would be thousands times faster And we'll waste all these cycles on JavaScript.
- adamredwoods 3y agohttp://www.andreadrian.de/oldcpu/Z80_number_cruncher.html http://www.andreadrian.de/oldcpu/Z80_number_cruncher.html
- flohofwoe 3y agoIt's even much worse than that ;) 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.html 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).
- teaearlgraycold 3y agoDamn, we’re in CPI territory
- s_gourichon 3y agoExcellent! A similar program, apparently from 3 years ago, did similar things, in Basic also, on the Amstrad CPC, a machine with the same processor, a little more RAM and more (27) colors or shades of green, depending on the screen it was hooked to. * 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 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=cpcold&fiche=19283&slot=2&part=A&type=.png https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol... https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcold&fiche=19283&slot=2&part=A&type=.png&green=1 https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol... https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcold&fiche=19283&slot=3&part=A&type=.png https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol... https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcold&fiche=19283&slot=3&part=A&type=.png&green=1 https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol... https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcold&fiche=19283&slot=4&part=A&type=.png https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol... https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcold&fiche=19283&slot=5&part=A&type=.png&green=1 https://www.cpc-power.com/extra_lire_fichier.php?extra=cpcol...
- ggambetta 3y agoThat's pretty impressive. The main difficulty with reflections in the ZX Spectrum is not so much the limited color palette, which can be dealt with, but the 2-color limit per 8x8 block. It's already a big issue with just lights and shadows as it is :-/
- s_gourichon 3y ago> the 2-color limit per 8x8 block. It's already a big issue with just lights and shadows as it is :-/ 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 https://cpcitor.itch.io/just-get-9 Hey, found this https://bbcmic.ro/?t=9ctpk https://bbcmic.ro/?t=9ctpk via "BBC BASIC raytracer in 432 characters (mastodon.me.uk)" https://news.ycombinator.com/item?id=39023056#39023768 https://news.ycombinator.com/item?id=39023056#39023768
- Angostura 3y agoI have absolutely no recollection of being able to put multiple statements on a line, separated by : - is that just my old brain forgetting stuff, or is there something else going on here?
- becurious 3y agoIt’s your brain. You could always do that in ZX Basic
- donio 3y agoStarting with the Spectrum to be precise. The ZX-81 version didn't have that yet.
- anonzzzies 3y agoMost (all?) basics could do that at the time: I used it a lot to get more on one screen (moving through numbered basic code was even more annoying than moving through code now). Also it saved a few bytes here and there which was quite crucial as there was not a lot of mem.
- ggambetta 3y agoTook me a while to remember this too (it had been 30 years)! I started using IF just GO TOing over a couple of lines whenever I had a more complex IF "block", but I had this nagging feeling that there was a better way :)
- lproven 3y agoAlmost all BASICs did that. It's just you.
- el_pepo 3y agoHave you tried to compile your program to machine code using Boriel Basic Compiler? It speeds up things a lot!
- mxroeoek 3y ago[dead]
- abbeyj 3y ago> Line 3000 starts with REM, short for “remark”. We call them “comments” these days, but the ZX Spectrum is British, the brainchild of mad genius Sir Clive Sinclair. 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 https://en.wikipedia.org/wiki/Dartmouth_BASIC#First_Edition
- KayEss 3y agoREM was quite common across a number of languages at the time. You might still see them in '.bat' files
- lproven 3y agoLittle fluffy REMs...
- lproven 3y agoExactly this. This is only one of several mistakes in the piece about Sinclair BASIC. It was the language of the first computer I owned, so I'm still a bit defensive about it. * 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 https://mastodon.me.uk/@coprolite9000/111762131524979212
- deanthecoder 3y agoCool project and great write up! I’ve literally gone through a very similar process just a couple of days ago! I used a threshold screen with random element added, which worked really well (and isn’t toooo slow). The Softek FP Compiler might help with performance, although failed to compile for me as it doesn’t support DEF FN. Mine: https://github.com/deanthecoder/ZXSpeculator/tree/main#experiments https://github.com/deanthecoder/ZXSpeculator/tree/main#exper...
- Bvlynicole 3y ago[dead]