As mentioned here I decided to create my own vector-implementation for 3D vectors. I will start with this here and continually extent this type and it’s helper modules as the need arises for our Raytracer.
For now I just want to begin with the basic declaration and the main-operations on a vector. I will try to give some background information on what a vector is in the mathematical sense and some rules for the operations as well but understand that I cannot give a detailed introduction into this. If you get interested in all the details of this stuff I recommend looking into the Wikipedia-article or searching for some literature on “Linear algebra”.
basic-vector type in 3D
We will only need vectors in common 3D-space (real 3-dim vector-space) and you can imagine a vector (and indeed we will define it to be) as just a 3-tuple of real (float) values. Of course we will associate those values with the x-, y- and z-coordinate in 3D space and there is a “deeper” meaning to this (the tuple is a coordinate-representation for you standard-base being the unit-vectors along your coordinate-axis) but – despite being fun – I think it’s pointless to dig to deep into this here and now.
As we might create a lot of those vectors I will go and implement them as a Struct as this is no big deal and might help performance quite a bit (I will not look too much for performance from the start as I strive for clarity but I try to include the obvious ones):
type Vector3 = struct val X : float val Y : float val Z : float new (x,y,z) = { X = x; Y = y; Z = z } override v.ToString() = sprintf "[%f, %f, %f]" v.X v.Y v.Z end
some special vectors
For now we will only need the zero-vector (all coordinates 0.0) and the vectors of length 1 pointing along the coordinate-lines:
let Create (x,y,z) = new Vector3 (x, y, z) let Zero = Create (0.0, 0.0, 0.0) let E1 = Create (1.0, 0.0, 0.0) let E2 = Create (0.0, 1.0, 0.0) let E3 = Create (0.0, 0.0, 1.0)
Their use will be explained more in the next section…
basic operations
There are two very basic operations on a vector-space: addition of two vectors and multiplication of a vector by a scalar (just a simple number). The first one can be imagined by “chaining” vectors (put the second to the end of the first, keeping it’s directions and yielding a new vector starting at the first vectors start point and ending at the chained endpoint – have a look at the Wiki-article for this) but the operation on the coordinates is very simple: just add coordinate-wise. The second is even more simple: just stretch the vector by factor equal to the scalar-value – or multiply every coordinate by it:
static member (+) (a : Vector3, b : Vector3) = new Vector3 (a.X + b.X, a.Y + b.Y, a.Z + b.Z) static member (-) (a : Vector3, b : Vector3) = new Vector3 (a.X - b.X, a.Y - b.Y, a.Z - b.Z) static member (*) (s : float, v : Vector3) = new Vector3 (s * v.X, s * v.Y, s * v.Z)
There are a couple of “laws” those operations hold but instead of stating them (a feat. better left in the wiki-article) I will take a few of them and melt them into unit-test:
[< Test >] member test.``coordinates of zero-vector should be 0.0``() = // ARRANGE let v = Vector.Zero // ACT let x,y,z = v.X, v.Y, v.Z // ASSERT x |> should equal 0.0 y |> should equal 0.0 z |> should equal 0.0 [< Tes t>] member test.``coordinates of base-vector E1 should be (1,0,0)``() = // ARRANGE let v = Vector.E1 // ACT let x,y,z = v.X, v.Y, v.Z // ASSERT x |> should equal 1.0 y |> should equal 0.0 z |> should equal 0.0 [< Tes t>] member test.``coordinates of base-vector E2 should be (0,1,0)``() = // ARRANGE let v = Vector.E2 // ACT let x,y,z = v.X, v.Y, v.Z // ASSERT x |> should equal 0.0 y |> should equal 1.0 z |> should equal 0.0 [< Tes t>] member test.``coordinates of base-vector E3 should be (0,0,1)``() = // ARRANGE let v = Vector.E3 // ACT let x,y,z = v.X, v.Y, v.Z // ASSERT x |> should equal 0.0 y |> should equal 0.0 z |> should equal 1.0 [< Test >] member test.``zero-vector should be the neutral element of vector-addition``() = // ARRANGE let v = Vector.Create (2.0, 3.0, 4.0) let zero = Vector.Zero // ACT let sum1 = v + zero let sum2 = zero + v // ASSERT sum1 |> should equal v sum2 |> should equal v [< Test >] member test.``vector addition operates by adding the coordinates``() = // ARRANGE let a = Vector.Create (2.0, 3.0, 4.0) let b = Vector.Create (1.5, 0.5, 0.0) // ACT let sum = a + b // ASSERT sum |> should equal (Vector.Create (3.5, 3.5, 4.0)) [< Test >] member test.``scalar-multiplication operates by scaling the coordinates``() = // ARRANGE let s = 2.5 let v = Vector.Create (1.0, 2.0, 3.0) // ACT let v' = s*v // ASSERT v' |> should equal (Vector.Create (2.5, 5.0, 7.5)) [< Test >] member test.``scalar-multiplication of a vector by 0.0 yields the zero-vector``() = // ARRANGE let v = Vector.Create (3.0, 2.25, -7.5) // ACT let v' = 0.0*v // ASSERT v' |> should equal Vector.Zero [< Test >] member test.``linear-combination of the base-vectors should equal it's corresponding coordinate-vector``() = // ARRANGE let a, b, c = 1.0, 3.5, -7.25 let v = Vector.Create(a,b,c) // ACT let v' = a*Vector.E1 + b*Vector.E2 + c*Vector.E3 // ASSERT v' |> should equal v [< Tes t>] member test.``vector-substraction operates by substracting the coordinates``() = // ARRANGE let a = Vector.Create (4.0, 2.5, 3.75) let b = Vector.Create (4.0, 5.0, 0.75) // ACT let dif = a-b // ASSERT dif |> should equal (Vector.Create (0.0, -2.5, 3.0)) [< Test >] member test.``substracting a vector from itself yields the zero-vector``() = // ARRANGE let v = Vector.Create (4.0, 3.0, -2.75) // ACT let v' = v-v // ASSERT v' |> should equal Vector.Zero [< Test >] member test.``scaling a vector by -1.0 yields the inverse of this vector``() = // ARRANGE let v = Vector.Create (3.0, 2.25, -7.5) // ACT let v' = -1.0*v let sum = v+v' // ASSERT sum |> should equal Vector.Zero [< Test >] member test.``substraction by a vector should equal addition by the inverse of it``() = // ARRANGE let a = Vector.Create (4.0, 2.5, 3.75) let b = Vector.Create (4.0, 5.0, 0.75) let dif = a-b // ACT let dif' = a + (-1.0)*b // ASSERT dif |> should equal (Vector.Create (0.0, -2.5, 3.0))
length of a vector
We will need the length of a given vector (indeed we often only need the square of this – a value that is even easier to calculate) so we start by implementing this.
A not on Tests
I really like the fluent style of testing you can achieve with F#, NUnit and the small FsUnit-file – so thanks for the great job creating this guys!
I will try to include many tests for this little pet-project. Not only to test (it will be hard to predict the outcome of a given ray-trace without running it first) but to give you some clues on how to use the operations and methods and to demonstrate some mathematical-laws/rules.
So let’s start right now with the Length of a vector. In two dimensions it’s easy to see (using the Pythagorean theorem), that the length of a vector is just
.
In 3D it’s not much deeper – you only have to use this theorem two times – by first projecting the vectors to (for example) the X-Z-Plane like this:
First project to the X-, Z- and Y-axis resulting in vectors
,
and
. Calculate the Length of their sum
with Pythagoras and then do the same with
by taking sides
and
(Yeah I mixed vectors and their length and yeah I don’t put the little arrow on top of the vectors – I see it like my LA prof – it’s a waste of ink(ok not really) and time).
Doing this (easy task) will get you the result: for the length of
will be
.
In F# this translates into:
let Len2 (v : Vector3) = v.X*v.X + v.Y*v.Y + v.Z*v.Z let Length (v : Vector3) = v |> Len2 |> sqrt
Here I separated the function in two parts, because we will need the Len2 function as well.
To help us keep it tidy I will implement those simple operations in a module called Vector. As we are at it: let’s put those “special” vectors in there too:
module Vector = let Create (x,y,z) = new Vector3 (x, y, z) let Zero = Create (0.0, 0.0, 0.0) let E1 = Create (1.0, 0.0, 0.0) let E2 = Create (0.0, 1.0, 0.0) let E3 = Create (0.0, 0.0, 1.0) let Len2 (v : Vector3) = v.X*v.X + v.Y*v.Y + v.Z*v.Z let Length (v : Vector3) = v |> Len2 |> sqrt
Here are some tests for this:
[< Tes t>] member test.``Length of zero-vector should be 0.0``() = // ARRANGE let v = Vector.Zero // ACT let len = Vector.Length v // ASSERT len |> should equal 0.0 [< Test >] member test.``length of base-vector E1 should be 1.0``() = // ARRANGE let v = Vector.E1 // ACT let len = Vector.Length v // ASSERT len |> should equal 1.0 [< Test >] member test.``length of base-vector E2 should be 1.0``() = // ARRANGE let v = Vector.E2 // ACT let len = Vector.Length v // ASSERT len |> should equal 1.0 [< Test >] member test.``length of base-vector E3 should be 1.0``() = // ARRANGE let v = Vector.E3 // ACT let len = Vector.Length v // ASSERT len |> should equal 1.0 [< Test >] member test.``scalar-multiplication of a vector by 2.0 doubles it's length``() = // ARRANGE let v = Vector.Create (3.0, 0.0, 4.0) let len = Vector.Length v // ACT let v' = 2.0*v let len' = Vector.Length v' // ASSERT len' |> should equal (2.0*len)
That’s it for now – next time we will have a look at the dot-product of two vectors.
Please let me know if you like the way I present those topics here! Is it to easy? To hard? To mixed? Is the math uninteresting or to you want more? Is this plain nonsense? Help me out with some comments here