├── .gitignore ├── README.md ├── image.ppm ├── raytracer.applescript ├── render.jpg └── test.ppm /.gitignore: -------------------------------------------------------------------------------- 1 | test_* 2 | output_* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # applescript-raytracer 2 | 3 | an implementation of [raytracing in one weekend](https://raytracing.github.io/books/RayTracingInOneWeekend.html) in applescript 4 | 5 | example render: 6 | 7 | ![rendered output of raytracer](render.jpg) 8 | 9 | this render took about 1 hour parallelized across 8 cpu cores 10 | 11 | to run it yourself: 12 | 13 | ```sh 14 | ./raytracer.applescript 15 | ``` 16 | 17 | this will output a [ppm image file](https://en.wikipedia.org/wiki/Netpbm#PPM_example) named `output_[timestamp].ppm` 18 | 19 | to spawn multiple worker processes, to take advantage of multiple cpu cores, run the script with the `smp` argument. no logging of incremental progress will be shown in this mode 20 | 21 | 22 | ```sh 23 | ./raytracer.applescript smp 24 | ``` 25 | 26 | you can edit `renderConfig` property [defined at the top of raytracer.applescript](raytracer.applescript#L7) to adjust the resolution, rays per pixel, and max bounces per ray, which will substantially affect the time taken to render. 27 | 28 | you can also change the `randomSeed` property to generate a different random layout of the smaller spheres 29 | 30 | with a few small changes the code also [runs on classic mac os](https://twitter.com/ur_friend_james/status/1353168983122464769) 31 | -------------------------------------------------------------------------------- /raytracer.applescript: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript 2 | 3 | property randomSeed: 20 4 | property numSMPWorkers: 16 5 | 6 | -- property renderConfig: {imageWidth: 400, samplesPerPixel: 100, maxBounces: 50} -- high quality 7 | -- property renderConfig: {imageWidth: 400, samplesPerPixel: 40, maxBounces: 50} -- med/high quality 8 | property renderConfig: {imageWidth: 400, samplesPerPixel: 4, maxBounces: 50} -- med quality 9 | -- property renderConfig: {imageWidth: 100, samplesPerPixel: 1, maxBounces: 50} -- low quality 10 | 11 | 12 | on makeColor(r, g, b) 13 | {red: r, green: g, blue: b} 14 | end 15 | 16 | on printRGB(r, g, b) 17 | set colr to (round (256 * r) rounding down) 18 | set colg to (round (256 * g) rounding down) 19 | set colb to (round (256 * b) rounding down) 20 | log ("" & colr & " " & colg & " " & colb) 21 | end 22 | 23 | on printColor(color) 24 | set colr to (round (256 * (red in color)) rounding down) 25 | set colg to (round (256 * (green in color)) rounding down) 26 | set colb to (round (256 * (blue in color)) rounding down) 27 | log ("" & colr & " " & colg & " " & colb) 28 | end 29 | 30 | on writeColor(outfile, color) 31 | set colr to (round (256 * (red in color)) rounding down) 32 | set colg to (round (256 * (green in color)) rounding down) 33 | set colb to (round (256 * (blue in color)) rounding down) 34 | write ("" & colr & " " & colg & " " & colb & " 35 | ") to outfile 36 | end 37 | on writeColorInl(outfile, color) 38 | -- slower than writeColor 39 | write (round (256 * (red in color)) rounding down) to outfile 40 | write " " to outfile 41 | write (round (256 * (green in color)) rounding down) to outfile 42 | write " " to outfile 43 | write (round (256 * (blue in color)) rounding down) to outfile 44 | write " 45 | " to outfile 46 | end 47 | on writeRGB(outfile, r,g,b) 48 | set colr to (round (256 * (r)) rounding down) 49 | set colg to (round (256 * (g)) rounding down) 50 | set colb to (round (256 * (b)) rounding down) 51 | write ("" & colr & " " & colg & " " & colb & " 52 | ") to outfile 53 | end 54 | 55 | on writePPMHeader(outfile,imageHeight,imageWidth) 56 | write "P3 57 | " & imageWidth & " " & imageHeight & " 58 | 255 59 | " to outfile 60 | end 61 | 62 | -- the purpose of this object is to wrap a list holding pixel data of an image 63 | -- in an object, because when lists assigned directly to local variables are 64 | -- appended to, applescript makes an O(n) copy of the list. when they are 65 | -- properties on an object however, an O(1) mutable append is used instead. 66 | -- this avoids pixel-by-pixel generation of images taking O(n^2) time 67 | on newImageData() 68 | script imagedata 69 | property pixels: {} 70 | 71 | on append(pixel) 72 | set end of my pixels to pixel 73 | end 74 | end 75 | return imagedata 76 | end 77 | 78 | on writePPMTestImage(filename) 79 | set imageHeight to 256 80 | set imageWidth to 256 81 | 82 | set imagedata to newImageData() 83 | 84 | log "building outputList" 85 | set imgrow to imageHeight - 1 86 | repeat while imgrow >= 0 87 | log "rendering row " & imgrow 88 | 89 | set imgcol to 0 90 | repeat with imgcol from 0 to imageWidth-1 91 | set r to imgcol / (imageWidth-1) 92 | set g to imgrow / (imageHeight-1) 93 | set b to 0.25 94 | 95 | imagedata's append(makeColor(r, g, b)) 96 | end 97 | set imgrow to imgrow - 1 -- decr loop var 98 | end 99 | 100 | log "writing output" 101 | set outfile to open for access filename with write permission 102 | writePPMHeader(outfile,imageHeight,imageWidth) 103 | 104 | 105 | set outputItems to count imagedata's pixels 106 | repeat with outputIndex from 1 to outputItems 107 | if outputIndex mod 1000 = 0 108 | -- show progress 109 | log "wrote " & (round (outputIndex/outputItems)*100) & "%" 110 | end 111 | writeColor(outfile, (item outputIndex of imagedata's pixels)) 112 | end 113 | log "done writing output" 114 | 115 | end writePPMTestImage 116 | 117 | on sqrt(x) 118 | x ^ 0.5 119 | end 120 | 121 | on abs(x) 122 | if x > 0 123 | x 124 | else 125 | -x 126 | end 127 | end 128 | 129 | -- trigonmetric functions 130 | -- from https://macosxautomation.com/applescript/sbrt/pgs/sbrt.02.htm 131 | -- this is dumb, but only used once at startup to calculate the camera fov 132 | 133 | on sine_of(x) 134 | repeat until x >= 0 and x < 360 135 | if x >= 360 then 136 | set x to x - 360 137 | end if 138 | if x < 0 then 139 | set x to x + 360 140 | end if 141 | end repeat 142 | 143 | --convert from degrees to radians 144 | set x to x * (2 * pi) / 360 145 | 146 | set answer to 0 147 | set numerator to x 148 | set denominator to 1 149 | set factor to -(x ^ 2) 150 | 151 | repeat with i from 3 to 40 by 2 152 | set answer to answer + numerator / denominator 153 | set numerator to numerator * factor 154 | set denominator to denominator * i * (i - 1) 155 | end repeat 156 | 157 | return answer 158 | end sine_of 159 | 160 | on cosine_of(x) 161 | repeat until x >= 0 and x < 360 162 | if x >= 360 then 163 | set x to x - 360 164 | end if 165 | if x < 0 then 166 | set x to x + 360 167 | end if 168 | end repeat 169 | 170 | --convert from degrees to radians 171 | set x to x * (2 * pi) / 360 172 | 173 | set answer to 0 174 | set numerator to 1 175 | set denominator to 1 176 | set factor to -(x ^ 2) 177 | 178 | repeat with i from 2 to 40 by 2 179 | set answer to answer + numerator / denominator 180 | set numerator to numerator * factor 181 | set denominator to denominator * i * (i - 1) 182 | end repeat 183 | 184 | return answer 185 | end cosine_of 186 | 187 | --x is in degrees 188 | on tan(x) 189 | set answer to sine_of(x) / (cosine_of(x)) 190 | return answer 191 | end tan 192 | 193 | -- vector 194 | 195 | on v3(x, y, z) 196 | {x:x,y:y,z:z} 197 | end 198 | 199 | on v3add(self, other) 200 | { ¬ 201 | x: x of self + x of other, ¬ 202 | y: y of self + y of other, ¬ 203 | z: z of self + z of other ¬ 204 | } 205 | end 206 | 207 | on v3addMut(self, other) 208 | set x of self to x of self + x of other 209 | set y of self to y of self + y of other 210 | set z of self to z of self + z of other 211 | self 212 | end 213 | 214 | on v3addScalar(self, scalar) 215 | { ¬ 216 | x: x of self + scalar, ¬ 217 | y: y of self + scalar, ¬ 218 | z: z of self + scalar ¬ 219 | } 220 | end 221 | 222 | on v3sub(self, other) 223 | { ¬ 224 | x: x of self - x of other, ¬ 225 | y: y of self - y of other, ¬ 226 | z: z of self - z of other ¬ 227 | } 228 | end 229 | 230 | on v3subScalar(self, scalar) 231 | { ¬ 232 | x: x of self - scalar, ¬ 233 | y: y of self - scalar, ¬ 234 | z: z of self - scalar ¬ 235 | } 236 | end 237 | 238 | 239 | on v3mul(self, other) 240 | { ¬ 241 | x: x of self * x of other, ¬ 242 | y: y of self * y of other, ¬ 243 | z: z of self * z of other ¬ 244 | } 245 | end 246 | 247 | on v3mulScalar(self, scalar) 248 | { ¬ 249 | x: x of self * scalar, ¬ 250 | y: y of self * scalar, ¬ 251 | z: z of self * scalar ¬ 252 | } 253 | end 254 | 255 | on v3div(self, other) 256 | { ¬ 257 | x: x of self / x of other, ¬ 258 | y: y of self / y of other, ¬ 259 | z: z of self / z of other ¬ 260 | } 261 | end 262 | 263 | on v3divScalar(self, scalar) 264 | { ¬ 265 | x: x of self / scalar, ¬ 266 | y: y of self / scalar, ¬ 267 | z: z of self / scalar ¬ 268 | } 269 | end 270 | 271 | on v3lengthSq(self) 272 | (x of self * x of self) + (y of self * y of self) + (z of self * z of self) 273 | end 274 | 275 | on v3length(self) 276 | v3lengthSq(self) ^ 0.5 -- x ^ 0.5 calculates sqrt(x) 277 | end 278 | 279 | on v3unit(v) 280 | v3divScalar(v, v3length(v)) 281 | end 282 | 283 | on v3dot(u, v) 284 | (x of u) * (x of v) + (y of u) * (y of v) + (z of u) * (z of v) 285 | end 286 | 287 | 288 | on v3cross(self, other) 289 | set res to v3(0,0,0) 290 | set x of res to y of self * z of other - z of self * y of other 291 | set y of res to z of self * x of other - x of self * z of other 292 | set z of res to x of self * y of other - y of self * x of other 293 | res 294 | end 295 | 296 | 297 | on v3origin() 298 | v3(0,0,0) 299 | end 300 | 301 | on v3clone(other) 302 | v3(x of other, y of other, z of other) 303 | end 304 | 305 | on v3isEmpty(self) 306 | x of self = 0 and y of self = 0 and z of self = 0 307 | end 308 | 309 | on v3print(self) 310 | log ("" & x in self & " " & y in self & " " & z in self) 311 | end 312 | 313 | on v3copyFrom(self, other) 314 | set x of self to x of other 315 | set y of self to y of other 316 | set z of self to z of other 317 | end 318 | 319 | on v3randomInRange(min, max) 320 | set rng to max - min 321 | return v3(random number * rng + min, random number * rng + min, random number * rng + min) 322 | end 323 | 324 | 325 | on randomInRange(min, max) 326 | set rng to max - min 327 | return random number * rng + min 328 | end 329 | 330 | on min(a, b) 331 | if a < b 332 | a 333 | else 334 | b 335 | end 336 | end 337 | 338 | on max(a, b) 339 | if a > b 340 | a 341 | else 342 | b 343 | end 344 | end 345 | 346 | -- raytracing 347 | 348 | on v3nearZero(self) 349 | -- Return true if the vector is close to zero in all dimensions. 350 | set s to 1e-8 351 | return (abs(x of self) < s) or (abs(y of self) < s) or (abs(z of self) < s) 352 | end 353 | 354 | on randomInUnitSphere() 355 | repeat 356 | set p to v3randomInRange(-1,1) 357 | if v3lengthSq(p) < 1 358 | return p 359 | end 360 | end 361 | end 362 | 363 | on randomUnitVector() 364 | v3unit(randomInUnitSphere()) 365 | end 366 | 367 | on randomInHemisphere(normal) 368 | set inUnitSphere to randomInUnitSphere() 369 | if (v3dot(inUnitSphere, normal) > 0.0) -- In the same hemisphere as the normal 370 | return inUnitSphere 371 | else 372 | return v3mulScalar(inUnitSphere, -1.0) 373 | end 374 | end 375 | 376 | on clamp(x, min, max) 377 | if (x < min) 378 | return min 379 | end 380 | if (x > max) 381 | return max 382 | end 383 | return x 384 | end 385 | 386 | on makeRay(origin, direction) 387 | {origin:origin, direction:direction} 388 | end 389 | 390 | on rayAt(r, t) 391 | v3add((origin of r), v3mulScalar((direction of r), t)) 392 | end 393 | 394 | on v3ToColor(v) 395 | makeColor(x of v, y of v, z of v) 396 | end 397 | 398 | on correctColor(c) 399 | -- sqrt for gamma correction 400 | set red of c to clamp(sqrt(red of c), 0.0, 0.999) 401 | set green of c to clamp(sqrt(green of c), 0.0, 0.999) 402 | set blue of c to clamp(sqrt(blue of c), 0.0, 0.999) 403 | c 404 | end 405 | 406 | on makeHitRecord() 407 | {t: 0, p: v3origin(), normal: v3origin(), frontFace: true, material: null} 408 | end 409 | 410 | on hitRecordCopyFrom(self, other) 411 | set t of self to t of other 412 | v3copyFrom(p of self, p of other) 413 | v3copyFrom(normal of self, normal of other) 414 | set frontFace of self to frontFace of other 415 | set material of self to material of other 416 | self 417 | end 418 | 419 | on hitRecordSetFaceNormal(self, r, outwardNormal) 420 | set frontFace of self to v3dot((direction of r), outwardNormal) < 0 421 | if frontFace of self 422 | set normal of self to outwardNormal 423 | else 424 | set normal of self to -outwardNormal 425 | end 426 | self 427 | end 428 | 429 | script spherePrototype 430 | -- property sphereCenter : v3 431 | -- property sphereRadius : number 432 | -- property material : material 433 | 434 | on hit(r,tMin,tMax,rec) 435 | set oc to v3sub((origin of r), my sphereCenter) 436 | set a to v3lengthSq(direction of r) 437 | set halfB to v3dot(oc, (direction of r)) 438 | set c to v3lengthSq(oc) - my sphereRadius*my sphereRadius 439 | set discriminant to halfB*halfB - a*c 440 | 441 | if (discriminant < 0) 442 | return false 443 | end if 444 | 445 | set sqrtd to sqrt(discriminant) 446 | 447 | -- Find the nearest root that lies in the acceptable range. 448 | set root to (-halfB - sqrtd) / a 449 | if (root < tMin or tMax < root) 450 | root = (-halfB + sqrtd) / a 451 | if (root < tMin or tMax < root) 452 | return false 453 | end if 454 | end if 455 | 456 | set t of rec to root 457 | set p of rec to rayAt(r, t of rec) 458 | set normal of rec to v3divScalar(v3sub(p of rec, my sphereCenter), my sphereRadius) 459 | 460 | set outwardNormal to v3divScalar(v3sub(p of rec, my sphereCenter), my sphereRadius) 461 | hitRecordSetFaceNormal(rec, r, outwardNormal) 462 | set material of rec to my material 463 | return true 464 | end hit 465 | end script 466 | 467 | on newSphere(_sphereCenter, _sphereRadius, _material) 468 | script sphere 469 | property parent: spherePrototype 470 | property sphereCenter: _sphereCenter 471 | property sphereRadius: _sphereRadius 472 | property material: _material 473 | end 474 | return sphere 475 | end 476 | 477 | script hittableListPrototype 478 | -- property objects : {} 479 | 480 | on clear() 481 | set my objects to {} 482 | end 483 | 484 | on add(object) 485 | set end of my objects to object 486 | end 487 | 488 | on hit(r, tMin, tMax, rec) 489 | set tempRec to makeHitRecord() 490 | set hitAnything to false 491 | set closestSoFar to tMax 492 | 493 | repeat with object in my objects 494 | if object's hit(r, tMin, closestSoFar, tempRec) 495 | set hitAnything to true 496 | set closestSoFar to t of tempRec 497 | hitRecordCopyFrom(rec, tempRec) 498 | end 499 | end 500 | 501 | return hitAnything 502 | end hit 503 | end 504 | 505 | on newHittableList(_objects) 506 | script hittableList 507 | property parent: hittableListPrototype 508 | property objects: _objects 509 | end 510 | return hittableList 511 | end 512 | 513 | 514 | on reflect(v, n) 515 | return v3sub(v, v3mulScalar(n, 2*v3dot(v, n))) 516 | end 517 | 518 | on refract(uv, n, etaiOverEtat) 519 | set cosTheta to min(v3dot(v3mulScalar(uv, -1), n), 1.0) 520 | set rOutPerp to v3mulScalar(v3add(uv, v3mulScalar(n, cosTheta)), etaiOverEtat) 521 | set rOutParallel to v3mulScalar(n, -1*sqrt(abs(1.0 - v3lengthSq(rOutPerp)))) 522 | return v3add(rOutPerp, rOutParallel) 523 | end 524 | 525 | on reflectance(cosine, refIdx) 526 | -- Use Schlick's approximation for reflectance. 527 | set r0 to (1-refIdx) / (1+refIdx) 528 | set r0 to r0*r0 529 | return r0 + (1-r0) * ((1 - cosine)^5) 530 | end 531 | 532 | on newLambertian(_albedo) 533 | script lambertian 534 | property albedo: _albedo 535 | on scatter(rIn, rec) 536 | set scatterDirection to v3add(normal of rec, randomUnitVector()) 537 | -- Catch degenerate scatter direction 538 | if v3nearZero(scatterDirection) 539 | set scatterDirection to normal of rec 540 | end 541 | { ¬ 542 | scattered: makeRay(p of rec, scatterDirection), ¬ 543 | attenuation: v3clone(my albedo), ¬ 544 | success: true ¬ 545 | } 546 | end 547 | end 548 | return lambertian 549 | end 550 | 551 | on newMetal(_albedo, _fuzz) 552 | script metal 553 | property albedo: _albedo 554 | property fuzz: clamp(_fuzz, 0, 1) 555 | on scatter(rIn, rec) 556 | set reflected to reflect(v3unit(direction of rIn), normal of rec) 557 | local fuzzDirection 558 | if my fuzz > 1 559 | set fuzzDirection to v3mulScalar(randomInUnitSphere(), my fuzz) 560 | else 561 | set fuzzDirection to v3origin() 562 | end 563 | set scattered to makeRay(p of rec, v3add(reflected, fuzzDirection)) 564 | set success to v3dot(direction of scattered, normal of rec) > 0 565 | { ¬ 566 | scattered: scattered, ¬ 567 | attenuation: v3clone(my albedo), ¬ 568 | success: success ¬ 569 | } 570 | end 571 | end 572 | return metal 573 | end 574 | 575 | on newDielectric(_indexOfRefraction) 576 | script dielectric 577 | property indexOfRefraction: _indexOfRefraction 578 | on scatter(rIn, rec) 579 | if frontFace of rec 580 | set refractionRatio to (1.0/(my indexOfRefraction)) 581 | else 582 | set refractionRatio to my indexOfRefraction 583 | end 584 | 585 | set unitDirection to v3unit(direction of rIn) 586 | 587 | set cosTheta to min(v3dot(v3mulScalar(unitDirection, -1), normal of rec), 1.0) 588 | set sinTheta to sqrt(1.0 - cosTheta*cosTheta) 589 | 590 | set cannotRefract to refractionRatio * sinTheta > 1.0 591 | 592 | local direction 593 | if cannotRefract or reflectance(cosTheta, refractionRatio) > random number 594 | set direction to reflect(unitDirection, normal of rec) 595 | else 596 | set direction to refract(unitDirection, normal of rec, refractionRatio) 597 | end 598 | 599 | set scattered to makeRay(p of rec, direction) 600 | 601 | { ¬ 602 | scattered: scattered, ¬ 603 | attenuation: v3(1.0, 1.0, 1.0), ¬ 604 | success: true ¬ 605 | } 606 | end 607 | end 608 | return dielectric 609 | end 610 | 611 | on newCamera(lookfrom, lookat, vup, vfov, aspectRatio) 612 | -- set theta to degrees_to_radians(vfov) 613 | set h to tan(vfov/2) -- this tan() function expects degrees 614 | set _viewportHeight to 2.0 * h 615 | set _viewportWidth to aspectRatio * _viewportHeight 616 | set _focalLength to 1.0 617 | 618 | 619 | set w to v3unit(v3sub(lookfrom, lookat)) 620 | set u to v3unit(v3cross(vup, w)) 621 | set v to v3cross(w, u) 622 | 623 | set _origin to v3clone(lookfrom) 624 | set _horizontal to v3mulScalar(u, _viewportWidth) 625 | set _vertical to v3mulScalar(v, _viewportHeight) 626 | 627 | -- lowerLeftCorner = origin - horizontal/2 - vertical/2 - w 628 | set _lowerLeftCorner to ¬ 629 | v3sub( ¬ 630 | v3sub( ¬ 631 | v3sub(_origin, v3divScalar(_horizontal, 2)), ¬ 632 | v3divScalar(_vertical, 2) ¬ 633 | ), ¬ 634 | w ¬ 635 | ) 636 | 637 | script camera 638 | property viewportHeight : _viewportHeight 639 | property viewportWidth : _viewportWidth 640 | property focalLength : _focalLength 641 | 642 | property origin : _origin 643 | property horizontal : _horizontal 644 | property vertical : _vertical 645 | 646 | -- lowerLeftCorner = origin - horizontal/2 - vertical/2 - v3(0, 0, focalLength) 647 | property lowerLeftCorner: _lowerLeftCorner 648 | 649 | on getRay(s, t) 650 | -- ray(origin, lower_left_corner + s*horizontal + t*vertical - origin) 651 | makeRay(my origin, v3sub(v3add(v3add(my lowerLeftCorner, v3mulScalar(my horizontal, s)), v3mulScalar(my vertical, t)), my origin)) 652 | end 653 | end 654 | end 655 | 656 | on rayColor(r, world, depth) 657 | set rec to makeHitRecord() 658 | set nearInfinity to 2.0e20 659 | set nearZero to 0.001 660 | 661 | -- If we've exceeded the ray bounce limit, no more light is gathered. 662 | if depth <= 0 663 | return v3origin() 664 | end 665 | 666 | set didHit to world's hit(r, nearZero, nearInfinity, rec) 667 | if didHit 668 | set rand to randomInHemisphere(normal of rec) 669 | set target to v3add(p of rec, rand) 670 | set dir to v3sub(target, p of rec) 671 | set material to material of rec 672 | if material = null 673 | log "no material on" 674 | log rec 675 | end 676 | 677 | -- bounce light 678 | set scatterResult to material's scatter(r, rec) 679 | if (success of scatterResult) 680 | return v3mul(attenuation of scatterResult, rayColor(scattered of scatterResult, world, depth-1)) 681 | end 682 | return v3(0,0,0) 683 | 684 | -- simple bounce light 685 | -- return v3mulScalar(rayColor(makeRay(p of rec, dir), world, depth-1), 0.5) 686 | 687 | -- color based on normal 688 | -- return v3mulScalar(v3add(normal of rec, v3(1,1,1)), 0.5) 689 | end 690 | 691 | set t to 0.5*(y of v3unit(direction of r) + 1.0) 692 | return v3add(v3mulScalar(v3(1.0, 1.0, 1.0),(1.0-t)), v3mulScalar(v3(0.5, 0.7, 1.0),t)) 693 | end 694 | 695 | on simpleScene() 696 | set materialGround to newLambertian(v3(0.5, 0.5, 0.5)) 697 | set materialBlue to newLambertian(v3(0.1, 0.2, 0.5)) 698 | -- set materialLeft to newMetal(v3(0.8, 0.8, 0.8), 1) 699 | set materialMetal to newMetal(v3(0.8, 0.6, 0.2), 1) 700 | set materialGlass to newDielectric(1.5) 701 | set world to newHittableList({}) 702 | world's add(newSphere(v3( 0.0, -100.5, -1.0), 100.0, materialGround)) -- ground 703 | world's add(newSphere(v3( 0.0, 0.0, -1.0), 0.5, materialBlue)) -- center 704 | world's add(newSphere(v3(-1.0, 0.0, -1.0), 0.5, materialGlass)) -- left 705 | world's add(newSphere(v3( 1.0, 0.0, -1.0), 0.5, materialMetal)) -- right 706 | return world 707 | end 708 | 709 | on randomScene() 710 | set world to newHittableList({}) 711 | 712 | set groundMaterial to newLambertian(v3(0.5, 0.5, 0.5)) 713 | world's add(newSphere(v3(0,-1000,0), 1000, groundMaterial)) 714 | 715 | set minPos to -3 716 | set maxPos to 3 717 | set numItems to 5 718 | set stepSize to round ((maxPos - minPos) / sqrt(numItems)) rounding up 719 | 720 | repeat with a from minPos to maxPos by stepSize 721 | repeat with b from minPos to maxPos by stepSize 722 | set chooseMat to (random number) 723 | set sphereCenter to v3(a + 0.9*(random number), 0.2, b + 0.9*(random number)) 724 | 725 | if (v3length(v3sub(sphereCenter, v3(4, 0.2, 0))) > 0.9) 726 | if (chooseMat < 0.8) 727 | -- diffuse 728 | set albedo to v3mul(v3randomInRange(0,1), v3randomInRange(0,1)) 729 | set sphereMaterial to newLambertian(albedo) 730 | world's add(newSphere(sphereCenter, 0.2, sphereMaterial)) 731 | else if (chooseMat < 0.95) 732 | -- metal 733 | set albedo to v3randomInRange(0.5, 1) 734 | set fuzz to randomInRange(0, 0.5) 735 | set sphereMaterial to newMetal(albedo, fuzz) 736 | world's add(newSphere(sphereCenter, 0.2, sphereMaterial)) 737 | else 738 | -- glass 739 | set sphereMaterial to newDielectric(1.5) 740 | world's add(newSphere(sphereCenter, 0.2, sphereMaterial)) 741 | end 742 | end 743 | end 744 | end 745 | 746 | set material1 to newDielectric(1.5) 747 | world's add(newSphere(v3(0, 1, 0), 1.0, material1)) 748 | 749 | set material2 to newLambertian(v3(0.4, 0.2, 0.1)) 750 | world's add(newSphere(v3(-4, 1, 0), 1.0, material2)) 751 | 752 | set material3 to newMetal(v3(0.7, 0.6, 0.5), 0.0) 753 | world's add(newSphere(v3(4, 1, 0), 1.0, material3)) 754 | 755 | return world 756 | end 757 | 758 | 759 | on writeRaytracedImage(filename, numWorkers, worker, imageWidth, samplesPerPixel, maxBounces) 760 | -- image 761 | set aspectRatio to 16.0 / 9.0 762 | set imageHeight to round (imageWidth / aspectRatio) rounding down 763 | 764 | -- world 765 | -- set world to simpleScene() 766 | set world to randomScene() 767 | 768 | set imagedata to newImageData() 769 | 770 | -- camera 771 | -- newCamera(lookfrom, lookat, vup, vfov, aspectRatio) 772 | -- set cam to newCamera(v3(-2,2,1), v3(0,0,-1), v3(0,1,0), 20, aspectRatio) 773 | set cam to newCamera(v3(13,2,3), v3(0,0,0), v3(0,1,0), 20, aspectRatio) 774 | 775 | log "begin raytracing with " & (count (world's objects)) & " objects" 776 | set raytracingStartTime to current date 777 | 778 | set chunkSize to round (imageHeight / numWorkers) rounding up 779 | set chunkStart to chunkSize * worker 780 | set nextChunkStart to min((chunkSize * (worker + 1)), imageHeight) 781 | set actualChunkSize to nextChunkStart - chunkStart 782 | 783 | set imgrow to nextChunkStart - 1 -- j 784 | set rowCount to 0 785 | repeat until imgrow < chunkStart 786 | set scanlineStartTime to current date 787 | 788 | set imgcol to 0 -- i 789 | repeat with imgcol from 0 to imageWidth-1 790 | set pixelColor to v3origin() 791 | repeat with sample from 0 to samplesPerPixel 792 | set u to (imgcol + random number) / (imageWidth-1) 793 | set v to (imgrow + random number) / (imageHeight-1) 794 | set r to cam's getRay(u, v) 795 | v3addMut(pixelColor, rayColor(r, world, maxBounces)) 796 | end repeat 797 | imagedata's append(correctColor(v3ToColor(v3divScalar(pixelColor, samplesPerPixel)))) 798 | end 799 | log "rendered scanline " & imgrow & " in " & (current date) - scanlineStartTime & "s ("& (round ((rowCount/actualChunkSize)*100))&"%)" 800 | 801 | set rowCount to rowCount + 1 802 | set imgrow to imgrow - 1 -- decr loop var 803 | end 804 | 805 | log "raytracing took " & (current date) - raytracingStartTime & "s" 806 | 807 | set outfile to open for access filename with write permission 808 | writePPMHeader(outfile,actualChunkSize,imageWidth) 809 | 810 | log "writing output" 811 | 812 | set outputItems to count imagedata's pixels 813 | repeat with outputIndex from 1 to outputItems 814 | if outputIndex mod 1000 = 0 815 | -- show progress 816 | log "wrote " & (round (outputIndex/outputItems)*100) & "%" 817 | end 818 | writeColor(outfile, (item outputIndex of imagedata's pixels)) 819 | end 820 | log "done writing output" 821 | 822 | end writeRaytracedImage 823 | 824 | on getWorkerPartFilename(base, worker) 825 | base&"part_"&worker&".ppm" 826 | end 827 | 828 | -- called automatically when this script is run 829 | on run argv 830 | local worker 831 | local numWorkers 832 | local outputFile 833 | 834 | set imageWidth to my renderConfig's imageWidth 835 | set samplesPerPixel to my renderConfig's samplesPerPixel 836 | set maxBounces to my renderConfig's maxBounces 837 | 838 | if argv is {} 839 | -- we're in normal (no parallel workers) mode 840 | set numWorkers to 1 841 | set worker to 0 842 | set outputFile to "output_"&(time of (current date))&".ppm" 843 | else 844 | set numWorkers to my numSMPWorkers 845 | 846 | if (item 1 of argv) as text is "smp" 847 | -- we're in the parent. spawn workers then collect results when they finish 848 | 849 | set outputFileBase to "output_"&(time of (current date)) 850 | set outputFile to outputFileBase&".ppm" 851 | 852 | -- run processes in parallel 853 | set commands to "" 854 | log "starting workers: "&numWorkers 855 | repeat with worker from 0 to numWorkers-1 856 | set commands to commands&"./raytracer.applescript "& worker & " " & outputFileBase & " " &" & " 857 | end 858 | 859 | log "running" 860 | log commands&"wait" 861 | do shell script commands&"wait" 862 | 863 | -- combine parts into output file 864 | do shell script "touch " & outputFile 865 | -- write full size image ppm header 866 | set outfile to open for access outputFile with write permission 867 | set aspectRatio to 16.0 / 9.0 868 | set imageHeight to round (imageWidth / aspectRatio) rounding down 869 | writePPMHeader(outfile,imageHeight,imageWidth) 870 | close access outfile 871 | 872 | repeat with worker from 0 to numWorkers-1 873 | -- iterate backwards because the parts need to be assembled in reverse order 874 | set workerReverse to (numWorkers-1) - worker 875 | set workerPartFile to getWorkerPartFilename(outputFileBase, workerReverse) 876 | -- strip off individual image part headers and append to output file 877 | do shell script "tail -n +4 "&workerPartFile&" >> "&outputFile&" && rm "&workerPartFile 878 | end 879 | 880 | -- quit at this point, so we don't fall through to the raytracing code 881 | return 882 | else 883 | -- we're in a worker 884 | set outputFileBase to (item 2 of argv) as text 885 | set worker to (item 1 of argv) as number 886 | set outputFile to getWorkerPartFilename(outputFileBase, worker) 887 | end 888 | end 889 | 890 | 891 | set everythingStartTime to current date 892 | 893 | log "rendering to "&outputFile 894 | do shell script "touch " & outputFile 895 | 896 | -- seed rng. necessary to make sure the workers generate the same world 897 | random number with seed my randomSeed 898 | 899 | -- actually do the raytracing and write the image (or image part) 900 | writeRaytracedImage(outputFile, numWorkers, worker, imageWidth, samplesPerPixel, maxBounces) 901 | 902 | log "done in " & (current date) - everythingStartTime & "s" 903 | 904 | -- don't print result 905 | "" 906 | end run 907 | -------------------------------------------------------------------------------- /render.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsdf/applescript-raytracer/bd608515785415a879b42322e875bb43fae79021/render.jpg --------------------------------------------------------------------------------