Skip to content

Instantly share code, notes, and snippets.

@benhardy
Last active December 29, 2015 22:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benhardy/7736511 to your computer and use it in GitHub Desktop.
Save benhardy/7736511 to your computer and use it in GitHub Desktop.
little functional raytracer. it may be longer that the "35 lines of javascript" one but is actually readable. (albeit in need of cleanup, optimizing, etc)
/**
* See bottom for file for example spheres scene
*/
import java.awt.Color
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
import math.sqrt
import math.min
import math.max
case class Vec(x:Double, y:Double, z:Double) {
def + (other:Vec) = Vec(x + other.x, y + other.y, z + other.z)
def unary_- = Vec(-x, -y, -z)
def - (other:Vec) = this + ( -other )
def * (k:Double) = Vec(k * x, k * y, k * z)
def / (k:Double) = this * (1.0 / k)
def dot(other:Vec) = x * other.x + y * other.y + z * other.z
def cross(other:Vec) = Vec(y * other.z + z * other.y, z * other.x - x * other.z, x * other.y - y * other.x)
lazy val length = sqrt(x * x + y * y + z * z)
lazy val unit = {
if (length > 0) {
this / length
}
else {
this
}
}
}
object Vec {
val X = Vec(1,0,0)
val Y = Vec(0,1,0)
val Z = Vec(0,0,1)
implicit def fromTuple(tuple:(Double,Double,Double)) = Vec(tuple._1, tuple._2, tuple._3)
}
case class Ray(from:Vec, direction:Vec) {
def along(k: Double) = from + direction * k
}
case class Finish(diffuse:Double, reflection:Double, ambient:Double, phong:Double = 0.0)
object Finish {
val Diffuse = Finish(diffuse = 1, reflection = 0, ambient = 0)
}
object Pigment {
val Black = Vec(0,0,0)
val Red = Vec(1,0,0)
val Green = Vec(0,1,0)
val Blue = Vec(0,0,1)
val White = Red + Green + Blue
val Yellow = Red + Green
val Magenta = Red + Blue
val Cyan = Blue + Green
}
import Pigment._
import Finish._
import Vec._
trait Shape {
def intersect(ray:Ray): Option[Double]
def normalFor(at:Vec): Vec
def pigment: Vec
def finish: Finish
}
case class Sphere(center:Vec, radius:Double,
override val pigment:Vec = White,
override val finish:Finish = Diffuse) extends Shape {
def intersect(ray: Ray): Option[Double] = {
val a = 2 * (ray.direction dot ray.direction)
val b = 2 * ((center dot ray.direction) - (ray.from dot ray.direction))
val c = (ray.from dot ray.from) + (center dot center) - radius * radius - 2 * (ray.from dot center)
val disc2 = b * b - 2 * a * c
if (disc2 < 0)
None
else {
val howFar = (b - sqrt(disc2)) / a
if (howFar > 0) Some(howFar) else None
}
}
def normalFor(at:Vec) = (at - center)
}
case class Plane(normal:Vec, distanceToOrigin:Double,
override val pigment:Vec = White,
override val finish:Finish = Diffuse) extends Shape {
def intersect(ray:Ray): Option[Double] = {
val dot = normal dot ray.direction
if (dot == 0.0) {
None
} else {
val howFar = distanceToOrigin * (normal dot ray.from) / dot
if (howFar > 0) Some(howFar) else None
}
}
def normalFor(any:Vec) = normal
}
case class Canvas(width: Int, height:Int) {
val image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
def paint(x:Int, y:Int, color:Vec) {
val rgb = new Color(color.x.toFloat, color.y.toFloat, color.z.toFloat)
image.setRGB(x, y, rgb.getRGB)
}
}
case class Camera(location: Vec = -Z, lookAt: Vec = Z, up: Vec = Y,
canvas: Canvas) {
val direction = (lookAt - location).unit
val right = (up cross direction).unit
val actualUp = (direction cross right).unit
val pixelSize = 1.0 / canvas.width
val aspectRatio = canvas.height.toDouble / canvas.width
val filmCenter = location + direction
def rayForPixel(x: Int, y:Int) = {
val xPixelDelta = right * pixelSize * (x - canvas.width /2)
val yPixelDelta = actualUp * pixelSize * -(y - canvas.width /2)
val rayDirection = (direction + xPixelDelta + yPixelDelta)//.unit
Ray(location, rayDirection)
}
}
object Sky {
def Blue(direction:Vec): Vec = {
val y = direction.unit.y
if (y < 0) {
Vec(0,y+1,1)
}
else {
Vec(y,1,1)
}
}
def Black(direction:Vec) = Pigment.Black
}
case class Light(position: Vec, brightness: Double)
case class Scene(
contents: Seq[Shape],
camera: Camera,
lights: Seq[Light],
sky: Vec => Vec = Sky.Black) {
val canvas = camera.canvas
def time[T](itemCount:Int, message:String)(work: =>T): T = {
val start = System.currentTimeMillis()
val result:T = work
val elapsed = System.currentTimeMillis() - start
val rate = itemCount.toFloat * 1000 / elapsed
System.err.print("%s in %.3fsec at %.3fpps\r".format(message, elapsed/1000.0, rate))
result
}
def render(filename:String): Unit = {
time(canvas.width * canvas.height, "Done") {
for (y <- 0 until canvas.height) {
val lineMessage = "Rendered line %d/%d".format(y + 1, canvas.height)
time(canvas.width, lineMessage) {
for (x <- 0 until canvas.width) {
val ray = camera.rayForPixel(x, y)
val color = trace(ray)
canvas.paint(x, y, color)
}
}
}
}
ImageIO.write(canvas.image, "png", new File(filename))
}
def trace(ray:Ray, maxDepth: Int = 3): Vec = {
closestIntersection(ray).
map { case (closest, howFar) => coloring(closest, ray, howFar, maxDepth) }.
getOrElse{ sky(ray.direction) }
}
def closestIntersection(ray: Ray): Option[(Shape,Double)] = {
val distances = for {
item <- contents
distance <- item.intersect(ray)
} yield (item, distance)
if (distances.isEmpty) {
None
} else {
val closest = distances.minBy(_._2)
Some(closest)
}
}
def coloring(shape:Shape, ray:Ray, howFar:Double, maxDepth:Int): Vec = {
val hit = ray along howFar
val normal = shape normalFor hit
val ambient = shape.finish.ambient
val n = normal dot normal
val effectiveLights = for {
light <- lights
hitToLight = light.position - hit
facing = normal.unit dot hitToLight.unit if !isBlocked(hit, hitToLight, maxDepth)
lit = facing if facing > 0
} yield lit
val illumination = effectiveLights.sum + ambient
// TODO phong shading
val reflected = if (shape.finish.reflection > 0 && maxDepth >0) {
val reflected = ray.direction - (normal * (2*(normal dot ray.direction)/n))
trace(Ray(hit, reflected), maxDepth -1) * shape.finish.reflection
} else {
Pigment.Black
}
val color = shape.pigment * illumination + reflected
colorNormalized(color)
}
def colorNormalized(color: Vec): Vec = {
val biggest = max(max(color.x, color.y), color.z)
if (biggest > 1)
color / biggest
else
color
}
def isBlocked(hit: Vec, hitToLight: Vec, maxDepth: Int): Boolean = {
closestIntersection(Ray(hit, hitToLight)).isDefined
}
}
object Trace {
def main(args:Array[String]) {
val camera = Camera(
location = (4.8, 5.5, -2.2),
lookAt = (2.2, 1.8, 1.5),
canvas = Canvas(400, 300)
)
val littleAmbience = Finish(
diffuse = 0.5, ambient = 0.2, reflection = 0.3, phong = 1
)
val cubeSize = 6
val objects = for {
x <- 0 to cubeSize; y <- 0 to cubeSize; z <- 0 to cubeSize
} yield Sphere(
center = Vec(x,y,z) * 0.6,
radius = 0.25,
pigment = Vec(x,y,z) / cubeSize,
finish = littleAmbience
)
val lights = List(
Light(position = ( -5.0, 100.0, -5.0), brightness = 0.8),
Light(position = (-100.0, 0.0, -30.0), brightness = 0.5)
)
val scene = Scene(objects, camera, lights)
scene.render("spheres.png")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment