5
\$\begingroup\$

I am working on a game engine and in order to learn the math behind graphics programming I decided to write my own mathmathical structs and operations.

I went for an union class that gives me the oppertunity to use an array and x, y, z, w accessors without using extra memory. Being the first time I'm using unions I am curious if I am using them the correct way. Also because both Quaternion and Vector4 have a lot of similarities I am trying to find out how I could share some more code between the two unions.

Quaternion.h

#ifndef CHEETAH_CORE_MATH_QUATERNION_H_
#define CHEETAH_CORE_MATH_QUATERNION_H_

#include "Vector4.h"
#include "Vector3.h"
#include "Mat4x4.h"

#include<math.h>

#include "Core/Core.h"

namespace cheetah
{
    template<typename T>
    union Quaternion
    {
    public:
        Quaternion();
        Quaternion(const T& axisX, const T& axisY, const T& axisZ, const T& degrees);
        Quaternion(const Vector3<T>& axis, const T& degrees);
        Quaternion(const T fill[4]);

        struct
        {
            T axisX, axisY, axisZ, degrees;
        };

        inline const T* get() const;
        inline Mat4x4<T> getMatrix() const;

        inline void normalize();
        inline Quaternion<T> normalize(const Quaternion<T>& vector) const;

        inline void operator *= (const T& rhs);
        inline void operator += (const T& rhs);
        inline void operator -= (const T& rhs);
        inline void operator /= (const T& rhs);

        inline Quaternion<T> operator + (const Vector4<T>& rhs) const;
        inline Quaternion<T> operator - (const Vector4<T>& rhs) const;

        inline T operator * (const Vector4<T>& rhs) const;

    private:
        struct
        {
            T m_data[4];
        };
    }; 

    template union CH_API Quaternion<float>;
    template union CH_API Quaternion<int>;
    template union CH_API Quaternion<double>;

    using Quaternionf = Quaternion<float>;
    using Quaternioni = Quaternion<int>;
    using Quaterniond = Quaternion<double>;
}

#include "Quaternion.inl"

#endif // !CHEETAH_CORE_MATH_QUATERNION_H_

Quaternion.inl

namespace cheetah
{
    // ctors
    template<typename T>
    inline Quaternion<T>::Quaternion()
        : m_data{ 0, 0, 0, 0 }
    {
    }

    template<typename T>
    inline Quaternion<T>::Quaternion(const T& axisX, const T& axisY, const T& axisZ, const T& degrees)
        : m_data{ axisX, axisY, axisZ, degrees }
    {
    }

    template<typename T>
    inline Quaternion<T>::Quaternion(const Vector3<T>& axis, const T& degrees)
        : m_data{ axis.x, axis.y, axis.z, degrees }
    {
    }

    template<typename T>
    inline Quaternion<T>::Quaternion(const T fill[4])
        : m_data{ fill[0], fill[1], fill[2], fill[3] }
    {
    }

    // getters
    template<typename T>
    inline const T* Quaternion<T>::get() const
    {
        return &m_data[0];
    }

    template<>
    inline Mat4x4<int> Quaternion<int>::getMatrix() const
    {
        Quaternion<int> quat = normalize(Quaternion<int>(axisX, axisY, axisZ, degrees));
        std::vector<int> mat = std::vector<int>
        {
            1 - 2 * quat.m_data[1] * quat.m_data[1] - 2 * quat.m_data[2] * quat.m_data[2],      2 * quat.m_data[0] * quat.m_data[1] - 2 * quat.m_data[2] * quat.m_data[3],          2 * quat.m_data[0] * quat.m_data[2] + 2 * quat.m_data[1] * quat.m_data[3],          0,
            2 * quat.m_data[0] * quat.m_data[1] + 2 * quat.m_data[2] * quat.m_data[3],          1 - 2 * quat.m_data[0] * quat.m_data[0] - 2 * quat.m_data[2] * quat.m_data[2],      2 * quat.m_data[1] * quat.m_data[2] - 2 * quat.m_data[0] * quat.m_data[3],          0,
            2 * quat.m_data[0] * quat.m_data[2] - 2 * quat.m_data[1] * quat.m_data[3],          2 * quat.m_data[1] * quat.m_data[2] + 2 * quat.m_data[0] * quat.m_data[3],          1 - 2 * quat.m_data[0] * quat.m_data[0] - 2 * quat.m_data[1] * quat.m_data[1],      0,
            0,                                                                                  0,                                                                                  0,                                                                                  1
        };

        return Mat4x4i(mat);
    }

