Untitled Ray Tracer Post
More progress on the Ray Tracer Challenge book by Jamis Buck, this week adding groups, bounding boxes, triangles, and loading of external models, somewhat brought together in this chessboard image:
The two sides are the same group of items with different transforms on them, the chess pieces are 3D models I bought from ivaxia on itch.io and then loaded (before now everything was generated programatically). I have no affinity for chess particularly, but it is another nod to the themes of the early ray traced images you'd find floating around on floppy disks shared with friends.
Picking apart some of what I worked on: the groups were interesting, as whilst it's nice being able to make compound shapes, they mostly thus far have just been used to unlock other features: bounding boxes to improve performance and then stringing together a lot of triangles to load up external 3D models. By grouping objects and testing ray intersects on the outer bounding box before the individual shapes you can save a lot of time, encouraging it to be used not just to group objects based on logical grouping (e.g., these three cylinders make a dumbbell shape), but also based on spatial grouping (e.g., put the left half of the scene in group 1, the right half of the scene in group 2).
One fun programmatic consequence of doing all this in OCaml is that the book assumes a sort of doubling linked tree structure for maintaining groups so you can both work out how things are contained and then the set of transforms to apply to the individual items. In OCaml I'm keeping things simple and immutable, so doubly linked structures are not ideal, and so in the end I render my groups to world space as I create them, which is actually a sensible optimisation to take anyway: it seems that it's common in ray tracers (citation needed) to have your world description as transforms on objects in local coordinates, and then render that to a globally specified scene where every object has the transforms applied before rendering, and here I was forced to do that from the start.
For loading data the book uses OBJ files, in part because they're common but in part because it's easy to build a parser for it, being a simple text based file format. However, one downside of OBJ format is that it doesn't handle colours or materials on items, and my standard set of test models I've used for 3D work since at least twenty years ago is the set of spaceships from the Archemedes version of Elite, which I found on the Internet many years ago in VRML. VRML was mid-nineties file format envisaged for having 3D models in these new fangled "web browsers", along when we also thought we'd have an imminent 3D default web experience via Hyper-G - heady days.
Anyway, VRML is a little messy to parse, and I found that it was superseded by an XML based format (of course it was) called X3D. So using the open source MeshLab I converted my VRML models to X3D, and then after chatting with Patrick about which OCaml XML library to use, ended up with the ability to load my store of legacy space ship models:
This starts to highlight the limits of a single light source. I cheated on the "engines" on the Cobra here by having the red colour have a channel values in excess of 100%, so they look like they're lit, but effectively are bright enough to remain bright even in shadow. Something I know from photography experience is that generally you want multiple sources of light in a scene, and so I'd like to bring that over to this, rather than relying on the "ambient" trick that Phong lighting does, whereby materials effectively emit a certain amount of background light themselves to make up for the lack of ambient lighting.
My code for parsing the X3D is a bit of a mess truth be told, which is lack of OCaml practice. Less so algorithmically, though I'm sure there could be improvements there, but still my brain does a top down implementation rather than thinking about building the components I need and working up to the solution, so I end up with these very hard to read, unweildly functions that I then need to go back and tidy up. But that's kinda the point of doing this, to get some more practice. This isn't helped by X3D being a somewhat weak format: you have lists of points that you link to lists of coordinates to make polygons and another list of colours that aligns with that list, and it's all very weakly tied together so there's a lot of scope for poorly formatted files that you should allow for. Not a great file format, and at some point I'll try swap it out for something else.
Whilst the 3D model loading was fun, I have to confess the most fun I've had with the ray tracer is making nice abstract animation pieces:
I retooled my library so that the SDL window wasn't the main way to use it, I can now just loop scene generation and save the results straight to disk, which I can then pull together with ffmpeg into a video to share. This gave me an excuse to get more into OCaml's support for multithreading, playing with Atomics for instance.
Performance is still a concern, particularly as I start to render animations that are a hundred or so frames. I had a hunch that my naive matrix implementation was a significant source of performance loss, due to how many allocations it has to do every calculation, and I was able to use Instruments on macOS to confirm that was indeed the case. It's quite nice that Instruments can extract function and module names form OCaml programs to let you see what's going on, and works nicely with OCaml domains (multithreading):
I'll tackle that next, as the chess scene on the opening was quite slow to render, but that'll have to wait for a while: this has been a lot of fun and I've learned a bunch, but it's back to the Python trenches as I have other commitments that'll take up all my keyboard time in the coming weeks.