Introduction

What is ray tracing?

Ray tracing is a rendering technique that can produce incredibly realistic lighting effects. Essentially, an algorithm can trace the path of light, and then simulate the way that the light interacts with the virtual objects it ultimately hits in the computer-generated world. Source

Running This Notebook

This blog post was made entirely in a Jupyter Notebook so you can download it (link to GitHub at top of post) and run it for yourself. I chose to write in Julia as opposed to Python for a number of reasons:

  • Julia is compiled and runs as almost as fast as C
  • Interactive and can be used in a Jupyter Notebook (the Ju in Jupyter stands for Julia after all)
  • Built for parallel computing, GPU computing, and shared memory computing
  • Supports mathematical symbols in code
  • Rather similar to use for someone used to numpy
  • I'm learning Julia right now and the best way to learn a new programming language is to use it!

I want to try using BibTeX in my blog posts (default is APA). Here is a test...

Test BibTeX citation (Rew & Davis, 1990)

Setup

{:width="70%"}

We need to model

  1. Light propagation
    • Timestepping: useful for clouds/smoke, or in our case, an atmosphere with changing refractive index
    • Event driven: traditional approach. only recompute on interaction with a surface
  2. Reflection
  3. Refraction

Defining Types and Functions

Let's first define a Ray type which has attributes:

  • a velocity (or propagation direction)
  • a position vector

It is mutable so we can use in-place operations on it.

mutable struct Ray
    ::Vector{Float64} # velocity vector
    ::Vector{Float64} # position vector
    # c::RGB             # colour
    # the alpha channel can deal with reflection/refraction splitting into two rays (recursive step)
end

Let's now define a FlatSurface. Other interesting easy to model surfaces are spheres. We can also model partial reflection and refraction.

abstract type Surface end

"""
The equation for a plane ax+by+cz+d=0
is given by the normal n̂=(a,b,c) and
a point x̅=(x,y,z)
"""
struct FlatSurface <: Surface
    ::Vector{Float64} # normal vector
    ::Vector{Float64} # position
    # n::Float64         # refractive index
    # the reflectivity and transmittivity can be calculated by the refractive index
end

struct SphereSurface <: Surface
    r::Float64         # radius
    ::Vector{Float64} # centre point
    #n::Float64        # refractive index
end

And work out how to draw these surfaces

using PyPlot

function drawSurface(s::FlatSurface,bbox::Array=[0,-10,-1,10,10,10])
    # since we are drawing on x-z plane, ⟂ vector is ±(-nz,0,nx)
    x_perp = [-s.[3],0,s.[1]]
    x_perp ./= norm(x_perp)
    
    bbox_r = norm([bbox[4]-bbox[1],bbox[5]-bbox[2],bbox[6]-bbox[3]])
    p = s. .- bbox_r.*x_perp
    p = s. .+ bbox_r.*x_perp
    
    plt.plot([p[1],p[1]],[p[3],p[3]],"-k")
    plt.axis("equal")
    
    bbox = [bbox[1],bbox[4],bbox[3],bbox[6]]
    plt.axis(bbox)
end

function drawSurface(s::SphereSurface,bbox::Array=[-1,-10,-1,10,10,10])
    #y = 0 # drawing on a x-z plane
    plt.gcf().gca().add_artist(plt.Circle((s.[1],s.[3]), s.r, fill=false))
    plt.axis("equal")
    
    bbox = [bbox[1],bbox[4],bbox[3],bbox[6]]
    plt.axis(bbox)
end
drawSurface (generic function with 4 methods)

Now I create a reflect function that modifies the ray's direction in place

Important: The ! symbol means the function can operate on the arguments in-place. As opposed to working on a copy.
function reflect!(ray::Ray,n̂::Vector{Float64})
    ray. .= ray. .- 2*dot(ray.,).*
    #x̅ = ⟶.x̅ .+ 0.001.*⟶.x̅
    #return Ray(v̅, ⟶.x̅)
end
reflect! (generic function with 1 method)

And now we have two intersect functions that work in place

Tip: This is an example of Julia’s multiple dispatch feature. It’s really powerful!
using LinearAlgebra

"""
check if ray is travelling towards a surface, and if so, reflect it
"""
function intersect!(ray::Ray,s::FlatSurface)
    if dot(ray.,s.) < 0 && dot(ray.,s.)  dot(s.,s.)
        reflect!(ray,s.n̂)
        # and refract
    end