    template<>
    inline Mat4x4<double> Quaternion<double>::getMatrix() const
    {
        Quaternion<double> quat = normalize(Quaternion<double>(axisX, axisY, axisZ, degrees));
        std::vector<double> mat = std::vector<double>
        {
            1 - 2 * quat.m_data[1] * quat.m_data[1] - 2 * quat.m_data[2] * quat.m_data[2],      2 * quat.m_data[0] * quat.m_data[1] - 2 * quat.m_data[2] * quat.m_data[3],          2 * quat.m_data[0] * quat.m_data[2] + 2 * quat.m_data[1] * quat.m_data[3],          0,
            2 * quat.m_data[0] * quat.m_data[1] + 2 * quat.m_data[2] * quat.m_data[3],          1 - 2 * quat.m_data[0] * quat.m_data[0] - 2 * quat.m_data[2] * quat.m_data[2],      2 * quat.m_data[1] * quat.m_data[2] - 2 * quat.m_data[0] * quat.m_data[3],          0,
            2 * quat.m_data[0] * quat.m_data[2] - 2 * quat.m_data[1] * quat.m_data[3],          2 * quat.m_data[1] * quat.m_data[2] + 2 * quat.m_data[0] * quat.m_data[3],          1 - 2 * quat.m_data[0] * quat.m_data[0] - 2 * quat.m_data[1] * quat.m_data[1],      0,
            0,                                                                                  0,                                                                                  0,                                                                                  1
        };

        return Mat4x4d(mat);
    }

    template<>
    inline Mat4x4<float> Quaternion<float>::getMatrix() const
    {
        Quaternion<float> quat = normalize(Quaternion<float>(axisX, axisY, axisZ, degrees));
        std::vector<float> mat = std::vector<float>
        {
            1.0f - 2 * quat.m_data[1] * quat.m_data[1] - 2.0f * quat.m_data[2] * quat.m_data[2],        2.0f * quat.m_data[0] * quat.m_data[1] - 2.0f * quat.m_data[2] * quat.m_data[3],            2.0f * quat.m_data[0] * quat.m_data[2] + 2.0f * quat.m_data[1] * quat.m_data[3],            0.0f,
            2.0f * quat.m_data[0] * quat.m_data[1] + 2.0f * quat.m_data[2] * quat.m_data[3],            1.0f - 2.0f * quat.m_data[0] * quat.m_data[0] - 2.0f * quat.m_data[2] * quat.m_data[2],     2.0f * quat.m_data[1] * quat.m_data[2] - 2.0f * quat.m_data[0] * quat.m_data[3],            0.0f,
            2.0f * quat.m_data[0] * quat.m_data[2] - 2.0f * quat.m_data[1] * quat.m_data[3],            2.0f * quat.m_data[1] * quat.m_data[2] + 2.0f * quat.m_data[0] * quat.m_data[3],            1.0f - 2.0f * quat.m_data[0] * quat.m_data[0] - 2.0f * quat.m_data[1] * quat.m_data[1],     0.0f,
            0.0f,                                                                                       0.0f,                                                                                       0.0f,                                                                                       1.0f
        };

        return Mat4x4f(mat);
    }

    template<typename T>
    inline Mat4x4<T> Quaternion<T>::getMatrix() const
    {
    }


