I’m Old, Part XIII: Rendering Spheres

When I was in college, I wrote a program that could render a sphere lit from a single point light source. I wrote it in C on a Mac Plus and the output was a window with a 1-bit dithered image of the sphere.

Aside – dithering, in imaging, is the process of reducing a image with some number of bits per pixel to an image with fewer bits per pixel. The Mac Plus had could only do black or white (no gray), so that’s 1 bit per pixel. A couple years later, I ended up writing a chapter in the first volume of the book Graphics Gems on ordered dithering.

It ran OK, and it looked pretty decent, but I wanted a larger image and didn’t want to wait too long for output. While the Macintosh was ostensibly faster running at 8 MHz, the school’s VAX had native floating point arithmetic, whereas my Mac did not.

I ported the program to the CS department’s VAX 11/750 and asked to borrow the reference manual for the line printer in the CS lab. I wrote the program such that it would print size information and gray scale values and then, this being Unix, wrote a separate program that could consume that output, dither it, then convert it to codes to send to the printer to print out an image.

The output was very satisfying. I set it up to print a sphere with a 512 pixel radius, which just fit onto one sheet of paper. It was a little slow though. The image took roughly an hour for the calculations.

anxiety

Like Brophy in High Anxiety, I wanted to blow it up. I thought that 4 pages by 4 pages would be good. I couldn’t do this in one shot because students had a limited amount of disk space and I would be out of space before half the image had rendered and it would take 16 hours. So I modified the program so that I could render it in tiles. Each tile would take an hour, but if I ran them in parallel, I might do much better than 16 hours. Plus, each job would use none of my disk space since they would just end up in the print queue. I wrote a script to generate all the pages as background jobs and set it loose.

And I very quickly drove the poor VAX into the ground. Oh well. I could wait.

Unfortunately, nobody else could and the head of the CS department was one of those people. When he could get a slice of CPU time, he spotted 16 jobs all belonging to me and all topping out the CPU. He killed them all and sent me an email to come talk to him. I did and explained what I was doing and he grudgingly agreed to let me run it to completion but I could only run the jobs late at night and only one at a time. Damn. Still, I did just that and ran maybe two or three per night, serially. After a week, I collected the pages, cut off the borders and taped the pages together and put it on the wall in the lab.

Not too long after, Mike Dashow made a little drawing of the Millennium Falcon with a speech balloon that read “I’ve got a bad feeling about this.”. He taped it to the wall next to the sphere. The whole thing stayed there for the next year.

I considered going up to 8×8, but decided against it because it would take me a month if I was consistent and printing out the sphere was also a little hard on the ribbon cartridge that the printer used. The pages from the 4×4 were looking decidedly lighter in the last few panels, and I didn’t have the spare cash to buy my own printer ribbons, so that didn’t happen.

I don’t have the original code anymore, but this evening I recreated it in F#, running in Mono on my Toshiba x64 laptop, running Ubuntu. Took me about an hour. I think the original code took me 4 or 5.

Here is the output of a 512 radius sphere, no dithering, just continuous tone gray.

output

Compared to the one hour of the original version on the VAX, this took 365 milliseconds. For grins, I upped it to the same size as the 4×4. It finished in 7.2 seconds.

For grins, here’s the source code. No command line arguments, and it writes a JPEG image to a fixed file, but hey.

open System
open System.Drawing
open System.Drawing.Imaging
 
type Vector = {
    X:single; Y:single; Z:single
}
 
let inline dot u v = u.X * v.X + u.Y * v.Y + u.Z * v.Z
 
let length2 v = dot v v
 
let inline length v = length2 v |> sqrt
 
let normalize v =
    let len = length v
    { X = v.X / len; Y = v.Y / len; Z = v.Z / len }
 
let cosanglebetween u v = (dot u v) / (length u * length v)
 
let pointOnSphere r x y =
    // x^2 + y^2 + z^2 = r^2
    // z^2 = r^2 - x^2 - y^2
    let z2 = (r * r) - (x * x) - (y * y)
    if z2 >= 0.0f then Some(sqrt z2, -sqrt z2) else None
 
 
let illum background ambient lightpos r x y =
    let lighting z1 z2 =
        let v1 = { X = x; Y = y; Z = z1 }
        let v2 = { X = x; Y = y; Z = z2 }
        let v = if length2 v1 < length2 v2 then v1 else v2
        let cosangle = cosanglebetween lightpos v
        if cosangle < 0.0f then 0.0f else cosangle
    match pointOnSphere r x y with
    | Some(posz, negz) -> lighting posz negz
    | None -> background
 
let kSphereRad = 512.0f
let kLightPos = { X = 2048.0f; Y = -2048.0f; Z = -2048.0f }
let kAmbient = 0.1f
let kBackground = 0.2f
 
let myIllum = illum kBackground kAmbient kLightPos kSphereRad
 
let toPixelValue brightness =
    let ibright = (brightness * 255.0f) |> int
    Color.FromArgb(ibright, ibright, ibright)
 
[<EntryPoint>]
let main argv = 
    let startTime = Environment.TickCount
    let bm = new Bitmap(2 * (int kSphereRad), 2 * (int kSphereRad), PixelFormat.Format24bppRgb)
    for y = 0 to ((int)(2.0f * kSphereRad)) - 1 do
        for x = 0 to ((int)(2.0f * kSphereRad)) - 1 do
            let pv = myIllum ((float32 x)-kSphereRad) ((float32 y)-kSphereRad) |> toPixelValue
            bm.SetPixel(x, y, pv)
    let endTime = Environment.TickCount
    printf "total time: %d ticks\n" (endTime - startTime)
 
    bm.Save("output.jpg", ImageFormat.Jpeg);
    0

4 thoughts on “I’m Old, Part XIII: Rendering Spheres”

  1. My code which computed fractals ran on the Xenix system at work. After each 32 pixels it would check the /dev/tty nodes to see it any of the serial ports had activity. If any users were active my code would “nice” itself.

    This is how I was able to consume weeks worth of CPU time.

    1. Clever. I never thought of doing anything like that. Probably because I was taking passive aggressive revenge on the Scheme users who routinely swallowed the CPU whole while doing their “homework” for class “assignments”.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.