~nprescott/ray-tracer

ref: 6f3b5e900ee148827e07f0702de06ca08b157d7e ray-tracer/tracer.lisp -rw-r--r-- 10.2 KiB
6f3b5e90Nolan Prescott clean up PPM wrapping RGB strings 1 year, 6 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
(defpackage :com.nprescott.raytracer
  (:use :common-lisp))

(defmacro defun-vector-binop (binop)
  `(defun ,(intern (concatenate 'string "VECTOR" (string binop))) (v1 v2)
     (vector (,binop (aref v1 0) (aref v2 0))
             (,binop (aref v1 1) (aref v2 1))
             (,binop (aref v1 2) (aref v2 2))
             (,binop (aref v1 3) (aref v2 3)))))

(defun-vector-binop +)
(defun-vector-binop -)
(defun-vector-binop *)
(defun-vector-binop /)

(defmacro vector-getters (aliases)
  `(progn
     ,@(loop for name in aliases
          for index = 0 then (1+ index)
          collect
            `(defmacro ,name (some-vector)
               `(aref ,some-vector ,,index)))))

(defmacro point (x y z)
  `(vector ,x ,y ,z 1))

(defmacro vec3 (x y z)
  `(vector ,x ,y ,z 0))

(vector-getters (RED GREEN BLUE))
(vector-getters (X Y Z W))

(defun negate-vector (v)
  (VECTOR- (vec3 0 0 0) v))

(defun scale-vector (v scalar)
  (VECTOR* v (vec3 scalar scalar scalar)))

(defun magnitude (some-vector)
  (labels ((square (n) (expt n 2)))
    (sqrt (+ (square (X some-vector))
             (square (Y some-vector))
             (square (Z some-vector))))))

(defun normalize (some-vector)
  (scale-vector some-vector (/ 1 (magnitude some-vector))))

(defun dot-product (v1 v2)
  (reduce #'+ (VECTOR* v1 v2)))

(defun cross-product (v1 v2)
  (vec3 (- (* (Y v1) (Z v2)) (* (Z v1) (Y v2)))
        (- (* (Z v1) (X v2)) (* (X v1) (Z v2)))
        (- (* (X v1) (Y v2)) (* (Y v1) (X v2)))))

(defun blend (c1 c2)
  (VECTOR* c1 c2))

(defstruct (canvas (:constructor make-canvas (&key width height)))
  width
  height
  (body (make-array (list width height) :initial-element (vector 0 0 0))))

(defun write-pixel (canvas x y color)
  (flet ((within-bounds (x y max-x max-y)
           (and (< x max-x) (< y max-y) (>= x 0) (>= y 0))))
    (when (within-bounds x y (canvas-width canvas) (canvas-height canvas))
      (setf (aref (canvas-body canvas) (floor x) (floor y)) color))))

(defun canvas-pixel-strings (c)
  (labels ((color->rgb (n) (floor (* 255 n)))
           (pixel->string (v) (format nil "~d ~d ~d"
                                      (color->rgb (Red v))
                                      (color->rgb (Green v))
                                      (color->rgb (Blue v)))))
    (let ((strings (list)))
      (dotimes (i (canvas-height c))
        (dotimes (j (canvas-width c))
          (push (pixel->string (aref (canvas-body c) j i)) strings)))
      ; FIXME: figure out how to pass 70 as an argument to format
      (format nil "~{~<~%~1,70:;~a~>~^ ~}" strings))))

(defun canvas->ppm (c)
  (format nil "P3~%~s ~s~%255~%~a~%"
          (canvas-width c) (canvas-height c) (canvas-pixel-strings c)))

(defun ppm->file (ppm-string path)
  (with-open-file (stream path :direction :output :if-exists :supersede)
    (format stream ppm-string)))

(defun matrix*vector (matrix vec)
  (let* ((m (array-dimension matrix 0))
         (n (length vec))
         (result (make-array m :initial-element 0)))
    (dotimes (i m result)
      (dotimes (j n)
        (incf (aref result i)
              (* (aref matrix i j)
                 (aref vec j)))))))

(defun matrix*matrix (a b)
  (let* ((m (array-dimension a 0))
         (n (array-dimension b 1))
         (common (array-dimension b 0))
         (result (make-array (list m n) :initial-element 0)))
    (dotimes (i m result)
      (dotimes (j n)
        (dotimes (k common)
          (incf (aref result i j)
                (* (aref a i k)
                   (aref b k j))))))))

(defun submatrix (m row column)
  (let ((result (make-array (mapcar #'1- (array-dimensions m)))))
    (loop for i below (array-dimension m 0)
       unless (= i row)
       do (loop for j below (array-dimension m 1)
             unless (= j column)
             do (let ((ii i)
                      (jj j))
                  (if (> i row) (setf ii (1- i)))
                  (if (> j column) (setf jj (1- j)))
                  (setf (aref result ii jj)
                        (aref m i j))))
       finally (return result))))

(defun cofactor (m row column)
  (if (oddp (+ row column))
      (- (minor m row column))
      (minor m row column)))

(defun determinant (m)
  (if (equalp (array-dimensions m) '(2 2))
      (- (* (aref m 0 0) (aref m 1 1))
         (* (aref m 0 1) (aref m 1 0)))
      (let ((result 0))
        (dotimes (i (array-dimension m 0) result)
          (incf result (* (cofactor m 0 i) (aref m 0 i)))))))

(defun minor (m row column)
  (determinant (submatrix m row column)))

(defun invertible? (m)
  (not (eq (determinant m) 0)))

(defun inverse (m)
  (when (invertible? m)
    (let ((d (determinant m))
          (result (make-array (array-dimensions m))))
      (dotimes (row (array-dimension m 0) result)
        (dotimes (column (array-dimension m 1))
          (setf (aref result column row)
                (/ (cofactor m row column) d)))))))

;;; since we're never taking the identity but for a 4x4 matrix...
(defvar identity-matrix #2A((1 0 0 0)
                            (0 1 0 0)
                            (0 0 1 0)
                            (0 0 0 1)))

(defun transpose (m)
  ;; isn't this wrong for non-square matrices?
  (let ((result (make-array (array-dimensions m))))
    (dotimes (i (array-dimension m 0) result)
      (dotimes (j (array-dimension m 1))
        (setf (aref result i j)
              (aref m j i))))))

(defun translation (x y z)
  (make-array '(4 4) :initial-contents `((1 0 0 ,x)
                                         (0 1 0 ,y)
                                         (0 0 1 ,z)
                                         (0 0 0  1))))

(defun scaling (x y z)
  (make-array '(4 4) :initial-contents `((,x  0  0 0)
                                         ( 0 ,y  0 0)
                                         ( 0  0 ,z 0)
                                         ( 0  0  0 1))))

(defun rotation-x (r)
  (make-array '(4 4) :initial-contents `((1     0         0      0)
                                         (0 ,(cos r) ,(-(sin r)) 0)
                                         (0 ,(sin r)   ,(cos r)  0)
                                         (0     0         0      1))))

(defun rotation-y (r)
  (make-array '(4 4) :initial-contents `((,(cos r)    0 ,(sin r) 0)
                                         (    0       1     0    0)
                                         (,(-(sin r)) 0 ,(cos r) 0)
                                         (    0       0     0    1))))

(defun rotation-z (r)
  (make-array '(4 4) :initial-contents `((,(cos r) ,(- (sin r)) 0 0)
                                         (,(sin r) ,(cos r)     0 0)
                                         (    0        0        1 0)
                                         (    0        0        0 1))))

(defun shearing (xy xx yx yz zx zy)
  (make-array '(4 4) :initial-contents `((  1 ,xy ,xx 0)
                                         (,yx   1 ,yz 0)
                                         (,zx ,zy   1 0)
                                         (  0   0   0 1))))

