z = direction
x = up.cross(z).normalized()
y = z.cross(x)
M = [x, y, z] z = direction
x = up.cross(z).normalized()
y = z.cross(x)
M = [x, y, z]Edit: I think it has been answered by others - up is in world-space, and the camera is not necessarily aligned horizontally.
y = up.reject(direction).normalized()
where a.reject(b) = a - b.project(a)
[1] https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml...When I was learning 3D graphics, I found this site an excellent resource. Here is the page with a more in-depth explanation of the 'look at' function:
In the code, direction is assumed to be a unit vector, and that's your new z. x = up.cross(direction) is perpendicular to both up and direction, but may have arbitrary length since up and direction aren't generally perpendicular to each other; so x must also be normalized to length of one. y = z.cross(x) is perpendicular to both z and x, and doesn't need normalizing since they were already perpendicular (though extra normalizing doesn't hurt and sometimes--probably not here for most purposes--helps to clean up rounding error).
It's almost never a good idea to use Euler angles unless you're modeling a system where they have physical meaning (e.g., robot arm, gimbal, etc.). The math is almost always simpler when you work in terms of the rotation matrix directly (or quaternions, or axis-angle rotation, or pretty much anything else).
1. Or mirroring, which is the same as a rotation but with any two of the basis vectors swapped or any one negated.