    // math
    template<>
    inline void Quaternion<int>::normalize()
    {
        const int n = 1 / sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ + degrees * degrees);
        m_data[0] *= n;
        m_data[1] *= n;
        m_data[2] *= n;
        m_data[3] *= n;
    }

    template<>
    inline void Quaternion<double>::normalize()
    {
        const double n = 1 / sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ + degrees * degrees);
        m_data[0] *= n;
        m_data[1] *= n;
        m_data[2] *= n;
        m_data[3] *= n;
    }

    template<>
    inline void Quaternion<float>::normalize()
    {
        const float n = 1.0f / sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ + degrees * degrees);
        m_data[0] *= n;
        m_data[1] *= n;
        m_data[2] *= n;
        m_data[3] *= n;
    }

    template<typename T>
    inline void Quaternion<T>::normalize()
    {
    }

    template<>
    inline Quaternion<int> Quaternion<int>::normalize(const Quaternion<int>& quat) const
    {
        const int n = 1 / sqrt(quat.axisX * quat.axisX + quat.axisY * quat.axisY + quat.axisZ * quat.axisZ + quat.degrees * quat.degrees);
        Quaternion<int> q(quat.axisX, quat.axisY, quat.axisZ, quat.degrees);
        q.m_data[0] *= n;
        q.m_data[1] *= n;
        q.m_data[2] *= n;
        q.m_data[3] *= n;

        return quat;
    }

    template<>
    inline Quaternion<double> Quaternion<double>::normalize(const Quaternion<double>& quat) const
    {
        const double n = 1 / sqrt(quat.axisX * quat.axisX + quat.axisY * quat.axisY + quat.axisZ * quat.axisZ + quat.degrees * quat.degrees);
        Quaternion<double> q(quat.axisX, quat.axisY, quat.axisZ, quat.degrees);
        q.m_data[0] *= n;
        q.m_data[1] *= n;
        q.m_data[2] *= n;
        q.m_data[3] *= n;

        return quat;
    }

    template<>
    inline Quaternion<float> Quaternion<float>::normalize(const Quaternion<float>& quat) const
    {
        const float n = 1.0f / sqrt(quat.axisX * quat.axisX + quat.axisY * quat.axisY + quat.axisZ * quat.axisZ + quat.degrees * quat.degrees);
        Quaternion<float> q(quat.axisX, quat.axisY, quat.axisZ, quat.degrees);
        q.m_data[0] *= n;
        q.m_data[1] *= n;
        q.m_data[2] *= n;
        q.m_data[3] *= n;

        return quat;
    }

    template<typename T>
    inline Quaternion<T> Quaternion<T>::normalize(const Quaternion<T>& quat) const
    {
    }

    template<typename T>
    inline void Quaternion<T>::operator *= (const T& rhs)
    {
        m_data[0] *= rhs;
        m_data[1] *= rhs;
        m_data[2] *= rhs;
        m_data[3] *= rhs;
    }

    template<typename T>
    inline void Quaternion<T>::operator += (const T& rhs)
    {
        m_data[0] += rhs;
        m_data[1] += rhs;
        m_data[2] += rhs;
        m_data[3] += rhs;
    }

    template<typename T>
    inline void Quaternion<T>::operator -= (const T& rhs)
    {
        m_data[0] -= rhs;
        m_data[1] -= rhs;
        m_data[2] -= rhs;
        m_data[3] -= rhs;
    }

    template<typename T>
    inline void Quaternion<T>::operator /= (const T& rhs)
    {
        m_data[0] /= rhs;
        m_data[1] /= rhs;
        m_data[2] /= rhs;
        m_data[3] /= rhs;
    }

    template<typename T>
    inline Quaternion<T> Quaternion<T>::operator + (const Vector4<T>& rhs) const
    {
        return Quaternion<T>(m_data[0] + rhs.x, m_data[1] + rhs.y, m_data[2] + rhs.z, m_data[3] + rhs.w);
    }

    template<typename T>
    inline Quaternion<T> Quaternion<T>::operator - (const Vector4<T>& rhs) const
    {
        return Quaternion<T>(m_data[0] + (-rhs.x), m_data[1] + (-rhs.y), m_data[2] + (-rhs.z), m_data[3] + (-rhs.w));
    }

    template<typename T>
    inline T Quaternion<T>::operator * (const Vector4<T>& rhs) const
    {
        return (m_data[0] * rhs.x) + (m_data[1] * rhs.y) + (m_data[2] * rhs.z) + (m_data[3] * rhs.w);
    }
}

