views:

1128

answers:

3

Hey all, I've got some 2D geometry. I want to take some bounding rect around my geometry, and then render a smaller version of it somewhere else on the plane. Here's more or less the code I have to do scaling and translation:

// source and dest are arbitrary rectangles.
float scaleX = dest.width / source.width;
float scaleY = dest.height / source.height;
float translateX = dest.x - source.x;
float translateY = dest.y - source.y;

glScalef(scaleX, scaleY, 0.0);
glTranslatef(translateX, translateY, 0.0);
// Draw geometry in question with its normal verts.

This works exactly as expected for a given dimension when the dest origin is 0. But if the origin for, say, x, is nonzero, the result is still scaled correctly but looks like (?) it's translated to something near zero on that axis anyways-- turns out it's not exactly the same as if dest.x were zero.

Can someone point out something obvious I'm missing?

Thanks!

FINAL UPDATE Per Bahbar's and Marcus's answers below, I did some more experimentation and solved this. Adam Bowen's comment was the tip off. I was missing two critical facts:

  1. I needed to be scaling around the center of the geometry I cared about.
  2. I needed to apply the transforms in the opposite order of the intuition (for me).

The first is kind of obvious in retrospect. But for the latter, for other good programmers/bad mathematicians like me: Turns out my intuition was operating in what the Red Book calls a "Grand, Fixed Coordinate System", in which there is an absolute plane, and your geometry moves around on that plane using transforms. This is OK, but given the nature of the math behind stacking multiple transforms into one matrix, it's the opposite of how things really work (see answers below or Red Book for more). Basically, the transforms are "applied" in "reverse order" to how they appear in code. Here's the final working solution:

// source and dest are arbitrary rectangles.
float scaleX = dest.width / source.width;
float scaleY = dest.height / source.height;
Point sourceCenter = centerPointOfRect(source);
Point destCenter = centerPointOfRect(dest);

glTranslatef(destCenter.x, destCenter.y, 0.0);
glScalef(scaleX, scaleY, 0.0);
glTranslatef(sourceCenter.x * -1.0, sourceCenter.y * -1.0, 0.0);
// Draw geometry in question with its normal verts.
+2  A: 

Scale, just like Rotate, operates from the origin. so if you scale by half an object that spans the segment [10:20] (on axis X, e.g.), you get [5:10]. The object therefore was scaled, and moved closer to the origin. Exactly what you observed.

This is why you apply Scale first in general (because objects tend to be defined around 0).

So if you want to scale an object around point Center, you can translate the object from Center to the origin, scale there, and translate back.

Side note, if you translate first, and then scale, then your scale is applied to the previous translation, which is why you probably had issues with this method.

Bahbar
Thank you for the reply. Can you spell out for a matrix-math dimwit what operations I would do, to do the appropriate translate-to-origin, then scale, then translate-back? I attempted stacking these three things in succession (translate by negative of source origin, then scale by scale factors, then translate by dest origin) in three calls before drawing the geometry but got no visible results.
quixoto
Ah, perhaps I need to do the translations relative to the center of the rect rather than the origin. Will try this.
quixoto
Nope. No dice with: translate-by-negative-center-of-source, then scale, then translate-by-positive-center-of-dest.
quixoto
See update to question. Thanks.
quixoto
+3  A: 

In OpenGL, matrices you specify are multiplied to the right of the existing matrix, and the vertex is on the far right of the expression.

Thus, the last operation you specify are in the coordinate system of the geometry itself. (The first is usually the view transform, i.e. inverse of your camera's to-world transform.)

Bahbar makes a good point that you need to consider the center point for scaling. (or the pivot point for rotations.) Usually you translate there, rotate/scale, then translate back. (or in general, apply basis transform, the operation, then the inverse). This is called Change of Basis, which you might want to read up on.

Anyway, to get some intuition about how it works, try with some simple values (zero, etc) then alter them slightly (perhaps an animation) and see what happens with the output. Then it's much easier to see what your transforms are actually doing to your geometry.

Update

That the order is "reversed" w.r.t. intuition is rather common among beginner OpenGL-coders. I've been tutoring a computer graphics course and many react in a similar manner. It becomes easier to think about how OpenGL does it if you consider the use of pushmatrix/popmatrix while rendering a tree (scene-graph) of transforms and geometries. Then the current order-of-things becomes rather natural, and the opposite would make it rather difficult to get anything useful done.

Marcus Lindblom
In short, change the order to glTranslate(dest); glScale(scale); glTranslate(source);
Adam Bowen
Adam: This doesn't seem to work as you suggest, although I'm intrigued-- can you elaborate on why the ordering would go like that, and perhaps offer a more explicit idea on how that would look relative to the existing code? Perhaps I'm just misinterpreting your pseudocode. Thanks!
quixoto
Nevermind: the "doesn't work" was a fatfingered value in this case. After fixing that, we're in business. See updated question for more. Thanks!
quixoto
A: 

I haven't played with OpenGL ES, just a bit with OpenGL.

It sounds like you want to transform from a different position as opposed to the origin, not sure, but can you try to do the transforms and draws that bit within glPushMatrix() and glPopMatrix() ?

e.g.

// source and dest are arbitrary rectangles.
float scaleX = dest.width / source.width;
float scaleY = dest.height / source.height;
float translateX = dest.x - source.x;
float translateY = dest.y - source.y;

glPushMatrix();
  glScalef(scaleX, scaleY, 0.0);
  glTranslatef(translateX, translateY, 0.0);
  // Draw geometry in question with its normal verts.
  //as if it were drawn from 0,0
glPopMatrix();

Here's a simple Processing sketch I wrote to illustrate the point:

import processing.opengl.*;
import javax.media.opengl.*;


void setup() {
  size(500, 400, OPENGL);
}

void draw() {
  background(255);
  PGraphicsOpenGL pgl = (PGraphicsOpenGL) g;
  GL gl = pgl.beginGL();  


  gl.glPushMatrix();
    //transform the 'pivot'
    gl.glTranslatef(100,100,0);
    gl.glScalef(10,10,10);
    //draw something from the 'pivot'
    gl.glColor3f(0, 0.77, 0);
    drawTriangle(gl);
  gl.glPopMatrix();
  //matrix poped, we're back to orginin(0,0,0), continue as normal
  gl.glColor3f(0.77, 0, 0);
  drawTriangle(gl);
  pgl.endGL();
}

void drawTriangle(GL gl){
  gl.glBegin(GL.GL_TRIANGLES);
  gl.glVertex2i(10, 0);
  gl.glVertex2i(0, 20);
  gl.glVertex2i(20, 20);
  gl.glEnd();
}

Here is an image of the sketch running, the same green triangle is drawn, with translation and scale applied, then the red one, outsie the push/pop 'block', so it is not affected by the transform:

alt text

HTH, George

George Profenza
Thanks George, but the push/pop isn't actually the issue; I'm already doing a push/pop outside the code that I made the snippet of. Thanks for illustrating with pictures, though! A good visualization.
quixoto
ok, so you want to grab a snapshot of a rectangular region and redraw that flat area as a smaller 'thumbnail' somewhere else on the screen ?
George Profenza