Sunday, December 13, 2020

Stereoscopic computing: anaglyph sprites on the Commodore 64

This article is part of a series -- you can read other entries

In our first 3D article we talked about the various types of stereoscopy available on computers. Modern systems can both generate a 1080p image and/or (with most video cards) a high-refresh-rate image, and most 3D displays or 3D-capable TVs are 1080p, so depending on whether you have an active or passive display system you can either use fast refresh (like my 120Hz DLP projector, which delivers a full 60Hz to each eye with active glasses) or an interlaced image and polarization (like my Vizio 3D TV and Mitsubishi Diamondcrysta monitor). This generates a high-quality, (usually) flicker-free high definition 3D picture.

However, classic computers invariably don't have either of those options, so we must resort to less satisfactory approaches. While some systems implemented a spinning shutter wheel as an active 3D display option, many older systems lack sufficient refresh rates to be sufficiently smooth and very old machines can't update the screen fast enough between eye views anyway. (That gives me a headache just thinking about it.) For most situations the typical choice will be either anaglyph, i.e., red-cyan glasses, or exploiting the Pulfrich effect. The latter, though not general purpose due to how the effect is generated, can be sufficiently convincing with the right application and we'll look at that in a later article. A third option is possible with modern displays but it, too, will be the subject of a later post. Today we'll try to get a primitive anaglyph effect working on the Commodore 64, and we'll do it with the classic and widely available red-cyan glasses you can get on Amazon or from specialty shops like Berezin (no affiliation, just a satisfied customer).

The basic concept with anaglyph is that the coloured glasses filter certain wavelengths of light, delivering different views to each eye. Since the red filter is over your left eye, your left eye only gets red (primarily), so the image that should be delivered to the left eye is tinted red. Likewise, as the cyan filter is over your right eye, the cyan filter should optimally admit only what is part of the right eye image. In practice, as anyone who's looked at anaglyph images knows, the strategy is imperfect: most full colour images will have some bleed-through and while colour selection and processing can reduce differences in brightness between the eyes, some amount of ghosting and retinal rivalry between the two sides is inevitable. Still, anaglyph 3D images require no special hardware other than the glasses and some well-constructed images can be very compelling.

To make objects stick out, the left (red) channel is separated further and further to the left from the right (cyan) channel; to make them recede, the left channel is separated further and further to the right. When the channels overlie exactly, they are seen at a neutral distance from the viewer as appropriate to the image's composition. The mnemonic is thus the three R's: red right recedes.

Unfortunately, conventional colour anaglyphs are difficult on a system like the C64 because there is only one fixed, limited palette and the shades available may not correspond well with the lens colours. You may be able to make the displayed colours more appropriate for your glasses by messing with the display settings or colour balance, but this naturally has other side effects. Additionally, there is no alpha channel, so overlaying objects (which is necessary to deliver two views in one image) just obscures what's behind them. Usually you would use a proportional shade of purple to deliver an appropriate level to each eye but the C64 has but one shade of purple, and you would have to manually figure out when to use it.

A way around the latter problem is to either dither or (as a special case of dithering) interlace. This reduces resolution but eliminates having to do costly alpha calculations and screen updates. One way of doing a 3D anaglyph display on a C64 is alternating red/black and blue/black lines in multicolour mode, as the red (VIC-II colour 2) and blue (VIC-II colour 6) shades are the closest shades to most red-cyan glasses. This gives you effectively a 160x100 monochrome image. For greater dynamic range you could also consider red/pink/black and blue/light blue/black on alternating lines, using black as the common background colour, and some of the CPU-assisted modes like FLI and Hires FLI can do even better. But this requires substantial precomputation to generate the image and thus is only generally useful for static images. How might we do this for a dynamic display?

While computers like the Amiga have playfields for overlaid elements, the VIC-II chip in the C64 really only has one option: sprites. Sprites do effectively have an alpha channel, but it is a single bit, so there is no translucency. Thus, if we want a dynamic 3D anaglyph display on the C64, a straightforward means is to interlace a blue sprite and a red sprite, yielding a composite 3D plane that can move in the Z-axis by changing their relationship to each other. And that's what we'll do here.

The illusion of depth is enhanced not only by the shift between the left and right channels, but also the size of the object, so we will need a routine to scale a sprite. For simplicity we'll just use a solid square block, which we can generate on the fly. Sprites on the C64 are 24x21, each row three bytes in length, up to 63 bytes in size. We will write a quick little utility routine that will turn a number into a string of bits, and then copy that to the same number of alternating lines, clearing the rest of the sprite so we can grow and shrink it at will.

The assembler source for this quick interlaced sprite scaler is on Github and can be cross-assembled with xa. We'll put the sprite at 832 ($0340) in the cassette buffer, which appears as SPRITE in the text. Here's some highlights.

        jsr $aefd
        jsr $ad9e
        jsr $b7f7
As a convenience for playing around in BASIC, these calls accept a comma after the SYS statement followed by an arbitrary expression, and then convert the result to a 16-bit integer and store it in $14 and $15. We only care about the low byte, so $14 is the entirety of the value. We clear the top row of the sprite, and if the parameter is zero, just copy that to the rest of the sprite (clears it). Otherwise, let's make enough one bits in the top row of the sprite by setting carry and rotating it through:
        ; turn x into x 1-bits
lup1    sec
        ror SPRITE
        ror SPRITE+1
        ror SPRITE+2
        dex
        bne lup1