end

function intersect!(ray::Ray,s::SphereSurface)
     = ray.-s.
    if dot(ray.,) < 0 && norm()  s.r
        reflect!(ray,r̅/norm())
        # and refract
    end
end
WARNING: using LinearAlgebra.reflect! in module Main conflicts with an existing identifier.
intersect! (generic function with 2 methods)

work out how to propagate the rays

function propagate!(ray::Ray, s::Surface, Δt::Float64)
    intersect!(ray,s)
    ray. .+= ray.*Δt # divide by refractive index
end
propagate! (generic function with 1 method)

Flat Mirror

Now to put it all together and plot the ray path as it hits a mirror

s = FlatSurface([0,0,1],[0,0,0])

rays = [ Ray([0.5+0.2*i,0,-1],[i,0,10]) for i  -1:0.5:1 ]

Δt = 0.05 # s
T = 15 # s

drawSurface(s)

for dt in 0:Δt:T
    for ray in rays
        #quiver(ray.x̅[1],ray.x̅[3],ray.v̅[1],ray.v̅[3],headaxislength=0,headlength=0,width=1e-4)
        scatter(ray.[1],ray.[3],s=5e-2)
        propagate!(ray,s,Δt)
    end
end

for ray in rays
    quiver(ray.[1],ray.[3],ray.[1],ray.[3],width=4e-3)
end

that took about 10 seconds

Spherical Mirror

Time to test ray tracing for a sphere target

s = SphereSurface(3,[4,0,2])

rays = [ Ray([0.55+0.2*i,0,-1],[i,0,8]) for i  -1:0.5:1 ]

Δt = 0.05 # s
T = 6 # s

drawSurface(s)

for dt in 0:Δt:T
    for ray in rays
        #quiver(ray.x̅[1],ray.x̅[3],ray.v̅[1],ray.v̅[3],headaxislength=0,headlength=0,width=1e-4)
        scatter(ray.[1],ray.[3],s=5e-2)
        propagate!(ray,s,Δt)
    end
end

for ray in rays
    quiver(ray.[1],ray.[3],ray.[1],ray.[3],width=4e-3)
end

that took 4 seconds...

Warning: None of this was optimised for parallel computing!
  • Now think about plotting this in 3D.
  • Add transparent materials with different refractive indicies.
  • Deal with coloured spheres (ray will have a RGBA value) and
  • could be possible to do a BRDF.

$~$

This is a fun side project to get me understanding how to write Julia code!

References

  1. Rew, R., & Davis, G. (1990). NetCDF: an interface for scientific data access. IEEE Computer Graphics and Applications, 10(4), 76–82. https://doi.org/10.1109/38.56302
using Plots
using LinearAlgebra
struct Ray
    ::Vector{Float64} # velocity vector
    ::Vector{Float64} # position vector
    #ζ::Vector{Float64} # polarisation vector
    n::Float64         # refractive index of the medium the ray is travelling in
    # c::RGB             # colour
    # the alpha channel can deal with reflection/refraction splitting into two rays (recursive step)
end
struct Camera
    f::Float64            # focal length in mm
    pixels::Vector{Int64} # rows, columns in a single frame
    dim::Vector{Float64}  # physical dimension of total pixels in mm
    
    ::Vector{Float64} # position in m
    point::Vector{Float64} # direction where camera is pointing
    rot::Float64           # rotation around pointing direction in rad
    # or a quaternion? 
end
#cam = Camera(100,[135,240],[135,240],[0,0,10],[0,0,-1],0)
cam = Camera(100,[10,16],[135,240],[0,0,10],[0,0,-1],0)
Camera(100.0, [10, 16], [135.0, 240.0], [0.0, 0.0, 10.0], [0.0, 0.0, -1.0], 0.0)
using LinearAlgebra

ray_queue = []
mid = cam.pixels./2
for i in 1:cam.pixels[1], j in 1:cam.pixels[2]
    
     = normalize([ (i-mid[1])/cam.f,(j-mid[2])/cam.f,cam.point[3] ])
    push!(ray_queue,Ray(v̅,cam.x̅,1.))
