To be able to edit code and run cells, you need to run the notebook yourself. Where would you like to run the notebook?

In the cloud (experimental)

Binder is a free, open source service that runs scientific notebooks in the cloud! It will take a while, usually 2-7 minutes to get a session.

On your computer

(Recommended if you want to store your changes.)

  1. Copy the notebook URL:
  2. Run Pluto

    (Also see: How to install Julia and Pluto)

  3. Paste URL in the Open box

Frontmatter

If you are publishing this notebook on the web, you can set the parameters below to provide HTML metadata. This is useful for search engines and social media.

Author 1

homework 7, version 1

👀 Reading hidden code
222 μs

Submission by: Jazzy Doe (jazz@mit.edu)

👀 Reading hidden code
6.8 ms

Oopsie! You need to update Pluto to the latest version

Close Pluto, go to the REPL, and type:

julia> import Pkg
julia> Pkg.update("Pluto")
👀 Reading hidden code
126 μs

Homework 7: Raytracing in 2D

18.S191, fall 2020

This notebook contains built-in, live answer checks! In some exercises you will see a coloured box, which runs a test case on your code, and provides feedback based on the result. Simply edit the code, run it, and the check runs again.

For MIT students: there will also be some additional (secret) test cases that will be run as part of the grading process, and we will look at your notebook and write comments.

Feel free to ask questions!

👀 Reading hidden code
522 μs
👀 Reading hidden code
16.0 μs

Let's create a package environment:

