4t0m1c
4t0m1c

Reputation: 329

Accurate Pinch Zoom

I'm trying to figure out how to create an accurate pinch zoom for my camera in Unity3D/C#. It must be based on the physical points on the terrain. The image below illustrates the effect I want to achieve.

enter image description here

The Camera is a child of a null which scales (between 0,1 and 1) to "zoom" as not to mess with the perspective of the camera.

So what I've come up with so far is that each finger must use a raycast to get the A & B points as well as the current scale of the camera parent.

EG: A (10,0,2), B (14,0,4), S (0.8,0.8,0.8) >> A (10,0,2), B (14,0,4), S (0.3,0.3,0.3)

The positions of the fingers will change but the hit.point values should remain the same by changing the scale.

BONUS: As a bonus, it would be great to have the camera zoom into a point between the fingers, not just the center.

Thanks so much for any help or reference.

EDIT: I've come up with this below so far but it's not accurate the way I want. It incorporates some of the ideas I had above and I think that the problem is that it shouldn't be /1000 but an equation including the current scale somehow.

if (Input.touchCount == 2) {
        if (!CamZoom) {
            CamZoom = true;
            var rayA = Camera.main.ScreenPointToRay (Input.GetTouch (0).position);
            var rayB = Camera.main.ScreenPointToRay (Input.GetTouch (1).position);
            int layerMask = (1 << 8);
            if (Physics.Raycast (rayA, out hit, 1500, layerMask)) {
                PrevA = new Vector3 (hit.point.x, 0, hit.point.z);
                Debug.Log ("PrevA: " + PrevA);
            }
            if (Physics.Raycast (rayB, out hit, 1500, layerMask)) {
                PrevB = new Vector3 (hit.point.x, 0, hit.point.z);
                Debug.Log ("PrevB: " + PrevB);
            }
            PrevDis = Vector3.Distance (PrevB, PrevA);
            Debug.Log ("PrevDis: " + PrevDis);
            PrevScaleV = new Vector3 (PrevScale, PrevScale, PrevScale);
            PrevScale = this.transform.localScale.x;
            Debug.Log ("PrevScale: " + PrevScale);
        }
        if (CamZoom) {
            var rayA = Camera.main.ScreenPointToRay (Input.GetTouch (0).position);
            var rayB = Camera.main.ScreenPointToRay (Input.GetTouch (1).position);
            int layerMask = (1 << 8);
            if (Physics.Raycast (rayA, out hit, 1500, layerMask)) {
                NewA = new Vector3 (hit.point.x, 0, hit.point.z);
            }
            if (Physics.Raycast (rayB, out hit, 1500, layerMask)) {
                NewB = new Vector3 (hit.point.x, 0, hit.point.z);
            }
            DeltaDis = PrevDis - (Vector3.Distance (NewB, NewA));
            Debug.Log ("Delta: " + DeltaDis);
            NewScale = PrevScale + (DeltaDis / 1000);
            Debug.Log ("NewScale: " + NewScale);
            NewScaleV = new Vector3 (NewScale, NewScale, NewScale);
            this.transform.localScale = Vector3.Lerp(PrevScaleV,NewScaleV,Time.deltaTime);
            PrevScaleV = NewScaleV;
            CamAngle();
        }
    }

Upvotes: 5

Views: 4104

Answers (2)

Steve Lillis
Steve Lillis

Reputation: 3256

Intro

I had to solve this same problem recently and started off with the same approach as you, which is to think of it as though the user is interacting with the scene and we need to figure out where in the scene their fingers are and how they're moving and then invert those actions to reflect them in our camera.

However, what we're really trying to achieve is much simpler. We simply want the to user feel like the area of the screen that they are pinching changes size with the same ratio as their pinch changes.

Aim

First let's summarise our goal and constraints:

  • Goal: When a user pinches, the pinched area should appear to scale to match the pinch.
  • Constraint: We do not want to change the scale of any objects.
  • Constraint: Our camera is a perspective camera.
  • Constraint: We do not want to change the field of view on the camera.
  • Constraint: Our solution should be resolution/device independent.

