:Publish Date: 2019-04-12 3d Graphics on the Commodore 64, Part One ========================================= *Published on 2019-04-12* This is something that came from an exercise I've given myself to improve my assembly language skills, which in turn is to improve `improve the ISA design for my homebrew vale8 computer <2019-04-12-the-vale8-instruction-set.html>`_. I'm interested in `demoscene culture `_ in general, and there's a lot of `demoscene-related `_ `documentation `_ for the C64 machine. After doing a custom font and basic scroller effects on the C64, I thought a more challenging project in 6502 might be to animate wireframe geometry (the seminal "spinning cube" effect). Here's the result, a spinning pyramid. .. raw:: html In this multi-part post series, we'll make this effect using C and 6502 assembly. The line drawing routines are in 6502 assembly, whereas the model's vertices are pre-calculated using a C program. Prerequisites ------------- * Understanding of C. * Understanding of 6502 assembly language. * Ability to assemble programs and run them in an `emulator `_, or on a C64. Further References ------------------ * `6502 Assembly `_ * `Commodore 64 memory map `_ * `Commodore 64 Programmer's Reference Guide `_ Disclaimer ---------- I'm a novice 6502 programmer. Demoscene veterans do this sort of thing with much better performance. I'm just showing the way that I worked out for myself. I know there are opportunities for optimization, and I'm looking forward to learning more of them. Procedure --------- We can divide the procedure of animating 3d geometry into a few steps. The program must follow each step for each frame of animation. Draw the Model's Faces `````````````````````` For each face, draw a line from one vertex of the face to the next, in a counterclockwise direction. Transform the 3d Model in the World ``````````````````````````````````` In our case, change its rotation about the y axis each frame. Transform Model Vertices to Display Coordinates ``````````````````````````````````````````````` Convert each vertex from its 3d coordinate space to a 2d pixel coordinate on the C64's 320x200 bitmap display. Prerequisites to Drawing Lines ------------------------------ Modern video cards support graphics APIs that are high enough in level as to provide either `immediate- `_ or `retained-mode `_ functions to draw primitives like lines and polygons. The C64 provides no such interface for drawing primitives in its bitmap modes. Instead, we set the color of each pixel individually. Since we'll implement line drawing routines that will tell us which pixels to set, we need to also make some routines to actually set those pixels. We'll understand how to use the C64's standard high-resolution bitmap mode and then create some subroutines to make it convenient to use with cartesian coordinates. Standard High-resolution Bitmap Mode ```````````````````````````````````` The C64 provides two bitmap modes. The one we will use is the standard high-resolution bitmap mode. This mode gives us 320x200 pixels, where each pixel may be one of 2 colors chosen from the `16-color palette `_. I'll refer to it as "bitmap mode" going forward. Enabling Bitmap Mode :::::::::::::::::::: To enter bitmap mode, set bit 5 of register $d011 to 1, as follows: .. literalinclude:: /_static/c64-3d-graphics-series/display.asm :language: none :start-after: vic_bitmap_enable :end-before: } Setting Bitmap Memory Location :::::::::::::::::::::::::::::: The location of the start of bitmap memory can be either $0000 or $2000 (8192). This number is in bytes *relative* to the start of the current VIC bank. The default VIC bank starts at memory location $0000. If we leave it there, we must set the start of bitmap memory to $2000 since there are some lower memory values we cannot modify. To set the start of bitmap memory to $2000, set bit 3 of register $d018 to 1: .. literalinclude:: /_static/c64-3d-graphics-series/display.asm :language: none :start-after: vic_bitmap_hi :end-before: } Bitmap Memory Arrangement ::::::::::::::::::::::::: The bitmap memory is an 8192-byte array of memory. Each bit of each memory address is used to set the color of a corresponding pixel on the display. From the perspective of a human accustomed to using the cartesian coordinate system, the VIC uses bitmap memory in an inconvenient way. We might have expected the memory to be mapped to the display something like this. .. image:: /_static/c64-3d-graphics-series/bitmap_mem_ideal.svg Instead, we have this. .. image:: /_static/c64-3d-graphics-series/bitmap_mem_real.svg One way to think of the bitmap memory is a 40x25 grid of cells, each of which is 8 pixels wide and 8 pixels high. This gives us 320 columns and 200 rows of pixels. The grid of cells starts in the top left corner of the display. The bitmap memory address corresponds to a row of 8 pixels in a cell. When we increment the bitmap memory address, we move down to the next row of pixels in the current cell. When we move past the bottom row of pixels in the cell, we move to the top row of pixels in the next cell to the right. When we move past the rightmost cell in the row of cells, we move down to the leftmost cell in the next row of cells. Once again, the value of each bitmap memory address controls the colors of one row of 8 pixels in a cell. Bit 7 (the highest bit) of the value controls the leftmost pixel, and bit 0 (the lowest bit) controls the rightmost pixel. Since each bit can be set to either 0 or 1, there are two possible colors for each pixel in any given cell. Bitmap Colors ::::::::::::: In bitmap mode, each byte of display memory $0400-$07e7 controls the color of all pixels in a corresponding 8x8 cell of the bitmap. The upper 4 bits of the byte determine the color of a pixel whose bit is 1 in bitmap memory, and the lower 4 bits of the byte determine the color of a pixel whose bit is 0 in bitmap memory. The display memory maps to bitmap cells left to right, top to bottom. The following example sets the second cell in the first row of cells (pixel rows 0-7 and columns 8-15) to an alternating cyan-and-black pattern, as shown in the previous images. .. code-block:: lda #$aa sta $2008 lda #$30 sta $0401 Here is a `test program <../_static/c64-3d-graphics-series/test_bitmap.asm>`_ demonstrating how bitmap and color memory work. .. image:: /_static/c64-3d-graphics-series/test_bitmap.png This is everything we need to know about bitmap mode before we implement our drawing routines. Pixel Placement Routines ```````````````````````` Our line drawing algorithms will give us the results in cartesian coordinates. This is typical, so we want to address the bitmap in cartesian terms, if possible. So let's define some drawing routines that take cartesian coordinates as input and modify the bitmap memory appropriately behind the scenes. We'll define several routines that allow us to control a "pen" on the display. With these routines, we can move the pen to (almost) any (x, y) coordinate on the display, move the pen relative distances in four directions, and set the color of the pixel where the pen is located. Here is the commented source code for all the functions. .. literalinclude:: /_static/c64-3d-graphics-series/pen.asm :language: none :start-at: ;; pen_init :end-at: rts .. literalinclude:: /_static/c64-3d-graphics-series/pen.asm :language: none :start-at: ;; pen_move_to :end-at: rts .. literalinclude:: /_static/c64-3d-graphics-series/pen.asm :language: none :start-at: ;; pen_move_up :end-at: rts .. literalinclude:: /_static/c64-3d-graphics-series/pen.asm :language: none :start-at: ;; pen_move_down :end-at: rts .. literalinclude:: /_static/c64-3d-graphics-series/pen.asm :language: none :start-at: ;; pen_move_left :end-at: rts .. literalinclude:: /_static/c64-3d-graphics-series/pen.asm :language: none :start-at: ;; pen_move_right :end-at: rts .. literalinclude:: /_static/c64-3d-graphics-series/pen.asm :language: none :start-at: ;; pen_px_set :end-at: rts Here is the complete `pen source file <../_static/c64-3d-graphics-series/pen.asm>`_. We can draw any shape we want from our assembly language programs using these pen routines. Here is a `test program <../_static/c64-3d-graphics-series/test_pen.asm>`_ that uses the pen to draw straight lines from the right side of the display to the left, then left to right, then top to bottom, then bottom to top. .. image:: /_static/c64-3d-graphics-series/test_pen.png You might have noticed the line from right to left does not originate from the far right side of the display. Examining our subroutine declaration again: .. literalinclude:: /_static/c64-3d-graphics-series/pen.asm :language: none :start-at: ;; pen_move_to :end-at: x coordinate We take two arguments, y and x. Because x is supplied in an 8-bit memory location, the maximum value of x is 255. However, the rightmost pixel column on the display is 319. Because the size of the memory location that stores x is smaller than 319, ``pen_move_to`` can't reach the right side of the display. Since our geometry will not be drawn onto the far right of the display, this is not a problem for our use case. If we want ``pen_move_to`` to reach past column 255, we can do that by taking a third argument holding one extra bit for x. When the bit is set, the column counter first counts to 255, then resets to zero, then counts again until reaching the value of the "x" argument. Wrapping Up ----------- Here is a `zip file <../_static/c64-3d-graphics-series/c64-3d-graphics-series-part-1.zip>`_ containing the test_bitmap and test_pen programs, as well as all the files needed to build and run them. In the next part of this series, we implement routines to draw lines directly from any pixel on the display to another, automatically calculating all the pixels in between. Please contact me on `the fediverse `_ or `Twitter `_ with any questions or comments.