Composition of translation, rotation and scaling

This is probably going to sound quite like a simplistic question to most of you…

I have used some code in openframeworks to compose incremental translation, rotation and scaling in the draw function this way

ofTranslate(ww/2, wh/2, 0); // put origin at center of screen

mat.scale(ofVec3f(scale, scale, 1.0));
mat.rotate(rot, 0, 0, 1);
mat.translate(ofVec3f(tx, ty, 0));

ofPushMatrix();
ofMultMatrix(mat);
img.setAnchorPercent(0.5, 0.5);
img.draw(0, 0);			
ofPopMatrix(); 

This allows to reproduce the behaviour of a usual gps navigation map apps except that it always rotates and scales in the middle of the screen (would be cool to do it under the pointer as well).
I can’t reproduce it in Cinder, probably because i can’t find an equivalent to ofMultMatrix and setAnchorPercent (?)

Again sorry if this sounds quite basic but any hep very much appreciated !

Something like this should do it, though you could generate a single model matrix that includes translating to the center of the screen and handles drawing your texture centred as well, but this is more one-to-one with your openFrameworks code.

    gl::translate( ww / 2, wh / 2 );
    
    glm::mat4 mat = glm::translate ( vec3 ( tx, ty, 0 ) )
                  * glm::toMat4 ( glm::angleAxis( rot, vec3 ( 0, 0, 1 ) ) )
                  * glm::scale ( vec3 ( scale, scale, 1 ) );
    
    gl::pushMatrices();
        gl::multModelMatrix( mat );
        // assuming img is a gl::TextureRef in cinder.
        gl::draw( img, img->getBounds() - img->getSize() / 2 );
    gl::popMatrices();

thanks ! i feel i am getting closer to a solution though i still don’t get it.

I want the axis of rotation and of scaling to stay at a specific point, i.e. the center of the screen.

if x2 and y2 are the coordinates of the screen center

glm::translate ( vec3 ( x2, y2, 0 ) )
* glm::toMat4 ( glm::angleAxis( float(rot), vec3 ( 0, 0, 1 ) ) )
* glm::scale ( vec3 ( scale, scale, 1 ) )
* glm::translate ( vec3 ( tx-x2, ty-y2, 0 ) )

but then, tx and ty are moving in the directions of the new rotated axis, i want them to be natural horizontal and vertical translations…

thanks again for your help but i am still stuck.

should i add that it is really a step by step composition of a matrix (just like using googlemaps you’ll find the right angle, scale and position in successive steps).

so it is more something like
mat = mat * delta_trans * delta_rot * delta_scaling
the problem then is that oncoming displacements will happen inside the previous reference system (which may have been rotated).

I also wonder if the fact that i am not able to convert my openframeworks code into cinder one is linked or not to the way openframeworks is handling these particular lines

ofPushMatrix();
ofMultMatrix(mat);
img.setAnchorPercent(0.5, 0.5);
img.draw(0, 0);
ofPopMatrix();

may be should i use separate matrices, one for the object (model matrix), one for the camera (view matrix) ?

shall i use inversion of matrix to find the pivot point (center of the screen) displacement in the object coordinate system ?

i suppose this is a rather common problem, isn’t there any examples around ?

anyway, i am still quite convinced that it should be pretty easy (once found) - if someone has a clue…

Hi,

your original code is not doing incremental transformation. It constructs a new matrix every step. Which is actually what you want, because it’s easier to control.

There’s a few steps you need to take to obtain the correct transformation:

  1. Start with a new identity matrix.
  2. In OpenGL, the order in which you perform translation, rotation and scaling is reversed: start with the last transformation, end with the first. More on that later.
  3. You initially want to rotate, scale and position your image around its own center. This is sometimes called the anchor of the image. You can do this by translating the image: glm::translate( -anchor ).
  4. Once you have adjusted the anchor, you can apply scaling and rotation.
  5. Finally, you want to draw your image relative to the center of the window, so do a glm::translate( center ).

The full code would then become:

vec3 center{ vec2( getWindowSize() ) * 0.5f , 0 };
vec3 scale{ 1, 1, 1 };
float angle{ 0 };
vec3 anchor{ vec2( mImage->getSize() ) * 0.5f, 0 };

// Remember: reverse order.
mTransform = glm::translate( center )
           * glm::toMat4( glm::angleAxis( angle, vec3( 0, 0, 1 ) ) )
           * glm::scale( scale ) * glm::translate( -anchor );