Vector4.h

#ifndef CHEETAH_ENGINE_MATH_VECTOR4_H_
#define CHEETAH_ENGINE_MATH_VECTOR4_H_

#include "Core/Core.h"
#include "Vector3.h"

namespace cheetah
{
    template<typename T>
    union Vector4
    {
        inline Vector4();
        inline Vector4(const T& fill);
        inline Vector4(const T fill[4]);
        inline Vector4(const Vector3<T>& fill, const T& w);
        inline Vector4(const T& x, const T& y, const T& z, const T& w);

        struct
        {
            T x, y, z, w;
        };

        inline const T* get() const;

        inline T magnitude() const;

        inline void operator *= (const T& rhs);
        inline void operator += (const T& rhs);
        inline void operator -= (const T& rhs);
        inline void operator /= (const T& rhs);

        inline Vector4<T> operator + (const Vector4<T>& rhs) const;
        inline Vector4<T> operator - (const Vector4<T>& rhs) const;

        inline T operator * (const Vector4<T>& rhs) const;

    private:
        struct
        {
            T m_data[4];
        };
    };

    template union CH_API Vector4<float>;
    template union CH_API Vector4<int>;
    template union CH_API Vector4<double>;

    using Vector4f = Vector4<float>;
    using Vector4i = Vector4<int>;
    using Vector4d = Vector4<double>;
}

#include "Vector4.inl"

#endif // !CHEETAH_ENGINE_MATH_VECTOR_H_

Vector4.inl

namespace cheetah
{
    // ctors
    template<typename T>
    inline Vector4<T>::Vector4()
        : m_data{ 0, 0, 0, 0 }
    {
    }

    template<typename T>
    inline Vector4<T>::Vector4(const T& fill)
        : m_data{ fill, fill, fill, fill }
    {
    }

    template<typename T>
    inline Vector4<T>::Vector4(const T fill[4])
        : m_data{ fill[0], fill[1], fill[2], fill[3] }
    {
    }

    template<typename T>
    inline Vector4<T>::Vector4(const Vector3<T>& fill, const T& w)
        : m_data{ fill.x, fill.y, fill.z, w }
    {
    }

    template<typename T>
    inline Vector4<T>::Vector4(const T& x, const T& y, const T& z, const T& w)
        : m_data{ x, y, z, w }
    {
    }

    // getters
    template<typename T>
    inline const T* Vector4<T>::get() const
    {
        return &m_data[0];
    }