👀 Reading hidden code
230 μs
👀 Reading hidden code
begin
import Pkg
Pkg.activate(mktempdir())
Pkg.add([
Pkg.PackageSpec(name="Plots", version="1.6-1"),
Pkg.PackageSpec(name="PlutoUI", version="0.6.8-0.6"),
])
using Plots
using PlutoUI
using LinearAlgebra
end
❔
  Activating new project at `/tmp/jl_KQihrG`
    Updating registry at `~/.julia/registries/General.toml`
   Resolving package versions...
    Updating `/tmp/jl_KQihrG/Project.toml`
  [91a5bcdd] + Plots v1.40.10
  [7f904dfe] + PlutoUI v0.6.11
    Updating `/tmp/jl_KQihrG/Manifest.toml`
  [66dad0bd] + AliasTables v1.1.3
  [d1d4a3ce] + BitFlags v0.1.9
  [d360d2e6] + ChainRulesCore v1.25.1
  [9e997f8a] + ChangesOfVariables v0.1.9
  [944b1d66] + CodecZlib v0.7.8
  [35d6a980] + ColorSchemes v3.29.0
  [3da002f7] + ColorTypes v0.12.0
  [c3611d14] + ColorVectorSpace v0.11.0
  [5ae59095] + Colors v0.13.0
  [34da2185] + Compat v4.16.0
  [f0e56b4a] + ConcurrentUtilities v2.5.0
  [187b0558] + ConstructionBase v1.5.8
  [d38c429a] + Contour v0.6.3
  [9a962f9c] + DataAPI v1.16.0
  [864edb3b] + DataStructures v0.18.20
  [ffbed154] + DocStringExtensions v0.9.3
  [460bff9d] + ExceptionUnwrapping v0.1.11
  [c87230d0] + FFMPEG v0.4.2
  [53c48c17] + FixedPointNumbers v0.8.5
  [1fa38f19] + Format v1.3.7
  [28b8d3ca] + GR v0.73.6
  [42e2da0e] + Grisu v1.0.2
  [cd3eb016] + HTTP v1.10.15
  [3587e190] + InverseFunctions v0.1.17
  [92d709cd] + IrrationalConstants v0.2.4
  [1019f520] + JLFzf v0.1.9
  [692b3bcd] + JLLWrappers v1.7.0
  [682c06a0] + JSON v0.21.4
  [b964fa9f] + LaTeXStrings v1.4.0
  [23fbe1c1] + Latexify v0.16.6
  [2ab3a3ac] + LogExpFunctions v0.3.28
  [e6f89c97] + LoggingExtras v1.1.0
  [1914dd2f] + MacroTools v0.5.15
  [739be429] + MbedTLS v1.1.9
  [442fdcdd] + Measures v0.3.2
  [e1d29d7a] + Missings v1.2.0
  [77ba4419] + NaNMath v1.0.3
  [4d8831e6] + OpenSSL v1.4.3
  [bac558e1] + OrderedCollections v1.8.0
  [69de0a69] + Parsers v2.8.1
  [b98c9c47] + Pipe v1.3.0
  [ccf2f8ad] + PlotThemes v3.3.0
  [995b91a9] + PlotUtils v1.4.3
  [91a5bcdd] + Plots v1.40.10
  [7f904dfe] + PlutoUI v0.6.11
  [aea7be01] + PrecompileTools v1.2.1
  [21216c6a] + Preferences v1.4.3
  [43287f4e] + PtrArrays v1.3.0
  [3cdcf5f2] + RecipesBase v1.3.4
  [01d81517] + RecipesPipeline v0.6.12
  [189a3867] + Reexport v1.2.2
  [05181044] + RelocatableFolders v1.0.1
  [ae029012] + Requires v1.3.1
  [6c6a2e73] + Scratch v1.2.1
  [992d4aef] + Showoff v1.0.3
  [777ac1f9] + SimpleBufferStream v1.2.0
  [a2af1166] + SortingAlgorithms v1.2.1
  [860ef19b] + StableRNGs v1.0.2
  [82ae8749] + StatsAPI v1.7.0
  [2913bbd2] + StatsBase v0.34.4
  [fd094767] + Suppressor v0.2.8
  [62fd8b95] + TensorCore v0.1.1
  [3bb67fe8] + TranscodingStreams v0.11.3
  [5c2747f8] + URIs v1.5.1
  [1cfade01] + UnicodeFun v0.4.1
  [1986cc42] + Unitful v1.22.0
  [45397f5d] + UnitfulLatexify v1.6.4
  [41fe7b60] + Unzip v0.2.0
  [6e34b625] + Bzip2_jll v1.0.9+0
  [83423d85] + Cairo_jll v1.18.2+1
  [ee1fde0b] + Dbus_jll v1.14.10+0
  [2702e6a9] + EpollShim_jll v0.0.20230411+1
  [2e619515] + Expat_jll v2.6.5+0
  [b22a6f82] + FFMPEG_jll v4.4.4+1
  [a3f928ae] + Fontconfig_jll v2.15.0+0
  [d7e528f0] + FreeType2_jll v2.13.3+1
  [559328eb] + FriBidi_jll v1.0.16+0
  [0656b61e] + GLFW_jll v3.4.0+2
  [d2c73de3] + GR_jll v0.73.6+0
  [78b55507] + Gettext_jll v0.21.0+0
  [7746bdde] + Glib_jll v2.82.4+0
  [3b182d85] + Graphite2_jll v1.3.14+1
  [2e76f6c2] + HarfBuzz_jll v8.5.0+0
  [aacddb02] + JpegTurbo_jll v3.1.1+0
  [c1c5ebd0] + LAME_jll v3.100.2+0
  [88015f11] + LERC_jll v3.0.0+1
  [1d63c593] + LLVMOpenMP_jll v18.1.7+0
  [dd4b983a] + LZO_jll v2.10.3+0
  [e9f186c6] + Libffi_jll v3.2.2+2
  [d4300ac3] + Libgcrypt_jll v1.11.0+0
  [7e76a0d4] + Libglvnd_jll v1.7.0+0
  [7add5ba3] + Libgpg_error_jll v1.51.1+0
  [94ce4f54] + Libiconv_jll v1.18.0+0
  [4b2f31a3] + Libmount_jll v2.40.3+0
  [89763e89] + Libtiff_jll v4.5.1+1
  [38a345b3] + Libuuid_jll v2.40.3+0
  [e7412a2a] + Ogg_jll v1.3.5+1
  [458c3c95] + OpenSSL_jll v3.0.16+0
  [91d4177d] + Opus_jll v1.3.3+0
  [36c8627f] + Pango_jll v1.56.1+0
  [30392449] + Pixman_jll v0.43.4+0
  [c0090381] + Qt6Base_jll v6.7.1+1
  [a44049a8] + Vulkan_Loader_jll v1.3.243+0
  [a2964d1f] + Wayland_jll v1.21.0+2
  [2381bf8a] + Wayland_protocols_jll v1.36.0+0
  [02c8fc9c] + XML2_jll v2.13.6+1
  [aed1982a] + XSLT_jll v1.1.42+0
  [ffd25f8a] + XZ_jll v5.6.4+1
  [f67eecfb] + Xorg_libICE_jll v1.1.1+0
  [c834827a] + Xorg_libSM_jll v1.2.4+0
  [4f6342f7] + Xorg_libX11_jll v1.8.6+3
  [0c0b7dd1] + Xorg_libXau_jll v1.0.12+0
  [935fb764] + Xorg_libXcursor_jll v1.2.3+0
  [a3789734] + Xorg_libXdmcp_jll v1.1.5+0
  [1082639a] + Xorg_libXext_jll v1.3.6+3
  [d091e8ba] + Xorg_libXfixes_jll v6.0.0+0
  [a51aa0fd] + Xorg_libXi_jll v1.8.2+0
  [d1454406] + Xorg_libXinerama_jll v1.1.5+0
  [ec84b674] + Xorg_libXrandr_jll v1.5.4+0
  [ea2f1a96] + Xorg_libXrender_jll v0.9.11+1
  [14d82f49] + Xorg_libpthread_stubs_jll v0.1.2+0
  [c7cfdc94] + Xorg_libxcb_jll v1.17.0+3
  [cc61e674] + Xorg_libxkbfile_jll v1.1.2+1
  [e920d4aa] + Xorg_xcb_util_cursor_jll v0.1.4+0
  [12413925] + Xorg_xcb_util_image_jll v0.4.0+1
  [2def613f] + Xorg_xcb_util_jll v0.4.0+1
  [975044d2] + Xorg_xcb_util_keysyms_jll v0.4.0+1
  [0d47668e] + Xorg_xcb_util_renderutil_jll v0.3.9+1
  [c22f9ab0] + Xorg_xcb_util_wm_jll v0.4.1+1
  [35661453] + Xorg_xkbcomp_jll v1.4.6+1
  [33bec58e] + Xorg_xkeyboard_config_jll v2.39.0+0
  [c5fb5394] + Xorg_xtrans_jll v1.5.1+0
  [3161d3a3] + Zstd_jll v1.5.7+1
  [35ca27e7] + eudev_jll v3.2.9+0
  [214eeab7] + fzf_jll v0.56.3+0
  [1a1c6b14] + gperf_jll v3.1.1+1
  [a4ae2306] + libaom_jll v3.11.0+0
  [0ac62f75] + libass_jll v0.15.2+0
  [1183f4f0] + libdecor_jll v0.2.2+0
  [2db6ffa8] + libevdev_jll v1.11.0+0
  [f638f0a6] + libfdk_aac_jll v2.0.3+0
  [36db933b] + libinput_jll v1.18.0+0
  [b53b4c65] + libpng_jll v1.6.47+0
  [f27f6e37] + libvorbis_jll v1.3.7+2
  [009596ad] + mtdev_jll v1.1.6+0
  [1270edf5] + x264_jll v2021.5.5+0
  [dfaa095f] + x265_jll v3.5.0+0
  [d8fb68d0] + xkbcommon_jll v1.4.1+2
  [0dad84c5] + ArgTools
  [56f22d72] + Artifacts
  [2a0f44e3] + Base64
  [ade2ca70] + Dates
  [8bb1440f] + DelimitedFiles
  [f43a241f] + Downloads
  [7b1f6079] + FileWatching
  [b77e0a4c] + InteractiveUtils
  [b27032c2] + LibCURL
  [76f85450] + LibGit2
  [8f399da3] + Libdl
  [37e2e46d] + LinearAlgebra
  [56ddb016] + Logging
  [d6f4376e] + Markdown
  [a63ad114] + Mmap
  [ca575930] + NetworkOptions
  [44cfe95a] + Pkg
  [de0858da] + Printf
  [3fa0cd96] + REPL
  [9a3f8284] + Random
  [ea8e919c] + SHA
  [9e88b42a] + Serialization
  [6462fe0b] + Sockets
  [2f01184e] + SparseArrays
  [10745b16] + Statistics
  [fa267f1f] + TOML
  [a4e569a6] + Tar
  [8dfed614] + Test
  [cf7118a7] + UUIDs
  [4ec0a83e] + Unicode
  [e66e0078] + CompilerSupportLibraries_jll
  [deac9b47] + LibCURL_jll
  [29816b5a] + LibSSH2_jll
  [c8ffd9c3] + MbedTLS_jll
  [14a3606d] + MozillaCACerts_jll
  [4536629a] + OpenBLAS_jll
  [05823500] + OpenLibm_jll
  [efcefdf7] + PCRE2_jll
  [83775a58] + Zlib_jll
  [8e850b90] + libblastrampoline_jll
  [8e850ede] + nghttp2_jll
  [3f19e933] + p7zip_jll
