Skip to content

Instantly share code, notes, and snippets.

@3gx
Forked from AdrianV/raytracer.nim
Created September 3, 2019 02:34
Show Gist options
  • Select an option

  • Save 3gx/7d7d4c5bb9a8233d039d46fb8cd5a819 to your computer and use it in GitHub Desktop.

Select an option

Save 3gx/7d7d4c5bb9a8233d039d46fb8cd5a819 to your computer and use it in GitHub Desktop.

Revisions

  1. @AdrianV AdrianV created this gist Jun 13, 2013.
    246 changes: 246 additions & 0 deletions raytracer.nim
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,246 @@
    import math
    import sequtils
    import sdl

    const
    width = 1280
    height = 720
    fov = 45.0
    max_depth = 6

    type
    TVec3 = array[3,float]
    TRay {.pure, final.} = object
    start: TVec3
    dir: TVec3
    TSphere {.pure, final.} = object
    center : TVec3
    radius : Float
    color : TVec3
    reflection: Float
    transparency: Float
    TLight {.pure, final.} = object
    position: TVec3
    color: TVec3
    TScene {.pure, final.} = object
    objects: seq[ref TSphere]
    lights: seq[ref TLight]


    proc newRay(start, dir: TVec3): TRay {.noInit, inline.} =
    result.start = start
    result.dir = dir

    template newVec3(x: float): TVec3 = [x,x,x]

    proc newLight(position, color: TVec3): ref TLight =
    new result
    result.position = position
    result.color = color

    template `-` (me: TVec3): TVec3 = [-me[0], -me[1], -me[2]]

    template declOpBinary(op: expr) =

    proc op(me, rhs: TVec3): TVec3 {.inline, noInit.} = [op(me[0], rhs[0]), op(me[1], rhs[1]), op(me[2], rhs[2])]

    proc op(me: TVec3, rhs: Float): TVec3 {.inline, noInit.} = [op(me[0], rhs), op(me[1], rhs), op(me[2], rhs)]

    template declOpBinaryAssign(op: expr) =

    proc op(me: var TVec3, rhs: TVec3) {.inline.} =
    op(me[0], rhs[0])
    op(me[1], rhs[1])
    op(me[2], rhs[2])

    proc op(me: var TVec3, rhs: float) {.inline.} =
    op(me[0], rhs)
    op(me[1], rhs)
    op(me[2], rhs)

    declOpBinary(`+`)
    declOpBinary(`-`)
    declOpBinary(`*`)
    declOpBinary(`/`)
    declOpBinaryAssign(`+=`)
    declOpBinaryAssign(`-=`)
    declOpBinaryAssign(`*=`)
    declOpBinaryAssign(`/=`)

    proc dot(v1, v2: TVec3): Float {.inline.} = v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2]

    proc magnitude(v: TVec3) : Float {.inline.} = sqrt(dot(v,v))

    proc normalize(v: TVec3): TVec3 {.inline, noInit.} =
    let m = v.magnitude()
    return [v[0] / m, v[1] / m, v[2] / m]

    proc newSphere(center: TVec3, radius: Float, color: TVec3, reflection: Float = 0.0, transparency: Float = 0.0): ref TSphere =
    new(result)
    result.center = center
    result.radius = radius
    result.color = color
    result.reflection = reflection
    result.transparency = transparency

    proc normalize(me: ref TSphere, v: TVec3): TVec3 {.inline, noInit.} = normalize(v - me.center)

    template intersectImpl(me: ref TSphere, ray: expr) : expr {.immediate, dirty.} =

    var vl = me.center - ray.start
    var a = dot(vl, ray.dir)
    if (a < 0) : # opposite direction
    return false
    var b2 = dot(vl, vl) - a * a
    var r2 = me.radius * me.radius
    if (b2 > r2) : # perpendicular > r
    return false

    proc intersect(me: ref TSphere, ray: TRay) : Bool {.inline.} =
    intersectImpl(me, ray)
    return true

    proc intersect(me: ref TSphere, ray: TRay, distance: var Float) : Bool {.inline.} =
    intersectImpl(me, ray)
    var c = sqrt(r2 - b2)
    var near = a - c
    var far = a + c
    distance = if (near < 0) : far else : near
    return true


    proc trace(ray: TRay, scene: TScene, depth: int): TVec3 =
    var nearest = inf
    var obj : ref TSphere

    # // search the scene for nearest intersection
    for o in scene.objects :
    var distance = inf
    if o.intersect(ray, distance) :
    if distance < nearest :
    nearest = distance
    obj = o

    if obj.isNil : return #newVec3(0)

    var point_of_hit = ray.dir * nearest
    point_of_hit += ray.start
    var normal = obj.normalize(point_of_hit)
    var inside = false

    var dot_normal_ray = dot(normal, ray.dir)
    if dot_normal_ray > 0 :
    inside = true
    normal = -normal
    dot_normal_ray = -dot_normal_ray

    #result = newVec3(0.0)
    var reflection_ratio = obj.reflection

    let normE5 = normal * 1.0e-5
    for lgt in scene.lights :

    let light_direction = normalize(lgt.position - point_of_hit)
    let r = newRay(point_of_hit + normE5, light_direction)

    # go through the scene check whether we're blocked from the lights

    var blocked = false
    for it in scene.objects:
    blocked = it.intersect(r)
    if blocked: break

    if not blocked :
    when true :
    var temp = lgt.color
    temp *= max(0.0, dot(normal, light_direction))
    temp *= obj.color
    temp *= (1.0 - reflection_ratio)
    result += temp
    else :
    result += lgt.color *
    max(0.0, dot(normal, light_direction)) *
    obj.color * (1.0 - reflection_ratio)


    var facing = max(0.0, - dot_normal_ray)
    var fresneleffect = reflection_ratio + (1.0 - reflection_ratio) * pow((1.0 - facing), 5.0)

    # compute reflection
    if depth < max_depth and reflection_ratio > 0 :
    var reflection_direction = ray.dir - normal * 2.0 * dot_normal_ray
    var reflection = trace(newRay(point_of_hit + normE5, reflection_direction), scene, depth + 1)
    result += reflection * fresneleffect


    # compute refraction
    if depth < max_depth and (obj.transparency > 0.0) :
    var ior = 1.5
    let CE = ray.dir.dot(normal) * -1.0
    ior = if inside : 1.0 / ior else: ior
    let eta = 1.0 / ior
    let GF = (ray.dir + normal * CE) * eta
    let sin_t1_2 = 1.0 - CE * CE
    let sin_t2_2 = sin_t1_2 * (eta * eta)
    if sin_t2_2 < 1.0 :
    let GC = normal * sqrt(1 - sin_t2_2)
    let refraction_direction = GF - GC
    let refraction = trace(newRay(point_of_hit - normal * 1.0e-4, refraction_direction),
    scene, depth + 1)
    result += refraction * (1.0 - fresneleffect) * obj.transparency


    proc render (scene: TScene, surface: PSurface) =
    discard LockSurface(surface)

    let eye = newVec3(0)
    var h = tan(fov / 360.0 * 2.0 * PI / 2.0) * 2.0
    var
    w = h * width.float / height.float
    const
    ww = width.float
    hh = height.float

    for y in 0 .. < height :
    let yy = y.float
    var row: ptr int32 = cast[ptr int32](cast[TAddress](surface.pixels) + surface.pitch.int32 * y)
    for x in 0 .. < width :
    let xx = x.float

    var dir = normalize([(xx - ww / 2.0) / ww * w,
    (hh/2.0 - yy) / hh * h,
    -1.0])
    let pixel = trace(newRay(eye, dir), scene, 0)
    #macFor x -> [0,1,2], col -> [r,g,b] :
    let r = min(255, round(pixel[0] * 255.0)).byte
    let g = min(255, round(pixel[1] * 255.0)).byte
    let b = min(255, round(pixel[2] * 255.0)).byte
    #auto rgb = map!("cast(ubyte)min(255, a*255+0.5)")(pixel[]);
    row[] = MapRGB(surface.format, r, g, b)
    row = cast[ptr int32](cast[TAddress](row) + sizeof(int32))
    UnlockSurface(surface)
    UpdateRect(surface, 0, 0, 0, 0)

    proc test() =
    if init(INIT_VIDEO) != 0:
    quit "SDL failed to initialize!"

    var screen = SetVideoMode(width, height, 32, SWSURFACE or ANYFORMAT)
    if screen.isNil:
    quit($sdl.getError())
    var scene: TScene
    scene.objects = @[newSphere([0.0, -10002.0, -20.0], 10000.0, [0.8, 0.8, 0.8]),
    newSphere([0.0, 2.0, -20.0], 4.0, [0.8, 0.5, 0.5], 0.5),
    newSphere([5.0, 0.0, -15.0], 2.0, [0.3, 0.8, 0.8], 0.2),
    newSphere([-5.0, 0.0, -15.0], 2.0, [0.3, 0.5, 0.8], 0.2),
    newSphere([-2.0, -1.0, -10.0], 1.0, [0.1, 0.1, 0.1], 0.1, 0.8)]
    scene.lights = @[newLight([-10.0, 20.0, 30.0], [2.0, 2.0, 2.0]) ]
    render(scene, screen)

    when isMainModule :

    import benchmark

    bench("duration"):
    test()
    #discard stdin.readline