(defstruct material
  (color (vec3 1 1 1))
  (ambient 0.1)
  (diffuse 0.9)
  (specular 0.9)
  (shininess 200))
(defstruct light intensity position)
(defstruct ray origin direction)
(defstruct sphere
  (transformation identity-matrix)
  (material (make-material)))
(defstruct crosspoint time object)

(defun ray-position (r time)
  (VECTOR+ (ray-origin r) (scale-vector (ray-direction r) time)))

(defun intersect (s r)
  (let* ((inv-ray (transform r (inverse (sphere-transformation s))))
         (sphere-to-ray (VECTOR- (ray-origin inv-ray) (point 0 0 0)))
         (a (dot-product (ray-direction inv-ray) (ray-direction inv-ray)))
         (b (* 2 (dot-product (ray-direction inv-ray) sphere-to-ray)))
         (c (- (dot-product sphere-to-ray sphere-to-ray) 1))
         (discriminant (- (expt b 2) (* 4 a c))))
    (if (< discriminant 0)
        (list)
        (list (make-crosspoint :time (/ (- (- b) (sqrt discriminant)) (* 2 a))
                               :object s)
              (make-crosspoint :time (/ (+ (- b) (sqrt discriminant)) (* 2 a))
                               :object s)))))

(defun hit (intersections)
  (flet ((positivep (x) (> (crosspoint-time x) 0)))
    (if (some #'positivep intersections)
        (first (sort (remove-if-not #'positivep (copy-seq intersections))
                     #'< :key #'crosspoint-time))
        nil)))

(defun transform (ray matrix)
  (let ((o (matrix*vector matrix (ray-origin ray)))
        (d (matrix*vector matrix (ray-direction ray))))
    (make-ray :origin o :direction d)))

;;; should probably just drop this entirely...
(defun set-transform (s transform)
  (setf (sphere-transformation s) transform))

(defun normal-at (sphere world-point)
  (let* ((object-point (matrix*vector (inverse (sphere-transformation sphere)) world-point))
         (object-normal (VECTOR- object-point (point 0 0 0)))
         (world-normal (matrix*vector (transpose (inverse (sphere-transformation sphere)))
                                      object-normal)))
    (setf (aref world-normal 3) 0)      ; FIXME submatrix 3,3 instead
    (normalize world-normal)))

(defun reflect (in normal)
  (VECTOR- in (scale-vector normal (* 2 (dot-product in normal)))))

(defun lighting (material light position eye-vector normal-vector)
  (let* ((effective-color (blend (material-color material) (light-intensity light)))
         (lightv (normalize (VECTOR- (light-position light) position)))
         (ambient (scale-vector effective-color (material-ambient material)))
         (light-dot-normal (dot-product lightv normal-vector))
         (black (vec3 0 0 0))
         (diffuse black)
         (specular black)
         (reflect-dot-eye (dot-product (reflect (negate-vector lightv) normal-vector)
                                       eye-vector)))
    (if (> light-dot-normal 0)
        (setf diffuse (scale-vector
                       (scale-vector effective-color (material-diffuse material))
                       light-dot-normal)))
    (if (> reflect-dot-eye 0)
        (setf specular (scale-vector (light-intensity light)
                                     (* (material-specular material)
                                        (expt reflect-dot-eye
                                              (material-shininess material))))))
    (reduce #'VECTOR+ (list ambient diffuse specular))))