Created
September 12, 2021 23:59
-
-
Save treeform/843e83c670d403bc74daefea974178bc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## Based on the work of https://github.com/edin/raytracer | |
## MIT License | |
## Copyright (c) 2021 Edin Omeragic | |
import benchy, chroma, math, times, glm | |
from pixie import Image, newImage, writeFile, setRgbaUnsafe | |
type Vec3 = glm.Vec3[float32] | |
{.push inline, noinit, checks: off.} | |
type | |
SurfaceType = enum | |
ShinySurface, CheckerBoardSurface | |
ObjectType = enum | |
Sphere, Plane | |
Camera = object | |
forward, right, up, pos: Vec3 | |
Ray = object | |
start, dir: Vec3 | |
Thing = ref object | |
surfaceType: SurfaceType | |
case objectType: ObjectType | |
of Sphere: | |
center: Vec3 | |
radius2: float32 | |
of Plane: | |
normal: Vec3 | |
offset: float32 | |
Intersection = object | |
thing: Thing | |
ray: Ray | |
dist: float32 | |
Light = object | |
pos: Vec3 | |
color: Color | |
Scene = ref object | |
maxDepth: int | |
things: seq[Thing] | |
lights: seq[Light] | |
camera: Camera | |
SurfaceProperties = object | |
diffuse, specular: Color | |
reflect, roughness: float32 | |
const | |
farAway: float32 = 1000000.0 | |
white = color(1.0, 1.0, 1.0) | |
grey = color(0.5, 0.5, 0.5) | |
black = color(0.0, 0.0, 0.0) | |
background = color(0.0, 0.0, 0.0) | |
defaultColor = color(0.0, 0.0, 0.0) | |
proc `*`(c: Color, k: float32): Color = color(k * c.r, k * c.g, k * c.b) | |
proc `*`(a: Color, b: Color): Color = color(a.r * b.r, a.g * b.g, a.b * b.b) | |
proc `+`(a: Color, b: Color): Color = color(a.r + b.r, a.g + b.g, a.b + b.b) | |
proc newCamera(pos: Vec3, lookAt: Vec3): Camera = | |
var | |
down = vec3(0.0f, -1.0f, 0.0f) | |
forward = lookAt - pos | |
result.pos = pos | |
result.forward = forward.normalize() | |
result.right = result.forward.cross(down) | |
result.up = result.forward.cross(result.right) | |
let | |
rightNorm = result.right.normalize() | |
upNorm = result.up.normalize() | |
result.right = rightNorm * 1.5 | |
result.up = upNorm * 1.5 | |
proc getNormal(obj: Thing, pos: Vec3): Vec3 = | |
case obj.objectType: | |
of Sphere: | |
return (pos - obj.center).normalize() | |
of Plane: | |
return obj.normal | |
proc objectIntersect(obj: Thing, ray: Ray): Intersection = | |
case obj.objectType: | |
of Sphere: | |
let | |
eo = obj.center - ray.start | |
v = eo.dot(ray.dir) | |
if v >= 0: | |
var dist = 0.0 | |
let disc = obj.radius2 - (eo.dot(eo) - (v * v)) | |
if disc >= 0: | |
dist = v - sqrt(disc) | |
if dist != 0.0: | |
result.thing = obj | |
result.ray = ray | |
result.dist = dist | |
of Plane: | |
let denom = obj.normal.dot(ray.dir) | |
if denom <= 0: | |
result.dist = (obj.normal.dot(ray.start) + obj.offset) / (-denom) | |
result.thing = obj | |
result.ray = ray | |
proc newSphere(center: Vec3, radius: float32, surfaceType: SurfaceType): Thing = | |
Thing(surfaceType: surfaceType, objectType: Sphere, center: center, | |
radius2: radius * radius) | |
proc newPlane(normal: Vec3, offset: float32, surfaceType: SurfaceType): Thing = | |
Thing(surfaceType: surfaceType, objectType: Plane, normal: normal, | |
offset: offset) | |
proc getSurfaceProperties(obj: Thing, pos: Vec3): SurfaceProperties = | |
case obj.surfaceType: | |
of ShinySurface: | |
result.diffuse = white | |
result.specular = grey | |
result.reflect = 0.7 | |
result.roughness = 250.0 | |
of CheckerBoardSurface: | |
let val = int(floor(pos.z) + floor(pos.x)) | |
if val mod 2 != 0: | |
result.reflect = 0.1 | |
result.diffuse = white | |
else: | |
result.reflect = 0.7 | |
result.diffuse = black | |
result.specular = white | |
result.roughness = 150.0 | |
proc newScene(): Scene = | |
result = Scene() | |
result.maxDepth = 5 | |
result.things = @[ | |
newPlane(vec3(0.0f, 1.0f, 0.0f), 0.0, CheckerBoardSurface), | |
newSphere(vec3(0.0f, 1.0f, -0.25f), 1.0, ShinySurface), | |
newSphere(vec3(-1.0f, 0.5f, 1.5f), 0.5, ShinySurface) | |
] | |
result.lights = @[ | |
Light(pos: vec3(-2.0f, 2.5f, 0.0f), color: color(0.49, 0.07, 0.07)), | |
Light(pos: vec3(1.5f, 2.5f, 1.5f), color: color(0.07, 0.07, 0.49)), | |
Light(pos: vec3(1.5f, 2.5f, -1.5f), color: color(0.07, 0.49, 0.071)), | |
Light(pos: vec3(0.0f, 3.5f, 0.0f), color: color(0.21, 0.21, 0.35)) | |
] | |
result.camera = newCamera(vec3(3.0f, 2.0f, 4.0f), vec3(-1.0f, 0.5f, 0.0f)) | |
proc intersections(scene: Scene, ray: Ray): Intersection = | |
var closest: float32 = farAway | |
result.thing = nil | |
for thing in scene.things: | |
let intersect = objectIntersect(thing, ray) | |
if (not isNil(intersect.thing)) and (intersect.dist < closest): | |
result = intersect | |
closest = intersect.dist | |
proc testRay(scene: Scene, ray: Ray): float32 = | |
let intersection = scene.intersections(ray) | |
if not isNil(intersection.thing): | |
return intersection.dist | |
return NaN | |
proc shade(scene: Scene, intersection: Intersection, depth: int): Color | |
proc traceRay(scene: Scene, ray: Ray, depth: int): Color = | |
let intersection = intersections(scene, ray) | |
if not isNil(intersection.thing): | |
return scene.shade(intersection, depth) | |
return background | |
proc getReflectionColor( | |
scene: Scene, thing: Thing, pos: Vec3, normal: Vec3, reflectDir: Vec3, | |
depth: int | |
): Color = | |
var | |
ray: Ray = Ray(start: pos, dir: reflectDir) | |
color = scene.traceRay(ray, depth + 1) | |
properties = getSurfaceProperties(thing, pos) | |
return color * properties.reflect | |
proc getNaturalColor(scene: Scene, thing: Thing, pos, norm, | |
reflectDir: Vec3 | |
): Color = | |
result = black | |
var | |
reflectDirNorm = reflectDir.normalize() | |
sp = getSurfaceProperties(thing, pos) | |
for light in scene.lights: | |
let | |
lightDist = light.pos - pos | |
lightVec = lightDist.normalize() | |
lightDistLen = lightDist.length() | |
ray = Ray(start: pos, dir: lightVec) | |
neatIntersection = scene.testRay(ray) | |
isInShadow = neatIntersection.classify != fcNan and | |
neatIntersection <= lightDistLen | |
if not isInShadow: | |
let | |
illumination = lightVec.dot(norm) | |
specular = lightVec.dot(reflectDirNorm) | |
var | |
lightColor = | |
if illumination > 0: light.color * illumination | |
else: defaultColor | |
specularColor = | |
if specular > 0: light.color * pow(specular, sp.roughness) | |
else: defaultColor | |
lightColor = lightColor * sp.diffuse | |
specularColor = specularColor * sp.specular | |
result = result + lightColor + specularColor | |
proc shade(scene: Scene, intersection: Intersection, depth: int): Color = | |
var | |
dir = intersection.ray.dir | |
scaled = dir * intersection.dist | |
pos = scaled + intersection.ray.start | |
normal = intersection.thing.getNormal(pos) | |
reflectDir = dir - (normal * normal.dot(dir) * 2) | |
naturalColor = background + getNaturalColor(scene, intersection.thing, | |
pos, normal, reflectDir) | |
reflectedColor: Color | |
if depth >= scene.maxDepth: | |
reflectedColor = grey | |
else: | |
reflectedColor = getReflectionColor(scene, intersection.thing, pos, normal, | |
reflectDir, depth) | |
return naturalColor + reflectedColor | |
proc getPoint(x, y: int, camera: Camera, screenWidth, screenHeight: int): Vec3 = | |
var | |
sw = float32(screenWidth) | |
sh = float32(screenHeight) | |
xf = float32(x) | |
yf = float32(y) | |
recenterX = (xf - (sw / 2.0)) / 2.0 / sw | |
recenterY = -(yf - (sh / 2.0)) / 2.0 / sh | |
vx = camera.right * recenterX | |
vy = camera.up * recenterY | |
v = vx + vy | |
z = camera.forward + v | |
return z.normalize() | |
proc renderScene(scene: Scene, sceneImage: Image) = | |
var ray: Ray | |
let | |
h = sceneImage.height | |
w = sceneImage.width | |
ray.start = scene.camera.pos | |
for y in 0 ..< h: | |
var pos = y * w | |
for x in 0 ..< w: | |
ray.dir = getPoint(x, y, scene.camera, h, w) | |
sceneImage.setRgbaUnsafe(x, y, scene.traceRay(ray, 0)) | |
pos = pos + 1 | |
proc render(): Image = | |
var | |
scene = newScene() | |
result = newImage(500, 500) | |
renderScene(scene, result) | |
render().writeFile("tests/raytracer.png") | |
timeIt "raytracer", 100: | |
discard render() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment