BIT-101
Bill Gates touched my MacBook Pro
Let’s do 3D for a while!
In the past few weeks, I’ve taken a break from making music. I’m sure I’ll get back to it, but I was doing it just about every day for five plus months and I needed to step away. I’ve been thinking about dipping my toes in 3D, so why not follow that path for a bit?
I installed Blender and rendered a gray cube, then got overwhelmed by the UI. There’s all kinds of menus and all kinds of panels. And the panels have menus, and the menus have panels. And I don’t know what most of it means. I’m sure I could learn, but it just wasn’t calling to me. I’m not a big UI fan. For coding, I don’t like fancy UIs or graphical text editors. I use neovim. For graphics, I don’t use PhotoShop or any other paint or vector graphics program. I create graphics in code, in neovim. For animations, I don’t use AfterEffects or any other animation package. I create frames in code and then create movies and gifs with ffmpeg - in neovim. For music, I am not a fan of DAWs (Digital Audio Workstations) like Ableton or Reason or whatever. I like Supercollider (which I code in neovim) and trackers, and hardware synths and sequencers.
So for 3D, there was an obvious solution…
POV-Ray (persistence of vision raytracer) is a 3D raytracing program that has been around for decades. I’m probably just going to call it “povray” or “pov” for the rest of this article. While it does have its own IDE on some platforms, you can also just write a .pov
scene file in plain text and render it with the povray command line tool. Guess which editor I’m writing my .pov
files in!
POV-Ray goes deep. Deeper than I expected when I started. There’s a lot to learn, but unlike Blender, it’s not all in your face, intimidating you. You can just write a simple scene and render it and keep adding things to it as you learn more. I know, I know, you can do that in Blender too. But when all those features are in my face, it’s distracting.
The purpose of this post isn’t to teach you povray. There are plenty of tutorials and videos going back many years on that. But I just wanted to walk through some modern solutions to rendering things with povray and the path I took. Maybe you’ll find it useful. I’ll also throw in some random videos I’ve created with pov recently. They won’t necessarily be related to the topics I’m discussing in the text at that point, but they should break things up and prevent a huge wall of text and code. Well… here’s one now!
Here’s a pretty simple scene file that renders a box sitting on a plane, with a sky-like background, some lights and a camera.
#version 3.7;
#include "stdinc.inc"
#include "stones.inc"
global_settings { assumed_gamma 1 }
light_source {
<-4, 4, -3>
White
}
light_source {
<8, 8, 0>
White
}
camera {
right x * image_width / image_height
location <2, 2, -4>
look_at <0, 0, 0>
}
box {
<-1, -1, -1>
<1, 1, 1>
texture { T_Stone10 }
}
plane {
<0, 1, 0>, -1
texture { T_Stone40 }
}
sky_sphere {
pigment {
gradient y
color_map {
[0 color White]
[1 color Blue]
}
scale 2
translate -1
}
}
Yeah, it’s not nothing, but each piece is relatively straightforward. The light sources have a position and a color (there are more complex light sources, but these work fine). The camera has a location and a point it’s looking at. It also has that first line which sets up the aspect ratio of the camera. That one is boilerplate. The box is made from two points and a texture. The plane has a vector showing it’s orientation: 1 on the y axis makes it extend out on the x and z axes. And it has a position on the y axis, -1 puts it right under the box. The sky sphere is a bit more complex, but you can see it makes a white/blue gradient on the y axis. And here’s what you get:
It uses some built-in textures that are included at the top of the file.
When I say procedural here, I don’t mean “generative” or “algorithmic”, aka “random”. I mean the difference between procedural code where you tell the computer what to do step by step, and declarative code, where you just describe what you want to have happen. POV-Ray scene files are declarative. You describe the scene and let pov figure out how to create it. As opposed to saying, for example…
This might look like in pseudocode:
scene = new Scene()
box = new Box(2, 2, 2)
box.position(0, 0, 0)
box.setTexture(Stone10)
scene.add(box)
...
Random video alert!
So, yeah, I set out to do something like that using Go. And I got pretty far. You can see my progress here:
In brief, the scene has an array of objects. You can create various objects and assign them different properties and when you are done, it will serialize the scene and all its objects into a .pov
file that you can then render with povray. In fact, the program even took care of running povray for you and displaying the image.
Say you have a sphere object with x, y, z and radius properties. This is a struct in Go. It get serialized as:
sphere {
<2, 3, 4> 2
}
Of course, it uses the struct properties to fill in the text that’s written to the .pov
file. It mostly does that with sprintf
statements sort of like sprintf("<%f, %f, %f> %f", sphere.x, sphere.y, sphere.z, sphere.radius)
. Pretty simple stuff.
Transforms can be used on objects or textures, patterns, etc. You can say, for example, translate <1, 2, 3>
or scale <2, 3, 4>
or rotate <10, 20, 30>
to transform on separate axes.
But you can also transform on all axes with one value: scale 5
. Or you can transform on only one axis like: scale 2*y
- scales by 2 on the y axis only.
So now there are three different transform syntaxes I had to account for. Not impossible. I started down that path, but it was getting a bit messy.
An object can have a texture, a material, a pigment, a normal, and a finish, which combine to define how the object looks. In fact, it can have more than one of all these things. And you can combine these in different ways. For example, you can add a pigment directly to an object, or you can put it inside a texture. And you can have pigments and textures that are predefined, like T_Stone10
and Blue
used above, or you can have colors defined with rgb <r, g, b>
and manually create textures on a lower level.
In short, a lot of permutations to account for in code. I did not go very far down that path.
To animate in povray, you tell it how many frames you want to render. Let’s say you want to render 60 frames. Now in your .pov
file you have access to a variable called clock
, which will be 0.0 on the first frame and 1.0 on the last frame. So you can rotate an object around the y axis like: rotate 360*y*clock
I was happy to see this because it’s very similar to how I animate in my own 2D rendering library. The render function gets passed a percent
value that ranges from 0.0 to 1.0 depending on what frame it’s rendering. I can use that in all kinds of ways to make single or smoothly looping animations.
But I struggled to find a good way to represent this clock
variable in my Go code. With all other values, I could use whatever expressions I wanted and distill them down to a final serialized value. With animation, I needed to preserve the expression, 360*y*clock
and serialize that.
The solution for the clock
and increasingly for many of the other issues I was running into, was just to save everything as strings. And eventually the absurdity of what I was doing hit me. I was using an external program to store a bunch of strings that I wrote to a text file. Why not just write the text file directly???
So that began phase two.
So I switched my workflow to just writing .pov
files. This was great. When I learned a new object or property I didn’t have to create a new object to represent it and figure out how to serialize it. I could just write it.
At this point I started diving into creating a great build system. I like using make
and I have F5
set up in neovim to just run make
which will look for a Makefile
in the current directory and execute it.
Here’s the Makefile
I’m currently using:
FRAMES=60
# ZEROS = number of digits in FRAMES
ZEROS=2
CYCLIC=true
WIDTH=800
HEIGHT=600
VERBOSE=false
CONSOLE=false
ANTIALIAS=true
FPS=30
# image or video
default: image
# if you change a variable above and want to force a re-render
# default: clean image
################
# Single frame
################
image: out/out.png
@eog ./out/out.png 2&> /dev/null
out/out.png: main.pov
@echo "building image..."
@rm -rf out
@mkdir -p out
@povray \
main.pov \
Antialias=$(ANTIALIAS) \
Display=false \
Verbose=$(VERBOSE) \
All_Console=$(CONSOLE) \
+W$(WIDTH) +H$(HEIGHT) \
Output_File_Name=./out/out.png
################
# Animation
################
video: out/out.mp4
@mpv -loop ./out/out.mp4 2&> /dev/null
out/out.mp4: out/frames
@echo "generating video..."
@ffmpeg \
-y \
-framerate $(FPS) \
-i ./out/frames/frame_%0$(ZEROS)d.png \
-s:v $(WIDTH)x$(HEIGHT) \
-c:v libx264 \
-profile:v high \
-crf 20 \
-pix_fmt yuv420p \
./out/out.mp4
out/frames: main.pov
@echo "generating frames..."
@rm -rf out
@mkdir -p out/frames
@povray \
main.pov \
Antialias=$(ANTIALIAS) \
Display=false \
+W$(WIDTH) +H$(HEIGHT) \
Final_Frame=$(FRAMES) \
Cyclic_Animation=$(CYCLIC) \
Verbose=$(VERBOSE) \
All_Console=$(CONSOLE) \
Output_File_Name=./out/frames/frame_.png
################
# Other
################
clean:
@echo "cleaning..."
@rm -rf out
I won’t go into this in too much depth, but will point out some things I am proud of.
Up top are all the variables and settings that get incorporated in the rest of the build.
Running make
will execute the first target, default
. You can set either image
or video
as a prerequisite here and it will run either of those targets.
The image
target will check out/out.png
. This will check main.pov
. If that main.pov
is newer than out/out.png
or if the png doesn’t exist, it will run that target and build the image. The image
target will the display the image.
Similarly, the video
target will check out/out.mp4
which will check the date of the out/frames
directory to see if it needs to re-render the video from the frames, and will check main.pov
to see if it needs to regenerate those frames.
This means that if you just spent 10 minutes rendering a video and run make
again when nothing has changed, it’s not going to start from scratch. It knows that everything is up to date and will just display the existing video.
This was all working great until I started doing some more complex renders. I started algorithmically generating lots of objects using various formulas. This requires things like variables and functions. POV-Ray has these things… kind of.
“Variables” are defined with #declare
statements (or #local
statements). But if you want to change the value of a variable you have to redeclare it. Like so:
#declare X = 0;
...
#declare X = X + 1;
You’re supposed to use capital letters to start variables in most places. And in spite of the fact that most povray code does not need semicolons, #declare
and #local
statements do.
There are two function-like structures: functions and macros. Functions are also declared and can have parameters (floats only) and can return a single value (float only).
#declare Foo = function(n) {
n * 2
};
#declare Bar = Foo(7);
// Bar will now equal 14
Functions can take vectors (<x, y, z>
values) but can only be used in specific transform type cases. Otherwise, if you want to affect more than one value, you’d have to use a macro and nest #declare
statements inside your function.
#macro Multiply2(X, Y, Z)
#declare X = X * 2;
#declare Y = Y * 2;
#declare Z = Z * 2;
#end
#declare X = 1;
#declare Y = 2;
#declare Z = 3;
Multiply(X, Y, Z);
// X, Y, Z will now equal 2, 4, 6
Note that functions are defined with brackets { }
while macros use #macro
and #end
. Also, function parameters should be lower case while macro params should be upper case. Odd syntax stuff like this makes variables, functions and macros rather painful.
Also note that I’m hardly an expert here. This stuff is a bit confusing and old fashioned, so I may have gotten some things wrong or overcomplicated it. I’m happy to be corrected if so.
So I wound up taking a hybrid approach. If I need to create a complex object that requires a lot of math, functions, recursion, etc., I’ve taken to building that in Go and just outputting a file that defines the object itself. I can then import that object into my scene file and instantiate it, transform it, add textures, materials, etc.
I’m not going to deep dive into that here, but if you want to take a look at an example, you can see https://github.com/bit101/pov_experiments/tree/main/250613b
And here’s the result of that:
I don’t know how the hell I would have done something that complex in povray only.
I always feel like I have to have a summary rather than just ending off on that last thought. But I’m not always sure what to summarize. Maybe I’m just showing off. Or documenting this so when I come back to it in a few years I can be like, “oh yeah, that’s how I handled that…”. Anyway, maybe you’ll see more content along these lines. Maybe I’ll ricochet off in some other direction next week. One way or the other, I’ll be back.
Comments? Best way to shout at me is on Mastodon