Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to rotate a vector by a given direction

I'm creating some random vectors/directions in a loop as a dome shape like this:

void generateDome(glm::vec3 direction)
{
    for(int i=0;i<1000;++i)
    {
        float xDir = randomByRange(-1.0f, 1.0f);
        float yDir = randomByRange(0.0f, 1.0f);
        float zDir = randomByRange(-1.0f, 1.0f);

        auto vec = glm::vec3(xDir, yDir, zDir);
        vec = glm::normalize(vec);

        ...
        //some transformation with direction-vector
     }
     ...
}

This creates vectors as a dome-shape in +y direction (0,1,0):

enter image description here

Now I want to rotate the vec-Vector by a given direction-Vector like (1,0,0). This should rotate the "dome" to the x-direction like this:

enter image description here

How can I achieve this? (preferably with glm)

like image 314
Bastl Avatar asked Jan 04 '14 16:01

Bastl


2 Answers

A rotation is generally defined using some sort of offset (axis-angle, quaternion, euler angles, etc) from a starting position. What you are looking for would be more accurately described (in my opinion) as a re-orientation. Luckily this isn't too hard to do. What you need is a change-of-basis matrix.

First, lets just define what we're working with in code:

using glm::vec3;
using glm::mat3;

vec3 direction;  // points in the direction of the new Y axis
vec3 vec;        // This is a randomly generated point that we will
                 // eventually transform using our base-change matrix

To calculate the matrix, you need to create unit vectors for each of the new axes. From the example above it becomes apparent that you want the vector provided to become the new Y-axis:

vec3 new_y = glm::normalize(direction);

Now, calculating the X and Z axes will be a tad more complicated. We know that they must be orthogonal to each other and to the Y axis calculated above. The most logical way to construct the Z axis is to assume that the rotation is taking place in the plane defined by the old Y axis and the new Y axis. By using the cross-product we can calculate this plane's normal vector, and use that for the Z axis:

vec3 new_z = glm::normalize(glm::cross(new_y, vec3(0, 1, 0)));

Technically the normalization isn't necessary here since both input vectors are already normalized, but for the sake of clarity, I've left it. Also note that there is a special case when the input vector is colinear with the Y-axis, in which case the cross product above is undefined. The easiest way to fix this is to treat it as a special case. Instead of what we have so far, we'd use:

if (direction.x == 0 && direction.z == 0)
{
    if (direction.y < 0) // rotate 180 degrees
       vec = vec3(-vec.x, -vec.y, vec.z);

    // else if direction.y >= 0, leave `vec` as it is.
}
else
{
    vec3 new_y = glm::normalize(direction);

    vec3 new_z = glm::normalize(glm::cross(new_y, vec3(0, 1, 0)));

    // code below will go here.
}

For the X-axis, we can cross our new Y-axis with our new Z-axis. This yields a vector perpendicular to both of the others axes:

vec3 new_x = glm::normalize(glm::cross(new_y, new_z));

Again, the normalization in this case is not really necessary, but if y or z were not already unit vectors, it would be.

Finally, we combine the new axis vectors into a basis-change matrix:

mat3 transform = mat3(new_x, new_y, new_z);

Multiplying a point vector (vec3 vec) by this yields a new point at the same position, but relative to the new basis vectors (axes):

vec = transform * vec;

Do this last step for each of your randomly generated points and you're done! No need to calculate angles of rotation or anything like that.

As a side note, your method of generating random unit vectors will be biased towards directions away from the axes. This is because the probability of a particular direction being chosen is proportional to the distance to the furthest point possible in a given direction. For the axes, this is 1.0. For directions like eg. (1, 1, 1), this distance is sqrt(3). This can be fixed by discarding any vectors which lie outside the unit sphere:

glm::vec3 vec;
do
{
    float xDir = randomByRange(-1.0f, 1.0f);
    float yDir = randomByRange(0.0f, 1.0f);
    float zDir = randomByRange(-1.0f, 1.0f);

    vec = glm::vec3(xDir, yDir, zDir);
} while (glm::length(vec) > 1.0f);  // you could also use glm::length2 instead, and avoid a costly sqrt().

vec = glm::normalize(vec);

This would ensure that all directions have equal probability, at the cost that if you're extremely unlucky, the points picked may lie outside the unit sphere over and over again, and it may take a long time to generate one that's inside. If that's a problem, it could be modified to limit the iterations: while (++i < 4 && ...) or by increasing the radius at which a point is accepted every iteration. When it is >= sqrt(3), all possible points would be considered valid, so the loop would end. Both of these methods would result in a slight biasing away from the axes, but in almost any real situation, it would not be detectable.

Putting all the code above together, combined with your code, we get:

void generateDome(glm::vec3 direction)
{
    // Calculate change-of-basis matrix
    glm::mat3 transform;

    if (direction.x == 0 && direction.z == 0)
    {
        if (direction.y < 0) // rotate 180 degrees
            transform = glm::mat3(glm::vec3(-1.0f, 0.0f  0.0f),
                                  glm::vec3( 0.0f, -1.0f, 0.0f),
                                  glm::vec3( 0.0f,  0.0f, 1.0f));

        // else if direction.y >= 0, leave transform as the identity matrix.
    }
    else
    {
        vec3 new_y = glm::normalize(direction);
        vec3 new_z = glm::normalize(glm::cross(new_y, vec3(0, 1, 0)));
        vec3 new_x = glm::normalize(glm::cross(new_y, new_z));

        transform = mat3(new_x, new_y, new_z);
    }


    // Use the matrix to transform random direction vectors
    vec3 point;
    for(int i=0;i<1000;++i)
    {
        int k = 4; // maximum number of direction vectors to guess when looking for one inside the unit sphere.
        do
        {
            point.x = randomByRange(-1.0f, 1.0f);
            point.y = randomByRange(0.0f, 1.0f);
            point.z = randomByRange(-1.0f, 1.0f);
        } while (--k > 0 && glm::length2(point) > 1.0f);

        point = glm::normalize(point);

        point = transform * point;
        // ...
    }
    // ...
}
like image 177
bcrist Avatar answered Oct 21 '22 17:10

bcrist


You need to create a rotation matrix. Therefore you need a identity Matrix. Create it like this with

glm::mat4 rotationMat(1); // Creates a identity matrix

Now your can rotate the vectorspacec with

rotationMat = glm::rotate(rotationMat, 45.0f, glm::vec3(0.0, 0.0, 1.0));

This will rotate the vectorspace by 45.0 degrees around the z-axis (as shown in your screenshot). Now your almost done. To rotate your vec you can write

vec = glm::vec3(rotationMat * glm::vec4(vec, 1.0));

Note: Because you have a 4x4 matrix you need a vec4 to multiply it with the matrix. Generally it is a good idea always to use vec4 when working with OpenGL because vectors in smaller dimension will be converted to homogeneous vertex coordinates anyway.

EDIT: You can also try to use GTX Extensions (Experimental) by including <glm/gtx/rotate_vector.hpp>

EDIT 2: When you want to rotate the dome "towards" a given direction you can get your totation axis by using the cross-product between the direction and you "up" vector of the dome. Lets say you want to rotate the dome "toward" (1.0, 1.0, 1.0) and the "up" direction is (0.0, 1.0, 0.0) use:

glm::vec3 cross = glm::cross(up, direction);
glm::rotate(rotationMat, 45.0f, cross);

To get your rotation matrix. The cross product returns a vector that is orthogonal to "up" and "direction" and that's the one you want to rotate around. Hope this will help.

like image 20
joschuck Avatar answered Oct 21 '22 15:10

joschuck