Vector2 class design

8f90f6a8b331c8fe24cc6ce6daa76f5c
0
HemoGoblin 101 Apr 21, 2013 at 19:56 c++ math programming

Hi, i am designing my framework right now and i was wondering about what features would people appreciate to see in its Vector2 class. I want to make is as good as possible.
Everything got access to x and y members of the class. So i was wondering, as an exception, should all the utility functions (length, normalize, set) be non-member non-friend functions or should they be regular methods? I want to preserve good, consistent design and extensibility.
The next thing is the return type of length function. The class is limited to arithmetic types however, what if user wants to use the length function on an integral vector? I got several options:

  • Always return T (the main template parameter).
  • Add another template parameter U and set it to float if T is not a floating-point type using std::conditional (it is using C++11).
  • Limit the function to floating-point based vectors.
  • Make 2 distinctive APIs using std::enable_if (one for integral vectors and the second for floating-point vectors).

Also, what kind of utility functions would you like to see? Clamp, length, lengthSquared, max, min, normalize, direction?
What about creating a generic function like apply, which will take a function as its parameter and apply its result to both x and y (so you could use functions like abs, sqrt or even custom lambdas like this: vector.apply(std::abs);)?
Thanks for your suggestions!

3 Replies

Please log in or register to post a reply.

A8433b04cb41dd57113740b779f61acb
0
Reedbeta 167 Apr 21, 2013 at 21:10

Two general points:

  1. Whether you use methods or non-member functions is really a matter of taste. It comes down to whether you’d rather write vec.length() or length(vec). Personally I prefer the latter style (non-member functions) as it better matches shader languages, it fits better with binary operations (writing dot(v, w) makes sense; v.dot(w) less so), and it fits better with a functional style (you clearly don’t expect normalize(v) to modify v, but what about v.normalize()? Does it leave v alone and return a normalized copy, or does it normalize v in-place?). However, in the end it’s a matter of personal taste, so you should do whichever feels better to you.

  2. Try not to spend a lot of time building stuff until you need it. I wouldn’t bother even thinking about the type of length() on integer vector types; just do whatever’s the simplest and least-surprising thing, which is probably making it return float always. (Are you ever going to use double-precision vectors? Doubt it. Float, int, and perhaps bool are the only vector types you’re likely to ever use for game stuff.) Likewise, with regard to utility functions, just add the ones you need. If you’re in the middle of coding something and you think “hey, I wish I had an abs() function for vectors”, then just go add it; it’ll take 30 seconds. I wouldn’t bother with a templatized lambda-ized apply() method, as you can easily add whatever utility methods you actually need.

B20d81438814b6ba7da7ff8eb502d039
0
Vilem_Otte 117 Apr 21, 2013 at 23:06

I’d also note that I don’t have templated vector (and generally math library) in my framework - templates takes a lot of time to compile and something like math library is very often used, then it takes quite a time to compile. Also in general, most functions for real numbers (e.g. floats, doubles, or fixed-point) are illogical to apply to integral numbers. F.e. think about sqrt - how do you want to sqrt integral number, whose square-root is real number? Give error message?

E.g. as integral numbers are sub-set of real numbers in math, a lot of functions for real numbers won’t give you any mathematical importance (or sense) for integral numbers.

Of course you can create some basic class that will contain basic arithmetic operations and create floating point vector, integer vector, etc. on top of it …

I’ll give you here a little list of functions I support for my float4 class:
Constructors (default, copy, single scalar number, components, from m128 value (e.g. 128-bit packed 4 floats), unaligned pointer to float …)
Memory allocation/delocation (overloaded new/delete and new[]/delete[], so my float4 is ALWAYS aligned to 16-byte boundary)
Swizzle through templates (e.g. you can shuffle the components at compile-time, ugly code, but sometimes usable)
Access & cast operators (e.g. operator[] and operator*() - for fp variables)
Unary arithmetics (+=, -=, *=, /= scalar and vector versions)
Binary arithmetics (+, -, *, / scalar and vector versions, all variants) (non-member)
Comparation and logical operators (<, >, &, |, \^, \~, ==, !=) (non-member)
Extended (non-member) functions:
horizontal addition, native reciprocal, per-component power, logarithm (e-base), exp (e), logarithm (binary), exp (binary), sqrt, rsqrt
absolute value, ceil, clamp, floor, fract, max/min (between vectors, vector-scalar, horizontal), mix (lerp), mod, round, roundToEven, smoothstep, step, truncation
distance, dot product, cross product, length, normalize, reflect, refract, AoS<->SoA conversions

