Physics and 3d models in Scheme
August 3, 2009
I finished two major parts of my Scheme 3d graphics framework this past week, and got it running on the iPhone: basic physics simulation and importing 3d models.
Apple's been getting some really terrible press about the iPhone App Store recently, from Google Voice being taken down to general crapness in their treatment of developers. I'm still confident that I'll be able to get my Scheme apps through the App Store, but please note that most of my work here is applicable for any Scheme game, whether it's on Windows, OS X, or whatever. I look forward to working on full PC games when I have enough resources.
In this video, you can see my current physics environment. I implemented velocity and acceleration, which lets me simulate gravity by applying a global acceleration downward of about 9.8 m/s. I proceed to toss balls up in the air randomly, and gravity takes care of the rest.
I then use the procedure `kick` to kick around balls. It simply pushes all the objects in the scene by adding a velocity vector to each object, giving them instant acceleration in the specified direction.
Lastly, I change gravity so the balls gravitate to other areas in the world. Pretty fun stuff.
I'm able to develop interactively using the technique described in my previous post about using remote REPLs.
I wrote a OBJ file parser so that I can finally import 3d models. It wasn't really that hard, and I think I will just post it here. It's a work in progress, of course, and will be tracked in my git project in the `lib` directory.
Honestly, I didn't do a whole of research before I wrote this, but first, it didn't seem like there was a simple OBJ loader written in C out there, and second, it's nice to have this native in Scheme anyway. It sure looks a lot prettier than this loader written in Objective-C.
I also found this blog post, which suggests using a Blender script to output... C header files with embedded data? That's crazy! I can't imagine having to re-compile my program every time I changed a model.
(define (read-map #!optional f)
(unfold (lambda (x) eof-object?)
(lambda (x) (if f (f x) x))
(lambda (x) (read))
(define (enforce-length name len lst)
(if (eq? (length lst) len)
(error name "assert-length failed")))
(enforce-length "vertex" 3 (read-map exact->inexact)))
(let ((v (vec3d-unit
(enforce-length "normal" 3
(list (vec3d-x v) (vec3d-y v) (vec3d-z v))))
(enforce-length "face" 3 (read-map (lambda (n) (- n 1)))))
(define (obj-parse-line obj line)
(define (appendd lst lst2)
(append (reverse lst) lst2))
(let ((type (read)))
(really-make-obj #f #f '() '() '()))
(define (obj-load file #!optional avoid-c-vectors?)
(define (convert data)
(list->vector (reverse data)))
(define (make-c-vectors mesh)
(if (not avoid-c-vectors?)
(vector->GLfloat* (obj-vertices mesh)))
(vector->GLfloat* (obj-normals mesh)))
(vector->GLushort* (obj-indices mesh))))))
(let ((mesh (make-obj)))
(let loop ()
(let ((line (read-line)))
(if (not (eof-object? line))
(obj-parse-line mesh line)
(obj-num-vertices-set! mesh (length (obj-vertices mesh)))
(obj-num-indices-set! mesh (length (obj-indices mesh)))
(obj-vertices-set! mesh (convert (obj-vertices mesh)))
(obj-normals-set! mesh (convert (obj-normals mesh)))
(obj-indices-set! mesh (convert (obj-indices mesh)))
There are a few caveats to my loader: it doesn't index normals (assumes every vertex has a normal), requires triangulation (faces can only have 3 vertices), and doesn't support texture mapping yet. I will add texture mapping soon, but as to the other caveats, I'm not sure if I want to complicate my loader when I can simply tell Blender to export things properly. Maybe someday though.
Now, it's a bit slow to load a model with about 3000 vertices and 2000 indices. Why not use Gambit's nice serialization features to speed this up?
Folks, *this* is why I use Scheme, and Gambit Scheme specifically. Gambit Scheme provides two interesting procedures: `object->u8vector` and `u8vector->object`. These procedures convert objects to byte vectors and back. Byte vectors can easily be written to files, too!
So, by adding the following two procedures to our OBJ loader, we immediately support a very efficient binary model format. `compress` basically takes the constructed mesh, converts it to a `u8vector` and saves it out to disk. `decompress` does the opposite.
(define (compress filename mesh)
(let* ((v (object->u8vector mesh))
(len (u8vector-length v))
(len-u8 (object->u8vector len))
(boot (u8vector-length len-u8)))
(write-subu8vector len-u8 0 boot)
(write-subu8vector v 0 (u8vector-length v))))))
(define (decompress filename)
(let* ((boot (read-u8))
(len-u8 (make-u8vector boot)))
(read-subu8vector len-u8 0 boot)
(let* ((len (u8vector->object len-u8))
(v (make-u8vector len)))
(read-subu8vector v 0 len)
I created a quick script to compress meshes:
;;; saves it with the ".gso" extension
(define (main filename)
(compress (string-append filename ".gso") (obj-load filename #f #t)))
I modified `obj-load` to take an extra parameter indicating if the file is compressed or not. So, lets see what we gained with this no-effort improvement:
james% du -h logo.obj*
(obj-load "logo.obj") => 1.05s
(obj-load "logo.obj.gso" #t) => .215s
We got a 32% compression and 5x load time speed increase!