A ShiftForward tech talk by João Costa / @JD557
Object: collision function $f:R^3 \rightarrow Boolean$
eg.: $sphere(x, y, z) = \sqrt{x^2 + y^2 + z^2} \leq 1$
case class Vec3d(x: Double, y: Double, z: Double) { ... }
type Object3d = Vec3d => Boolean
val sphere: Object3d =
(v: Vec3d) => sqrt(v.x*v.x + v.y*v.y + v.z*v.z) <= 1.0
Note: this is not ray marching
def traceRay(x: Double, y: Double, obj: Object3d, eps: Double) = {
val initVector = Vec3d(x, y, zNear)
val delta = initVector.normalize
def traceRayAux(pos: Vec3d): Color = obj(pos) match {
case true =>
Color(255, 255, 255)
case false =>
if (pos.z > zFar) Color(0, 0, 0)
else traceRayAux(pos + delta*eps)
}
traceRayAux(initVector)
}
Colors/3d textures can be easilly implemented:
type Object3d = Vec3d => Option[Color]
This will be ommited in this talk, as it adds unnecessary complexity to most examples.
Object: signed distance function of a point to its surface $sdf:R^3 \rightarrow R$
eg.: $sphere(x, y, z) = \sqrt{x^2 + y^2 + z^2} - 1$
case class Vec3d(x: Double, y: Double, z: Double) { ... }
type DistanceField = Vec3d => Double
val sphere: DistanceField =
(v: Vec3d) => sqrt(v.x*v.x + v.y*v.y + v.z*v.z) - 1.0
def traceRay(x: Double, y: Double, sdf: DistanceField, eps: Double) = {
val initVector = Vec3d(x, y, zNear)
val delta = initVector.normalize
def traceRayAux(pos: Vec3d): Color = sdf(pos) match {
case dist if dist <= 0 =>
Color(255, 255, 255)
case dist =>
if (pos.z > zFar) Color(0, 0, 0)
else traceRayAux(pos + delta*max(dist, eps))
}
traceRayAux(initVector)
}
$Color(r,g,b) = (iterations \times 5, 50, 50)$
def translate(delta: Vec3d)(sdf: DistanceField): DistanceField =
(v: Vec3d) => sdf(v - delta)
def rotateX(theta: Double)(sdf: DistanceField): DistanceField =
(v: Vec3d) => sdf(v.rotX(-theta))
def rotateY(theta: Double)(sdf: DistanceField): DistanceField =
(v: Vec3d) => sdf(v.rotY(-theta))
def rotateZ(theta: Double)(sdf: DistanceField): DistanceField =
(v: Vec3d) => sdf(v.rotZ(-theta))
def scale(s: Double)(sdf: DistanceField): DistanceField =
(v: Vec3d) => sdf(v / s) * s
A simple illumination model: $$ ambient + \sum_L shadow_L (diffuse_L + specular_L) $$
def normal(p: Vec3d, scene: DistanceField, eps: Double): Vec3d = {
val dx = scene(p + (eps, 0, 0)) - scene(p - (eps, 0, 0))
val dy = scene(p + (0, eps, 0)) - scene(p - (0, eps, 0))
val dz = scene(p + (0, 0, eps)) - scene(p - (0, 0, eps))
new Vec3d(dx, dy, dz).normalized
}
def ambientOcclusion(p: Vec3d, n: Vec3d, scene: DistanceField,
epsilon: Double, iter: Int) = {
val bias = scene(p)
def occlusionAux(i: Int, accum: Double, total: Double): Double =
i match {
case 0 => accum / total
case i =>
val expected = bias + (i * epsilon)
val real = scene(p + n * (i * epsilon))
val decay = pow(2, i)
occlusionAux(i-1,
accum+(expected-real)/decay, total+expected/decay)
}
1 - occlusionAux(iter, 0, 0)
}
def shadow(p: Vec3d, lPos: Vec3d, sdf: DistanceField, eps: Double, k: Int) = {
val init = (lPos - p).normalized
def shadowAux(delta: Vec3d, acc: Double): Double =
if (delta.size > (lPos - p).size) acc
else
if (sdf(p + delta) <= 0.0) 0.0
else if (delta.size * k <= 1.0)
shadowAux(delta + init * max(sdf(p + delta), eps), acc)
else
shadowAux(delta + init * max(sdf(p + delta), eps),
min(acc, k * dist / delta.size))
shadowAux(initVector * eps, 1.0)
}
K is a constant to define how smooth how shadows will be.
The smaller the k, the smoother the shadows. 32 is a nice value.