I am facing problems trying to make 3d objects clickable by mouse. For intersection checking I use ray casting. Code I found, ported for my solution:
Exactly picking
bool RaySphereIntersect(Vector3, Vector3, float);
bool TestIntersection(Matrix projectionMatrix, Matrix viewMatrix, Matrix worldMatrix, Vector3 origin, float radius, int m_screenWidth, int m_screenHeight, int mouseX, int mouseY)
{
float pointX, pointY;
Matrix inverseViewMatrix, translateMatrix, inverseWorldMatrix;
Vector3 direction, rayOrigin, rayDirection;
bool intersect, result;
// Move the mouse cursor coordinates into the -1 to +1 range.
pointX = ((2.0f * (float)mouseX) / (float)m_screenWidth) - 1.0f;
pointY = (((2.0f * (float)mouseY) / (float)m_screenHeight) - 1.0f) * -1.0f;
// Adjust the points using the projection matrix to account for the aspect ratio of the viewport.
pointX = pointX / projectionMatrix._11;
pointY = pointY / projectionMatrix._22;
// Get the inverse of the view matrix.
inverseViewMatrix=XMMatrixInverse(NULL, viewMatrix);
// Calculate the direction of the picking ray in view space.
direction.x = (pointX * inverseViewMatrix._11) + (pointY * inverseViewMatrix._21) + inverseViewMatrix._31;
direction.y = (pointX * inverseViewMatrix._12) + (pointY * inverseViewMatrix._22) + inverseViewMatrix._32;
direction.z = (pointX * inverseViewMatrix._13) + (pointY * inverseViewMatrix._23) + inverseViewMatrix._33;
// Get the origin of the picking ray which is the position of the camera.
// Get the world matrix and translate to the location of the sphere.
// Now get the inverse of the translated world matrix.
inverseWorldMatrix= XMMatrixInverse(NULL, worldMatrix);
// Now transform the ray origin and the ray direction from view space to world space.
rayOrigin=XMVector3TransformCoord(origin, inverseWorldMatrix);
rayDirection=XMVector3TransformNormal(direction, inverseWorldMatrix);
// Normalize the ray direction.
rayDirection=XMVector3Normalize(rayDirection);
// Now perform the ray-sphere intersection test.
intersect = RaySphereIntersect(rayOrigin, rayDirection, radius);
if (intersect == true)
return true;
else
return false;
}
bool RaySphereIntersect(Vector3 rayOrigin, Vector3 rayDirection, float radius)
{
float a, b, c, discriminant;
// Calculate the a, b, and c coefficients.
a = (rayDirection.x * rayDirection.x) + (rayDirection.y * rayDirection.y) + (rayDirection.z * rayDirection.z);
b = ((rayDirection.x * rayOrigin.x) + (rayDirection.y * rayOrigin.y) + (rayDirection.z * rayOrigin.z)) * 2.0f;
c = ((rayOrigin.x * rayOrigin.x) + (rayOrigin.y * rayOrigin.y) + (rayOrigin.z * rayOrigin.z)) - (radius * radius);
// Find the discriminant.
discriminant = (b * b) - (4 * a * c);
// if discriminant is negative the picking ray missed the sphere, otherwise it intersected the sphere.
if (discriminant < 0.0f)
return false;
else
return true;
}
How do I create sphere
D3DSphere(float x, float y, float z, float radius, float r, float g, float b, float a)
{
this->x = x;
this->y = y;
this->z = z;
this->radius = radius;
this->shape = GeometricPrimitive::CreateSphere(radius*2.0f);
this->world = Matrix::Identity;
this->world = XMMatrixMultiply(this->world, Matrix::CreateTranslation(x, y, z));
this->index = vsphere;
d3dsphere[vsphere] = this;
vsphere++;
}
How do I call raycaster
void Game::LButtonUp(int x, int y)
{
Vector3 eye(camx, camy, camz);
Vector3 at(Vector3::Zero);
m_view = Matrix::CreateLookAt(eye, at, Vector3::UnitY);
for (int i = 0; i < vsphere; i++)
{
if (TestIntersection(m_projection, m_view, d3dsphere[i]->world, eye, d3dsphere[i]->radius, 800, 600, x, y))
{
MessageBoxW(NULL, L"LOL", L"It works", MB_OK);
break;
}
}
}
Nothing happens by clicking, but if I rotate camera, perpendicularly to XOY, sometimes, clicking near the sphere, message box appears.
Update
MessageBox appears independently on camera angle, and it seems, that it detects intersection correctly, but mirrored, relatively to the window center. For example, if sphere is at (0, window.bottom-20) point then I will get MessageBox if I click at (0, 20) point.
What if calculation of the direction of the picking ray is wrong, if it was wrote for left-handed system, and I use right-handed?
Probably, because of the right-handed system, that is used by default in DirectX Tool Kit the next section from caster
pointX = ((2.0f * (float)mouseX) / (float)m_screenWidth) - 1.0f;
pointY = (((2.0f * (float)mouseY) / (float)m_screenHeight) - 1.0f) * -1.0f;
Should be changed to
pointX = (((2.0f * (float)mouseX) / (float)m_screenWidth) - 1.0f) * -1.0f;
pointY = (((2.0f * (float)mouseY) / (float)m_screenHeight) - 1.0f);
Important
That code also will work wrong because of depth independence, i.e. you may select sphere that is situated behind the sphere you clicking. For solve that I changed the code:
float distance3(float x1, float y1, float z1, float x2, float y2, float z2)
{
float dx=x1-x2;
float dy=y1-y2;
float dz=z1-z2;
return sqrt(dx*dx+dy*dy+dz*dz);
}
void Game::LButtonUp(int x, int y)
{
Vector3 eye(camx, camy, camz);
Vector3 at(Vector3::Zero);
m_view = Matrix::CreateLookAt(eye, at, Vector3::UnitY);
int last_index=-1;
float last_distance=99999.0f;//set the obviously highest value, may happen in your scene
for (int i = 0; i < vsphere; i++)
{
if (TestIntersection(m_projection, m_view, d3dsphere[i]->world, eye, d3dsphere[i]->radius, 800, 600, x, y))
{
float distance=distance3(camx,camy,camz, d3dsphere[i]->x, d3dsphere[i]->y, d3dsphere[i]->z);
if(distance<last_distance)
{
last_distance=distance;
last_index=i;
}
}
}
d3dsphere[last_index];//picked sphere
}
Related
I am writing a program with OpenGL/GLUT using the fixed function pipeline (I know, I know, it's university). I've written the Quaternion class from scratch with help of other implementations and the internet and it essentially works fine, but it's rotating along every axis in the opposite direction to what I thought it would.
I thought about posting this on the Math stack exchange, but given it's OpenGL/GLUT I thought it would be better understood here.
The axis below are: green -> Y, red -> X, blue -> Z. The darker sides are the positive directions. I've rotated it a little to show the positive Z axis. The axes are the world coordinate axes.
I have defined pressing "a" as a positive rotation in Y. The right hand rule states that this is a counterclockwise rotation in the Y axis. The image shows the rotation of the cube after pressing "a". As you can see, it has rotated clockwise. This occurs for every axis.
My Quaternion class:
#define _USE_MATH_DEFINES
#include <cmath>
#include "Quaternion.h"
#include "Utility.h"
Quaternion::Quaternion() : X(0), Y(0), Z(0), W(1) {}
Quaternion::Quaternion(Vector3D axis, float angle) {
float mag = Vector3D::magnitude(axis);
angle = utility::toRadians(angle);
float sine = sinf(angle * 0.5f);
// Divide by magnitude for pure quaternion
X = axis.X * sine / mag;
Y = axis.Y * sine / mag;
Z = axis.Z * sine / mag;
W = cosf(angle * 0.5f);
}
Quaternion::Quaternion(float x, float y, float z, float w) :
X(x), Y(y), Z(z), W(w) {}
float Quaternion::magnitude(const Quaternion &q) {
return sqrtf(q.X * q.X + q.Y * q.Y + q.Z * q.Z + q.W * q.W);
}
Quaternion Quaternion::normalise(const Quaternion &q) {
float mag = magnitude(q);
return Quaternion(q.X / mag, q.Y / mag, q.Z / mag, q.W / mag);
}
std::array<float, 16> Quaternion::toMatrix(const Quaternion& q) {
float X = q.X;
float Y = q.Y;
float Z = q.Z;
float W = q.W;
return {
1 - 2*(Z*Z + Y*Y), 2*(X*Y - W*Z), 2*(Z*X + W*Y), 0,
2*(X*Y + W*Z), 1 - 2*(X*X + Z*Z), 2*(Y*Z - W*X), 0,
2*(Z*X - W*Y), 2*(Y*Z + W*X), 1 - 2*(X*X + Y*Y), 0,
0, 0, 0, 1
};
}
Quaternion Quaternion::conjugate(const Quaternion& q) {
return Quaternion(-q.X, -q.Y, -q.Z, q.W);
}
Quaternion operator*(Quaternion lhs, Quaternion rhs) {
return lhs *= rhs;
}
Quaternion& Quaternion::operator*=(const Quaternion& rhs) {
float rhsX = rhs.getX(), rhsY = rhs.getY(),
rhsZ = rhs.getZ(), rhsW = rhs.getW();
Quaternion q;
q.X = W * rhsX + X * rhsW + Y * rhsZ - Z * rhsY;
q.Y = W * rhsY - X * rhsZ + Y * rhsW + Z * rhsX;
q.Z = W * rhsZ + X * rhsY - Y * rhsX + Z * rhsW;
q.W = W * rhsW - X * rhsX - Y * rhsY - Z * rhsZ;
*this = q;
return *this;
}
// Quaternion->Vector multiplication is not commutiative, must be Q*V
Vector3D operator*(Quaternion lhs, Vector3D rhs) {
Quaternion pure = Quaternion(rhs.X, rhs.Y, rhs.Z, 0);
Quaternion right = pure * Quaternion::conjugate(lhs); // v * q-1
Quaternion left = lhs * right; // q * (v * q-1)
return Vector3D(left.getX(), left.getY(), left.getZ());
}
float Quaternion::getX() const { return X; }
float Quaternion::getY() const { return Y; }
float Quaternion::getZ() const { return Z; }
float Quaternion::getW() const { return W; }
std::ostream& operator<<(std::ostream& ostream, Quaternion& q)
{
ostream << q.X << " " << q.Y << " "
<< q.Z << " " << q.W << " " << std::endl;
return ostream;
}
Pressing "a" (and similar) calls:
void GameManager::handleKeyboardInput() {
// ...
if (keyboard->isPressed('a')) {
ship->rotate(Axis::y, Direction::positive, dt);
}
// ...
glutPostRedisplay();
}
Which, when the object updates itself in the display function, calls:
void Ship::rotate(const Axis axis, const Direction direction, const float dt) {
int sign = direction == Direction::positive ? 1 : -1;
float speed = sign * ROTATION_SPEED;
if (axis == Axis::x) {
rotation = Quaternion(Vector3D::right(), speed * dt) * rotation;
}
else if (axis == Axis::y) {
rotation = Quaternion(Vector3D::up(), speed * dt) * rotation;
}
else if (axis == Axis::z) {
rotation = Quaternion(Vector3D::forward(), speed * dt) * rotation;
}
}
Where rotation is the object's current rotation stored as a Quaternion, left multiplied so it's local coordinates.
The Vectors are:
Vector3D Vector3D::up() { return Vector3D(0, 1, 0); }
Vector3D Vector3D::right() { return Vector3D(1, 0, 0); }
Vector3D Vector3D::forward() { return Vector3D(0, 0, 1); }
And finally the object is drawn in the main display loop:
glPushMatrix();
glMultMatrixf(Quaternion::toMatrix(rotation).data());
glColor3f(1.0, 1.0, 1.0);
glutWireCube(8);
glPopMatrix();
The easy solution is to just flip the signs of my rotations, but I'd rather know what's wrong. Thank you.
Edit: For what it's worth, I can confirm that if I hold "a" such that the cube is rotated 45 degrees (so a little more rotated than the second image), my cube's rotation (x, y, z, w) = (0, 0.389125, 0, 0.918102), which agrees with this page if I set y = 1 and the angle (radians) to 0.785398 (45 degrees). So the final rotation quaternion is correct, but my cube still rotates in the wrong direction. This makes me think there is something wrong with my code.
This is the final quaternion rotation matrix after holding "a" until it rotates 45 degrees clockwise:
{0.705871, 0, 0.708341, 0, }
{0, 1, 0, 0, }
{-0.708341, 0, 0.705871, 0, }
{0, 0, 0, 1, }
Which according to the same site is { [ 0, 1, 0 ], 45.1000806 }, which is what I want, but still the rotation is clockwise, not anticlockwise.
You build your matrix in row-major order, but glMultMatrixf expects a column-major matrix. Transposing a rotation matrix is equivalent to inverting it, i.e. rotating in the opposite direction.
To fix it, either build your matrix in column-major order, transpose it, or use glMultTransposeMatrixf.
i need to implement arcball camera. I got something similar, but it works very crookedly (the angle changes sharply, when turning to the right / left, the camera raises up / down strongly).
Here is my source code, can you tell me where I went wrong:
bool get_arcball_vec(double x, double y, glm::vec3& a)
{
glm::vec3 vec = glm::vec3((2.0 * x) / window.getWidth() - 1.0, 1.0 - (2.0 * y) / window.getHeight(), 0.0);
if (glm::length(vec) >= 1.0)
{
vec = glm::normalize(vec);
}
else
{
vec.z = sqrt(1.0 - pow(vec.x, 2.0) - pow(vec.y, 2.0));
}
a = vec;
return true;
}
...
void onMouseMove(double x, double y) {
if (rightMouseButtonPressed) {
glm::vec3 a,b;
cur_mx = x;
cur_my = y;
if (cur_mx != last_mx || cur_my != last_my)
if (get_arcball_vec(last_mx, last_my, a) && get_arcball_vec(cur_mx, cur_my, b))
viewport.getCamera().orbit(a,b);
last_mx = cur_mx;
last_my = cur_my;
...
void Camera::orbit(glm::vec3 a, glm::vec3 b)
{
forward = calcForward();
right = calcRight();
double alpha = acos(glm::min(1.0f, glm::dot(b, a)));
glm::vec3 axis = glm::cross(a, b);
glm::mat4 rotationComponent = glm::mat4(1.0f);
rotationComponent[0] = glm::vec4(right, 0.0f);
rotationComponent[1] = glm::vec4(up, 0.0f);
rotationComponent[2] = glm::vec4(forward, 0.0f);
glm::mat4 toWorldCameraSpace = glm::transpose(rotationComponent);
axis = toWorldCameraSpace * glm::vec4(axis, 1.0);
glm::mat4 orbitMatrix = glm::rotate(glm::mat4(1.0f), (float)alpha, axis);
eye = glm::vec4(target, 1.0) + orbitMatrix * glm::vec4(eye - target, 1.0f);
up = orbitMatrix * glm::vec4(up, 1.0f);
}
I use this code to map 2D mouse position to the sphere:
Vector3 GetArcBallVector(const Vector2f & mousePos) {
float radiusSquared = 1.0; //squared radius of the sphere
//compute mouse position from the centre of screen to interval [-half, +half]
Vector3 pt = Vector3(
mousePos.x - halfScreenW,
halfScreenH - mousePos.y,
0.0f
);
//if length squared is smaller than sphere diameter
//point is inside
float lengthSqr = pt.x * pt.x + pt.y * pt.y;
if (lengthSqr < radiusSquared){
//inside
pt.z = std::sqrtf(radiusSquared - lengthSqr);
}
else {
pt.z = 0.0f;
}
pt.z *= -1;
return pt;
}
To calculate rotation, I use the last (startPt) and current (endPt) mapped position and do:
Quaternion actRot = Quaternion::Identity();
Vector3 axis = Vector3::Cross(endPt, startPt);
if (axis.LengthSquared() > MathUtils::EPSILON) {
float angleCos = Vector3::Dot(endPt, startPt);
actRot = Quaternion(axis.x, axis.y, axis.z, angleCos);
}
I prefer to use Quaternions over matrices since they are easy to multiply (for acumulated rotation) and interpolate (for some smooting).
I need the points of a "rotating cylinder". I've got some lines of a curve and looking for an implementation to calculate 16 points arount them to get a cycle. My implemenation looks like:
QPointF MappingModel::calcThicknessYarns(float fCurrentX, float fCurrentY, int iCurrentCounter)
{
QPointF nextPoint;
int iAmountOfThicknessYarns = 16;
float fSingleAngle = 360.0 / iAmountOfThicknessYarns;
float fCurrentAngle = fSingleAngle * iCurrentCounter;
nextPoint = getXY(fCurrentAngle, 1.0, 1.0, fCurrentX, fCurrentY);
return nextPoint;
}
and
QPointF MappingModel::getXY(float angle, float width, float height, float xOffset, float yOffset)
{
QPointF xy;
float FI = angle*PIdev;
xy.setX((width * qCos(FI)) + xOffset) ;
xy.setY((height * qSin(FI)) - yOffset);
return xy;
}
My looks like this:
http://www.directupload.net/file/d/3741/e8bafwni_jpg.htm
http://www.directupload.net/file/d/3741/tkykay7w_jpg.htm
As you can see, the the vertical cylinder works fine, but the curve is false
This may help.
It shouldn't be difficult to adapt Vertex to QPointF.
typedef struct {
double x;
double y;
} Vertex;
/**
* #param radius Radius of circle
* #param angle Degrees
* #return The offset of vertex from the center of circle given radius and angle
*/
Vertex getVerticeOffset(double radius, double angle) {
double yOffset = -radius*cos((90 - angle)*M_PI/180); // convert to radians
return Vertex({(angle >= 90 && angle < 270 ? -1 : 1) * sqrt(pow(radius, 2) - pow(yOffset, 2)), yOffset});
}
There is a fast way to draw circle like this
void DrawCircle(float cx, float cy, float r, int num_segments)
{
float theta = 2 * 3.1415926 / float(num_segments);
float c = cosf(theta);//precalculate the sine and cosine
float s = sinf(theta);
float t;
float x = r;//we start at angle = 0
float y = 0;
glBegin(GL_LINE_LOOP);
for(int ii = 0; ii < num_segments; ii++)
{
glVertex2f(x + cx, y + cy);//output vertex
//apply the rotation matrix
t = x;
x = c * x - s * y;
y = s * t + c * y;
}
glEnd();
}
I am wondering if there is a similar way to draw ellipse where its major/minor axes vector and size are both known.
If we take your example we can use an internal radius of 1 and apply horizontal/vertical radius separately in order to get an ellipse:
void DrawEllipse(float cx, float cy, float rx, float ry, int num_segments)
{
float theta = 2 * 3.1415926 / float(num_segments);
float c = cosf(theta);//precalculate the sine and cosine
float s = sinf(theta);
float t;
float x = 1;//we start at angle = 0
float y = 0;
glBegin(GL_LINE_LOOP);
for(int ii = 0; ii < num_segments; ii++)
{
//apply radius and offset
glVertex2f(x * rx + cx, y * ry + cy);//output vertex
//apply the rotation matrix
t = x;
x = c * x - s * y;
y = s * t + c * y;
}
glEnd();
}
There is no way to draw a curve in openGL, just a lot of straight lines. But if you used vertex buffer objects then you won't have to send each vertex to the graphics card which will be much faster.
My Java Example
If the ellipse is ((x-cx)/a)^2 + ((y-cy)/b)^2 = 1 then change the glVertex2f call to
glVertext2d(a*x + cx, b*y + cy);
To simplify the sums, lets suppose for a while that the ellipse is centred at the origin.
If the ellipse is rotated so that the semi-major axis (of length a) makes an angle theta with the x axis, then the ellipse is the set of points p so that p' * inv(C) * p = 1, where C is the matrix R(theta) * D * R(theta)' where ' denotes transpose and D is the diagonal matrix with entries a*a,b*b (b the length of the semi-minor axis). If L is the cholesky factor (eg here) of C then the ellipse is the set of points p so that (inv(L) * p)'*(inv(L) *p ) = 1, so that L maps the unit circle to the ellipse. If we have computed L as ( u 0 ; v w) (just once, before the loop) then the glVertexf call becomes glVertex2f( u*x + cx, v*x + w*y + cy);
L can be calculated like this (where C is cos(theta) and S is sin(theta)):
u = sqrt( C*C*a*a + S*S*b*b); v = C*S*(a*a-b*b); w = a*b/u;
I'm new to c++ 3D, so I may just be missing something obvious, but how do I convert from 3D to 2D and (for a given z location) from 2D to 3D?
You map 3D to 2D via projection. You map 2D to 3D by inserting the appropriate value in the Z element of the vector.
It is a matter of casting a ray from the screen onto a plane which is parallel to x-y and is at the required z location. You then need to find out where on the plane the ray is colliding.
Here's one example, considering that screen_x and screen_y ranges from [0, 1], where 0 is the left-most or top-most coordinate and 1 is right-most or bottom-most, respectively:
Vector3 point_of_contact(-1.0f, -1.0f, -1.0f);
Matrix4 view_matrix = camera->getViewMatrix();
Matrix4 proj_matrix = camera->getProjectionMatrix();
Matrix4 inv_view_proj_matrix = (proj_matrix * view_matrix).inverse();
float nx = (2.0f * screen_x) - 1.0f;
float ny = 1.0f - (2.0f * screen_y);
Vector3 near_point(nx, ny, -1.0f);
Vector3 mid_point(nx, ny, 0.0f);
// Get ray origin and ray target on near plane in world space
Vector3 ray_origin, ray_target;
ray_origin = inv_view_proj_matrix * near_point;
ray_target = inv_view_proj_matrix * mid_point;
Vector3 ray_direction = ray_target - ray_origin;
ray_direction.normalise();
// Check for collision with the plane
Vector3 plane_normal(0.0f, 0.0f, 1.0f);
float denominator = plane_normal.dotProduct(ray_direction);
if (fabs(denom) >= std::numeric_limits<float>::epsilon())
{
float num = plane_normal.dotProduct(ray.getOrigin()) + Vector3(0, 0, z_pos);
float distance = -(num/denom);
if (distance > 0)
{
point_of_contact = ray_origin + (ray_direction * distance);
}
}
return point_of_contact
Disclaimer Notice: This solution was taken from bits and pieces of Ogre3D graphics library.
The simplest way is to do a divide by z. Therefore ...
screenX = projectionX / projectionZ;
screenY = projectionY / projectionZ;
That does perspective projection based on distance. Thing is it is often better to use homgeneous coordinates as this simplifies matrix transformation (everything becomes a multiply). Equally this is what D3D and OpenGL use. Understanding how to use non-homogeneous coordinates (ie an (x,y,z) coordinate triple) will be very helpful for things like shader optimisations however.
One lame solution:
^ y
|
|
| /z
| /
+/--------->x
Angle is the angle between the Ox and Oz axes (
#include <cmath>
typedef struct {
double x,y,z;
} Point3D;
typedef struct {
double x,y;
} Point2D
const double angle = M_PI/4; //can be changed
Point2D* projection(Point3D& point) {
Point2D* p = new Point2D();
p->x = point.x + point.z * sin(angle);
p->y = point.y + point.z * cos(angle);
return p;
}
However there are lots of tutorials on this on the net... Have you googled for it?