Epic.Vectors
Epic.Vectors is a generic C# vector library.
The problem
So what problem is Epic.Vectors trying to solve?
There are already C# libraries for vectors, like the OpenTK one. The problem with these libraries is that they are tied to a specific library. This library is deliberately library agnostic; the developer has no ties with any major code that uses Epic.Vectors.
Thought process
This section describes the rationale for developing Epic.Vectors and how its design patterns were chosen.
Okay, so let's say we want to write a C# program that does a lot of vector math.
Step 1: arrays
Initially, we try to write it with arrays of integers:
// Array initialize syntax makes our code look awesome:
int[] a = {0, 1, 0};
int[] b = {1, 1};
// However, we can't do a+b; instead we have to do this:
int[] c = {a.X() + b.X(),
a.Y() + b.Y(),
a.Z() + 0};
Pros: 1. We get to use the array initializer syntax.
Cons: 1. We can't have vector operators.
Step 2: array wrappers
Okay so now we decide to use a wrapper class for vectors.
public class Vector<T>
{
public T[] Values { get; private set; }
public Vector(T[] values)
{
Values = values;
}
public static Vector<T> operator +(Vector<T> v1, Vector<T> v2)
{
throw new NotImplementedException();
}
public override string ToString()
{
return string.Join(", ", Values);
}
}
public static class Extensions
{
public static Vector<T> ToVector<T>(this T[] array)
{
return new Vector<T>(array);
}
}
So now we can do this:
var a = new [] {0, 1, 2}.ToVector();
var b = new [] {9, 9, 9}.ToVector();
var c = a+b;
Console.WriteLine(c); // Output: 9, 10, 11
Pros:
- We can still use the nice array initializer syntax, although there's a little boilerplate around it.
- We can implement the + operator.
- We now have generics
- We have a ToString method
Features
Lots of ways to create vectors
// You can use the friendly array initializer syntax with an extension method:
var aVector = new[]{1,2,3}.ToVector();
{
// You can use a generic method that doesn't require you to explicitly specify the type:
var v1 = Vector.Create(6, 7, 4);
// Note: if you were required to specify the type,
// the code would look like this:
//var v1 = Vector.Create<int>(6, 7, 4);
// You can create vectors that require a certain number of components:
var v2 = Vector3.Create(0, 0, 1);
// You can use the constructor:
var v3 = new Vector3<int>(0, 0, 1);
}
Combining vectors to create more vectors
// Combining vectors is pretty easy:
{
var a = Vector3.Create(0, 1, 2);
var b = Vector4.Create(-1, a);
var a = Vector4.Create(a.Xy, b.Zw);
Console.WriteLine(b);
Console.ReadKey();
}
// You can also use extension methods to combine vectors:
{
var a = Vector3.Create(0, 2, 3);
var b = a.ToVector4(6); // b = 0, 2, 3, 6
var c = b.Yx.ToVector3(9); // c = 2, 0, 9
}
Implements IEnumerable
// Implements IEnumerable:
foreach(var element in aVector)
{
Console.WriteLine(element);
}
Implements IReadOnlyList
// Implements IReadOnlyList:
for(var i = 0; i < aVector.Count; i++)
{
Console.WriteLine(aVector[i]);
}
Supports operators like *, +, /, -
// Supports operators:
{
var a = new[] { 0, 1, 2 }.ToVector();
var b = new[] { 0, 1, 2 }.ToVector();
var c = a*b;
Console.WriteLine(c);
}
Vector types can specify how many elements
// Includes types that force a specific number of elements in a vector:
{
var a = new[] {0, 1, 2}.ToVector3();
// The following line compiles, but crashes
// at runtime because only two coordinates
// were passed in
var b = new[] {0, 1}.ToVector3();
var c = new[] {0, 1}.ToVector2();
var d = new[] {0, 1, 9, 10}.ToVector4();
}
X, Y, Z, and W access
// Supports X, Y, Z, and W access:
{
var ex1 = aVector.X + aVector.Y;
}
// Supports combinations of X, Y, Z, and W access, in any order:
{
var ex2 = aVector.Xyz;
var ex3 = aVector.Xy;
var ex3 = aVector.Zyx;
var ex3 = aVector.Zywx;
}
Reactive Extensions
// Supports any operations supported by Epic.Numbers.
// Here, we're adding two event streams of integers.
{
Numbers.Arithmetic.Plus.PlusUtil<IObservable<int>, IObservable<int>, IObservable<int>>.Plus = Add;
var a = Vectors.Create(new CustomSubject<int>(0), new CustomSubject<int>(3));
var b = Vectors.Create(new CustomSubject<int>(0), new CustomSubject<int>(8));
var aObs = a.Select(subj => subj.AsObservable()).ToVector();
var bObs = b.Select(subj => subj.AsObservable()).ToVector();
var c = aObs + bObs;
c.X.Subscribe(Console.WriteLine);
a.X.OnNext(9);
b.X.OnNext(7);
Console.ReadKey();
}
private static IObservable<int> Add(IObservable<int> arg1, IObservable<int> arg2)
{
return arg1.CombineLatest(arg2, (a, b) => a + b);
}
// Here, we're adding two vectors, where each element
// of each vector is an event stream of integers.
{
Numbers.Arithmetic.Plus.PlusUtil<IObservable<Vector3<int>>, IObservable<Vector3<int>>, IObservable<Vector3<int>>>.Plus = Add;
var a = Vectors.Create(new CustomSubject<Vector3<int>>(Vector3.Default<int>()), new CustomSubject<Vector3<int>>(Vector3.Create(4, 4, 4)));
var b = Vectors.Create(new CustomSubject<Vector3<int>>(Vector3.Create(0, 0, 0)), new CustomSubject<Vector3<int>>(Vector3.Create(8, 8, 8)));
var aObs = a.Select(subj => subj.AsObservable()).ToVector();
var bObs = b.Select(subj => subj.AsObservable()).ToVector();
var c = aObs + bObs;
c.X.Subscribe(Console.WriteLine);
a.X.OnNext(Vector3.Create(0, 3, 2));
b.X.OnNext(Vector3.Create(0, 3, 2));
Console.ReadKey();
}
private static IObservable<Vector3<int>> Add(IObservable<Vector3<int>> arg1, IObservable<Vector3<int>> arg2)
{
return arg1.CombineLatest(arg2, (a, b) => a + b);
}
Limitations
No interface types
There are no interface types that represent vectors. This is required because of this error in C#.
Vectors with different dimensions
You can't add a Vector2<T>
and a Vector3<T>
; you have to convert one of them to the other type first (e.g., add a zero at the end of the 2D vector). This applies to any differing vector types (Vector<T>
, Vector4<T>
, Vector3<T>
, or Vector2<T>
). Example:
var v1 = Vector2.Create(1, 1);
var v2 = Vector3.Create(2, 2, 2);
var v3 = v1 + v2; // this causes a compile-time error.
To add a zero at the end of v1
, you could do this:
var v1 = Vector2.Create(1, 1);
var v1_3 = v1.ToVector3(0);
var v2 = Vector3.Create(2, 2, 2);
var v3 = v1 + v2; // this causes a compile-time error.
You can, however, add a Vector<T>
and a Vector<T>
of course; and Vector<T>
objects can have arbitrary dimensions. Like this:
var v1 = Vector.Create(1, 1);
var v2 = Vector.Create(2, 2, 2);
var v3 = v1 + v2; // this compiles and results in v3 = 3, 3, 2
Vectors with different types for each component
You can't have a Vector where the X value is a double
and the Y value is an int
for example.
Interacting between two vectors with different generic types
You can't use operators on two vectors with differing generic type parameters:
var doubles = Vector.Create(1.0, 2.0);
var ints = Vector.Create(1, 2);
var result = doubles + ints; // this causes a compile-time error
Instead, you should convert them to be the same type, like this:
var doubles = Vector.Create(1.0, 2.0);
var ints = Vector.Create(1, 2);
var result = doubles + ints.Cast<double>().ToVector(); // this does NOT cause a compile-time error
Does not support vectors with unknown sizes
Because the Vector classes implement IReadOnlyList, it is impossible for them to have an unknown size. This means that vectors of infinite size are not supported, as would be possible if the Vector class used IEnumerable only.