void Update()
    {
        if (Input.touchCount == 0)
        {
            if (currentTouchOperation == TouchOperation.SELECT)
            {
                voxelArray.TouchUp();
            }
            if (currentTouchOperation == TouchOperation.MOVE)
            {
                movingAxis.TouchUp();
            }
            currentTouchOperation = TouchOperation.NONE;
        }
        else if (Input.touchCount == 1)
        {
            Touch touch = Input.GetTouch(0);

            RaycastHit hit;
            if (currentTouchOperation != TouchOperation.SELECT)
            {
                selectingXRay = true;
            }
            bool rayHitSomething = Physics.Raycast(cam.ScreenPointToRay(Input.GetTouch(0).position),
                                                   out hit, Mathf.Infinity, selectingXRay ? Physics.DefaultRaycastLayers : NO_XRAY_MASK);
            Voxel         hitVoxel         = null;
            int           hitFaceI         = -1;
            TransformAxis hitTransformAxis = null;
            ObjectMarker  hitMarker        = null;
            if (rayHitSomething)
            {
                GameObject hitObject = hit.transform.gameObject;
                if (hitObject.tag == "Voxel")
                {
                    hitVoxel = hitObject.GetComponent <Voxel>();
                    hitFaceI = Voxel.FaceIForDirection(hit.normal);
                    if (hitFaceI == -1)
                    {
                        hitVoxel = null;
                    }
                }
                else if (hitObject.tag == "ObjectMarker")
                {
                    hitMarker = hitObject.GetComponent <ObjectMarker>();
                }
                else if (hitObject.tag == "MoveAxis")
                {
                    hitTransformAxis = hitObject.GetComponent <TransformAxis>();
                }

                if ((hitVoxel != null && hitVoxel.substance != null && hitVoxel.substance.xRay) ||
                    (hitMarker != null && (hitMarker.gameObject.layer == 8 || hitMarker.gameObject.layer == 10)))     // xray or SelectedObject layer
                {
                    // allow moving axes through xray substances
                    RaycastHit newHit;
                    if (Physics.Raycast(cam.ScreenPointToRay(Input.GetTouch(0).position),
                                        out newHit, Mathf.Infinity, NO_TRANSPARENT_MASK))
                    {
                        if (newHit.transform.tag == "MoveAxis")
                        {
                            hitVoxel         = null;
                            hitFaceI         = -1;
                            hitMarker        = null;
                            hitTransformAxis = newHit.transform.GetComponent <TransformAxis>();
                        }
                    }
                }
            }
            if (hitVoxel != null)
            {
                lastHitVoxel = hitVoxel;
                lastHitFaceI = hitFaceI;
            }
            else if (hitMarker != null)
            {
                lastHitVoxel = null;
                lastHitFaceI = -1;
            }

            if (touch.phase == TouchPhase.Began)
            {
                if (currentTouchOperation == TouchOperation.SELECT)
                {
                    voxelArray.TouchUp();
                }
                // this seems to improve the reliability of double-taps when things are running slowly.
                // I think it's because there's not always a long enough gap between taps
                // for the touch operation to be cleared.
                currentTouchOperation = TouchOperation.NONE;
            }

            if (currentTouchOperation == TouchOperation.NONE)
            {
                if (GUIPanel.PanelContainingPoint(touch.position) != null)
                {
                    currentTouchOperation = TouchOperation.GUI;
                }
                else if (touch.phase != TouchPhase.Moved && touch.phase != TouchPhase.Ended && touch.tapCount == 1)
                {
                }   // wait until moved or released, in case a multitouch operation is about to begin
                else if (!rayHitSomething)
                {
                    voxelArray.TouchDown(null);
                }
                else if (hitVoxel != null)
                {
                    if (touch.tapCount == 1)
                    {
                        currentTouchOperation = TouchOperation.SELECT;
                        voxelArray.TouchDown(hitVoxel, hitFaceI);
                        selectingXRay = hitVoxel.substance != null && hitVoxel.substance.xRay;
                    }
                    else if (touch.tapCount == 2)
                    {
                        voxelArray.DoubleTouch(hitVoxel, hitFaceI);
                    }
                    else if (touch.tapCount == 3)
                    {
                        voxelArray.TripleTouch(hitVoxel, hitFaceI);
                    }
                    UpdateZoomDepth();
                }
                else if (hitMarker != null)
                {
                    currentTouchOperation = TouchOperation.SELECT;
                    voxelArray.TouchDown(hitMarker);
                    selectingXRay = hitMarker.objectEntity.xRay;
                    UpdateZoomDepth();
                }
                else if (hitTransformAxis != null)
                {
                    if (touch.tapCount == 1)
                    {
                        currentTouchOperation = TouchOperation.MOVE;
                        movingAxis            = hitTransformAxis;
                        movingAxis.TouchDown(touch);
                    }
                    else if (touch.tapCount == 2 && lastHitVoxel != null)
                    {
                        voxelArray.DoubleTouch(lastHitVoxel, lastHitFaceI);
                    }
                    else if (touch.tapCount == 3 && lastHitVoxel != null)
                    {
                        voxelArray.TripleTouch(lastHitVoxel, lastHitFaceI);
                    }
                    UpdateZoomDepth();
                }
            } // end if currentTouchOperation == NONE

            else if (currentTouchOperation == TouchOperation.SELECT)
            {
                if (hitVoxel != null)
                {
                    voxelArray.TouchDrag(hitVoxel, hitFaceI);
                }
                if (hitMarker != null)
                {
                    voxelArray.TouchDrag(hitMarker);
                }
            }
            else if (currentTouchOperation == TouchOperation.MOVE)
            {
                movingAxis.TouchDrag(touch);
            }
        } // end if touch count is 1
        else if (Input.touchCount == 2)
        {
            if (currentTouchOperation == TouchOperation.NONE)
            {
                currentTouchOperation = TouchOperation.CAMERA;
                UpdateZoomDepth();
            }
            if (currentTouchOperation != TouchOperation.CAMERA)
            {
                return;
            }

            Touch touchZero = Input.GetTouch(0);
            Touch touchOne  = Input.GetTouch(1);

            Vector2 touchZeroPrevPos = touchZero.position - touchZero.deltaPosition;
            Vector2 touchOnePrevPos  = touchOne.position - touchOne.deltaPosition;

            float prevTouchDeltaMag = (touchZeroPrevPos - touchOnePrevPos).magnitude;
            float touchDeltaMag     = (touchZero.position - touchOne.position).magnitude;

            float deltaMagnitudeDiff = prevTouchDeltaMag - touchDeltaMag;

            float scaleFactor = Mathf.Pow(1.005f, deltaMagnitudeDiff / cam.pixelHeight * 700f);
            if (scaleFactor != 1)
            {
                pivot.localScale *= scaleFactor;
                if (pivot.localScale.x > MAX_ZOOM)
                {
                    pivot.localScale = new Vector3(MAX_ZOOM, MAX_ZOOM, MAX_ZOOM);
                }
                if (pivot.localScale.x < MIN_ZOOM)
                {
                    pivot.localScale = new Vector3(MIN_ZOOM, MIN_ZOOM, MIN_ZOOM);
                }
            }

            Vector3 move = (touchZero.deltaPosition + touchOne.deltaPosition) / 2;
            move *= 300f;
            move /= cam.pixelHeight;
            Vector3 pivotRotationEuler = pivot.rotation.eulerAngles;
            pivotRotationEuler.y += move.x;
            pivotRotationEuler.x -= move.y;
            if (pivotRotationEuler.x > 90 && pivotRotationEuler.x < 180)
            {
                pivotRotationEuler.x = 90;
            }
            if (pivotRotationEuler.x < -90 || (pivotRotationEuler.x > 180 && pivotRotationEuler.x < 270))
            {
                pivotRotationEuler.x = -90;
            }
            pivot.rotation = Quaternion.Euler(pivotRotationEuler);
        }
        else if (Input.touchCount == 3)
        {
            if (currentTouchOperation == TouchOperation.NONE)
            {
                currentTouchOperation = TouchOperation.CAMERA;
                UpdateZoomDepth();
            }
            if (currentTouchOperation != TouchOperation.CAMERA)
            {
                return;
            }

            Vector2 move = new Vector2(0, 0);
            for (int i = 0; i < 3; i++)
            {
                move += Input.GetTouch(i).deltaPosition;
            }
            move           *= 4.0f;
            move           /= cam.pixelHeight;
            pivot.position -= move.x * pivot.right * pivot.localScale.z;
            pivot.position -= move.y * pivot.up * pivot.localScale.z;
        }
    }