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