With all that in mind, and given that we know that with a perspective camera objects appear larger when they're closer and smaller when they're further, it seems that the only solution for scaling what the user sees is to move the camera in/out from the scene.

Solution

In order to make the scene look larger at our focal point, we need to position the camera so that a cross-section of the camera's frustum at the focal point is equivalently smaller.

Here's a diagram to better explain:

solution

The top half of the image is the "illusion" we want to achieve of making the area the user expands twice as big on screen. The bottom half of the image is how we need to move the camera to position the frustum in a way that gives that impression.

The question then becomes how far do I move the camera to achieve the desired cross-section?

For this we can take advantage of the relationship between the frustum's height h at a distance d from the camera when the camera's field of view angle in degrees is θ.

trig

Since our field of view angle θ is constant per our agreed constraints, we can see that h and d are linearly proportional.

linearly proportional

This is useful to know because it means that any multiplication/division of h is equally reflected in d. Meaning we can just apply our multipliers directly to the distance, no extra calculation to convert height to distance required!

Implementation

So we finally get to the code.

First, we take the user's desired size change as a multiple of the previous distance between their fingers:

Touch touch0 = Input.GetTouch(0);
Touch touch1 = Input.GetTouch(1);
Vector2 prevTouchPosition0 = touch0.position - touch0.deltaPosition;
Vector2 prevTouchPosition1 = touch1.position - touch1.deltaPosition;
float touchDistance = (touch1.position - touch0.position).magnitude;
float prevTouchDistance = (prevTouchPosition1 - prevTouchPosition1).magnitude;
float touchChangeMultiplier = touchDistance / prevTouchDistance;

Now we know by how much the user wants to scale the area they're pinching, we can scale the camera's distance from its focal point by the opposite amount.

The focal point is the intersection of the camera's forward ray and the thing you're zooming in on. For the sake of a simple example, I'll just be using the origin as my focal point.

Vector3 focalPoint = Vector3.zero;
Vector3 direction = camera.transform.position - focalPoint;
float newDistance = direction.magnitude / touchChangeMultiplier;
camera.transform.position = newDistance * direction.normalized;
camera.transform.LookAt(focalPoint);

That's all there is to it.

Bonus

This answer is already very long. So to briefly answer your question about making the camera focus on where you're pinching:

  • When you first detect a 2 finger touch, store the screen position and related world position.
  • When zooming, move the camera to put the world position back at the same screen position.

Upvotes: 6

Javier
Javier

Reputation: 23

This is a small example:

        if (_Touches.Length == 2)
        {
            Vector2 _CameraViewsize = new Vector2(_Camera.pixelWidth, _Camera.pixelHeight);

            Touch _TouchOne = _Touches[0];
            Touch _TouchTwo = _Touches[1];

            Vector2 _TouchOnePrevPos = _TouchOne.position - _TouchOne.deltaPosition;
            Vector2 _TouchTwoPrevPos = _TouchTwo.position - _TouchTwo.deltaPosition;

            float _PrevTouchDeltaMag = (_TouchOnePrevPos - _TouchTwoPrevPos).magnitude;
            float _TouchDeltaMag = (_TouchOne.position - _TouchTwo.position).magnitude;

            float _DeltaMagDiff = _PrevTouchDeltaMag - _TouchDeltaMag;

            _Camera.transform.position += _Camera.transform.TransformDirection((_TouchOnePrevPos + _TouchTwoPrevPos - _CameraViewsize) * _Camera.orthographicSize / _CameraViewsize.y);

            _Camera.orthographicSize += _DeltaMagDiff * _OrthoZoomSpeed;
            _Camera.orthographicSize = Mathf.Clamp(_Camera.orthographicSize, _MinZoom, _MaxZoom) - 0.001f;

            _Camera.transform.position -= _Camera.transform.TransformDirection((_TouchOne.position + _TouchTwo.position - _CameraViewsize) * _Camera.orthographicSize / _CameraViewsize.y);

        }

In the second video of this tutorial explains it

Upvotes: 1

Related Questions