end
ray_queue
160-element Array{Any,1}:
 Ray([-0.03987063033659809, -0.06977360308904665, -0.9967657584149521], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.03989640385035983, -0.05984460577553974, -0.9974100962589957], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.03991825129156682, -0.049897814114458525, -0.9979562822891704], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.03993615319154359, -0.03993615319154359, -0.9984038297885898], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.03995009355511379, -0.02996257016633534, -0.9987523388778446], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.039960059900174684, -0.019980029950087342, -0.9990014975043671], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.039966043288678706, -0.009991510822169676, -0.9991510822169677], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.039968038348871575, 0.0, -0.9992009587217893], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.039966043288678706, 0.009991510822169676, -0.9991510822169677], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.039960059900174684, 0.019980029950087342, -0.9990014975043671], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.03995009355511379, 0.02996257016633534, -0.9987523388778446], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.03993615319154359, 0.03993615319154359, -0.9984038297885898], [0.0, 0.0, 10.0], 1.0)
 Ray([-0.03991825129156682, 0.049897814114458525, -0.9979562822891704], [0.0, 0.0, 10.0], 1.0)
 ⋮
 Ray([0.04991521613769645, -0.029949129682617866, -0.9983043227539289], [0.0, 0.0, 10.0], 1.0)
 Ray([0.049927657307386346, -0.019971062922954537, -0.9985531461477268], [0.0, 0.0, 10.0], 1.0)
 Ray([0.04993512647599832, -0.009987025295199663, -0.9987025295199663], [0.0, 0.0, 10.0], 1.0)
 Ray([0.04993761694389223, 0.0, -0.9987523388778446], [0.0, 0.0, 10.0], 1.0)
 Ray([0.04993512647599832, 0.009987025295199663, -0.9987025295199663], [0.0, 0.0, 10.0], 1.0)
 Ray([0.049927657307386346, 0.019971062922954537, -0.9985531461477268], [0.0, 0.0, 10.0], 1.0)
 Ray([0.04991521613769645, 0.029949129682617866, -0.9983043227539289], [0.0, 0.0, 10.0], 1.0)
 Ray([0.049897814114458525, 0.03991825129156682, -0.9979562822891704], [0.0, 0.0, 10.0], 1.0)
 Ray([0.04987546680538165, 0.04987546680538165, -0.9975093361076329], [0.0, 0.0, 10.0], 1.0)
 Ray([0.04984819415974839, 0.05981783299169806, -0.9969638831949676], [0.0, 0.0, 10.0], 1.0)
 Ray([0.049816020459101065, 0.0697424286427415, -0.9963204091820212], [0.0, 0.0, 10.0], 1.0)
 Ray([0.049778974257458246, 0.07964635881193319, -0.9955794851491648], [0.0, 0.0, 10.0], 1.0)
using Plots

RGBA(12,10,8,.9)
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
const DEPTH = 5
5
abstract type Object end
abstract type Sphere <: Object end

"""
The equation for a plane ax+by+cz+d=0 is given by the normal n̂=(a,b,c) and
a point x̅=(x,y,z). A mirror reflects all light rays.
"""
struct Mirror <: Object
    ::Vector{Float64} # normal vector
    ::Vector{Float64} # position
end

"""
I am modelling these as dielectric spheres which will reflect and refract light.
"""
struct ClearSphere <: Sphere
    r::Float64         # radius
    ::Vector{Float64} # centre point
    n::Float64         # refractive index
end

struct SolidSphere <: Sphere
    r::Float           # radius
    ::Vector{Float64} # centre point
    c::RGB             # colour
end

struct MirrorSphere <: Sphere
    r::Float           # radius
    ::Vector{Float64} # centre point
end

struct SkyBox <: Sphere
    r::Float           # radius
    ::Vector{Float64} # centre point
    
    # define a colour gradient somehow
    c::RGB            # colour 1
    c::RBG            # colour 2
end
struct Miss end

struct Intersection{T<:Object}
    object::T
    distance::Float64
    point::Vector{Float64}
end

function intersect_dist(photon::Photon,s::Mirror)
    D = -dot(photon. - s.,s.)
    D /= dot(photon.,s.)
    return 
end

function intersect(photon::Photon,s::Mirror,ϵ::Float64=3e-3)
    
    
    x̅_intersect =  + D*x̅_1
end

function intersect(photon::Photon,s::Sphere,ϵ::Float64=3e-3)
     = photon. - s.
    
    a = dot(photon.,photon.)
    b = 2*dot(photon.,)
    c = dot(,) - s.r
    
    Δ = b^2-4a*c
    
    n_roots = (Δ < 0) ? 0 : 2
    if Δ == 0 n_roots = 1 end
    
    if n_roots == 0
        return Miss
    else
        dist = (-b - Δ)/(2a)
        return Intersection(s,dist,)
    end
    