This is actually base-class for float4 for all my stuff. You should also implement matrices (and some interaction between them - AoS->SoA can return either four float4 or directly one mat4, etc.). Also I didn’t wrote it over-night, everytime I need to add something I’ll just add it to library, so it grew kinda big (it’s approaching 100 kB size … of course you can #define a lot of stuff (whether u want to use SSE intrinsics, or not, which one of them you wanna use, etc.)).

Ceee4d1295c32a0c1c08a9eae8c9459d
0
v71 105 Apr 22, 2013 at 10:38

Here is something by which , hopefully , you will get inspiration from

#include <assert.h>
#include <math.h>

template <class T>
struct CVec2
{

  CVec2() { v[0]=0.0f; v[1]=0.0f; }

  CVec2(const T& x, const T& y)
  {
    v[0]=x; v[1]=y;
  }

  CVec2( T *xy )
  {
     // be carefull no range check
     v[0]=xy[0];   v[1]=xy[1];
  }

__forceinline CVec2<T>& operator=(const CVec2<T>& CVec)
{
   if ( &CVec==this )
  return *this;
v[0]=CVec.v[0];
v[1]=CVec.v[1];
return *this;
}

__forceinline T& operator[](int i)  
  {
    //assert((i>=0)&&(i<2)); // uncomment this for range check
    return v[i];
  }

  __forceinline CVec2<T>& operator*=(const T s)
  {
    v[0] *= s;
    v[1] *= s;
    return *this;
  }

  __forceinline CVec2<T>& operator*=( const CVec2<T> & s)
  {
    v[0] *= s[0];
    v[1] *= s[1];
    return *this;
  }

  __forceinline CVec2<T>& operator/=(const T s)
  {
    v[0] /= s;
    v[1] /= s;
    return *this;
  }

  __forceinline CVec2<T>& operator+=(const CVec2<T>& that)
  {
    v[0] += that.v[0];
    v[1] += that.v[1];
    return *this;
  }

__forceinline CVec2<T>& operator-=(const CVec2<T>& that)
  {
    v[0] -= that.v[0];
    v[1] -= that.v[1];
    return *this;
  }

__forceinline bool operator< ( const CVec2<T>& a ) const
  {
const size_t Size = 2*sizeof(T);
return memcmp(v,a.v,Size) < 0;
  }

__forceinline bool operator== ( const CVec2<T>& a ) const
{
    const size_t Size = 2*sizeof(T);
    return memcmp( v,a.v,Size) == 0;
}

__forceinline bool operator<= (const CVec2<T>& a) const
{
    const size_t Size = 2*sizeof(T);
    return memcmp( v,a.v,Size) <= 0;
}

__forceinline bool operator> (const CVec2<T>& a) const
{
    const size_t Size = 2*sizeof(T);
    return memcmp( v,a.v,Size) > 0;
}

__forceinline bool operator>= (const CVec2<T>& a) const
{
    const size_t Size = 2*sizeof(T);
    return memcmp( v,a.v,Size) >= 0;
}

__forceinline bool operator!= (const CVec2<T>& a ) const
{
    const size_t Size = 2*sizeof(T);
    return memcmp( v,a.v,Size) != 0;
}

__forceinline void set(const T a, const T B)
  {
    v[0] = a;
    v[1] = b;
  }

__forceinline T length() const
  {
return sqrtf( v[0]*v[0]+v[1]*v[1] );
  }

__forceinline T squaredlength() const
  {
return ( v[0]*v[0]+v[1]*v[1] );
  }

__forceinline void normalize()
  {
T d=sqrt(v[0]*v[0]+v[1]*v[1]);
v[0]/=d;
v[1]/=d;
  }

  __forceinline void zero ()
  {
v[0] = (T)(0.0f);
v[1] = (T)(0.0f);
  }

  __forceinline void fabsf()
  {
v[0]=fabs(v[0]);
v[1]=fabs(v[1]);
  }

  __forceinline CVec2<T> inverse()
  {
return CVec<T>( -v[0], -v[1] );
  }

T v[2];

operator T* () const {return (T*) v;}

};

// external functions

template <class T>
__forceinline bool operator==(const CVec2<T>& a, const CVec2<T>& B)
{
  if ( a.v[0]!=b.v[0] ) return 0 ;
if ( a.v[1]!=b.v[1] ) return 0 ;
return 1;
}

template <class T>
__forceinline bool operator!=(const CVec2<T>& a, const CVec2<T>& B)
{
  return !(a== B);
}

template <class T>
__forceinline CVec2<T> operator*(const CVec2<T>& v, const T s)
{
  return CVec2<T>(v.v[0]*s, v.v[1]*s);
}

template <class T>
__forceinline CVec2<T> operator*(const T s, const CVec2<T>& v)
{
  return CVec2<T>( s*v.v[0], s*v.v[1] );
}

template <class T>
__forceinline CVec2<T> operator*(const CVec2<T>& v, const CVec2<T>& s)
{
  return CVec2<T>(s.v[0]*v.v[0], s.v[1]*v.v[1]);
}

template <class T>
__forceinline CVec2<T> operator/(const CVec2<T>& v, const T s)
{
  return CVec2<T>(v.v[0]/s, v.v[1]/s);
}

template <class T>
__forceinline CVec2<T> operator/( const T  s ,const CVec2<T>& v)
{
return CVec2<T>(s/v.v[0],  s/v.v[1]);
}

template <class T>
__forceinline CVec2<T> operator/(const CVec2<T>& v, const CVec2<T>& s)
{
return CVec2<T>(v.v[0]/s.v[0],  v.v[1]/s.v[1]);
}

template <class T>
__forceinline CVec2<T> operator+(const CVec2<T>& a, const CVec2<T>& B)
{
  return CVec2<T>(a.v[0]+b.v[0], a.v[1]+b.v[1]);
}

template <class T>
__forceinline CVec2<T> operator-(const CVec2<T>& a, const CVec2<T>& B)
{
  return CVec2<T>(a.v[0]-b.v[0],a.v[1]-b.v[1]);
}

template <class T>
__forceinline CVec2<T> operator-(const CVec2<T>& v)
{
  return CVec2<T>(-v.v[0], -v.v[1]);
}

// dot product

template <class T>
__forceinline T dot(const CVec2<T>& a, const CVec2<T>& B)
{
  return ( a.v[0]*b.v[0] +  a.v[1]*b.v[1] );
}

template <class T>
__forceinline CVec2<T> orthogonal( const CVec2<T>& a  )
{
// note that also a.v[1],-a.v[0] is orthogonal
return CVec2<T>( -a.v[1] ,a.v[0] );
}

// cross product

template <class T>
__forceinline CVec2<T> cross( const CVec2<T>& a , const CVec2<T>& b )
{
return CVec2<T>( a.v[0]*b.v[1] ,- a.v[1]*b.v[0] );
}

// gets absolute value vector

template <class T >
__forceinline CVec2<T> fabs( const CVec2<T> &v )
{
return CVec2<T>( fabsf(v.v[0]),fabsf(v.v[1]) );
}

// linear interpolation

template <class T>
__forceinline CVec2<T> lerp( const CVec2<T>&a , const CVec2<T>&b ,T t)
{
return CVec2<T>( a+ t*b );
}

// Calculates the projection of 'p' onto 'q'.

template <class T>
__forceinline CVec2<T> project( const CVec2<T> &p, const CVec2<T> &q)
{
T magnitudesqr=( q.v[0]*q.v[0]+q.v[1]*q.v[1] );
T num= (p.v[0]*q.v[0] +  p.v[1]*q.v[1] ) / magnitudesqr ;
return num * q ;
}

// Calculates the components of 'p' perpendicular to 'q'.

template <class T>
__forceinline CVec2<T> perpendicular(const CVec2<T> &p, const CVec2<T> &q )
{
T magnitudesqr=( q.v[0]*q.v[0]+q.v[1]*q.v[1] );
T num= (p.v[0]*q.v[0] +  p.v[1]*q.v[1] ) / magnitudesqr ;
return p - num * q;
}

// orthogonalization

template < class T>
__forceinline CVec2<T> orthogonalize( const CVec2<T> &v1, CVec2<T> &v2)
{
// Performs Gram-Schmidt Orthogonalization on the 2 basis vectors to
// turn them into orthonormal basis vectors.
    v2 = v2 - project(v2, v1);
    v2.normalize();
return v2;
}

// vector reflection

template < class T>
__forceinline CVec2<T> reflect(const CVec2<T> &v1, const CVec2<T> &v2 )
{
    // Calculates reflection vector from entering ray direction 'i'
    // and surface normal 'n'.
    return v1 - 2.0f * project(v1, v2);
}

// not that this 'cross' product gives the perpendicular
// vector to vector'v' contained in the same plane
// the 'cross' term is loosely applied here

template < class T >
__forceinline CVec2<T> cross( const CVec2<T> &v )
{
return CVec2<T>( v.v[1],-v.v[0] );
}

// normalize a vector

template < class T >
__forceinline CVec2<T> normalize( const CVec2<T> &v )
{
T denum=sqrt( v[0]*v[0]+v[1]*v[1] );
return CVec2<T>( v[0]/denum,v[1]/denum );
}

///////////////////////////////////////////////////////////
// function to be used when sorting elements

template < class T >
struct CVec2IsGreater
{
   bool operator()(const CVec2<T> &a, const CVec2<T> & B)
   {
      //compare a and b and return either true or false.
      return (a[0]*a[0]+a[1]*a[1]) > (b[0]*b[0]+b[1]*b[1]);
   }
};

//////////////////////////////////////////////
// define some types
// if you plan to use the vertex aray as source of floating point
// computation for example, in a 2d or 3d application,  be carfeull
// to avoid numeric truncation

typedef CVec2<float>        CVec2f;
typedef CVec2<double>      CVec2d;
typedef CVec2<signed int>  CVec2i; // using this numeric format may cause truncation
typedef CVec2<signed short>   CVec2s; // using this numeric format may cause truncation
typedef CVec2<signed char>    CVec2b; // using this numeric format may cause truncation
typedef CVec2<unsigned char>  CVec2ub; // using this numeric format may cause truncation
typedef CVec2<unsigned int>   CVec2ui; // using this numeric format may cause truncation