7.0 s

Exercise 1: Walls

As discussed the lecture, event-driven simulations are the traditional method used for raytracing. Here, we look for any objects in our path and analytically determine how far away they are. From there, we take one big timestep all the way to the surface boundary, calculate refraction or reflection to see what direction we are moving in, and then seek out any other object we could potentially run into.

So let's start simple with determining when a ray of light could intersect with a wall.

Exercise 1.1 - what is a wall?

To start, let's create the concept of a wall. For our purposes, walls will be infinitely long, so we only need to create an object that has a position and a normal vector at that position:

👀 Reading hidden code
570 μs
abstract type Object end
👀 Reading hidden code
380 μs
struct Wall <: Object
"Position"
position::Vector{Float64}

"Normal vector"
normal::Vector{Float64}
end
👀 Reading hidden code
1.4 ms
test_wall = Wall(
[8,-1],
normalize([-3,1]),
)
👀 Reading hidden code
21.7 μs
plot_scene([test_wall], size=(400,200))
👀 Reading hidden code
3.7 s
plot_object! (generic function with 1 method)
👀 Reading hidden code
1.0 ms
plot_scene (generic function with 1 method)
👀 Reading hidden code
1.9 ms

In our simulations, we will enclose our scene in a box of four walls, to make sure that no rays can escape the scene. We have written this box (i.e. vector of walls) below, but we are still missing the roof.

👀 Reading hidden code
258 μs
box_scene = [
Wall(
[10,0],
[-1,0]
),
Wall(
[-10,0],
[1,0]
),
Wall(
[0,-10],
[0,1]
),
# your code here
]
👀 Reading hidden code
29.8 μs
plot_scene(box_scene, legend=false, size=(400,200))
👀 Reading hidden code
46.5 ms

👉 Modify the definition of box_scene to be a vector of 4 walls, instead of 3. The fourth wall should be positioned at [0,10], and point downwards.

👀 Reading hidden code
211 μs

Keep working on it!

The answer is not quite right.

👀 Reading hidden code
17.7 ms

In the next exercise, we will find the intersection of a ray of light and a wall. To represent light, we create a struct called Photon, holding the position and travel direction of a single particle of light. We also include the index of refraction of the medium it is currently traveling in, we will use this later.

👀 Reading hidden code
313 μs
struct Photon
"Position vector"
p::Vector{Float64}