    //math
    template<typename T>
    inline T Vector4<T>::magnitude() const
    {
        return sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2) + pow(w, 2));
    }

    // operators
    template<typename T>
    inline void Vector4<T>::operator *= (const T& rhs)
    {
        m_data[0] *= rhs;
        m_data[1] *= rhs;
        m_data[2] *= rhs;
        m_data[3] *= rhs;
    }

    template<typename T>
    inline void Vector4<T>::operator /= (const T& rhs)
    {
        m_data[0] /= rhs;
        m_data[1] /= rhs;
        m_data[2] /= rhs;
        m_data[3] /= rhs;
    }

    template<typename T>
    inline void Vector4<T>::operator += (const T& rhs)
    {
        m_data[0] += rhs;
        m_data[1] += rhs;
        m_data[2] += rhs;
        m_data[3] += rhs;
    }

    template<typename T>
    inline void Vector4<T>::operator -= (const T& rhs)
    {
        m_data[0] -= rhs;
        m_data[1] -= rhs;
        m_data[2] -= rhs;
        m_data[3] -= rhs;
    }

    template<typename T>
    inline Vector4<T> Vector4<T>::operator + (const Vector4<T>& rhs) const
    {
        return Vector4<T>(m_data[0] + rhs.x, m_data[1] + rhs.y, m_data[2] + rhs.z, m_data[3] + rhs.w);
    }

    template<typename T>
    inline Vector4<T> Vector4<T>::operator - (const Vector4<T>& rhs) const
    {
        return Vector4<T>(m_data[0] + (-rhs.x), m_data[1] + (-rhs.y), m_data[2] + (-rhs.z), m_data[3] + (-rhs.w));
    }

    template<typename T>
    inline T Vector4<T>::operator * (const Vector4<T>& rhs) const
    {
        return (m_data[0] * rhs.m_data[0]) + (m_data[1] * rhs.m_data[1]) + (m_data[2] * rhs.m_data[2]) + (m_data[3] * rhs.m_data[3]);
    }
}

The core file included defines the CH_API macro that expands to either dllexport or dllimport like below:

Core.h

#ifdef CH_PLATFORM_WINDOWS
#ifdef CH_BUILD_DLL
#define CH_API __declspec(dllexport)
#else 
#define CH_API __declspec(dllimport)
#endif // CH_BUILD_DLL
#else 

#error Cheetah currently only supports windows!
#endif // CH_PLATFORM_WINDOWS
\$\endgroup\$
  • \$\begingroup\$ You missed Core/Core.h, which seems to be needed by the other headers. \$\endgroup\$ – Toby Speight Jan 13 at 14:23
  • \$\begingroup\$ @TobySpeight it declares CH_API macro that expands to either dllimport or dllexport. If you want I can add the file but it contains also useless other defines \$\endgroup\$ – Rick Nijhuis Jan 13 at 14:31
  • 1
    \$\begingroup\$ I think my main criticism would be that there's got to be code out there to implement this already, why re-invent the wheel. Also sqrt is likely to be too slow for a real game. \$\endgroup\$ – markspace Jan 13 at 18:24
  • \$\begingroup\$ @markspace as stated above, for learning purposes. I could use libraries like glm but then I would have a much more difficult time actually understanding what is happening. Thanks for the suggestion about sqrt, I will look in to it. \$\endgroup\$ – Rick Nijhuis Jan 13 at 18:32
  • 1
    \$\begingroup\$ I'm dubious about the definition inline void Quaternion<T>::operator += (const T& rhs). It doesn't make sense to add a quaternion (or vector) to a scalar. \$\endgroup\$ – David Knipe Jan 13 at 21:30
5
\$\begingroup\$

The utility of writing your own vector/quaternion code for the purposes of writing a game seems very minimal, considering its been done many times before. Glm/eigen are notable examples, but there's plenty of others. They also support portable vector instruction implementations that have gone through rigorous testing, an extremely time consuming and arguably boring task. This isn't to say there's no value in writing your own math library, but is the product you're trying to make/learn about a math library or a game engine? If you were hired by someone to write a game engine, would they really appreciate you working on a math library?

If you're truly interested in exploring the development of a math library, I would look at the source of existing ones and try to understand the rationale behind them. For example, they might often declare a byte alignment of their types -- why would that be important? What does their code look like when compiled to assembly compared to yours?


Do not label the w component of quaternions as degrees, it is very incorrect and will surely cause confusion.


An integer quaternion is bizarre and I don't know how that would be used.


The template specializations seem unnecessary and confusing.


There's no 3D vector rotation implementation on the quaternions. There's also no quaternion-quaternion multiplication, so they cannot be composed easily.


It seems like it would be fine to expose the array data since the xyzw data is exposed already.