gl::pushMatrices();
gl::multModelMatrix( mTransform );
gl::draw( mImage );
gl::popMatrices();

The trick will then be to calculate the correct scale and rotation when you’re interacting. Have a look at the CanvasUi class that I wrote for this. Its usage is very similar to the CameraPersp class of Cinder.

CanvasUi mCanvas;

void YourApp::setup()
{
    mCanvas.connect( getWindow() );
}

void YourApp::draw()
{
    gl::pushModelMatrix();
    gl::setModelMatrix( mCanvas.getModelMatrix() );

    // ... draw your image here ...
    gl::draw( mImage );

    gl::popModelMatrix();
}

It doesn’t do rotation, but this can be added by storing the current rotation on mouse down, then calculating the angle between the clicked position and the anchor using glm::atan and adding that to the stored rotation to find the current rotation.

-Paul

1 Like

Dear Paul,

Thanks a lot for your reply, this really helped me to get back on the right tracks !

I tried to derive a minimal example that illustrates what i was looking for, objects positions combined from translations, rotations and scalings, with an axis placed at the center of screen (see integral code below).

I wonder why the toLocal function works when ones deactivate the homogenous coordinates while the inverse function toScreen needs it (at least that’s what i found).

vec2 toScreen( mat4 mat, vec2 posLocal ) {
  return vec2( mat * vec4( posLocal, 0, 1 ) ); // homogeneous coords !
}

Also, I am using both the modelMatrix and the viewMatrix - it appeared to me that it was simpler this way if you want to combine several transformations on the modelMatrix.

Anyway, here is the simplest code i could derive, thanks again.

#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"

using namespace ci; using namespace ci::app; using namespace std;

class Trs : public App {
public:
	float angle = 0;
	float angle2 = 0;

	vec2 toLocal( mat4 mat, vec2 posScreen ) { // to object Space
		return vec2( glm::inverse( mat ) * vec4( posScreen, 0, 0 ) ); // deactivate homogeneous coordinates !
	}
	
	void draw() {
		gl::clear();		

		auto rect = Rectf(0, 0, 20, 10); // the shape to draw
		
		gl::pushViewMatrix();
		gl::setViewMatrix(glm::translate(gl::getViewMatrix(), vec3( vec2(getWindowSize()) * 0.5f, 0)));
		gl::pushModelMatrix();

		// translation to the center of the rectangle
		vec2 pivot = rect.getCenter();
		mat4 mat = glm::translate( vec3( -pivot, 0) );
		gl::setModelMatrix( mat );
		gl::drawStrokedRect(rect);

		// combine a translation and then a rotation
		pivot = - toLocal( mat, vec2(100, 0) );
		angle += 0.1;
		mat = glm::toMat4( glm::angleAxis( angle, vec3( 0, 0, 1 ) ) )
			* glm::translate( vec3( -pivot, 0) );
		gl::setModelMatrix( mat );
		gl::drawStrokedRect(rect);

		// combine same translation with scale and other rotation
		angle2 += 0.05;
		mat = glm::toMat4( glm::angleAxis( angle2, vec3( 0, 0, 1 ) ) )
			* glm::scale( vec3(2, 2, 1) )
			* glm::translate( vec3( -pivot, 0) );
		gl::setModelMatrix( mat );
		gl::drawSolidRect(rect);

		gl::popModelMatrix();
		gl::popViewMatrix();
	}
};

CINDER_APP( Trs, RendererGl)

When going from local to screen, the 3D position gets a 4th component W (set to 1) and is then multiplied by the projection matrix. The result is a 4D vector where the W-component is not equal to 1. The GPU then implicitly divides the XYZ-components by W to find the final projected position.

When going from screen to local, you can simply decide to set the W-component to 1, because there is no way to know what it was prior to division. If you properly want to reconstruct the original 3D position, you’ll need to at least figure out the Z-component of the screen position, usually by sampling the depth buffer and scaling it by the near and far clip values. ** Your XYZ-components are then compatible with a W-component of 1, so multiplying it by the inverse view-projection matrix will yield the original world space position.

Hope this helps.

-Paul

**) alternatively, you can just use the sampled depth value, bring it in the range [-1…1] (float z = 2.0 * depth - 1.0;) and multiply it by the inverse projection matrix. Finally, do the perspective divide. See this post.