"Direction vector"
l::Vector{Float64}

"Current Index of Refraction"
ior::Real
end
👀 Reading hidden code
1.6 ms
test_photon = Photon([-1, 2], normalize([1,-.8]), 1.0)
👀 Reading hidden code
24.9 ms
👀 Reading hidden code
324 ms
plot_photon_arrow! (generic function with 2 methods)
👀 Reading hidden code
2.5 ms




Exercise 1.2 - how far is the wall?

We will write a function that finds the location where a photon hits the wall. Instead of moving the photon forward in small timesteps until we reach the wall, we will compute the intersection directly, making use of the fact that the wall is a geometrically simple object.

Our function will return one of two possible types: a Miss or a Intersection. We define these types below, and both definitions need some elaboration.

👀 Reading hidden code
482 μs
struct Miss end
👀 Reading hidden code
805 μs
struct Intersection{T<:Object}
object::T
distance::Float64
point::Vector{Float64}
end
👀 Reading hidden code
2.5 ms
Miss

is a struct with no fields. It does not contain any information, except the fact that it is a Miss. You create a new Miss object like so:

👀 Reading hidden code
288 μs
a_miss = Miss()
👀 Reading hidden code
12.2 μs
Intersection

is a parametric type. The first field (object) is of type T, and T is a subtype of Object. Have a look at the definition above, and take note of how we write such statements in Julia syntax.

We also could have used Object directly as the type for the field object, but what's special about parametric types is that T becomes "part of the type". Let's have a look at an example:

👀 Reading hidden code
381 μs
test_intersection_1 = Intersection(test_wall, 3.0, [1.0,2.0])
👀 Reading hidden code
17.3 μs
Intersection{Wall}
typeof(test_intersection_1)
👀 Reading hidden code
13.9 μs

You see that Wall is included in the type. This will be very useful later, when we want to do something different depending on the intersected object (wall, sphere, etc.) using multiple dispatch. We can write one method for ::Intersection{Sphere}, and one for ::Intersection{Wall}.

👀 Reading hidden code
300 μs



Wall geometry

So, how do we find the location where it hits the wall? Well, because our walls are infinitely long, we are essentially trying to find the point at which 2 lines intersect.

To do this, we can combine a few dot products: one to find how far away we are, and another to scale that distance. Mathematically, it would look like:

D=(praypwall)n^^n^,

where p is the position, ^ is the direction of the light, and n^ is the normal vector for the wall. subscripts i, r, and w represent the intersection point, ray, and wall respectively. The result is D, the amount that the photon needs to travel until it hits the wall.

👉 Write a function intersection_distance that implements this formula, and returns D. You can use dot(a,b) to compute the vector dot product ab.

👀 Reading hidden code
568 μs
intersection_distance (generic function with 1 method)
function intersection_distance(photon::Photon, wall::Wall)
return missing
end
👀 Reading hidden code
395 μs

Here we go!

Replace missing with your answer.

👀 Reading hidden code
30.6 ms





Exercise 1.3 - hitting the wall

👉 Write a function intersection that takes a photon and a wall, and returns either a Miss or an Intersection, based on the result of intersection_distance(photon, wall) =D.

If D is positive, then the photon will hit the wall, and we should return an Intersection. We already have the intersected object, and we have D, our intersection distance. To find the intersection point, we use the photon's position and velocity.

pintersection=pray+D^

If D is negative (or zero), then the wall is behind the photon - we should return a Miss.

Floating points

We are using floating points (Float64) to store positions, distances, etc., which means that we need to account for small errors. Like in the lecture, we will not check for D > 0, but D > ϵ with ϵ = 1e-3.

👀 Reading hidden code
864 μs
intersection (generic function with 1 method)
function intersection(photon::Photon, wall::Wall; ϵ=1e-3)
return missing
end
👀 Reading hidden code
1.3 ms

Here we go!

Replace missing with your answer.

👀 Reading hidden code
2.0 ms
👀 Reading hidden code
32.7 μs
missing
test_intersection = intersection(dizzy, test_wall)
👀 Reading hidden code
13.8 μs
👀 Reading hidden code
1.7 ms
👀 Reading hidden code
311 ms





Exercise 1.4 - which wall?

We are now able to find the Intersection of a single photon with a single wall (or detect a Miss). Great! To make our simulation more interesting, we will combine multiple walls into a single scene.

👀 Reading hidden code
496 μs
philip = Photon([3, 0], normalize([.5,-1]), 1.0)
👀 Reading hidden code
23.6 ms
👀 Reading hidden code
11.7 ms
let
p = plot_scene(ex_1_scene, legend=false, size=(400,200))
plot_photon_arrow!(p, philip, 5)
end
👀 Reading hidden code
1.4 ms

When we shoot a photon at the scene, we compute the intersections between the photon and every object in the scene. Click on the vector below to see all elements:

👀 Reading hidden code
194 μs
all_intersections = [intersection(philip, o) for o in ex_1_scene]
👀 Reading hidden code
23.5 μs

There are two misses and three intersections. Just what we hoped!

👀 Reading hidden code
1.2 ms
👀 Reading hidden code
5.4 ms

So which of these five results should we use to determine what the photon does next? It should be the closest intersection.