end
function plot_object!(p, s::Mirror)
	# old_xlims = xlims(p)
	# old_ylims = ylims(p)
	
	perp = [s.[3], -s.[1]] # perpendicular vector for plotting in x-z axis
	
	a = wall. .+ 20.*perp
	b = wall. .- 20.*perp
	
	line = [a, b]
	
	plot!(p, first.(line), last.(line), label="Wall")
	# xlims!(p, old_xlims)
	# xlims!(p, old_xlims)
end

function plot_object!(p,s::Union{SolidSphere,ClearSphere})
    # old_xlims = xlims(p)
	# old_ylims = ylims(p)
    
    # xlims!(p, old_xlims)
	# xlims!(p, old_xlims)
end
function drawSurface(s::FlatSurface,bbox::Array=[0,-10,-1,10,10,10])
    # since we are drawing on x-z plane, ⟂ vector is ±(-nz,0,nx)
    x_perp = [-s.[3],0,s.[1]]
    x_perp ./= norm(x_perp)
    
    bbox_r = norm([bbox[4]-bbox[1],bbox[5]-bbox[2],bbox[6]-bbox[3]])
    p = s. .- bbox_r.*x_perp
    p = s. .+ bbox_r.*x_perp
    
    plt.plot([p[1],p[1]],[p[3],p[3]],"-k")
    plt.axis("equal")
    
    bbox = [bbox[1],bbox[4],bbox[3],bbox[6]]
    plt.axis(bbox)
end
Meta.parse("x+y") |> typeof
Expr

START AGAIN

  1. Rasterisation
  2. Ray Tracing
  3. Ray Marching
@time begin
    using Plots
    using LinearAlgebra
    using Test
    using ColorVectorSpace
    using Images, FileIO
end
 17.414947 seconds (31.48 M allocations: 1.676 GiB, 3.40% gc time)
struct Ray
    ::Vector{Float64} # velocity (direction) vector
    ::Vector{Float64} # position vector
    c::RGB             # colour
    n::Float64         # refractive index of the medium the ray is travelling in
    #ζ::Vector{Float64} # polarisation vector
    
    # the alpha channel can deal with reflection/refraction splitting into two rays (recursive step)
end

#=
struct Photon
	"Position vector"
	p::Vector{Float64}

	"Direction vector"
	l::Vector{Float64}

	"Color associated with the photon"
	c::RGB
	
	ior::Real
end
=#
abstract type Object end
struct Miss end


struct Intersection{T<:Object}
	obj::T             # intersection object
	dist::Float64      # distance
	::Vector{Float64} # position vector
end
Base.isless(a::Miss, b::Miss) = false
Base.isless(a::Miss, b::Intersection) = false
Base.isless(a::Intersection, b::Miss) = true
Base.isless(a::Intersection, b::Intersection) = a.dist < b.dist
reflect(::Vector{Float64}, ::Vector{Float64})::Vector{Float64} =  - 2*dot(,)*
#=
normalize( - 2 * dot(, ) * )

function reflect!(ray::Ray,n̂::Vector{Float64})
    ray. .= ray. .- 2*dot(ray.,).*
    #x̅ = ⟶.x̅ .+ 0.001.*⟶.x̅
    #return Ray(v̅, ⟶.x̅)
end
=#
reflect (generic function with 1 method)
function refract_copy(
		::Vector, ::Vector,
		old_ior, new_ior
	)
	
	r = old_ior / new_ior
	
	n̂_oriented = if -dot(, ) < 0
		-
	else
		
	end
	
	c = -dot(, n̂_oriented)
	
	if abs(c) > 0.999
		
	else
		f = 1 - r^2 * (1 - c^2)
		if f < 0
			
		else
			normalize(r *  + (r*c - sqrt(f)) * n̂_oriented)
		end
	end
end
refract_copy (generic function with 1 method)
function refract(::Vector{Float64},::Vector{Float64},n_old::Float64,n_new::Float64)::Vector{Float64}
    
    r = n_old / n_new
    
    #=
    v_n̂ = dot(,)  0 ? - :  
    
    v_n = -dot(,v_n̂)
    =#
    
    v_n = -dot(,)
    s = sign(v_n)
    
    if abs(v_n) > 0.999 # angle of incidence essentially zero
        return 
    else
        Δ = 1 - r^2 * (1 - v_n^2)
        if Δ < 0 
            #return v̅    # no refraction (this should be total internal reflection)
            return reflect(,s*)
        else
            return r* + (r*s*v_n - Δ)*s*
        end
    end