We then duplicate it on alternating rows (unless it's 1x1 or 2x2).
        sbc #0 ; ror cleared carry, so -1
        lsr
        beq clrs ; no copies

        tay
        clc
lup2    lda SPRITE
        sta SPRITE+6,x
        lda SPRITE+1
        sta SPRITE+7,x
        lda SPRITE+2
        sta SPRITE+8,x
        txa
        adc #6
        tax
        dey
        bne lup2
And then we clear the rest of the sprite. We assemble this to 49152 and load it, then set some parameters. After you load the binary into VICE from wherever you assembled it to (and remember to type NEW after loading), you can cut and paste these POKEs into VICE.

poke53281,0:poke53280,0:poke646,12:poke53287,6:poke53288,2
poke2040,13:poke2041,13:poke53271,3:poke53277,3
poke53248,150:poke53249,100:poke53250,150:poke53251,102:poke53264,0
sys49152,10:poke53269,3

You'll get this display.

We have set the sprite colours, made them double size for ease of use, and positioned them so that they are interlaced. If you put on anaglyph glasses at this point, it's just a block at neutral distance. Let's write a little BASIC code to move them around as a proof of concept. You can cut and paste this into VICE as well:

10 rem midpoint at x=10
20 forx=0to20:poke53250,160-x:poke53248,140+x:poke53251,110-x:poke53249,108-x
30 sys49152,x:for y=0to50:next:next
40 forx=20to0step-1:poke53250,160-x:poke53248,140+x:poke53251,110-x
50 poke53249,108-x:sys49152,x:for y=0to50:next:next
60 goto 20

With glasses on, you will see the block swing from receding into the distance and protruding into your view by sliding and scaling.

There are two things to note with this primitive example. The first is that the steps end up separating the red and blue components quite a bit; combined with the afterimage from the bleedthrough, we end up seeing two blocks at their furthest extent. We can solve that problem by separating the sprites at a slower rate (say, half) than the scaling rate. This limits how far the composite plane can be moved in the Z-axis, but there are usually other optical limits to this generally, so we're not losing as much as one would think.

The second thing to note is it's kind of jerky because it's not updating the sprite registers fast enough (the delay loop is just there so you can see each step of the animation; the lack of smoothness is because of the computations and POKEs necessary on each "frame"). We'll solve this problem by rewriting the whole thing in assembly language. For style points we'll add a background (a crosshatch) as a neutral plane, and flip the sprite priority bits for the composite plane as it moves forward and back so that it also has proper occlusion.

The assembler source for the "complete" demo is also on Github (and also cross-assembled with xa), but notable parts are discussed below. We will write it with a small BASIC loader so we can just LOAD and RUN it.

        .word $0801
        * = $0801

        ; 64738 sys2061

        .word $080b
        .word $fce2
        .byte $9e, $32, $30, $36, $31
        .byte $00, $00, $00
The motion routine is more or less a direct translation of the BASIC proof of concept except we will separate the sprites by only half of the scaled size for less of a "double vision" effect, so we change the constants to match. Here VALUE is still $14, which we're still using merely as a holding location even though we are no longer servicing BASIC directly, and HVALUE is a free zero space location for the temporary result of the math.
mlup    lda VALUE
        clc
        lsr
        sta HVALUE

        lda #150
        sec
        sbc HVALUE
        sta 53250
        lda #110
        sbc VALUE
        sta 53251

        lda #108
        sbc VALUE
        sta 53249
        clc
        lda HVALUE
        adc #140
        sta 53248
At the end we wait a couple jiffies so that the animation is actually visible, check for RUN/STOP, and if not pressed cycle the position in VALUE back and forth. MODE is $15, since we don't use it for anything else either here, and is initialized to zero when we start.
        ; wait two jiffies per frame
        lda #0
        sta $a2
waitt   lda $a2
        cmp #2
        bcc waitt

        ; check run/stop
        lda 203
        cmp #63
        bne cycle
        lda #0
        sta 53269
        lda #147
        jmp $ffd2

cycle   lda MODE
        bne decr

incr    inc VALUE
        lda VALUE
        cmp #21
        beq *+5
        jmp mlup
        sta MODE
        ; fall thru
        
decr    dec VALUE
        lda VALUE
        cmp #$ff
        beq *+5
        jmp mlup
        lda #0
        sta VALUE
        sta MODE
        jmp mlup
Here is an animated GIF of the result you can view with anaglyph glasses, though the result in an emulator or on a real C64 is smoother.

As you can see, the composite sprite recedes and protrudes appropriately, and depth cueing is helped by flipping the priority bits as it goes past the "zero" point (the crosshatch) where VALUE, in this coordinate system, is defined as 10.

While an anaglyph composite sprite approach clearly has drawbacks, it's still in my opinion the best means for independent motion of planar objects in the Z-axis and gives the most flexibility for "true" 3D on classic machines of this era. But the Pulfrich effect doesn't have its colour limitations and can be useful in certain specific situations, so we'll look at that in the next article.

No comments:

Post a Comment

Comments are subject to moderation. Be nice.


See more of my general vintage computing projects,
mostly microcomputers, 6502, PalmOS, 68K/Power Mac
and Unix workstations, but that's not all. Be kind, REWIND and PLAY.

Buy Me a Coffee at ko-fi.com

Old VCR is advertisement- and donation-funded, and what I get
goes to maintaining the hardware here at Floodgap.
I don't drink coffee, but the Mr Pibb doesn't buy itself. :-)
Thanks for reading. -- Cameron Kaiser