Because we used two different types for hits and misses, we can express this in a charming way. We define what it means for one to be better than the other:

👀 Reading hidden code
324 μs
begin
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.distance < b.distance
end
👀 Reading hidden code
1.2 ms

And we can now use all of Julia's built in functions to work with a vector of hit/miss results. For example, we can sort it:

👀 Reading hidden code
254 μs
sort(all_intersections)
👀 Reading hidden code
13.6 μs

And we can take the minimum:

👀 Reading hidden code
221 μs
missing
minimum(all_intersections)
👀 Reading hidden code
11.7 μs

Note that we did not define the sort and minimum methods ourselves! We only added methods for Base.isless.

By taking the minimum, we have found our closest hit! Let's turn this into a function.

👉 Write a function closest_hit that takes a photon and a vector of objects. Calculate the vector of Intersections/Misses, and return the minimum.

👀 Reading hidden code
303 μs
closest_hit (generic function with 1 method)
function closest_hit(photon::Photon, objects::Vector{<:Object})
return missing
end
👀 Reading hidden code
532 μs
missing
test_closest = closest_hit(philip, ex_1_scene)
👀 Reading hidden code
14.6 μs
Error message

type Missing has no field point

Stack trace

Here is what happened, the most recent locations are first:

  1. getproperty
    from Base.jl:42
  2. 		scatter!(p, test_closest.point[1:1], test_closest.point[2:2], label="Closest hit")	
let
p = plot_scene(ex_1_scene)
plot_photon_arrow!(p, philip, 4; label="Philip")
scatter!(p, test_closest.point[1:1], test_closest.point[2:2], label="Closest hit")
p |> as_svg
end
👀 Reading hidden code
---





Exercise 2: Mirrors

👀 Reading hidden code
425 μs

Now we're going to make a bold claim: All walls in this simulation are mirrors. This is just for simplicity so we don't need to worry about rays stopping at the boundaries.

We are already able to find the intersection of a light ray with a mirror, but we still need to tell our friendly computer what a reflection is.

👀 Reading hidden code
289 μs

Exercise 2.1 - reflect

For this one, we need to implement a reflection function. This one is way easier than refraction. All we need to do is find how much of the light is moving in the direction of the surface's normal and subtract that twice.

2=12(1n^)n^

Where 1 and 2 are the photon directions before and after the reflection off a surface with normal n^. Let's write that in code:

👀 Reading hidden code
362 μs
reflect (generic function with 1 method)
reflect(ℓ₁::Vector, n̂::Vector)::Vector = ℓ₁ - 2 * dot(ℓ₁, n̂) * n̂
👀 Reading hidden code
512 μs

👉 Verify that the function reflect works by writing a simple test case:

👀 Reading hidden code
204 μs

👀 Reading hidden code
69.2 μs

Exercise 2.2 - step

Our event-driven simulation is a stepping method, but instead of taking small steps in time, we take large steps from one collision event to the next.

👉 Write a function interact that takes a photon and a hit::Intersection{Wall} and returns a new Photon at the next step. The new photon is located at the hit point, its direction is reflected off the wall's normal and the photon.ior is reused.

👀 Reading hidden code
379 μs
interact (generic function with 1 method)
function interact(photon::Photon, hit::Intersection{Wall})
return missing
end
👀 Reading hidden code
408 μs

Hint

Intersection contains the intersected object, so you can retrieve the wall using hit.object, and the normal using hit.object.normal.

👀 Reading hidden code
59.9 ms
Error message