end
refract (generic function with 1 method)
#args = (normalize([0,1,0.1]),normalize([0,0,1]), 1.5, 1.)
#refract2(args...) == refract(args...)
#refract2(args...), refract(args...)
struct Surface
	r::Float64 # Reflectivity
	t::Float64 # Transmission
	c::RGBA    # Color
	n::Float64 # index of refraction
end
abstract type Object end
#abstract type Sphere <: Object end

"""
The equation for a plane ax+by+cz+d=0 is given by the normal n̂=(a,b,c) and
a point x̅=(x,y,z). A mirror reflects all light rays.
"""
struct Mirror <: Object
    ::Vector{Float64} # normal vector
    ::Vector{Float64} # position
end

"""
I am modelling these as dielectric spheres which will reflect and refract light.
"""
struct Sphere <: Object
    r::Float64         # radius
    ::Vector{Float64} # centre point
    s::Surface         # properties of the sphere
    #n::Float64         # refractive index
end


struct SkyBox <: Object
    r::Float64         # radius
    ::Vector{Float64} # centre point
    
    # define a colour gradient somehow
    c::Function        # colour function
end
#=
function intersection(photon::Photon, sphere::S; ϵ=1e-3) where {S <: Union{Skybox,Sphere}}
    a = dot(photon.,photon.)
    b = 2*dot(photon., photon. - sphere.)
    c = dot(photon. - sphere., photon. - sphere.) - sphere.r^2
    
    Δ = b^2 - 4*a*c
=#
function intersect(ray::Ray,s::S,ϵ::Float64=3e-3) where {S <: Union{SkyBox,Sphere}}
     = ray. - s.
    
    a = dot(ray.,ray.)
    b = 2*dot(ray.,)
    c = dot(,) - s.r^2
    
    Δ = b^2-4a*c
    
    n_roots = (Δ < 0) ? 0 : 2
    if Δ == 0 n_roots = 1 end
    
    if n_roots == 0
        return Miss()
    else
        dist = (-b - Δ)/(2a)
        return Intersection(s,dist,ray. + dist*ray.)
    end
    
end
intersect (generic function with 2 methods)
struct Camera <: Object
    f::Float64            # focal length in mm
    resolution::Tuple{Int64,Int64} # rows, columns in a single frame
    aperture_width::Float64  # physical dimension of total pixels in mm
    
    ::Vector{Float64} # position in m
    
    #point::Vector{Float64} # direction where camera is pointing
    #rot::Float64           # rotation around pointing direction in rad
    # or a quaternion? 
end
function init_rays(cam::Camera)
	
	# Physical size of the aperture/image/grid
	aspect_ratio = cam.resolution[1] / cam.resolution[2]
	dim = (
		cam.aperture_width, 
		cam.aperture_width / aspect_ratio
	)

	# The x, y coordinates of every pixel in our image grid
	# relative to the image center
	xs = LinRange(-0.5* dim[1], 0.5 * dim[1], cam.resolution[1])
	ys = LinRange(0.5* dim[2], -0.5 * dim[2], cam.resolution[2])
	
	pixel_positions = [[x, y, cam.f] for y in ys, x in xs]
	directions = normalize.(pixel_positions)
	
	Ray.( directions,[cam.], [zero(RGB)], [1.0])
