Now Reading
ZX Spectrum Raytracer – Gabriel Gambetta

ZX Spectrum Raytracer – Gabriel Gambetta

2024-01-24 09:20:12

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 DIMensions 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, and t_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 a REM 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:

See Also



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

Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top