MethodError: no method matching interact(::Main.workspace#3.Photon, ::Missing)

Closest candidates are:

interact(::Main.workspace#3.Photon, ::Main.workspace#3.Intersection{Main.workspace#3.Wall}) at ~/work/disorganised-mess/disorganised-mess/hw7.jl#==#2c6defd0-1ca1-11eb-17db-d5cb498f3265:1

interact(::Main.workspace#3.Photon, ::Main.workspace#3.Intersection{Main.workspace#3.Sphere}) at ~/work/disorganised-mess/disorganised-mess/hw7.jl#==#427747d6-1ca1-11eb-28ae-ff50728c84fe:1

Stack trace

Here is what happened, the most recent locations are first:

  1. test_new_photon = interact(philip, test_closest)
test_new_photon = interact(philip, test_closest)
👀 Reading hidden code
---
Error message

Another cell defining test_new_photon contains errors.

let
p = plot_scene(ex_1_scene)
plot_photon_arrow!(p, philip, 4; label="Philip")
plot_photon_arrow!(p, test_new_photon, 4; label="Philip after interaction")
p |> as_svg
end
👀 Reading hidden code
---

For convenience, we define a function step_ray that combines these two actions: it finds the closest hit and computes the interaction.

👀 Reading hidden code
205 μs
step_ray (generic function with 1 method)
function step_ray(photon::Photon, objects::Vector{<:Object})
hit = closest_hit(photon, objects)
interact(photon, hit)
end
👀 Reading hidden code
607 μs

Great! Next, we will repeat this action to trace the path of a photon.

👀 Reading hidden code
193 μs

Exercise 2.3 - accumulate

👉 Write a function trace that takes an initial Photon, a vector of Objects and N, the number of steps to make. Return a vector of Photons. Try to use accumulate.

👀 Reading hidden code
315 μs
trace (generic function with 1 method)
function trace(photon::Photon, scene::Vector{<:Object}, N)
return missing
end
👀 Reading hidden code
572 μs
👀 Reading hidden code
13.0 ms
Error message

MethodError: no method matching length(::Missing)

Closest candidates are:

length(::Union{Base.KeySet, Base.ValueIterator}) at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/base/abstractdict.jl:58

length(::Union{LinearAlgebra.Adjoint{T, S}, LinearAlgebra.Transpose{T, S}} where {T, S}) at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/LinearAlgebra/src/adjtrans.jl:171

length(::Union{DataStructures.OrderedRobinDict, DataStructures.RobinDict}) at ~/.julia/packages/DataStructures/95DJa/src/ordered_robin_dict.jl:86

...

Stack trace

Here is what happened, the most recent locations are first:

  1. length(g::Base.Generator{Missing, var"#7#8"})
  2. _similar_shape(itr::Base.Generator{Missing, var"#7#8"}, #unused#::Base.HasLength)
  3. collect(itr::Base.Generator{Missing, var"#7#8"})
  4. 		line = [philip.p, [r.p for r in path]...]	plot!(p, first.(line), last.(line), lw=5, color=:pink)
let
p = plot_scene(ex_1_scene, legend=false, xlim=(-11,11), ylim=(-11,11))
path = trace(philip, ex_1_scene, mirror_test_ray_N)
line = [philip.p, [r.p for r in path]...]
plot!(p, first.(line), last.(line), lw=5, color=:pink)
plot_photon_arrow!(p, philip)
plot_photon_arrow!.([p], path)
p
end |> as_svg
👀 Reading hidden code
---

Recap

In Exercise 3 and 4, we will add a Sphere type, and our scene will consist of Walls (mirrors) and Spheres (lenses). But before we move on, let's review what we have done so far.

Our main character is a Photon, which bounces around a scene made up of Walls.

  1. Using intersection(photon, wall::Wall) we can find either an Intersection (containing the wall, the distance and the point) or a Miss.

  2. Our scene is just a Vector or objects, and we compute the intersection between the photon and every object.

  3. By adding Base.isless methods we have told Julia how to compare hit/miss results, and we get the closest one using minimum(all_intersections).

  4. We wrote a function interact(photon, hit::Intersection{Wall}) that returns a new photon after interacting with a wall collision.

We repeat these four steps to trace a ray through the scene.


In the next two exercises we will reuse some of the functionality that we have already written, using multiple dispatch! For example, we add a method intersection(photon, sphere::Sphere), and steps 2 and 3 magically also work with spheres!

👀 Reading hidden code
791 μs





Exercise 3: Spheres

Now that we know how to bounce light around mirrors, we want to simulate a spherical lens to make things more interesting. Let's define a Sphere.

👀 Reading hidden code
534 μs
struct Sphere <: Object
# Position
center::Vector{Float64}
# Radius
radius::Real
# Index of refraction
ior::Real
end
👀 Reading hidden code
1.5 ms
plot_object! (generic function with 2 methods)
👀 Reading hidden code
1.5 ms
example_sphere = Sphere(
[7, -6],
2,
1.5,
)
👀 Reading hidden code
23.1 μs
plot_scene([example_sphere], size=(400,200), legend=false, xlim=(-15,15), ylim=(-10,10))
👀 Reading hidden code
535 ms

Exercise 3.1

Just like with the Wall, our first step is to be able to find the intersection point of a ray of light and a sphere.

This one is a bit more challenging than the intersction with the wall, in particular because there are 3 potential outcomes of a line interacting with a sphere:

  • No intersection

  • 1 intersection

  • 2 intersections

As shown below:

👀 Reading hidden code
558 μs
👀 Reading hidden code
36.2 ms

So we need a way of finding all of these.

To start, let's look at the intersection of a point and a sphere. So long as the relative distance between the photon and the sphere's center satisfies the sphere equation, we can be considered inside of the sphere. More specifically, we are inside the sphere if:

(xsxp)2+(ysyp)<r2.

where the s and p subscripts represent the sphere and photon, respectively. We know we are on the sphere if

(xsxp)2+(ysyp)=r2.

Let's rewrite this in vector notation as:

(RS)(RS)=r2,

where R and S are the x, y, and z location of the photon and sphere, respectively.

Returning to the timestepping example from above, we know that our ray is moving forward with time such that R=R0+vdt=R0+t. We now need to ask ourselves if there is any time when our ray interacts with the sphere. Plugging this in to the dot product from above, we get

(R0+tS)(R0+tS)=r2

To solve this for t, we first need to reorder everything into the form of a polynomial, such that:

t2()+2t(R0S)+(R0S)(R0S)r2=0.

This can be solved with the good ol' fashioned quadratic equation:

b±b24ac2a,

where a=, b=2(R0S), and c=(R0S)(R0S)r2

If the quadratic equation returns no roots, there is no intersection. If it returns 1 root, the ray just barely hits the edge of the sphere. If it returns 2 roots, it goes right through!

The easiest way to check this is by looking at the discriminant d=b24ac.

Number of roots={0,if d<01,if d=02,if d>0

In the case that there are 2 roots, the second root corresponds to when the ray would interact with the far edge of the sphere if there were no refraction or reflection!; therefore, we only care about returning the closest point.

With all this said, we are ready to write some code.

👉 Write a new method intersection that takes a Photon and a Sphere, and returns either a Miss or an Intersection, using the method described above. Go back to Exercise 1.3 where we defined the first method, and see how we adapt it to a sphere.

👀 Reading hidden code
1.1 ms
intersection (generic function with 2 methods)
function intersection(photon::Photon, sphere::Sphere; ϵ=1e-3)
return missing
end
👀 Reading hidden code
1.3 ms
missing
sphere_intersection = intersection(philip, test_sphere)
👀 Reading hidden code
14.4 μs
👀 Reading hidden code
1.5 ms
test_sphere = Sphere(
[7, -6],
2,
1.5,
)
👀 Reading hidden code
17.3 μs
ex_3_scene = [test_sphere, box_scene...]
👀 Reading hidden code
32.9 ms

👉 Change the definition of test_sphere to test different situations:

  • Hit the circle

  • Miss the circle

  • Start inside the cricle (you should hit the exit boundary)

👀 Reading hidden code
414 μs





Exercise 4: Lenses

For this, we will start with refraction from the surface of water and then move on to a spherical lens.

So, how does refraction work? Well, every time light enters a new medium that is more dense than air, it will bend towards the normal to the surface, like so:

👀 Reading hidden code
42.7 ms

This can be described by Snell's law:

n1n2=v2v1=sin(θ2)sin(θ1)

Here, n is the index of refraction, v is the speed (not velocity (sorry for the notation!)), and θ is the angle with respect to the surface normal. Any variables with an subscript of 1 are in the outer medium (air), and any variables with a subscript 2 are in the inner medium (water).

This means that we can find the angle of the new ray of light as

sin(θ2)=n1n2sin(θ1)

The problem is that sin is slow, so we typically want to rewrite this in terms of vector operations. This means that we want to rewrite everything to be in terms of dot products, but because AB=|A||B|cos(θ), we really want to rewrite everything in terms of cosines first. So, using the fact that sin(θ)2+cos(θ)2=1, we can rewrite the above equation to be:

sin(θ2)=n1n21cos(θ1)2

We also know that

cos(θ2)=1sin(θ2)2=1(n1n2)2(1cos(θ1)2).

Finally, we know that the new light direction should be the same as the old one, but shifted towards (or away) from the normal according to the new refractive index. In particular:

n22=n11+(n1cos(θ1)n2cos(θ2))n^,

where n^ is the normal from the water's surface. Rewriting this, we find:

2=(n1n2)1+((n1n2)cos(θ1)cos(θ2))n^.

Now, we already know cos(θ2) in terms of cos(θ1), so we can just plug that in... But first, let's do some simplifications, such that

r=n1n2

and

c=n^1.

Now, we can rewrite everything such that

2=r1+(rc1r2(1c2))n^.

The last step is to write this in code with a function that takes the light direction, the normal, and old and new indices of refraction:

👀 Reading hidden code
821 μs
refract (generic function with 1 method)
function refract(
ℓ₁::Vector, n̂::Vector,
old_ior, new_ior
)
r = old_ior / new_ior
n̂_oriented = if -dot(ℓ₁, n̂) < 0
-n̂
else
end
c = -dot(ℓ₁, n̂_oriented)
normalize(r * ℓ₁ + (r*c - sqrt(1 - r^2 * (1 - c^2))) * n̂_oriented)
end
👀 Reading hidden code
1.0 ms

Now to move on to lenses. Like in lecture, we will focus exclusively on spherical lenses. Ultimately, there isn't a big difference between a lens and a spherical drop of water. It just has a slightly different refractive index and it's normal is defined slightly differently.

md"

Now to move on to lenses. Like in lecture, we will focus exclusively on spherical lenses. Ultimately, there isn't a big difference between a lens and a spherical drop of water. It just has a slightly different refractive index and it's normal is defined slightly differently.
"
👀 Reading hidden code
203 μs

We need a helper functions to find the normal of the sphere's surface at any position. Remember that the normal will always be pointing perpendicularly from the surface of the sphere. This means that no matter what point you are at, the normal will just be a normalized vector of your current location minus the sphere's position:

👀 Reading hidden code
211 μs
sphere_normal_at (generic function with 1 method)
function sphere_normal_at(p::Vector{Float64}, s::Sphere)
normalize(p - s.center)
end
👀 Reading hidden code
480 μs

👉 Write a new method for interact that takes a photon and a hit of type Intersection{Sphere}, that implements refraction. It returns a new Photon positioned at the hit point, with the refracted velocity and the new index of refraction.

👀 Reading hidden code
1.2 ms

Hint

You can use ray.ior == 1.0 to check whether this is a ray entering or leaving the sphere.

👀 Reading hidden code
2.2 ms
interact (generic function with 2 methods)
function interact(photon::Photon, hit::Intersection{Sphere})
return missing
end
👀 Reading hidden code
404 μs

To test your code, modify the definition of test_lens_photon and test_lens below.

👀 Reading hidden code
222 μs
test_lens_photon = Photon([0,0], [1,0], 1.0)
👀 Reading hidden code
18.5 μs
test_lens = Sphere(
[5, -1.5],
3,
1.5,
)
👀 Reading hidden code
31.4 μs
Error message

MethodError: no method matching interact(::Main.workspace#3.Photon, ::Missing)

Closest candidates are:

interact(::Main.workspace#3.Photon, ::Main.workspace#3.Intersection{Main.workspace#3.Wall}) at ~/work/disorganised-mess/disorganised-mess/hw7.jl#==#2c6defd0-1ca1-11eb-17db-d5cb498f3265:1

interact(::Main.workspace#3.Photon, ::Main.workspace#3.Intersection{Main.workspace#3.Sphere}) at ~/work/disorganised-mess/disorganised-mess/hw7.jl#==#427747d6-1ca1-11eb-28ae-ff50728c84fe:1

Stack trace

Here is what happened, the most recent locations are first:

  1. step_ray(photon::Main.workspace#3.Photon, objects::Vector{Main.workspace#3.Object})
    	hit = closest_hit(photon, objects)		interact(photon, hit)end
  2. Show more...
this suckz 💣
let
scene = [test_lens, box_scene...]
N = 3
p = plot_scene(scene, legend=false, xlim=(-11,11), ylim=(-11,11))
path = accumulate(1:N; init=test_lens_photon) do old_photon, i
step_ray(old_photon, scene)
end
line = [test_lens_photon.p, [r.p for r in path]...]
plot!(p, first.(line), last.(line), lw=5, color=:red)
p
end |> as_svg
👀 Reading hidden code
---

By defining a method for interact that takes a sphere intersection, we are now able to use the machinery developed in Exercise 2 to simulate a scene with both lenses and mirrors. Let's try it out!

👀 Reading hidden code
204 μs
👀 Reading hidden code
788 μs
Error message

MethodError: no method matching interact(::Main.workspace#3.Photon, ::Missing)

Closest candidates are:

interact(::Main.workspace#3.Photon, ::Main.workspace#3.Intersection{Main.workspace#3.Wall}) at ~/work/disorganised-mess/disorganised-mess/hw7.jl#==#2c6defd0-1ca1-11eb-17db-d5cb498f3265:1

interact(::Main.workspace#3.Photon, ::Main.workspace#3.Intersection{Main.workspace#3.Sphere}) at ~/work/disorganised-mess/disorganised-mess/hw7.jl#==#427747d6-1ca1-11eb-28ae-ff50728c84fe:1

Stack trace

Here is what happened, the most recent locations are first:

  1. step_ray(photon::Main.workspace#3.Photon, objects::Vector{Main.workspace#3.Object})
    	hit = closest_hit(photon, objects)		interact(photon, hit)end
  2. Show more...
let
scene = [test_lens, box_scene...]
p = plot_scene(scene, legend=false, xlim=(-11,11), ylim=(-11,11))
path = accumulate(1:sphere_test_ray_N; init=test_lens_photon) do old_photon, i
step_ray(old_photon, scene)
end
line = [test_lens_photon.p, [r.p for r in path]...]
plot!(p, first.(line), last.(line), lw=5, color=:red)
p
end |> as_svg
👀 Reading hidden code
---

Spherical aberration

Now we can put it all together into an image of spherical aberration!

👀 Reading hidden code
251 μs

👉 Recreate the spherical aberration figure from the lecture (around the end of the video), and make the index of refraction interactive using a Slider. Or make something else!

👀 Reading hidden code
293 μs

👀 Reading hidden code
67.4 μs

Exercise XX: Lecture transcript

(MIT students only) Please see the link for hw 7 transcript document on Canvas. We want each of you to correct about 500 lines, but don’t spend more than 20 minutes on it. See the the beginning of the document for more instructions.

👉 Please mention the name of the video(s) and the line ranges you edited:

👀 Reading hidden code
405 μs

Abstraction, lines 1-219; Array Basics, lines 1-137; Course Intro, lines 1-144 (for example)

lines_i_edited = md"""
Abstraction, lines 1-219; Array Basics, lines 1-137; Course Intro, lines 1-144 (_for example_)
"""
👀 Reading hidden code
278 μs

Before you submit

Remember to fill in your name and Kerberos ID at the top of this notebook.

👀 Reading hidden code
432 μs

Function library

Just some helper functions used in the notebook.

👀 Reading hidden code
227 μs
hint (generic function with 1 method)
👀 Reading hidden code
469 μs
almost (generic function with 1 method)
👀 Reading hidden code
458 μs
still_missing (generic function with 2 methods)
👀 Reading hidden code
845 μs
keep_working (generic function with 2 methods)
👀 Reading hidden code
812 μs
👀 Reading hidden code
11.3 ms
correct (generic function with 2 methods)
correct(text=rand(yays)) = Markdown.MD(Markdown.Admonition("correct", "Got it!", [text]))
👀 Reading hidden code
851 μs
not_defined (generic function with 1 method)
👀 Reading hidden code
778 μs
TODO
👀 Reading hidden code
24.5 μs
TODO_note (generic function with 1 method)
TODO_note(text) = Markdown.MD(Markdown.Admonition("warning", "TODO note", [text]))
👀 Reading hidden code
463 μs