title: opengl camera zoom to cursor, avoiding > 90deg fov
body: <p>I'm trying to set up a google maps style zoom-to-cursor control for my opengl camera. I'm using a similar method to the one suggested <a href="https://stackoverflow.com/questions/21561724/opengl-google-maps-style-2d-camera-zoom-to-mouse-cursor">here</a>. Basically, I  get the position of the cursor, and calculate the width/height of my perspective view at that depth using some trigonometry. I then change the field of view, and calculate how to much I need to translate in order to keep the point under the cursor in the same apparent position on the screen. That part works pretty well. </p>
  
  <p>The issue is that I want to limit the fov to be less than 90 degrees. When it ends up >90, I cut it in half and then translate everything away from the camera so that the resulting scene looks the same as with the larger fov. The equation to find that necessary translation isn't working, which is strange because it comes from pretty simple algebra. I can't find my mistake. Here's the relevant code.</p>
  
  <pre><code>void Visual::scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
  {
      glm::mat4 modelview = view*model;
      glm::vec4 viewport = { 0.0, 0.0, width, height };
  
      float winX = cursorPrevX;
      float winY = viewport[3] - cursorPrevY;
      float winZ;
  
      glReadPixels(winX, winY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &amp;winZ);
      glm::vec3 screenCoords = { winX, winY, winZ };
  
      glm::vec3 cursorPosition = glm::unProject(screenCoords, modelview, projection, viewport);
  
      if (isinf(cursorPosition[2]) || isnan(cursorPosition[2])) {
          cursorPosition[2] = 0.0;
      }
  
      float zoomFactor = 1.1;
      // = zooming in 
      if (yoffset &gt; 0.0)
          zoomFactor = 1/1.1;
  
      //the width and height of the perspective view, at the depth of the cursor position 
      glm::vec2 fovXY = camera.getFovXY(cursorPosition[2] - zTranslate, width / height);
      camera.setZoomFromFov(fovXY.y * zoomFactor, cursorPosition[2] - zTranslate);
  
      //don't want fov to be greater than 90, so cut it in half and move the world farther away from the camera to compensate
      //not working...
      if (camera.Zoom &gt; 90.0 &amp;&amp; zTranslate*2 &gt; MAX_DEPTH) {
          float prevZoom = camera.Zoom;
          camera.Zoom *= .5;
  
          //need increased distance between camera and world origin, so that view does not appear to change when fov is reduced
          zTranslate = cursorPosition[2] - tan(glm::radians(prevZoom)) / tan(glm::radians(camera.Zoom) * (cursorPosition[2] - zTranslate));
      }
      else if (camera.Zoom &gt; 90.0) {
          camera.Zoom = 90.0;
      }
  
      glm::vec2 newFovXY = camera.getFovXY(cursorPosition[2] - zTranslate, width / height);
  
      //translate so that position under the cursor does not appear to move.
      xTranslate += (newFovXY.x - fovXY.x) * (winX / width - .5);
      yTranslate += (newFovXY.y - fovXY.y) * (winY / height - .5);
  
      updateView = true;
  }
  </code></pre>
  
  <p>The definition of my view matrix. Called ever iteration of the main loop.</p>
  
  <pre><code>void Visual::setView() {
      view = glm::mat4();
  
      view = glm::translate(view, { xTranslate,yTranslate,zTranslate });
  
      view = glm::rotate(view, glm::radians(camera.inclination), glm::vec3(1.f, 0.f, 0.f));
      view = glm::rotate(view, glm::radians(camera.azimuth), glm::vec3(0.f, 0.f, 1.f));
  
      camera.Right = glm::column(view, 0).xyz();
      camera.Up = glm::column(view, 1).xyz();
      camera.Front = -glm::column(view, 2).xyz(); // minus because OpenGL camera looks towards negative Z.
      camera.Position = glm::column(view, 3).xyz();
  
      updateView = false;
  }
  </code></pre>
  
  <p>Field of view helper functions.</p>
  
  <pre><code>glm::vec2 getFovXY(float depth, float aspectRatio) {
      float fovY = tan(glm::radians(Zoom / 2)) * depth;
      float fovX = fovY * aspectRatio;
  
      return glm::vec2{ 2*fovX , 2*fovY };
  }
  
  //you have a desired fov, and you want to set the zoom to achieve that.
  //factor of 1/2 inside the atan because we actually need the half-fov. Keep full-fov as input for consistency 
  void setZoomFromFov(float fovY, float depth) {
      Zoom = glm::degrees(2 * atan(fovY / (2 * depth)));
  }
  </code></pre>
  
  <p>The equations I'm using can be found from the diagram <a href="http://wiki.panotools.org/Field_of_View" rel="nofollow noreferrer">here</a>. Since I want to have the same field of view dimensions before and after the angle is changed, I start with</p>
  
  <pre><code>fovY = tan(theta1) * d1 = tan(theta2) * d2
  d2 = (tan(theta1) / tan(theta2)) * d1
  
  d1 = distance between camera and cursor position, before fov change = cursorPosition[2] - zTranslate
  d2 = distance after
  theta1 = fov angle before
  theta2 = fov angle after = theta1 * .5
  </code></pre>
  
  <p>Appreciate the help. </p>