Grab'n drag camera? (or, a little geometry problem)


I might not use the proper search keywords, but I haven’t found any answer to this question: how to make an actual grab-and-drag camera – a camera that moves parallel to a plane (seen in perspective) in such a way that the point of the plane that was projected on cursor’s location when the player pressed the mouse button, remains under cursor’s location while the player holds the button and moves the mouse, like if the cursor was pinned onto the plane.

I’ve just read a recent post on the forums: it seems to ask a similar question but it’s got no proper answer.

My code does the following:

  1. retrieve the proper plane’s point from cursor’s position on screen (using Camera.ViewportPointToRay and line-plane intersection)
  2. get the delta vector between this plane’s point and the previous plane’s point retrieved (from cursor’s position on previous frame)
  3. translate the cam to the opposite of this delta vector.

I thought that way I would always get the right distance to move the camera (since it’s moving parallel to the plane), whatever the distance of the plane’s point from the camera… But I guess I was wrong since the cursor is still not “pinned” onto the plane (the result is not really better than on my “basic” version, when I translated the cam at an arbitrary constant speed and relative only to the cursor’s movement)

There might be an additional step to translate the cam, instead of just using the opposite of the delta vector; but I don’t know what operations/transformations, I am not good enough in solid geometry.
Yet my case is rather simple: the plane is normal to the z axis, thus the cam translates on x,y only.

Can anyone answer this little solid geometry problem?

EDIT - SOLUTION (principle – for code plz see the answer I posted below):

I’ve just come up with the solution and I think other people might be interested:

My mistake was on step 2, when getting the “previous position” that I will substract from the position of the plane’s point I retrieve from the cursor’s position on the current frame:

One should not use the plane’s point that was retrieved on the previous frame (corresponding to the cursor’s position on the previous frame).

Instead, because the cam moves every frame, one must retrieve – on the current frame thus with the current cam position – the plane’s point corresponding to the cursor’s position on the previous frame.

Next Beat Games asked for my coded solution hence it’s cleaner that I post it as an answer:

Notes: My code spans over a few functions and global variables, so there are a few parameters you have to figure out from their names (explicit enough I think), and I can just hope it’s clear enough when taken out of context.
Note as well that it’s not optimized as I use Unity’s built-in projection methods instead of custom instructions on matrices that would probably save a lot of math operations. But at least it works! ^^

First you need a method to retrieve the point on the plane (in 3D space) from a viewport’s point:

	Vector3 getPosOnPlane(Vector2 posOnViewport)
		Vector3 pt = new Vector3(posOnViewport.x, posOnViewport.y, 0);
		Ray d = camera.ViewportPointToRay(pt);
		float t = (planePos.z - d.origin.z) / d.direction.z;
		float x = d.direction.x * t + d.origin.x;
		float y = d.direction.y * t + d.origin.y;
		return new Vector3(x, y, planePos.z);

Then the method that updates camera’s position target point:

	void computeTgtPt(Vector2 currPosOnScreen)
		Vector3 inputPosOnPlane = getPosOnPlane(currPosOnScreen);
		Vector3 posOnPlaneFromPrevTouchPos = getPosOnPlane(prevTouchPos);
		Vector3 move = posOnPlaneFromPrevTouchPos - inputPosOnPlane;
		targetPt.x += move.x; //Note: targetPt is a global variable because it's initialized at camera's position at the begining of a touch
		targetPt.y += move.y;

In Update() you call computeTgtPt(…) with touch or cursor position as parameter whenever a touch is in effect and moves.
(Note: as I’m using viewport positions instead of screen positions so that it works in the editor as well, you might have to transform Input.mousePosition from screen coordinates into viewport coordinates.)

What I do then in Update() is to move the camera toward targetPt at a steady speed (and smooth/limit the movement as it gets close to its target). Plus I have added contextual momentum that I spared you as well for simplicity (To add momentum you may call computeTgtPt(…) with a virtual target point that takes it into account).

But if you want the plane to just appear sticked to the finger/cursor, I guess you can just set camera’s position to targetPt.

I normally use the following code to manually drag my camera in touch based games.

public void manualPan (Vector3 panDirection) {
		cameraTarget = null;
		panDirection.z *= 1.5f;
		panDirection = new Vector3(Mathf.Clamp(panDirection.x, panClamp[0], panClamp[1]), 0, Mathf.Clamp(panDirection.z, panClamp[0], panClamp[1]));
		transform.position += panDirection;

I generally feed in the touch delta from a single finger touch (I am multiplying the z value due to the aspect ratio of the screen). You could surely use the same code and feed in ray hit locations or mouse cursor positions.

I should mention that my camera here has a fixed rotation so I do not need to rotate the vector of the panning direction or anything. The cameras rotation is fixed and it’s Y value is controlled via pinching/pulling. Sounds like you might have it rotating freely in 3d space?