Constructing a std::vector to initialize a matrix does not seem ideal.


sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2) + pow(w, 2)) This optimizes correctly on gcc but will be unnecessarily slow in unoptimized builds.


Nitpicky, but using a union like this feels wrong. I understand that it's working as intended, but it doesn't seem typical. If you forgot that the data structure is a union, it might be possible to forget that data and xyzw have the same storage, rather than putting them in a union within a class or struct.

\$\endgroup\$
  • \$\begingroup\$ Thanks, I also write it because I have fun doing it, this is mainly a hobby project, I do agree with you on the renaming of the degrees to w and exposing the array so I change those, I will also remove the integer quaternion specialization. The fact that the utilities seem minimal is because currently I have added only the utilities I need, I will for sure extend the classes with extra funtionality when I need the functionality. \$\endgroup\$ – Rick Nijhuis Jan 13 at 21:34
  • \$\begingroup\$ I understand the sentiment but beware that it can incur unnecessary technical debt by writing it yourself. You may want to briefly put on a producer hat and estimate the total time you are likely to use this code, the amount of time spent on potential bugs (design/functional/performance), and the potential time saved with writing automated tests. Testing a math library is a good learning experience because there's few dependencies and a gentle introduction to using test frameworks. I say this because I think you would be confronted with things like how to properly test an integer quaternion. \$\endgroup\$ – butt Jan 13 at 21:40
  • 1
    \$\begingroup\$ Thanks for your additional findings, I will take a look at a library, still might go with building my own because I had fun trying stuf out, it is a good idea to try to write tests, havent done that before in c++ so might try to find a nice test framework. If you know any that work well for you let me know! \$\endgroup\$ – Rick Nijhuis Jan 13 at 21:46
5
\$\begingroup\$

In addition to butt's comments:

Avoid repetition

There are lots of places in the code where you are unnecessarily repeating yourself, in particular when it comes to types. For example, take this line:

Quaternion<int> quat = normalize(Quaternion<int>(axisX, axisY, axisZ, degrees));

You are writing Quaternion<int> on both sides of the declaration. You can easily get rid of one by using auto. But why are you explicitly constructing a copy of the quaternion in the first place? You can just use *this, and then the whole line reduces to:

auto quat = normalize(*this);

In this line:

std::vector<int> mat = std::vector<int> {...};

You also repeat the type twice. Either use auto on the left, or just write:

std::vector<int> mat {...};

In fact, if you want to return a quaternion, and the compiler already knows the return type of a member function, then you don't need to explicitly specify this a second time. For example, operator+() can be written as:

template<typename T>
inline Quaternion<T> Quaternion<T>::operator+(const Vector4<T>& rhs) const
{
    return {m_data[0] + rhs.x, m_data[1] + rhs.y, m_data[2] + rhs.z, m_data[3] + rhs.w};
}

Also, butt already mentioned the template specializations, this is another case of unnecessary repetition.

Another thing is that you don't have to write Quaternion<T> inside the definition of class Quaternion, you can omit the <T>. A lot of repetition can be avoided if you would define all member functions inside class Quaternion instead of putting them in "Quaternion.inl", but if you think it is better to have those separated then you'll have to live with it.

Make functions that don't use member variables static

A function that does not access member variables should be made static, so it can be used without needing an instance of the class. For example:

inline Quaternion<T> normalize(const Quaternion<T>& vector) const;

This should be written as:

static inline Quaternion normalize(const Quaternion& vector);
\$\endgroup\$
  • \$\begingroup\$ Thanks for your findings, good catch of the normalize method, that one needs to be static for sure, will change it. I think I will keep my implementations inside the .inl file, this for providing a clear interface for users using the engine. I agree on the avoid repetition, at first I went for showing all the types because I thought it would be more clear but that is obviously not really the case and it indeed does add a lot of repetition. \$\endgroup\$ – Rick Nijhuis Jan 14 at 7:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.