end
init_rays (generic function with 1 method)
test_cam = Camera(-1,(20,10),9,[0,0,100])
Camera(-1.0, (20, 10), 9.0, [0.0, 0.0, 100.0])
test_rays = init_rays(test_cam)
10×20 Array{Ray,2}:
 Ray([-0.877266, 0.438633, -0.194948], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)   …  Ray([0.877266, 0.438633, -0.194948], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.912636, 0.354914, -0.202808], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)      Ray([0.912636, 0.354914, -0.202808], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.942163, 0.261712, -0.20937], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)       Ray([0.942163, 0.261712, -0.20937], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.963518, 0.160586, -0.214115], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)      Ray([0.963518, 0.160586, -0.214115], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.974755, 0.054153, -0.216612], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)      Ray([0.974755, 0.054153, -0.216612], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.974755, -0.054153, -0.216612], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)  …  Ray([0.974755, -0.054153, -0.216612], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.963518, -0.160586, -0.214115], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)     Ray([0.963518, -0.160586, -0.214115], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.942163, -0.261712, -0.20937], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)      Ray([0.942163, -0.261712, -0.20937], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.912636, -0.354914, -0.202808], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)     Ray([0.912636, -0.354914, -0.202808], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
 Ray([-0.877266, -0.438633, -0.194948], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)     Ray([0.877266, -0.438633, -0.194948], [0.0, 0.0, 100.0], RGB{N0f8}(0.0,0.0,0.0), 1.0)
function closest_hit(ray::Ray, objects::Vector{<:Object})
	hits = intersect.([ray], objects)
	
	return minimum(hits)
end
closest_hit (generic function with 1 method)
# +(::RGB{FixedPointNumbers.Normed{UInt8,8}}, ::RGB{Float64})

#isa(zero(RGB),RGB{Float64})
function gradient_skybox_color(, skybox)
	r = skybox.r
	c = zero(RGB)
    
    #@show norm(x̅),x̅
	
	if [1] < r && [1] > -r
		c += RGB(([1]+r)/(2.0*r), 0, 0)
	end

	if [2] < r && [2] > -r
		c += RGB(0,0,([2]+r)/(2.0*r))
	end

	if [3] < r && [3] > -r
		c += RGB(0,([3]+r)/(2.0*r), 0)
	end

	return c
end
gradient_skybox_color (generic function with 1 method)
function interact(ray::Ray, hit::Intersection{SkyBox}, ::Any, ::Any)
	
	ray_colour = hit.obj.c(hit., hit.obj)
	return Ray(hit., ray., ray_colour, ray.n)
end
interact(ray::Ray, ::Miss, ::Any, ::Any) = ray

function interact(ray::Ray,hit::Intersection{Sphere},::Any,::Any)
    return Ray(ray., reflect(ray.,normalize(ray.-hit.)),ray.c,ray.n)
end
#reflect(v̅::Vector{Float64}, n̂::Vector{Float64})::Vector{Float64} = v̅ - 2*dot(v̅,n̂)*n̂
interact (generic function with 3 methods)
function step_ray(ray::Ray, objs::Vector{T}, num_intersects) where {T <: Object}

	if num_intersects == 0
		ray
	else
		hit = closest_hit(ray, objs)
		interact(ray, hit, num_intersects, objs)
	end
end
step_ray (generic function with 1 method)
extract_colors(rays) = map(ray -> ray.c, rays)
extract_colors(test_rays)
function ray_trace(objs::Vector{T}, cam::Camera, num_intersects::Int64 = 4) where {T <: Object}
    
    rays = init_rays(cam)
    
    new_rays = step_ray.(rays, [objs],[num_intersects])
    
    extract_colors(new_rays)
end
ray_trace (generic function with 2 methods)
struct Sphere <: Object
    r::Float64         # radius
    ::Vector{Float64} # centre point
    s::Surface         # properties of the sphere
    #n::Float64         # refractive index
end
Sphere(20., [0.,0.,-25.], Surface(1.0, 0.0, RGBA(1,1,1,0.0), 1.5))
Sphere(20.0, [0.0, 0.0, -25.0], Surface(1.0, 0.0, RGBA{Float64}(1.0,1.0,1.0,0.0), 1.5))
sky = SkyBox(1000, [0.0, 0.0, 0.0], gradient_skybox_color)

main_scene = [
	sky,
	Sphere(20., [0.,0.,-25.], Surface(1.0, 0.0, RGBA(1,1,1,0.0), 1.5)),
	
	Sphere(20., [0.,50.,-100.], Surface(0.0, 1.0, RGBA(0,0,0,0.0), 1.0)),
	
	Sphere(20., [-50.,0.,-25.], Surface(0.0, 0.0, RGBA(0, .3, .8, 1), 1.0)),
	
	Sphere(20., [30., 25., -60.], Surface(0.0, 0.75, RGBA(1,0,0,0.25), 1.5)),
	
	Sphere(20., [50., 0., -25.], Surface(0.5, 0.0, RGBA(.1,.9,.1,0.5), 1.5)),
	
	Sphere(20., [-30., 25., -60.], Surface(0.5, 0.5, RGBA(1,1,1,0), 1.5)),
]
7-element Array{Object,1}:
 SkyBox(1000.0, [0.0, 0.0, 0.0], gradient_skybox_color)
 Sphere(20.0, [0.0, 0.0, -25.0], Surface(1.0, 0.0, RGBA{Float64}(1.0,1.0,1.0,0.0), 1.5))
 Sphere(20.0, [0.0, 50.0, -100.0], Surface(0.0, 1.0, RGBA{Float64}(0.0,0.0,0.0,0.0), 1.0))
 Sphere(20.0, [-50.0, 0.0, -25.0], Surface(0.0, 0.0, RGBA{Float64}(0.0,0.3,0.8,1.0), 1.0))
 Sphere(20.0, [30.0, 25.0, -60.0], Surface(0.0, 0.75, RGBA{Float64}(1.0,0.0,0.0,0.25), 1.5))
 Sphere(20.0, [50.0, 0.0, -25.0], Surface(0.5, 0.0, RGBA{Float64}(0.1,0.9,0.1,0.5), 1.5))
 Sphere(20.0, [-30.0, 25.0, -60.0], Surface(0.5, 0.5, RGBA{N0f8}(1.0,1.0,1.0,0.0), 1.5))
sky.r
1000.0
function gradient_skybox_color2(, skybox)
	r = skybox.r
	c = RGB(0.5,0.5,0.5)

	return c
end
gradient_skybox_color2 (generic function with 1 method)
basic_camera = Camera(-5,(300,200), 160, [0,20,100])
#test_cam = Camera(-10,(20,10),9,[0,0,100])
@time let
	scene = [sky]
	ray_trace(main_scene, basic_camera)
end
  0.185918 seconds (2.82 M allocations: 189.074 MiB, 18.09% gc time)
x = Array{T,1} where T<:Object
isa(main_scene,x)
true
Pkg.add("ImageMagick")
UndefVarError: Pkg not defined

Stacktrace:
 [1] top-level scope at In[31]:1
 [2] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1091
 [3] execute_code(::String, ::String) at /Users/eway/.julia/packages/IJulia/rWZ9e/src/execute_request.jl:27
 [4] execute_request(::ZMQ.Socket, ::IJulia.Msg) at /Users/eway/.julia/packages/IJulia/rWZ9e/src/execute_request.jl:86
 [5] #invokelatest#1 at ./essentials.jl:710 [inlined]
 [6] invokelatest at ./essentials.jl:709 [inlined]
 [7] eventloop(::ZMQ.Socket) at /Users/eway/.julia/packages/IJulia/rWZ9e/src/eventloop.jl:8
 [8] (::IJulia.var"#15#18")() at ./task.jl:356
using Images, FileIO
earth = load(download("https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Whole_world_-_land_and_oceans_12000.jpg/1280px-Whole_world_-_land_and_oceans_12000.jpg"))
function get_index_rational(A, x, y)
	a, b = size(A)
	
	u = clamp(floor(Int, x * (a-1)) + 1, 1, a)
	v = clamp(floor(Int, y * (b-1)) + 1, 1, b)
	A[u,v]
end

function image_skybox(img)
	f = function(, skybox)
		lon = atan(-[1], [3])
		lat = -atan([2], norm([[1,3]]))

		get_index_rational(img, (lat/(π)) + .5, (lon/2π) + .5)
	end
	
	SkyBox(1000, [0.0, 0.0, 0.0], f)
end
image_skybox (generic function with 1 method)
escher_sphere = Sphere(10,[0,0,0], Surface(1.0, 0.0, RGBA(1,1,1,0.0), 1.5))

struct Sphere <: Object
    r::Float64         # radius
    ::Vector{Float64} # centre point
    s::Surface         # properties of the sphere
    #n::Float64         # refractive index
end
escher_cam = Camera(-3,(300,300),30,[0,0,30])
#Camera(-5,(30,20), 160, [0,20,100])
Camera(-3.0, (300, 300), 30.0, [0.0, 0.0, 30.0])
earth_skybox = image_skybox(earth)

let
	scene = [earth_skybox, escher_sphere]
	ray_trace(scene, escher_cam, 4)
end
escher_sphere = Sphere(00,[0,0,0], Surface(1.0, 0.0, RGBA(1,1,1,0.0), 1.5))
scene = [earth_skybox, escher_sphere]
ray_trace(scene, escher_cam, 4)