Tuesday, May 3, 2016

How does Canvas determine its clip bounds?

Leave a Comment

I've been doing some work with Android's Canvas, specifically trying to determine how its getClipBounds results are determined. I understand Canvas internally keeps a transform Matrix which is updated as I call translate, scale, etc. but trying to replicate that Matrix's results has baffled me.

@Override public void onDraw(Canvas canvas) {     Rect clipBounds;     RectF viewport;      canvas.save();     canvas.concat(translationMatrix);     //viewportMatrix.preConcat(canvas.getMatrix());     viewportMatrix.set(canvas.getMatrix());      clipBounds = canvas.getClipBounds();     viewport = GetViewport();      Log.d("clipBounds", clipBounds.toString() + " (" + clipBounds.width() + ", " + clipBounds.height() + ")");     Log.d("viewport", viewport.toString() + " (" + viewport.width() + ", " + viewport.height() + ")");      //drawing is done here      canvas.restore(); }  //viewport code modeled after http://stackoverflow.com/a/17142856/2245528 private RectF GetViewport() {     RectF viewport = new RectF();      viewportMatrix.mapRect(viewport, originalViewport);      return viewport; }  private void Translate(float x, float y) {     translationMatrix.postTranslate(x, y);      invalidate(); }  private void Scale(float scaleFactor, PointF focusPoint) {     if (focusPoint == null) {         translationMatrix.postScale(scaleFactor, scaleFactor);     }     //keep the focus point in focus if possible     else {         translationMatrix.postScale(scaleFactor, scaleFactor, focusPoint.x, focusPoint.y);     }      invalidate(); }  private final Matrix translationMatrix = new Matrix(); private final RectF originalViewport = new RectF(); private final Matrix viewportMatrix = new Matrix(); 

originalViewport is set to 0, 0, canvas.getWidth(), canvas.getHeight(). Translate and Scale are called from gesture event handlers, which are working correctly.

The part that confuses me is viewportMatrix. It doesn't seem to matter whether I do

viewportMatrix.set(canvas.getMatrix()); 

or

viewportMatrix.preConcat(canvas.getMatrix()); 

or even a one-time call to

viewportMatrix.set(canvas.getMatrix()); 

at the beginning followed by side-by-side Translate/Scale calls to the two matrixes. I've even tried completely ignoring the Canvas's built-in Matrix and rewriting GetViewport as

//viewport code modeled after http://stackoverflow.com/a/17142856/2245528 private RectF GetViewport() {     RectF viewport = new RectF();      translationMatrix.mapRect(viewport, originalViewport);      return viewport; } 

I can never seem to match getClipBounds(), and the discrepancies are fairly serious:

with viewportMatrix.set(canvas.getMatrix):

clipBounds: Rect(-97, -97 - 602, 452) (699, 549)
viewport: RectF(97.04178, 97.06036, 797.04175, 647.06036) (700.0, 550.0)

with viewportMatrix.preConcat(canvas.getMatrix):

clipBounds: Rect(-97, -96 - 602, 453) (699, 549)
viewport: RectF(2708.9663, 2722.2754, 3408.9663, 3272.2754) (700.0, 550.0)

with translationMatrix.mapRect:

clipBounds: Rect(-96, -96 - 603, 453) (699, 549)
viewport: RectF(96.73213, 96.85794, 796.7321, 646.8579) (700.0, 550.0)

with a one-shot call to viewportMatrix.preConcat(canvas.getMatrix()) followed by side-by-side Translate/Scale calls:

clipBounds: Rect(-96, -97 - 603, 452) (699, 549)
viewport: RectF(96.57738, 97.78168, 796.5774, 647.7817) (700.0, 550.0)

with a one-shot call to viewportMatrix.set(canvas.getMatrix()) followed by side-by-side Translate/Scale calls:

clipBounds: Rect(-96, -96 - 603, 453) (699, 549)
viewport: RectF(96.40051, 96.88153, 796.4005, 646.88153) (700.0, 550.0)

I can't even check the Canvas source, as all the Matrix code vanishes into private native calls whose code isn't shown.

Why are my GetViewport calls so grossly off, and what's going on behind-the-scenes with getClipBounds?

1 Answers

Answers 1

I read through this answer on GameDev SE, which uses matrix inversion to swap between screen and world coordinate systems:

To go from screen to world space simply use Vector2.Transform. This is commonly used to get the location of the mouse in the world for object picking.

Vector2.Transform(mouseLocation, Matrix.Invert(Camera.TransformMatrix)); 

To go from world to screen space simply do the opposite.

Vector2.Transform(mouseLocation, Camera.TransformMatrix); 

Inspired by this approach, I tried inverting the transformation matrix like this:

private RectF GetViewport() {     RectF viewport = new RectF();     Matrix viewportMatrix = new Matrix();      translationMatrix.invert(viewportMatrix);      viewportMatrix.mapRect(viewport, originalViewport);      return viewport; } 

This viewport correctly matches the results returned by getClipBounds.

This matrix tutorial explains what I'd noticed from the results of transform calls, but hadn't applied to my matrix:

In a computer program the camera doesn’t move at all and in actuality, the world is just moving in the opposite direction and orientation of how you would want the camera to move in reality.

In order to understand this correctly, we must think in terms of two different things:

  1. The Camera Transformation Matrix: The transformation that places the camera in the correct position and orientation in world space (this is the transformation that you would apply to a 3D model of the camera if you wanted to represent it in the scene).
  2. The View Matrix: This matrix will transform vertices from world-space to view-space. This matrix is the inverse of the camera’s transformation matrix described above.
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment