summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2020-07-14 15:42:26 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2020-07-14 21:26:51 -0400
commit7fd397b331fc1cb48de3c54df04aa0de7b5bbaaf (patch)
tree9b4229189cf4283f63aba25527a43379e6cae5e1
parent50f655d1a459930200c2b90e78f554535aa3d66c (diff)
[r6rs] Add implementation.
-rw-r--r--r6rs-raytracer.scm413
1 files changed, 413 insertions, 0 deletions
diff --git a/r6rs-raytracer.scm b/r6rs-raytracer.scm
new file mode 100644
index 0000000..2d3f525
--- /dev/null
+++ b/r6rs-raytracer.scm
@@ -0,0 +1,413 @@
+;;; Copyright © 2020 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+;;;
+;;; This program is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; This program is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(import (rnrs (6))
+ (rnrs lists (6)))
+
+(define (println . items)
+ (for-each display items)
+ (newline))
+
+(define (sum list) (fold-left + 0 list))
+(define (square x) (* x x))
+
+(define (some n) (cons 'some n))
+(define (none) 'none)
+
+(define (is-some? n) (and (pair? n) (eq? 'some (car n))))
+(define (is-none? n) (eq? 'none n))
+
+(define (unwrap n)
+ (if (is-some? n)
+ (cdr n)
+ (error 'unwrap "Tried to unwrap `none'.")))
+
+(define (map-option proc n)
+ (if (is-some? n)
+ (some (proc (unwrap n)))
+ n))
+
+(define-syntax maybe-bind
+ (syntax-rules ()
+ ((maybe-bind ((name option) ...)
+ body)
+ (if (for-all is-some? (list option ...))
+ (let ((name (unwrap option))
+ ...)
+ body)))))
+
+
+;;;
+;;; Image encoding.
+;;;
+
+;; Encode the WIDTH by HEIGHT image given as PIXELS into the portable pixmap
+;; format (PPM), writing the result to `(current-output-port)'.
+(define (write-ppm width height pixels)
+ (define (delimit-values values)
+ (cond ((null? values)
+ (newline))
+ ((= 1 (length values))
+ (display (car values))
+ (delimit-values (cdr values)))
+ (else
+ (display (car values))
+ (display " ")
+ (delimit-values (cdr values)))))
+
+ ;; Magic
+ (delimit-values '("P3"))
+
+ ;; Dimensions
+ (delimit-values (list width height))
+
+ ;; Depth
+ (delimit-values '("255"))
+
+ ;; Image contents
+ (for-each delimit-values (vector->list pixels)))
+
+
+;;;
+;;; I really don't want to deal with all of my vectors being lists. Also, I'm
+;;; going to guess that -- at least in CHICKEN -- this is going to be faster
+;;; than passing around list.
+;;;
+
+(define-record-type vec3 (fields x y z))
+
+;; Macro for destructuring a vec3.
+(define-syntax vec3-bind
+ (syntax-rules ()
+ ((vec3-bind ((names vec) ...)
+ body)
+ (let-values ((names (values (vec3-x vec)
+ (vec3-y vec)
+ (vec3-z vec)))
+ ...)
+ body))))
+
+;; Return the sum of VECS, as in vector space addition.
+(define (vec3+ . vecs)
+ (define (add u v)
+ (vec3-bind (((x1 y1 z1) u) ((x2 y2 z2) v))
+ (make-vec3 (+ x1 x2) (+ y1 y2) (+ z1 z2))))
+ (if (zero? (length vecs))
+ (make-vec3 0 0 0)
+ (fold-left add (car vecs) (cdr vecs))))
+
+;; Return the difference of VECS, as in vector space subtraction.
+(define (vec3- . vecs)
+ (define (sub u v)
+ (vec3-bind (((x1 y1 z1) u) ((x2 y2 z2) v))
+ (make-vec3 (- x1 x2) (- y1 y2) (- z1 z2))))
+ (if (zero? (length vecs))
+ (make-vec3 0 0 0)
+ (fold-left sub (car vecs) (cdr vecs))))
+
+;; Return the vector U scaled by a constant C, as in vector space scalar
+;; multiplication.
+(define (vec3* c u)
+ (vec3-bind (((x y z) u))
+ (make-vec3 (* c x) (* c y) (* c z))))
+
+;; Return the dot product of the vectors U and V.
+(define (vec3-dot u v)
+ (+ (* (vec3-x u) (vec3-x v))
+ (* (vec3-y u) (vec3-y v))
+ (* (vec3-z u) (vec3-z v))))
+
+;; Return the cross product of the vectors U and V.
+(define (vec3-cross u v)
+ (vec3-bind (((x1 y1 z1) u) ((x2 y2 z2) v))
+ (make-vec3 (- (* y1 z2) (* z1 y2))
+ (- (* z1 x2) (* x1 z2))
+ (- (* x1 y2) (* y1 x2)))))
+
+;; Return a list (x y z) of the components of vector U.
+(define (vec3->list u)
+ (vec3-bind (((x y z) u))
+ (list x y z)))
+
+;; Return the magnitude of vector U.
+(define (vec3-magnitude u)
+ (vec3-bind (((x y z) u))
+ (sqrt (+ (square x) (square y) (square z)))))
+
+;; Return the normal vector parallel to vector U.
+(define (vec3-normalize u)
+ (vec3* (/ 1 (vec3-magnitude u)) u))
+
+
+
+(define-record-type ray (fields origin direction))
+
+;; Return the position of RAY at time T.
+(define (ray-point-at ray t)
+ (vec3+ (ray-origin ray) (vec3* t (ray-direction ray))))
+
+;; Convert D, a value in degrees, to radians.
+(define (degrees->radians d)
+ (let ((pi 3.1415926535897932384626433))
+ (* d (/ pi 360))))
+
+;; Return the ray corresponding to the point X, Y on the viewport plane.
+(define (coordinate->ray x y)
+ (let* ((dist 1.0)
+ (top (* dist (tan (degrees->radians camera-fov))))
+ (right (* top image-aspect-ratio))
+ (bottom (- top))
+ (left (- right))
+ (W (vec3-normalize (vec3- camera-position camera-target)))
+ (U (vec3-normalize (vec3-cross camera-up W)))
+ (V (vec3-cross W U))
+ (corner (vec3+ camera-position
+ (vec3* left U)
+ (vec3* bottom V)
+ (vec3* (- dist) W)))
+ (across (vec3* (* 2 right) U))
+ (up (vec3* (* 2 top) V)))
+ (make-ray camera-position
+ (vec3-normalize
+ (vec3+ corner
+ (vec3* x across)
+ (vec3* y up)
+ (vec3* (- 1) camera-position))))))
+
+
+;;;
+;;; Shapes and generic procedures for working with them.
+;;;
+
+(define-record-type plane (fields p0 n material))
+
+(define (intersect-plane ray shape t-min t-max)
+ (let* ((normal (vec3-normalize (plane-n shape)))
+ (denominator (vec3-dot (ray-direction ray) normal)))
+ (if (zero? denominator)
+ (none)
+ (let ((t (/ (vec3-dot (vec3- (plane-p0 shape) (ray-origin ray))
+ normal)
+ denominator)))
+ (if (<= t-min t t-max)
+ (some t)
+ (none))))))
+
+(define-record-type sphere (fields center radius material))
+
+(define (intersect-sphere ray shape t-min t-max)
+ (let* ((oc (vec3- (ray-origin ray) (sphere-center shape)))
+ (A (vec3-dot (ray-direction ray) (ray-direction ray)))
+ (B (* 2.0 (vec3-dot oc (ray-direction ray))))
+ (C (- (vec3-dot oc oc) (square (sphere-radius shape))))
+
+ (discriminant (- (square B) (* 4 A C)))
+ (t (if (positive? discriminant)
+ (let ((p (/ (+ (- B) (sqrt discriminant)) (* 2 A)))
+ (m (/ (- (- B) (sqrt discriminant)) (* 2 A))))
+ (if (>= m t-min) m p))
+ (/ (- B) (* 2 A)))))
+ (if (and (not (negative? discriminant))
+ (<= t-min t t-max))
+ (some t)
+ (none))))
+
+;; If RAY intersects SHAPE with T-MIN ≤ t ≤ T-MAX, return (some . t). Otherwise,
+;; return 'none.
+(define (intersect ray shape t-min t-max)
+ (let ((proc (cond ((plane? shape) intersect-plane)
+ ((sphere? shape) intersect-sphere))))
+ (proc ray shape t-min t-max)))
+
+(define (normal-plane shape position)
+ (vec3-normalize (plane-n shape)))
+
+(define (normal-sphere shape position)
+ (vec3-normalize (vec3- position (sphere-center shape))))
+
+;; Return the normal vector of SHAPE at POSITION.
+(define (normal shape position)
+ (let ((proc (cond ((plane? shape) normal-plane)
+ ((sphere? shape) normal-sphere))))
+ (proc shape position)))
+
+;; Return the material associated with SHAPE's surface.
+(define (get-material shape)
+ (let ((proc (cond ((plane? shape) plane-material)
+ ((sphere? shape) sphere-material))))
+ (proc shape)))
+
+
+;;;
+;;; Lights, and generic procedures for working with them.
+;;;
+
+(define-record-type light-sample (fields intensity position direction))
+(define-record-type spot-light (fields from to intensity exponent cutoff-angle))
+
+(define (spot-light-at light point)
+ (let* ((position (spot-light-from light))
+ (target (spot-light-to light))
+ (direction (vec3- position point))
+ (cutoff (spot-light-cutoff-angle light))
+ (pf (vec3-normalize (vec3- point position)))
+ (intensity (if (< (vec3-dot pf (vec3-normalize (vec3- target position)))
+ (cos (degrees->radians cutoff)))
+ (make-vec3 0.00 0.00 0.00)
+ (vec3* (* (/ 1 (square (vec3-magnitude direction)))
+ (expt (vec3-dot pf (vec3-normalize (vec3- target position)))
+ (spot-light-exponent light)))
+ (spot-light-intensity light)))))
+ (make-light-sample intensity position (vec3-normalize direction))))
+
+;; Return the <light-sample> produced by LIGHT at POINT.
+(define (light-at light point)
+ (let ((proc (cond ((spot-light? light) spot-light-at))))
+ (proc light point)))
+
+
+;;;
+;;; Materials.
+;;;
+
+(define-record-type material (fields ka kd ks kr kt p ior))
+(define (diffuse-material ka kd) (make-material ka kd '() '() '() '() '()))
+(define (phong-material ka kd ks p) (make-material ka kd ks '() '() p '()))
+
+;; Compute reflected vector, by mirroring l around n.
+(define (reflect l n)
+ (vec3- (vec3* (* 2.00 (vec3-dot n l)) n) l))
+
+(define (shade-pixel shape position origin)
+ (let* ((ka (material-ka (get-material shape)))
+ (kd (material-kd (get-material shape)))
+ (ks (material-ks (get-material shape)))
+ (p (material-p (get-material shape)))
+ (normal (normal shape position))
+ (Ia (vec3-bind (((x1 y1 z1) ka)
+ ((x2 y2 z2) ambient-light))
+ (make-vec3 (* x1 x2) (* y1 y2) (* z1 z2))))
+ (Id (apply vec3+
+ (map (lambda (light)
+ (let* ((sample (light-at light position))
+ (direction (light-sample-direction sample))
+ (intensity (light-sample-intensity sample))
+ (scalar (max (vec3-dot normal direction) 0)))
+ (vec3-bind (((x1 y1 z1) kd)
+ ((x2 y2 z2) intensity))
+ (make-vec3 (* x1 x2 scalar)
+ (* y1 y2 scalar)
+ (* z1 z2 scalar)))))
+ lights)))
+ (Is (if (not (null? ks))
+ (apply vec3+
+ (map (lambda (light)
+ (let* ((sample (light-at light position))
+ (point (light-sample-position sample))
+ (intensity (light-sample-intensity sample))
+ (l (vec3-normalize (vec3- point position)))
+ (v (vec3-normalize (vec3- origin position)))
+ (r (reflect l normal))
+ (scalar (expt (max 0 (vec3-dot v r)) p)))
+ (vec3-bind (((x1 y1 z1) ks)
+ ((x2 y2 z2) intensity))
+ (make-vec3 (* x1 x2 scalar)
+ (* y1 y2 scalar)
+ (* z1 z2 scalar)))))
+ lights))
+ (make-vec3 0.00 0.00 0.00))))
+ (vec3-bind (((x y z) (vec3+ Ia Id Is)))
+ (make-vec3 (min 1.0 x) (min 1.0 y) (min 1.0 z)))))
+
+
+;;;
+;;; Scene graph.
+;;;
+
+;; Arbitrarily-chosen parameters for rendering.
+
+(define image-width 1920)
+(define image-height 1080)
+(define image-aspect-ratio (/ image-width image-height))
+
+(define camera-position (make-vec3 8.00 5.00 9.00))
+(define camera-target (make-vec3 0.25 0.00 0.50))
+(define camera-up (make-vec3 0.00 1.00 0.00))
+(define camera-fov 30)
+
+;; Color of light in the scene that does not originate from a light source.
+(define ambient-light (make-vec3 0.01 0.01 0.01))
+(define lights (list (make-spot-light (make-vec3 10.00 10.00 5.00)
+ (make-vec3 0.00 0.00 0.00)
+ (make-vec3 100.00 96.00 88.00)
+ 50
+ 15)))
+(define shapes (list (make-sphere (make-vec3 -0.25 0.00 0.25)
+ 1.25
+ (phong-material (make-vec3 1.0 0.2 0.2)
+ (make-vec3 1.0 0.2 0.2)
+ (make-vec3 2.0 2.0 2.0)
+ 20))
+ (make-plane (make-vec3 0.00 -1.25 0.00)
+ (make-vec3 0.00 1.00 0.00)
+ (diffuse-material (make-vec3 1.0 1.0 0.2)
+ (make-vec3 1.0 1.0 0.2)))))
+
+;; Return the nearest shape with which RAY intersects as (some . (shape .
+;; point)), if any. Otherwise, return 'none.
+(define (ray-intersect-scene ray)
+ (map-option
+ (lambda (pair)
+ (list (car pair)
+ (ray-point-at ray (cadr pair))))
+ (fold-left (lambda (a b)
+ (cond ((is-none? a) b)
+ ((is-none? b) a)
+ (else
+ (maybe-bind ((t1 a) (t2 b))
+ (if (> (cadr t1) (cadr t2)) b a)))))
+ (none)
+ (map (lambda (shape)
+ (let ((intersection (intersect ray shape 0.001 10000)))
+ (map-option (lambda (t)
+ (list shape t))
+ intersection)))
+ shapes))))
+
+(let ((image (make-vector (* image-width image-height) '(0 0 0))))
+ (define (coordinate->index x y)
+ (+ x (* y image-width)))
+ (define (screen->viewport x y)
+ (values (/ x image-width)
+ (/ (- image-height 1 y)
+ image-height)))
+ (define (vec3->color u)
+ (map (lambda (n) (exact (round (* 255 n)))) (vec3->list u)))
+ (let loop1 ((x 0))
+ (let loop2 ((y 0))
+ (unless (>= y image-height)
+ (vector-set! image (coordinate->index x y)
+ (let-values (((x y) (screen->viewport x y)))
+ (let* ((ray (coordinate->ray x y))
+ (intersection (ray-intersect-scene ray)))
+ (if (is-some? intersection)
+ (let-values (((shape position) (apply values (unwrap intersection))))
+ (vec3->color (shade-pixel shape position (ray-origin ray))))
+ (list 0 0 0)))))
+ (loop2 (+ y 1))))
+ (unless (>= x (- image-width 1))
+ (loop1 (+ x 1))))
+ (write-ppm image-width image-height image))