It still feels like an easy hack
Most likely because you miss experience with 3D rotations. Maybe it helps to comment some of your statements:
"Z rotates with X and Y"
This is no problem - Z axis (and Y) must rotate if you rotate somthing around it's X axis - otherwise it's no rotation.
Same if you rotate around Y: Z & X rotate, Y stays the same.
I am trying to rotate an object in my game by using quaternions to avoid gimbal lock
I hear this often but it's nonsense. People tend to think Gimbal Lock is the bad guy each time they have a problem with rotations and Quats will fix it.
Matrices and Quaternions both can do any rotation in one step - there is no difference in regard to Gimbal Lock.
Euler angles may in deed cause unwanted Ginbal Lock problems, because the represent a series of multiple rotations in specified order.
But for human interaction they are still most comfortable, which is why we use them for editing animation curves or FPS camera.
So, by this example we used 2 Euler angles to get expected behaviour, but we still need math like quats or matrices to get the work done.
void TransformComponent::Rotate(glm::vec3 axis, float angle)
{
m_rotation *= glm::angleAxis(angle, axis);
}
There are 2 issues about this code:
First, you modify an orientation by rotation. Over time, floting point precission will cause degeneration of m_rotation.
Unlike position, orientation requires some constraints to be hold to stay a valid orientation:
A matrix needs its axis unit length and orthogonal to each other, a Quaternion must stay normalized.
So, you may need to orthonormalize or normalize m_rotation after such an operation.
(Or you avoid the issue by keeping only the euler angles like i did).
Second, and that may really help you in practice (in contrast to the other blah blah):
I suggest you make two functions for world OR local space:
void TransformComponent::RotateGlobal(glm::vec3 axis, float angle)
{
m_rotation *= glm::angleAxis(angle, axis);
}
void TransformComponent::RotateLocal(glm::vec3 axis, float angle)
{
m_rotation = glm::angleAxis(angle, axis) * m_rotation;
}
This way you always make clear if you rotate around the local object space or global world space.
(Note the only difference is multiplication order - i don't know glm's convention and it may be the other way around - maybe that caused some of your initial problems)