ZX Spectrum Raytracer – Gabriel Gambetta
I like raytracers; in truth I’ve written half a book about them. Most likely much less recognized is my love for the ZX Spectrum, the 1982 house laptop I grew up with, and which began my curiosity in graphics and programming. This machine is so ridiculously underpowered for in the present day’s requirements (and even for Eighties requirements), the inevitable query is, to what extent may I port the Pc Graphics from Scratch raytracer to a ZX Spectrum?
The ZX Spectrum has a 3.5 MHz Z80 processor (1,000 instances slower than present computer systems) that may’t multiply numbers (!!!), 48 KB of RAM (1,000,000 instances smaller), and a 256×176 graphics mode (~200 instances decrease decision) able to displaying 15 colours (1,000,000 instances fewer – and with some uncommon quirks). That is an fascinating setup for a CPU-intensive graphics utility!
My plan was to implement this in Sinclair BASIC, the built-in programming language of the Spectrum. This isn’t simply BASIC, however an historic, very restricted dialect of BASIC. For instance, the one management constructions are FOR
and IF
(and IF
has no ENDIF
); all variables are international; there aren’t any perform calls, solely GO TO
and GO SUB
; and so on. It’s additionally interpreted, so tremendous gradual. However no less than it implements multiplications in software program! I may at all times rewrite the raytracer in assembler if I cared about efficiency.
I arrange a minimal atmosphere: I write BASIC code utilizing VS Code, compile it utilizing BAS2TAP, and run it on the FUSE emulator. This gave me a fairly respectable iteration velocity.
As an apart, I hadn’t written BASIC in one thing like 30 years, and I used to be stunned at how shortly all of it got here again. I used to be between 4 and 10 after I was doing this, so I assume it sticks within the mind like something you study at that age, like languages and accents. Now let’s get coding prefer it’s 1984!
My first iteration was fairly simple: I ported the starter CGFS raytracing code to BASIC with out a lot tweaking, outputting a 32×22-block picture, and to my shock, it labored effectively:
The quantity within the higher left nook, 879.76, is the time it took to render this picture, in seconds. Sure, that’s virtually quarter-hour. Right here’s the identical scene rendered by the CGFS raytracer in a few second, utilizing the identical scene and have set:
The Spectrum model doesn’t look unhealthy, contemplating the constraints! Let’s check out the code:
1 BRIGHT 1: CLS 5 LET ROX = 0 6 LET ROY = 0 7 LET ROZ = 0 8 LET TMIN = 0 9 LET TMAX = 10000 10 FOR X = 0 TO 31 20 FOR Y = 0 TO 21 30 LET RDX = (X - 16) / 32 31 LET RDY = (11 - Y) / 32 32 LET RDZ = 1 40 GO SUB 1000 50 PAPER COL 51 PRINT AT Y, X; " " 100 NEXT Y 105 GO SUB 3000: PRINT AT 0, 0; TIME 110 NEXT X 120 STOP 1000 REM ===== TraceRay ===== 1001 REM Params: (ROX, ROY, ROZ): ray origin; (RDX, RDY, RDZ): ray path; (TMIN, TMAX): wished ranges of t 1002 REM Returns: COL: pixel shade 1010 LET COL = -1: LET MINT = 0 1100 RESTORE 9000 1101 READ NS 1102 FOR S = 1 TO NS 1110 READ SCX, SCY, SCZ, SRAD, SCOL 1200 LET COX = ROX - SCX 1201 LET COY = ROY - SCY 1202 LET COZ = ROZ - SCZ 1210 LET EQA = RDX*RDX + RDY*RDY + RDZ*RDZ 1211 LET EQB = 2*(RDX*COX + RDY*COY + RDZ*COZ) 1212 LET EQC = (COX*COX + COY*COY + COZ*COZ) - SRAD*SRAD 1220 LET DISC = EQB*EQB - 4*EQA*EQC 1230 IF DISC < 0 THEN GO TO 1500 1240 LET T1 = (-EQB + SQR(DISC)) / 2*EQA 1241 LET T2 = (-EQB - SQR(DISC)) / 2*EQA 1250 IF T1 >= TMIN AND T1 <= TMAX AND (T1 < MINT OR COL = -1) THEN LET COL = SCOL: LET MINT = T1 1300 IF T2 >= TMIN AND T2 <= TMAX AND (T2 < MINT OR COL = -1) THEN LET COL = SCOL: LET MINT = T2 1500 NEXT S 1999 IF COL = -1 THEN LET COL = 0 2000 RETURN 3000 REM ===== Get timestamp in seconds ===== 3001 LET TIME = (65536*PEEK 23674 + 256*PEEK 23673 + PEEK 23672) / 50 3002 RETURN 8998 REM ===== Sphere knowledge ===== 8999 REM Sphere depend, adopted by (SCX, SCY, SCZ, SRAD, COLOR) 9000 DATA 4 9001 DATA 0, -1, 4, 1, 2 9002 DATA 2, 0, 4, 1, 1 9003 DATA -2, 0, 4, 1, 4 9004 DATA 0, -5001, 0, 5000, 6
The construction of the code ought to look acquainted in the event you’re accustomed to raytracers generally, and with the CGFS raytracer specifically, regardless of being written in an historic dialect of BASIC. I’ll nonetheless stroll by the code to level out the quirks of the Spectrum.
First, line numbers. Each line needed to have a quantity, so you could possibly use GO TO
or GO SUB
. Strains supported a number of statements separated by a colon – particularly helpful for the IF ... THEN
assertion, contemplating there’s no END IF
!
You’ll discover the road numbers are everywhere. The Spectrum BASIC editor was line-oriented, so whereas it was potential to alter line numbers, it was very time-consuming. So that you’d quantity your strains in multiples of 10, so that you had “house” so as to add strains in between if wanted.
We begin with this:
1 BRIGHT 1: CLS
The Spectrum has a fairly quirky graphics mode. I’ll get into the small print within the subsequent part. For now, let’s simply say that BRIGHT 1
chooses the brilliant model of the colour palette, and CLS
clears the display. So we’re prepared to begin drawing one thing.
Then comes the primary loop of the rayrtacer:
5 LET ROX = 0 6 LET ROY = 0 7 LET ROZ = 0 8 LET TMIN = 0 9 LET TMAX = 10000 10 FOR X = 0 TO 31 20 FOR Y = 0 TO 21 30 LET RDX = (X - 16) / 32 31 LET RDY = (11 - Y) / 32 32 LET RDZ = 1 40 GO SUB 1000 50 PAPER COL 51 PRINT AT Y, X; " " 100 NEXT Y 105 GO SUB 3000: PRINT AT 0, 0; TIME 110 NEXT X 120 STOP
Strains 5 to 9 set a few of the parameters which can be fixed all through the primary loop. BASIC had arrays however they have been fairly inconvenient to make use of, so utilizing them to signify factors and vectors was a non-starter. So the ray origin RO
is represented by the three variables ROX
, ROY
and ROZ
.
Strains 10 to 110 type the primary loop, iterating over the canvas (32×22 squares). After every cross of the inside loop, rendering a column of squares, line 105 does the equal of a perform name: GO SUB 3000
transfers management stream to the subroutine at line 3000:
3000 REM ===== Get timestamp in seconds ===== 3001 LET TIME = (65536*PEEK 23674 + 256*PEEK 23673 + PEEK 23672) / 50 3002 RETURN
Line 3000 begins with REM
, brief for “comment”. We name them “feedback” today, however the ZX Spectrum is British, the brainchild of mad genius Sir Clive Sinclair. So this line is only a remark.
The magical incantation in line 3001 reads the present timestamp in seconds. How? PEEK
takes a reminiscence deal with and returns its contents. All this line does is learn a 24-bit quantity saved in reminiscence, representing the inner FRAME
counter; this counter is incremented each 20ms, so we divide it by 50 to transform it to seconds, and retailer it within the variable TIME
.
Each variable in this system is international, so RETURN
in line 3002 simply returns stream management to the caller, and the “return worth” of the perform is implicitly the TIME
international variable. This GO SUB
/ RETURN
mechanism is similar to CALL
/ RET
in meeting.
Lastly, line 120 terminates this system.
Now let’s check out the inside loop. Strains 30 to 32 convert canvas coordinates to viewport coordinates (CanvasToViewport
in CGFS). The ray path is represented by (RDX, RDY, RDZ)
.
Line 40 does one other “perform name”, this time to the equal of TraceRay
. When it returns, the variable COL
will comprise the colour of regardless of the ray hit.
Strains 50 and 51 lastly draw the block. That is completed by setting the PAPER
(background) shade and drawing an area (extra on this later).
Now let’s check out TraceRay
beginning at line 1000. It begins with a remark block documenting the implicit inputs and outputs:
1000 REM ===== TraceRay ===== 1001 REM Params: (ROX, ROY, ROZ): ray origin; (RDX, RDY, RDZ): ray path; (TMIN, TMAX): wished ranges of t 1002 REM Returns: COL: pixel shade
As a result of there aren’t any perform arguments or return values, every part is international, implicit, and by conference. On this case, the inputs are (ROX, ROY, ROZ)
, (RDX, RDY, RDZ)
, TMIN
and TMAX
, and the return worth is within the variable COL
. This represents an index into the mounted shade palette of the ZX Spectrum.
Line 1010 initializes the values we have to preserve monitor of the closest intersection discovered to date, and the colour of the sphere on the intersection:
1010 LET COL = -1: LET MINT = 0
Then we begin the “for every sphere” loop:
1100 RESTORE 9000 1101 READ NS 1102 FOR S = 1 TO NS 1110 READ SCX, SCY, SCZ, SRAD, SCOL
Line 1100 resets a “knowledge pointer” to line 9000, which comprises the scene knowledge:
8998 REM ===== Sphere knowledge ===== 8999 REM Sphere depend, adopted by (SCX, SCY, SCZ, SRAD, COLOR) 9000 DATA 4 9001 DATA 0, -1, 4, 1, 2 9002 DATA 2, 0, 4, 1, 1 9003 DATA -2, 0, 4, 1, 4 9004 DATA 0, -5001, 0, 5000, 6
The READ
assertion in line 1101 reads the primary worth (the quantity 4 in line 9000) into the variable NS. Then line 1102 begins the “for every sphere” loop, and the very first thing we do in line 1110 is learn the 5 values defining a sphere into variables. After that first batch of READ
statemends the information pointer is now on the first worth of line 9002, able to be learn in the course of the subsequent iteration of the loop.
Strains 1200 to 1300 resolve an easy ray-sphere intersection equation, with strains 1250 and 1300 maintaining monitor of the closest intersection:
1200 LET COX = ROX - SCX 1201 LET COY = ROY - SCY 1202 LET COZ = ROZ - SCZ 1210 LET EQA = RDX*RDX + RDY*RDY + RDZ*RDZ 1211 LET EQB = 2*(RDX*COX + RDY*COY + RDZ*COZ) 1212 LET EQC = (COX*COX + COY*COY + COZ*COZ) - SRAD*SRAD 1220 LET DISC = EQB*EQB - 4*EQA*EQC 1230 IF DISC < 0 THEN GO TO 1500 1240 LET T1 = (-EQB + SQR(DISC)) / 2*EQA 1241 LET T2 = (-EQB - SQR(DISC)) / 2*EQA 1250 IF T1 >= TMIN AND T1 <= TMAX AND (T1 < MINT OR COL = -1) THEN LET COL = SCOL: LET MINT = T1 1300 IF T2 >= TMIN AND T2 <= TMAX AND (T2 < MINT OR COL = -1) THEN LET COL = SCOL: LET MINT = T2
We end the loop checking if there have been no intersections, through which case we set the colour to 0 (black), and return:
1999 IF COL = -1 THEN LET COL = 0 2000 RETURN
And that’s all there may be to it. We get our tremendous gradual, tremendous low-res output:
I nonetheless discover it fairly spectacular that this solely takes 50 strains of comparatively simple code in an underpowered early 80s machine!
However that is only a begin. Why follow 32×22 when the usable pixel dimensions of the display are 256×176?
You would possibly assume that growing the decision of this raytracer is so simple as altering the outer loop to 256×176 as an alternative of 32×22 and drawing particular person pixels utilizing PLOT
as an alternative of chunky squares utilizing PRINT
. This could be 64 instances slower (16 hours as an alternative of quarter-hour) however it could work – besides within the quirky graphics mode of the ZX Spectrum!
The primary model of the ZX Spectrum had a grand whole of 16 KB of RAM, so reminiscence effectivity was completely crucial (I had the significantly extra luxurious 48 KB mannequin). To assist save reminiscence, video RAM was break up in two blocks: a bitmap block, utilizing one bit per pixel, and an attributes block, utilizing one byte per 8×8 block of pixels. The attributes block would assign two colours to that block, known as INK
(foreground) and PAPER
(background).
So you could possibly use PLOT
to set or clear the bit comparable to a pixel, which might then take one of many two colours assigned to that block. This implies every 8×8-pixel block can present one or two totally different colours, however by no means three or extra.
This all labored nice for text-based functions, since characters have been additionally 8×8 blocks, however for something graphic, particularly video games, it was tremendous limiting. This limitation provides Spectrum video games its very attribute aesthetic, as a result of artists needed to work round this, normally by designing screens and sprites aligned to a 8×8 pixel grid, or going full monochrome, or accepting that attribute conflict was a truth of life.
Again to the raytracer. Rising the decision is straightforward. Coping with attribute conflict, not a lot.
There’s no good resolution: it doesn’t matter what I do, every 8×8 block can present as much as two colours. So what I did was implement an approximation algorithm. I accumulate the colours current within the 8×8 block, discover the commonest and second most typical, and draw each pixel utilizing one of many two.
The outer loop modifications a bit to mirror the upper decision and the processing on 8×8-block chunks:
10 FOR X = 0 TO 255 STEP 8 20 FOR Y = 0 TO 175 STEP 8 ... 500 NEXT Y 505 GO SUB 3000: PRINT AT 0, 0; TIME 510 NEXT X 520 STOP
Then we hint the 64 rays, gathering the colours in an array:
30 DIM C(64) 31 LET CI = 1 32 DIM A(8) 120 REM --- For every 8x8 block, accumulate the pixel colours and their counts --- 125 FOR U = X TO X+7 126 FOR V = Y TO Y+7 130 LET RDX = (U - 128) / 256 131 LET RDY = (V - 88) / 256 132 LET RDZ = 1 140 GO SUB 1000 141 LET C(CI) = COL 142 LET CI = CI + 1 143 LET A(COL+1) = A(COL+1) + 1 160 NEXT V 161 NEXT U
Line 30 DIM
ensions the variable C
as a 64-element array. Array indexes begin at 1, so line 31 initializes CI
(C
-index) to 1. Line 32 creates one other array A
which can maintain the colour counts.
Strains 140 to 143 name TraceRay
and retailer the outcomes: the pixel shade in C
, and the up to date shade depend in A
. Colours go from 0 to 7 however indexes go from 1 to eight, so we have to use COL+1
because the index.
Subsequent we have to discover probably the most and second-most frequent colours:
199 REM --- Discover probably the most and second most frequent colours on this 8x8 block --- 201 LET MFC = 0 202 FOR C = 1 TO 8 203 IF A(C) > MFC THEN LET MFC = A(C): LET MFI = C 204 NEXT C 205 LET FCOL = MFI - 1 207 LET II = MFI: LET MFC = 0: LET MFI = 0 208 FOR C = 1 TO 8 209 IF C <> II AND A(C) > MFC THEN LET MFC = A(C): LET MFI = C 210 NEXT C 211 LET SCOL = MFI - 1
Time to attract some pixels. If all of the pixels are the identical shade, simply paint the block:
259 REM --- If there's just one shade, paint the entire block -- 260 IF SCOL <> -1 THEN GO TO 300 270 POKE 22528 + X/8 + 32*(21-Y/8), 64 + FCOL * 8 280 GO TO 500
That POKE
requires a proof. POKE
places a byte in a reminiscence deal with. The primary parameter is the deal with of this 8×8 block within the attributes block. The second parameter, the byte representing the INK and PAPER values, is the mixture of the INK shade shifted left 3 bits, plus a bit to activate the BRIGHT attribute.
If not all pixels are the identical shade, we have to plot them individually. The PAPER shade of the block is ready to probably the most frequent shade (so there’s fewer pixels to plot), we go over the array, and any pixel that isn’t probably the most frequent shade is drawn with INK shade set to the second most frequent shade:
300 REM --- In any other case set the PAPER to probably the most frequent shade, and draw every part else within the second most frequent shade -- 301 LET CI = 1 310 FOR U = X TO X+7 311 FOR V = Y TO Y+7 320 IF C(CI) <> FCOL THEN PLOT INK SCOL; PAPER FCOL; U, V 321 LET CI = CI + 1 350 NEXT V 351 NEXT U
This works fairly effectively!
Attribute conflict nonetheless occurs. Have a look at this magnified half:
With a grid overlaid to point out block boundaries, the issue is less complicated to see. The 2 blocks that look “unsuitable” ought to have three colours: black, yellow, and both inexperienced or pink. However the Spectrum can’t do this, so that is what the algorithm above finally ends up doing.
You’ll be able to check out the full source code for this iteration.
The subsequent factor to note is that it’s simply ridiculously gradual – over 17 hours! Even on the emulator hitting 20,000% velocity, it takes some time to render. Can we do higher?
I went for an optimization cross. Right here’s what I did:
-
For every 8×8 block, hint rays for the 4 corners, and if the colour is similar in all, paint the entire block. More often than not this does 4 rays per block as an alternative of 64, so by itself it hastens rendering by 16x. In fact if there have been small objects that fell totally inside a block, the raytracer would miss them; however for this check scene, it feels prefer it’s a good approximation.
-
Keep away from multiplications and divisions in any respect prices. The Z80 can’t do multiplication in {hardware} (not to mention division), so BASIC implements it in software program, and it’s gradual.
-
Hardcode some constants based mostly on assumptions. Notably, the ray origin is at all times (0, 0, 0),
t_min
is at all times 0, andt_max
is at all times+inf
, in order that saves some computation. -
Precompute values when potential. Why retailer the spehre radius as knowledge and sq. it, when it may be saved squared to start with?
-
Transfer computed values to outer loops when potential. For instance, values associated to X are fixed for each Y, and might be computed fewer instances.
-
Inlined the “most frequent shade” subroutine, and specialised the primary case to not ignore any shade.
-
Tweaked the road numbers to ensure
GO SUB
didn’t land on aREM
line; imagine it or not, processing a line that comprises a remark takes time! -
Used shorter variable names. This BASIC is interpreted, so each time you reference a variable, it’s appeared up by title…
-
I additionally tried some optimizations that didn’t work, like studying the
DATA
into an array first, or placing sure expressions into variables. I’ve the imprecise feeling that the order through which variables are outlined is vital – I must learn extra about this. -
There’s some optimizations that assist marginally, however hinder readability, so I selected to not implement them.
All in all, the result’s fairly good. The picture is pixel-identical, however the runtime is all the way down to 2 hours and a bit:
You’ll be able to check out the full source code for this iteration.
Initially I had stopped myself right here; given the constraints of the atmosphere, I felt like there wasn’t far more that may very well be completed.
The plain subsequent step is to implement lighting. The lighting equations and algorithms are comparatively simple, however the primary drawback is the very restricted set of colours the ZX Spectrum can signify. To recap, it’s a hard and fast set of seven colours, in regular and vibrant variations, plus black:
Even when I had the sunshine depth worth at each pixel, I can’t simply multiply it by the sphere shade to get the shaded shade, like I can do trivially in RGB. What to do?
Tradeoffs, that’s what. I can simulate shades of a shade by alternating the colour and black in the correct quantities. I can do that on a 8×8 block foundation, setting the INK
to the colour, PAPER
to black. The tradeoff is that there shall be attribute clashing.
How one can determine whether or not to plot a pixel or go away it black? My first thought was to make use of the sunshine depth, an actual quantity between 0.0 and 1.0, because the likelihood {that a} pixel can be plotted with the colour (and left black in any other case). This labored, but it surely appeared ugly. There’s one thing higher, known as ordered dithering. The thought is to have a matrix of thresholds, one per pixel, that helps decide whether or not to plot the pixel. The thresholds are organized in such a manner that they produce repeatable, pleasing patterns of pixels for any depth stage. There’s a 8×8 dithering matrix, which inserts completely the 8×8 shade blocks I’m processing, so it was surprisingly simple to implement.
For the sake of simplicity, I made a decision to have only one directional gentle. Even with ordered dithering, there should not sufficient shades I can show that may adequately signify the nuances of a number of lights illuminating the identical object. For a similar cause, I went for diffuse lighting solely, no specular part.
So the objective was to render one thing like this:
How shut may I get to that on a humble ZX Spectrum?
Listed here are the related modifications I made to the code:
I may not use the 4-rays-per-8×8-block trick, as a result of the sunshine depth at every pixel may very well be totally different. May have computed one depth per block, however I didn’t wish to lose gentle decision. So efficiency took a giant hit in comparison with the earlier iteration. The exception is that if the 4 corners of the 8×8 block are black, through which case I can safely ignore it.
The lighting half is fairly easy: within the TraceRay
subroutine, I wanted to maintain monitor of the index of the closest sphere (so I additionally needed to load the sphere knowledge into an array S
at the beginning of this system). After the sphere loop, if the ray hits any sphere, I compute the intersection between the ray and the sphere, the conventional at that time within the sphere, and at last the illumination at that time:
1601 LET NX = DX*MT - S(CS, 1): LET NY = DY*MT - S(CS, 2): LET NZ = DZ*MT - S(CS, 3) 1610 LET PL = AI 1615 LET NL = (NX*LX + NY*LY + NZ*LZ) 1620 IF NL > 0 THEN LET PL = PL + DI * NL / SQR(NX*NX + NY*NY + NZ*NZ)
In that fragment, CS
is the index of the Closest Sphere; PL
is a brand new output variable representing Pixel Lighting; (LX
, LY
, LZ
), DI
and AI
are set elsewhere, and signify the path of the sunshine, its depth, and the depth of the ambient gentle, respectively. For efficiency causes, LX
, LY
, LZ
signify a normalized vector, so I can skip the SQR
and the division in line 1620.
I don’t want to seek out the second most frequent shade in every 8×8 block anymore, as a result of every block will solely show probably the most frequent shade and black.
I added some code to load the Bayer ordered dither matrix into an array:
3 GO SUB 7000 ... 6999 REM ===== Initialize 8x8 Bayer matrix ===== 7000 DIM H(64) 7001 RESTORE 7100 7002 FOR I = 1 TO 64 7003 READ H(I): LET H(I) = H(I) / 64 7004 NEXT I 7005 RETURN 7100 DATA 0, 32, 8, 40, 2, 34, 10, 42 7101 DATA 48, 16, 56, 24, 50, 18, 58, 26 7102 DATA 12, 44, 4, 36, 14, 46, 6, 38 7103 DATA 60, 28, 52, 20, 62, 30, 54, 22 7104 DATA 3, 35, 11, 43, 1, 33, 9, 41 7105 DATA 51, 19, 59, 27, 49, 17, 57, 25 7106 DATA 15, 47, 7, 39, 13, 45, 5, 37 7107 DATA 63, 31, 55, 23, 61, 29, 53, 21
And at last, earlier than plotting a pixel, I examine its gentle depth with the corresponding threshold within the Bayer matrix:
320 IF C(CI) > 0 AND H(CI) <= L(CI) THEN PLOT U, V
I ran this iteration, and actually, I stared at it in disbelief for a very good minute:
To start with, it really works!
It’s fairly gradual in comparison with the earlier iteration, largely as a result of the lacking 4-rays-per-block trick, plus the extra lighting calculations. But it surely’s not that unhealthy.
Attribute clashing continues to be there, and it’s much more apparent now. May this be improved? Perhaps. The yellow/pink clashes appear like may very well be improved by making the blocks pink and yellow, and forgoing the shading element (as a result of there can be no black). For inexperienced/yellow and blue/yellow, appears like black/yellow, blue/yellow and once more black/yellow would make it look higher. Hmmmm. Perhaps I’ll get again to this.
You’ll be able to check out the full source code for this iteration.
At this level I’m feeling fairly snug with the atmosphere, I’m coding prefer it’s 1984, so I wish to see how far I can take this. Subsequent step: shadows.
A lot of the items are already in place. The theory is comparatively easy: earlier than computing lighting for a degree, want to determine whether or not there’s an object between the purpose and the sunshine, blocking it (i.e. casting a shadow). I simply needed to implement a specialised model of TraceRay
that traces from the intersection of the first ray and a sphere, within the path of the directional gentle, and returns as quickly because it finds any intersection:
2090 REM ----- Specialised TraceRay for shadow checks ----- 2091 REM Params: (IX, IY, IZ): ray begin; (LX, LY, LZ): ray path (directional gentle vector) 2092 REM Returns: H = 1 if the ray intersects any sphere, H = 0 in any other case 2093 REM Optimizations: (TMIN, TMAX) hardcoded to (epsilon, +inf) 2100 LET A = 2*(LX*LX + LY*LY + LZ*LZ) 2110 FOR S = 1 TO NS 2111 LET CX = IX - S(S,1): LET CY = IY - S(S,2): LET CZ = IZ - S(S,3) 2120 LET B = -2*(CX*LX + CY*LY + CZ*LZ) 2130 LET C = (CX*CX + CY*CY + CZ*CZ) - S(S, 4) 2140 LET D = B*B - 2*A*C 2150 IF D < 0 THEN GO TO 2210 2160 LET D = SQR(D) 2170 LET T = (B + D) / A 2180 IF T > 0.01 THEN LET H = 1: RETURN 2190 LET T = (B - D) / A 2200 IF T > 0.01 THEN LET H = 1: RETURN 2210 NEXT S 2220 LET H = 0: RETURN
That is known as proper earlier than computing illumination:
1600 LET IX = DX*MT: LET IY = DY*MT: LET IZ = DZ*MT 1601 LET NX = IX - S(CS, 1): LET NY = IY - S(CS, 2): LET NZ = IZ - S(CS, 3) 1610 LET PL = AI 1612 GO SUB 2100: IF H = 1 THEN RETURN 1615 LET NL = (NX*LX + NY*LY + NZ*LZ) 1620 IF NL > 0 THEN LET PL = PL + DI * NL / SQR(NX*NX + NY*NY + NZ*NZ)
And right here’s what comes out…
Examine with the output of the CGFS raytracer:
Fairly gradual as a result of further computation (again to 17 hours), however positively value it!
The plain subsequent step can be to implement reflections. However it could be virtually not possible to mix colours collectively in a significant manner. So objects can be both totally reflective or not reflective in any respect, and it could simply look bizarre. Recursion can be an fascinating drawback: the Spectsum helps it, however as a result of there aren’t any native variables, every recursive name would overwrite the worldwide variables, so I’d need to handle my very own stack. Doable, however doesn’t sound definitely worth the effort.
One other axis is efficiency. I may rewrite the entire thing in meeting and see how briskly can I make it go. I may management how a lot precision I want, so perhaps fixed-point math would do it (or a much less exact model of SQR
). Perhaps another time!
Lastly, the attribute conflict at object boundaries nonetheless bothers me. I’ve a few concepts which may enhance the state of affairs, though the constraints of the Spectrum are such that it’ll by no means be 100% mounted.
This was a enjoyable weekend undertaking. Completely pointless, however enjoyable!
It was good to write down Sinclair BASIC after 30 years. Though the language is similar, I’m not – I discovered myself pondering higher-level ideas after which translating them to BASIC. I don’t know whether or not it is because trendy languages give me a greater vocabulary to assume in, that I can then translate to BASIC, or as a result of I’m not 10 anymore. Could possibly be each.
Particularly, this program makes even handed use of GO TO
, which as everybody is aware of, it’s Thought-about Dangerous™. Again within the day it was just about all we had: no perform calls, solely subroutines utilizing GO SUB
; no WHILE
or REPEAT
; IF
doesn’t have END IF
; FOR
doesn’t have BREAK
or CONTINUE
(the key phrases exist, however they don’t do what you assume they do). So utilizing GO TO
is unavoidable. And positive, it may result in spaghetti code, but it surely doesn’t need to; my very own code right here, though admittedly easy, is structured cleanly.
I additionally missed the immediacy and the simplicity of the atmosphere. No frameworks, no dependencies, barely any abstractions (even multiplication is carried out in software program!). The ZX Spectrum was totally knowable. The entire Z80 instruction set, the quirks of the ROM and the ALU, every part suits in your head fairly simply. You could possibly cause about peformance all the way down to the processor cycle stage – no caches or pipelines or the rest to make your life troublesome. I miss all that. Children These Days™ won’t ever get to expertise an atmosphere like this, and that makes me unhappy.
9999 STOP