I would like to be able to rotate a QGraphicsItem based on its center, and scale it based on the top left corner.
When I try to combine rotation and scaling, the item also apparently moves...
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsTextItem>
void testTransformations(QGraphicsScene* s)
{
qreal angle = 30, scaleX = 2, scaleY = 1;
// Reference rotated not scaled
QGraphicsTextItem* ref = new QGraphicsTextItem("bye world");
ref->setFont(QFont("Arial", 20));
ref->setDefaultTextColor(Qt::green);
s->addItem(ref);
qreal center0X = ref->boundingRect().center().x();
qreal center0Y = ref->boundingRect().center().y();
QTransform t0;
t0.translate(center0X, center0Y);
t0.rotate(angle);
t0.translate(-center0X, -center0Y);
ref->setTransform(t0);
// Reference scaled not rotated
QGraphicsTextItem* ref1 = new QGraphicsTextItem("bye world");
ref1->setFont(QFont("Arial", 20));
ref1->setDefaultTextColor(Qt::yellow);
s->addItem(ref1);
QTransform t;
t.scale(scaleX, scaleY);
ref1->setTransform(t);
// Rotate around center of resized item
QGraphicsTextItem* yyy = new QGraphicsTextItem("bye world");
yyy->setDefaultTextColor(Qt::red);
yyy->setFont(QFont("Arial", 20));
s->addItem(yyy);
qreal center1X = yyy->boundingRect().center().x() * scaleX;
qreal center1Y = yyy->boundingRect().center().y() * scaleY;
// in my code I store the item size, either before or after the resize, and use it to determine the center - which is virtually the same thing as this for a single operation
QTransform t1;
t1.translate(center1X, center1Y);
t1.rotate(angle);
t1.translate(-center1X, -center1Y);
t1.scale(scaleX, scaleY);
yyy->setTransform(t1);
// rotated around center of bounding rectangle
QGraphicsTextItem* xxx = new QGraphicsTextItem("bye world");
xxx->setDefaultTextColor(Qt::blue);
xxx->setFont(QFont("Arial", 20));
s->addItem(xxx);
qreal center2X = xxx->boundingRect().center().x();
qreal center2Y = xxx->boundingRect().center().y();
QTransform t2;
t2.translate(center2X, center2Y);
t2.rotate(angle);
t2.translate(-center2X, -center2Y);
t2.scale(scaleX, scaleY);
xxx->setTransform(t2);
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsScene s;
QGraphicsView view(&s);
s.setSceneRect(-20, -20, 500, 500);
view.show();
testTransformations(&s);
return app.exec();
}
Result:
green is rotated, not scaled (or scaled a different amount)
yellow is scaled, not rotated
blue is scaled, and rotated by the center of bounding rectangle (which is not resized)
red is scaled and rotated around center
It is evident now to me that the transformations operate correctly - if I resize and rotate an item, I get first the yellow then the red item.
Yet, what I need, is if an item is already rotated (green) then scaled, to behave like the blue - stretch in the same direction, without jumping, while if an item is first scaled, then rotated, to behave like the red... Even more complicated, the original item (green) may have had scaling applied, so my simple solution of using the bounding rectangle wouldn't work.
I have tried to calculate the change... Always with weird results.
Is it possible to scale a rotated item, based on top left, without also moving it, while also rotating it around its center ?
It may require incremental transformations, and would be odd to get different results based on the order they are applied.
Edit: I have been experimenting with position adjustments, since the transformations have failed, but I have not been able to get a formula for a transform function that will give me smooth visual transition of the type:
1) rotate item (pinned to center)
2) scale item (pinned to top left) without jumping
3) rotate item (pinned to center)
where step 2 would also include an offset in position. I just don't know how to do it.
The way I see it, in the fragment for the "red" transform, I would have o add a mapToScene(somePoint) before and after the transform, and perform a correction (moveBy) based on result.
This would not be a great fix, but still... If only I knew how to adjust the position of the item after resize so it doesn't jump, it would still be a fix...
OK, sorry for the late edit, it seems like it is impossible to directly achieve that in Qt.
You can however use simple trigonometry to calculate the offset from the center you get when rotating around the 0,0 origin and manually move the item to compensate for the displacement.
Just imagine a circle with center 0,0 and radius a line from the center of the circle to the center of the item's bounding box. As you rotate the item around the 0,0 origin, the item's bounding box center will always sit on the circle, so you can calculate the offset for a given angle of rotation and then rotate and adjust the position of the item to match the center of its previous state.
Here is a little illustration, as you can see, after rotation the item is moved by the offset in order to make it appear as if it rotated around its center and not around the top left. Naturally, since the origin is still 0,0 it will scale as you intended.
EDIT: Even easier, no trig, just using Qt functionality, this method will rotate the item around its center while the transform origin is top left:
static void offsetRotation(qreal angle, QGraphicsItem * i) {
QPointF c = i->mapToScene(i->boundingRect().center());
i->setRotation(angle);
QPointF cNew = i->mapToScene((i->boundingRect()).center());
QPointF offset = c - cNew;
i->moveBy(offset.x(), offset.y());
}
However, I noticed something odd - when the origin is set to top left, scaling doesn't really keep the top left corner in the same place as I expected based on my long experience with graphics design software. Instead the item will slowly drift away as its scale increases. The origin point does seem to have some effect on the scaling, but I would certainly not call it adequate by any measure. So depending on what exactly you want to achieve, you may have to use the same offset adjustment trick for scaling as well. As an item scales, it will keep on reporting the same position, but if you map that to the scene, you will realize it actually changes, so you can track that change and compensate in order to produce adequate behavior with a respective offsetScale method.
All this comes at a cost thou, your coordinates will end up being all over the place mess for the sake of keeping the visual output as expected. This may prove to be a complication later on. One solution would be to create your own "public" coordinates for your items, and internally manage all that mess to normalize the end result.
Hopefully someone else may offer a cleaner solution, from my experience with Qt it seems like the people who worked on the graphics classes have had little to no experience with graphics workflow, no doubt they were excellent programmers, but the end result is Qt's graphics classes are often counter-intuitive or even totally incapable of working in the manner people have come to expect from professional graphics software. Perhaps a more pragmatic mind may offer a better remedy for this problem.
This is my attempt to solve the problem - it works but it is not a great solution:
on any transformation, I store m31() and m32().
If transformation is scaling, I offset by change between old and new m31() and m32()
Related
I have an orthographic camera in an osg scene. There are flat objects (planes) which will be displayed in that scene, and I want to be able to have the camera automatically point at them to as to view them head-on, and also to be zoomed to show as much of the plane as possible without chopping off any edges.
So far I've oriented myself correctly to look at the plane:
osg::Vec3d eye; // dummy
osg::Vec3d center; // we only really care about this here
osg::Vec3d up; // dummy
cameraManipulator->getViewMatrixAsLookAt(eye, center, up);
osg::Vec3d desiredVec(velocityX, velocityY, velocityZ); // velocity of the plane I want to look at
desiredVec.normalize();
desiredVec = -desiredVec; // invert
desiredVec += center; // align
cameraManipulator->setViewMatrixAsLookAt(desiredVec, center, osg::Vec3d(0, 0, -1));
This positions me so that I'm viewing the plane head-on, but it's zoomed in way too far, so I think what I need to do is offset my camera back a bit along the velocity vector by some value, and I don't know how to calculate that value. Something like this:
osg::Vec3d dir = desiredVec - center; // the direction (vector) in which we want to move
dir.normalize();
double scaleFactor = (width * height) / 2; // test
desiredVec += (dir * scaleFactor); // add it to desiredVec to move back in that direction (by scaleFactor)
The scaleFactor "test" stuff seems to work ok, it produces a view of the plane with a margin around the edge. I imagine this is what needs to change.
Basically, how do I calculate the distance I need to move the camera back by in order to view all of the current plane? I have information about the plane's velocity, size etc.
When using an orthographic camera, the size of the rendered objects (zoom effect) is not achieved by moving the camera, but by the size of the projection frustum you set on the camera.
None of the OSG camera manipulators implements a zooming effect for an ortho camera.
See this forum topic as a reference: http://forum.openscenegraph.org/viewtopic.php?t=10763&view=next
I am working with OpenGL and I wanted to invert the image. So I went here, asked a question and finally I had the following code:
glMatrixMode(GL_PROJECTION);
glScalef(-1,1,1);
glTranslatef(-width(),0,0);
From what I understand from this, the position of every pixel gets inverted, so the pixels that were on the right of the image are now on the same absolute position, but are the left of the image, so I have to move the entire thing back exactly as many pixels as its wide: 360 (which is the size of the "canvas", so in the snippents the function width() is being used)! So to undo this process I would invert the image again and then move it back to where it came from:
glMatrixMode(GL_PROJECTION);
glScalef(-1,1,1);
glTranslatef(width(),0,0);
Nope, blackscreen. I have to do exactly the same thing twice to undo the flipping: I have to move with -360 every time I flip the image. Why?
It's exactly as Daniel Fischer mentioned in the comment. Here is an illustration of the process.
What you must have in mind is that the transformations operate on the transformed coordinate systems.
We start with the image (grey) on the screen (green):
Then we scale the image. So the origin is preserved, but the x-axis is mirrored.
No we have to move the image onto the screen again. Because the x-axis points to the left (but we want to move the image to the right), we have to use a negative offset for the translation:
If we flip the image again, the following happens. The origin is preserved and the x-axis is mirrored:
So we must translate the image by a negative offset:
Another way of undoing the flip is undoing the operations (but in the opposite order):
glTranslatef(width, 0, 0);
glScalef(-1,1,1);
The mathematical reason for that is that inversion reverses the oder. If we have Matrix A = B * C then A^-1 = (C^-1 * B^-1).
I am implementing a pan tool in our software's 3D view which is supposed to work much like the grab tool of, say, Photoshop or Acrobat Reader. That is, the point the user grabs onto with the mouse (clicks and holds, then moves the mouse) stays under the mouse cursor as the mouse moves.
This is a common paradigm and one that's been asked about on SO before, the best answer being to this question about the technique in OpenGL. There is another that also has some hints, and I have been reading this very informative CodeProject article. (It doesn't explain many of its code examples' variables etc, but from reading the text I think I understand the technique.) But, I have some implementation issues because my 3D environment's navigation is set up quite differently to those articles, and I am seeking some guidance.
My technique - and this might be fundamentally flawed, so please say so - is:
The scene 'camera' is stored as two D3DXVECTOR3 points: the eye position and a look point. The view matrix is constructed using D3DXMatrixLookAtLH like so:
const D3DXVECTOR3 oUpVector(0.0f, 1.0f, 0.0f); // Keep up "up", always.
D3DXMatrixLookAtLH(&m_oViewMatrix, &m_oEyePos, &m_oLook, &oUpVector);
When the mouse button is pressed, shoot a ray through that pixel and find: the coordinate (in unprojected scene / world space) of the pixel that was clicked on; the intersection of that ray with the near plane; and the distance between the near-plane point and object, which is the length between those two points. Store this and the mouse position, and the original navigation (eye and look).
// Get the clicked-on point in unprojected (normal) world space
D3DXVECTOR3 o3DPos;
if (Get3DPositionAtMouse(roMousePos, o3DPos)) { // fails if nothing under the mouse
// Mouse location when panning started
m_oPanMouseStartPos = roMousePos;
// Intersection at near plane (z = 0) of the ray from camera to clicked spot
D3DXVECTOR3 oRayVector;
CalculateRayFromPixel(m_oPanMouseStartPos, m_oPanPlaneZ0StartPos, oRayVector);
// Store original eye and look points
m_oPanOriginalEyePos = m_oEyePos;
m_oPanOriginalLook = m_oLook;
// Store the distance between near plane and the object, and the object position
m_dPanPlaneZ0ObjectDist = fabs(D3DXVec3Length(&(o3DPos - m_oPanPlaneZ0StartPos)));
m_oPanOriginalObjectPos = o3DPos;
Get3DPositionAtMouse is a known-ok method which picks a 3D coordinate under the mouse. CalculateRayFromPixel is a known-ok method which takes in a screen-space mouse coordinate and casts a ray, and fills the other two parameters with the ray intersection at the near plane (Z = 0) and the normalised ray vector.
When the mouse moves, cast another ray at the new position, but using the old (original) view matrix. (Thanks to Nico below for pointing this out.) Calculate where the object should be by extending the ray from the near plane the distance between the object and near plane (this way, the original object and new object points should be in parallel plane to the near plane.) Move the eye and look coordinates by this much. Eye and Look are set from their original (when panning started) values, with the difference being from the original mouse and new mouse positions. This is to reduce any precision loss from incrementing or decrementing by granular (integer) pixel movements as the mouse moves, ie it calculates the whole difference in navigation every time.
// Set navigation back to original (as it was when started panning) and cast a ray for the mouse
m_oEyePos = m_oPanOriginalEyePos;
m_oLook = m_oPanOriginalLook;
UpdateView();
D3DXVECTOR3 oRayVector;
D3DXVECTOR3 oNewPlaneZPos;
CalculateRayFromPixel(roMousePos, oNewPlaneZPos, oRayVector);
// Now intersect that ray (ray through the mouse pixel, using the original navigation)
// to hit the plane the object is in. Function uses a "line", so start at near plane
// and the line is of the length of the far plane away
D3DXVECTOR3 oNew3DPos;
D3DXPlaneIntersectLine(&oNew3DPos, &m_oPanObjectPlane, &oNewPlaneZPos, &(oRayVector * GetScene().GetFarPlane()));
// The eye/look difference /should/ be as simple as:
// const D3DXVECTOR3 oDiff = (m_oPanOriginalObjectPos - oNew3DPos);
// But that lags and is slow, ie the objects trail behind. I don't know why. What does
// work is to scale the from-to difference by the distance from the camera relative to
// the whole scene distance
const double dDist = D3DXVec3Length(&(oNew3DPos - m_oPanOriginalEyePos));
const double dTotalDist = GetScene().GetFarPlane() - GetScene().GetNearPlane();
const D3DXVECTOR3 oDiff = (m_oPanOriginalObjectPos - oNew3DPos) * (1.0 + (dDist / dTotalDist));
// Adjust the eye and look points by the same amount, so orthogonally changed
m_oEyePos = m_oPanOriginalEyePos + oDiff;
m_oLook = m_oPanOriginalLook + oDiff;
Diagram
This diagram is my working sketch for implementing this:
and hopefully explains the above much more simply than the text. You can see a moving point, and where the camera has to move to keep that point at the same relative position. The clicked-on point (the ray from the camera to the object) is just to the right of the straight-ahead ray representing the center pixel.
The problem
But, as you've probably guessed, this doesn't work as I hope. What I wanted to see was the clicked-on object moving with the mouse cursor. What I actually see is that the object moves in the direction of the mouse, but not enough, ie it does not keep the clicked-on point under the cursor. Secondly, the movement flickers and jumps around, jittering by up to twenty or thirty pixels sometimes, then flickers back. If I replace oDiff with something constant this doesn't occur.
Any ideas, or code samples showing how to implement this with DirectX (D3DX, DX matrix order, etc) will be gratefully read.
Edit
Commenter Nico below pointed out that when calculating the new position using the mouse cursor's moved position, I needed to use the original view matrix. Doing so helps a lot, and the objects stay near the mouse position. However, it's still not exact. What I've noticed is that at the center of the screen, it is exact; as the mouse moves further from the center, it gets out by more and more. This seemed to change based on how far away the object was, too. By pure 'I have no idea what I'm doing' guesswork, I scaled this by a factor of the near/far plane and how far away the object was, and this brings it very close to the mouse cursor, but still a few pixels away (1 to, say, 30 at the extreme edge of the screen, which is enough to make it feel wrong.)
Here's how i solve this problem.
float fieldOfView = 45.0f;
float halfFOV = (fieldOfView / 2.0f) * (DEGREES_TO_RADIANS);
float distanceToObject = // compute the world space distance from the camera to the object you want to pan
float projectionToWorldScale = distanceToObject * tan( halfFov );
Vector mouseDeltaInScreenSpace = // the delta mouse in pixels that we want to pan
Vector mouseDeltaInProjectionSpace = Vector( mouseDeltaInScreenSpace.x * 2 / windowPixelSizeX, mouseDeltaInScreenSpace.y * 2 / windowPixelSizeY ); // ( the "*2" is because the projection space is from -1 to 1)
// go from normalized device coordinate space to world space (at origin)
Vector cameraDelta = -mouseDeltaInProjectionSpace * projectionToWorldScale;
// now translate your camera by "cameraDelta".
Note this works for an field of view apsect ratio of 1, i think you would have to break up the "scale" into separate x and y components if they vertical field of view was different than the horizontal field of view
Also, you mentioned a "look at" vector. I'm not sure how my math would need to change for that since my camera is always looking straight down the z-axis.
One problem is your calculation of the new 3d position. I am not sure if this is the root cause, but you might try it. If it doesn't help, just post a comment.
The problem is that your offset vector is not parallel to the znear plane. This is because the two rays are not parallel. Therefore, if the have the same length behind znear, the distance of the end point to the znear plane cannot be equal.
You can calculate the offset vector with the theorem of intersecting lines. If zNearA and zNearB are the intersection points of the znear plane with ray A and ray B respectively, then the theorem states:
Length(original_position - cam_position) / Length(offset_vector) = Length(zNearA - cam_position) / Length(zNearB - zNearA)
And therefore
offset_vector = Length(original_position - cam_position) / Length(zNearA - cam_position) * (zNearB - zNearA)
Then you can be sure to move on a line that is parallel to the znear plane.
Just try it out and see if it helps.
I'm using Qt 4.7 QPainter to draw some polygons, etc into a widget. I am hoping to alter the coordinate system so that (0,0) is at the center of my widget, and the x/y axis behave in a standard "Cartesian" way (ie. y increases going "up" and decreases going "down"). In other words, I want the coordinates to be "math"-like not "computer graphics"-like, if you know what I mean. :-)
I'm trying to do this using setViewport() and setWindow() rather than do the math myself, as it would be nice to be able to just call the draw methods directly with my coordinates.
Here's what I've got so far:
// Setup coordinates
double screenWidth = width();
double screenHeight = height();
double windowWidth = 100.0;
double windowHeight = (screenHeight / screenWidth) * windowWidth;
painter.setViewport(0, 0, screenWidth, screenHeight);
painter.setWindow(-(windowWidth / 2.0), -(windowHeight / 2.0), windowWidth, windowHeight);
// Draw stuff
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::blue);
painter.drawRect(-10, -10, 20, 20);
Now this works just fine, in that it draws a nice blue square in the middle of the screen. The problem is, I have to say that the upper left corner is (-10, -10). I'd like to be able to make it (-10, 10), as that is what it would be in Cartesian coords.
I tried messing with setWindow/setViewport to get this "y-axis flip", but to no avail. This seems like a really easy/basic thing to do, but after scouring the Qt docs and the web, I can't figure it out!
Thanks,
Chris
Use class QMatrix. It specifies 2D transformations. QMatrix is set to QPainter.
But remember, in your case, if you convert your widget's coords to Cartesian coords, you will have to put first point at (-10,-10) (not at (-10,10) as you did mentioned) to draw a rect, which has center at (0,0), because Y-axis now grows up and X-Axis now grows right.
All you need is to transform your coord system this way:
translate origin from (0,0) to the middle of the widget:
scale Y-axis by -1 factor:
Here is the code, typed in paintEvent() function of a widget:
QPainter pn( this );
int w_2 = width() / 2;
int h_2 = height() / 2;
{ // X- and Y-Axis drawing
pn.setPen( Qt::blue );
pn.drawLine( 0, h_2, width(), h_2); // X-Axis
pn.drawLine( w_2, 0 , w_2, height() ); // Y-Axis
}
QMatrix m;
m.translate( w_2, h_2 );
m.scale( 1, -1 );
pn.setMatrix( m );
pn.setPen( Qt::NoPen );
pn.setBrush( QBrush( Qt::blue, Qt::Dense4Pattern ) );
pn.drawRect( -10, -10, 20, 20 );
result:
update apr 07, 2014
This question was asked a long time ago and many things have changed since. For those asking themselves the same question today (beginnings of 2014) then my personal answer is that since Qt 4.3 it is possible to solve problem with text flipping more easier.
You are right. Text also gets filpped because it is drawn with the same painter. You can draw text at the end, when all flipped drawings are done, if it is possible. This method is not convinient because of new calculations of texts position. Also you will need to drop settings for painter.
Now I would recommend you to use QGraphicsView, because of huge support of 2D painting. Also for each QGraphicsItem ItemIgnoresTransformations flag can be set, which allows it to ignore inherited transformations (i.e., its position is still anchored to its parent, but the parent or view rotation, zoom or shear transformations are ignored). This flag is useful for keeping text label items horizontal and unscaled, so they will still be readable if the graphics view is transformed
The above answer will also flip text, "p" will be "b". To avoid that you have to flip back the y-axis before text is drawn, and you have to change sign on y-coord for the text position when you draw it. This is a little bit ugly I think, or is there a better way?
As stated above, drawing text also appears flipped upside down. There is an easy solution to it, see below. We will temporary disable the world transform for the text drawing. Note that text is not scaled anymore.
in your painting code we want to draw text on coordinate QPointF P;
Painter pn( this );
// calculate the point with the transform
QPointF p = pm.transform().map(P);
// Disable Transform temporary
pn.setWorldMatrixEnabled(false);
// draw it ordinary, no scaling etc
pn.drawText(p, QString("HI FRIENDS!"));
// Enable the transform again
pn.setWorldMatrixEnabled(true);
I needed to flip the y-axis in order to paint lines and polygons using Qt from points defined in Java coordinates. I imagine others will need to do this in porting from Java to Qt coordinate systems. The discussion above was helpful. My solution was:
painter.translate(0,height());
painter.scale(1.0, -1.0);
and then proceed to draw the lines and polygons.
I need to know what the visible height of a display object will be after I change it's rotationX value.
I have an application that allows users to lay out a floor in 3D space. I want the size of the floor to automatically stretch after a 3D rotation so that it always covers a certain area.
Anyone know a formula for working this out?
EDIT: I guess what I am really trying to do is convert degrees to pixels.
On a 2D plane say 100 x 100 pixels, a -10 degree change on rotationX means that the plane has a gap at the top where it is no longer visible. I want to know how many pixels this gap will be so that I can stretch the plane.
In Flex, the value for the display objects height property remains the same both before and after applying the rotation, which may in fact be a bug.
EDIT 2: There must be a general math formula to work this out rather than something Flash/Flex specific. When viewing an object in 3D space, if the object rotates backwards (top of object somersaults away from the viewer), what would the new visible height be based on degrees of rotation? This could be in pixels, metres, cubits or whatever.
I don't have a test case, but off the top of my head I'd guess something like:
var d:DisplayObject;
var rotationRadians:Number = d.rotationX * Math.PI / 180;
var visibleHeight:Number = d.height * Math.cos(rotationRadians);
This doesn't take any other transformations into account, though.
Have you tried using the object's bounding rectangle and testing that?
var dO:DisplayObject = new DisplayObject();
dO.rotation = 10;
var rect:Rectangle = dO.getRect();
// rect.topLeft.y is now the new top point.
// rect.width is the new width.
// rect.height is the new height.
As to the floor, I would need more information, but have you tried setting floor.percentWidth = 100? That might work.
Have you checked DisplayObject.transform.pixelBounds? I haven't tried it, but it might be more likely to take the rotation into account.
Rotation actually changes DisplayObject's axis's (i.e. x and y axes are rotated). That is why you are not seeing the difference in height. So for getting the visual height and y you might try this.var dO:DisplayObject = new DisplayObject();
addChild();
var rect1:Rectangle = dO.getRect(dO.parent);
dO.rotation = 10;
var rect2:Rectangle = dO.getRect(dO.parent);
rect1 and rect2 should be different in this case. If you want to check the visual coordinates of the dO then just change dO.parent with root.