Camera FOV possible BUG

I was testing creating multiple cameras, and setting them at 90 degree angles.

I made 2 cameras, one facing the front and one facing right at a 90 degree offset, set their FOV-X to 90 degrees, and saw that their edges do not align.

It looks like there is 0.75-1.5 of a degree missing between the cameras.

I know that you have to set the FOV-Y on the camera, but it’s not hard to calculate that from FOV-X

[java]
float xFOV = 90;

Camera cam = new Camera(app.getSettings().getWidth(), app.getSettings().getHeight());
cam.setFrustumPerspective(xFOV()/((float)cam.getWidth() / (float)cam.getHeight()), (floatcam.getWidth() / (float)cam.getHeight(), 0.01f, 5000f);
[/java]

By these calculations if the resolution is 1920x1080 FOV-Y comes out to be 1920/1080 = 1.7777777778
90/1.7777777778 = 50.624999999… which is what gets set according to the debugger.

If we check the math and go back… 50.624999999999*1.7777777778=89.9999999999 which is close enough to 90.

I checked the viewport layout and there are no strange overlays, not only that I tested it by making a gap in them and still I see a part missing.

The cameras are turned exactly at 90 degrees to each other, and keep that relationship through out.

So where is the missing degree coming from? Is there a problem in the setFrustumPerspective?
Is there a way to test the true degrees you are getting from the cameras FOV?

We can’t really see your code but you can see all of our code. Sounds like you might need to look in Camera.java to see what that method is doing.

Since I don’t know how you are detecting the missing 0.75 degree or whatever, I also can’t comment if that is normal for the rounding errors you are getting. It does sound like you should be setting up the frustum with a less ‘overflowy’ way.

@pspeed

I’m not really detecting the 0.75, that was an estimate. It looks to me that it gets larger the larger the camera angles gets. When my FOVX was 45…the lost view was about 0.75, when the FOVX was 90…the lost angle was almost 9 degrees.

I can post my code…it just might be too much to go through… but lets give it a try…

This is a multipass camera setup with multiple camera offsets. The multipass works fine…the offsets do not on the other hand.

The cameras all have internal nodes, which are attached to each other in an hierarchy with offsets. Main node is the one that is moved with the main camera, and the other cameras attach their nodes to the main node and then update their position according to the changes in the internal node every frame.

This is the camera controller:

[java]
/**

  • This class controls and syncs multiple cameras together to work in unison.

  • It is time sensitive when this class can be initialized. It has to be done after

  • the application is initialized but before any 3D contend is loaded.

  • @author serebrennik
    */
    public class SpanCameraSystemState extends StateBase
    {

    /**This needs to be overridden in the inheriting class */
    public static final String STATE_NAME = “CameraSystemState”;

    private MavreAppBase mApp = null;
    private RenderManager mRenderManager = null;
    private AppStateManager mStateManager = null;
    private Node mRootNode = null;
    private String mLoadedProfile = “1_0_45.asc”;
    private int mNumOfCameras = 1;
    private float mCameraOffset[] = null;

    protected ArrayList<DistanceCameraSystem> mCamArray = new ArrayList();
    protected CameraType mCameraType = null;

    public SpanCameraSystemState(MavreAppBase app, CameraType cameraType, int numOfCameras)
    {
    /*Define our member Variables/
    mApp = app;
    mRenderManager = mApp.getRenderManager();
    mStateManager = mApp.getStateManager();
    mRootNode = mApp.getRootNode();
    mCameraType = cameraType;
    mNumOfCameras = numOfCameras;
    mCameraOffset = new float[mNumOfCameras];

     /**Redefines some systems, cameras, viewports, etc...*/
     redefineInitSystems();
    

    }

    /**

    • Returns the name of this State
    • @return
      */
      @Override
      public String getName()
      {
      return STATE_NAME;
      }

    @Override
    protected void initializeState(AppStateManager stateManager, MavreApplication app)
    {
    }

    /**

    • Redefines and deletes some systems that were created by
    • SimpleApplication and replaces them with our own.
      */
      private void redefineInitSystems()
      {
      reconfigureCameras();
      reconfigureDefaultInput();
      }

    /**

    • Deletes the initial default Camera and View port and

    • implements its own
      */
      public void reconfigureCameras()
      {
      try
      {

       /**Remove the Main viewport*/
       //renderManager.getRenderer().deleteFrameBuffer(renderManager.getMainView("Default").getOutputFrameBuffer());
       mRenderManager.getMainView("Default").clearScenes();
       mRenderManager.removeMainView(mRenderManager.getMainView("Default"));        
      
       mApp.setViewPort(null);
       mApp.setCamera(null);
       
       /**Create our cameras*/
       createCameras();
      

      }
      catch(Exception e)
      {
      Log.getInstance().severe(Log.getInstance().generateError(e));
      }

    }

    /**

    • Create our cameras, defining the number, layout and field of view

    • @param numOfCameras
      */
      private void createCameras()
      {
      /*Load our camera profile, default values are main screen = 1, getCamSeparationAngle = 0, getFOVX=90 or 45/
      CameraProfile cProf = loadCameraProfile(mLoadedProfile);

      /*Set our window in the correct monitor/
      setWindowPosition(cProf);

      /*Create all our cameras/
      for(int i = 0; i < mNumOfCameras; i++)
      {
      mCamArray.add(new DistanceCameraSystem(mApp.getSettings().getWidth()/mNumOfCameras, mApp.getSettings().getHeight(), mCameraType, mRenderManager));
      mCamArray.get(i).setFrustumPerspective(cProf.getFOVX()/((float)mCamArray.get(i).getWidth() / (float)mCamArray.get(i).getHeight()),
      (float)mCamArray.get(i).getWidth() / (float)mCamArray.get(i).getHeight());
      mCamArray.get(i).setViewPort(getCamLB(i+1), getCamRB(i+1), 0f, 1f);
      mCamArray.get(i).attachScene(mRootNode);

       if(mNumOfCameras == 1 || (i+1) == cProf.getMainScreenNum())
       {
           mApp.setCamera(mCamArray.get(i));
           mApp.setViewPort(mCamArray.get(i).getMainViewPort());
       }
      

      }

      /*Generate camera offsets/
      generateCameraOffsets(cProf);
      }

    /**

    • Calculates and sets the correct Y position for the main window
    • @param cameraProf
      /
      void setWindowPosition(CameraProfile cameraProf)
      {
      if(mNumOfCameras == 1)
      {
      mApp.getDisplayUtil().setWindowPositionOffset(0);
      }
      else
      {
      mApp.getDisplayUtil().setWindowPositionOffset((1-cameraProf.getMainScreenNum())
      (mApp.getSettings().getWidth()/mNumOfCameras));
      }
      }

    /**

    • Generates the camera offsets for all the possible cameras

    • and links the cameras together. Offsets are stored in mCameraOffset

    • @param cameraProf
      */
      void generateCameraOffsets(CameraProfile cameraProf)
      {
      float[] angles = {0f,0f,0f};

      for(int i = 0-(cameraProf.getMainScreenNum()-1); i < mNumOfCameras-(cameraProf.getMainScreenNum()-1); i++)
      {
      if(i<0)
      {
      /*Generate offsets for cameras left of the main camera/
      mCameraOffset[i+(cameraProf.getMainScreenNum()-1)] = (((icameraProf.getFOVX())-cameraProf.getCamSeparationAngle()) * FastMath.DEG_TO_RAD);
      }
      else if(i>0)
      {
      /*Generate offsets for cameras right of the main camera/
      mCameraOffset[i+(cameraProf.getMainScreenNum()-1)] = (((i
      cameraProf.getFOVX())+cameraProf.getCamSeparationAngle()) * FastMath.DEG_TO_RAD);
      }
      else
      {
      /*Generate offset for the main camera/
      mCameraOffset[i+(cameraProf.getMainScreenNum()-1)] = 0;
      }

       if(i != 0)
       {   
           /**Attach slave cameras node to main cameras node*/
           ((DistanceCameraSystem)mApp.getCamera()).getCameraNode().attachChild(mCamArray.get(i+(cameraProf.getMainScreenNum()-1)).getCameraNode());
           
           /**Create an offset for the slave cameras node*/
           angles[1] = -1* mCameraOffset[i+(cameraProf.getMainScreenNum()-1)];
           mCamArray.get(i+(cameraProf.getMainScreenNum()-1)).getCameraNode().getLocalRotation().fromAngles(angles);
           
       }
       else
       {
           mApp.getRootNode().attachChild(mCamArray.get(i+(cameraProf.getMainScreenNum()-1)).getCameraNode());
       }
      

      }
      }

    /**

    • Returns the cameras left viewport boundary
    • @param camNumber camera number starting from the left 1, 2, …
    • @return
      */
      private float getCamLB(int camNumber)
      {
      return ((float)camNumber-1f);
      }

    /**

    • Returns the cameras right viewport boundary
    • @param camNumber camera number starting from the left 1, 2, …
    • @return
      */
      private float getCamRB(int camNumber)
      {
      return ((float)camNumber);
      }

    /**

    • Loads the passed in CameraProfile
    • @param profile
    • @return
      */
      private CameraProfile loadCameraProfile(String profile)
      {
      ArrayList<String> file = (ArrayList<String>) mApp.getAssetManager().loadAsset(“Config/CameraProfile/” + profile);
      String[] camProfile = file.get(1).split(" ");
      return new CameraProfile(Integer.parseInt(camProfile[0]), Float.parseFloat(camProfile[1]), Float.parseFloat(camProfile[2]));
      }

    /**

    • Removed the old FlyCam controls and places new ones on top of the existing camera.
      /
      private void reconfigureDefaultInput()
      {
      try
      {
      /
      *
      * Delete the old com.jme3.app.FlyCamAppState state so its
      * not confused with the com.metronaviation.mavre.base.app.input.FlyCamAppState state.
      */
      if (mStateManager.getState(com.jme3.app.FlyCamAppState.class) != null)
      {
      com.jme3.app.FlyCamAppState state = mStateManager.getState(com.jme3.app.FlyCamAppState.class);
      mStateManager.detach(state);
      }

      /**Remove the old flyCam as a listener from inputManager*/
      mApp.getFlyByCamera().unregisterInput();
      
      /**Create and setup the new flyCam*/
      if (mStateManager.getState(FlyCamAppState.class) != null) 
      {
          mApp.SetFlyByCamera(null);
          mApp.SetFlyByCamera(new FlyByCamera(mApp.getCamera()));
          mApp.getFlyByCamera().setMoveSpeed(1f);
          mApp.getFlyByCamera().setZoomSpeed(0f);
          mStateManager.getState(FlyCamAppState.class).setCamera( mApp.getFlyByCamera() ); 
      }           
      

      }
      catch(Exception e)
      {
      Log.getInstance().severe(Log.getInstance().generateError(e));
      }
      }

    /**

    • Updates the position and rotation of the salve cameras
      */
      private void updateCameras()
      {
      /*Update our camera positions/
      for (int i=0; i<mNumOfCameras; i++)
      {
      /*Make sure we avoid changing the main camera/
      if(mCamArray.get(i) != mApp.getCamera())
      {
      /*Set the cameras location/
      mCamArray.get(i).updateFromNode();
      }
      }

    }

    @Override
    public void update(float tpf)
    {
    super.update(tpf);

     /**Update our camera positions*/
    updateCameras();
    

    }

    @Override
    public void onMessage(MessageBase message)
    {
    //DO NOTHING
    }

}
[/java]

This is the camera itself:

[java]

public class DistanceCameraSystem extends Camera
{
public final String MAIN_CAM_NAME = “MainCam” + UUID.randomUUID().toString();
public final String MAIN_VP_NAME = “MainVP” + UUID.randomUUID().toString();
public final String DIST_CAM_NAME = “DistCam” + UUID.randomUUID().toString();
public final String DIST_VP_NAME = “DistVP” + UUID.randomUUID().toString();

private final float MAX_CLIP_RATIO = 10000f; 
private final float CLIP_OVERLAP_PERCENT = 0.1f;
    
private RenderManager mRenderManager = null;

private ViewPort mMainVP = null;
private ViewPort mDistVP = null;

private Camera mDistCam = null;

private Node mCamNode = new Node();

private CameraType mCamType = CameraType.NORMAL;
private boolean mIsAttached = false;

private float mNearCPlane = 0f;
private float mMidCPlane = 0f;
private float mFarCPlane = 0f;
    
/**
 * ctor
 */
public DistanceCameraSystem(CameraType cameraType, RenderManager rm) 
{
    this(640, 480, cameraType, rm);      
}

public DistanceCameraSystem(int width, int height, CameraType cameraType, RenderManager rm) 
{
    super(width, height);        
    
    mCamType = cameraType;
    mRenderManager = rm;
    
    setupCameras();      
}

/**
 * Creates the Systems Cameras and Viewports
 */
private void setupCameras()
{       
    mNearCPlane = mCamType.getMinRange();
    mMidCPlane = computeMidCPlane();
    mFarCPlane = mCamType.getMaxRange();
    
    /**Create our cameras*/
    this.setName(MAIN_CAM_NAME);      
   
    /**Get our initial default view distances*/
    this.setFrustumNear(mNearCPlane);
    this.setFrustumFar(mFarCPlane);
    
    if(mCamType == CameraType.MULTIPASS_LONGDIST)
    {
        mDistCam = new Camera(this.width, this.height);
        mDistCam.copyFrom(this);
        mDistCam.setName(DIST_CAM_NAME);            
    }
   
    this.attachToRenderManager();       
}

/**
 * Removes the used views from the render loops, and frees up the resources. 
 */
public void detachFromRenderManager()
{
    if(mIsAttached)
    {
        /**
         * This method needs to be tested with just using mMainVP and mDistVP instead of going through 
         * mRenderManager every time, but in the notes it said that for some reason it won't work that way. 
         */
        
        if(mDistVP != null)
        {
            /**Remove the Distance viewport*/
            mRenderManager.getPreView(DIST_VP_NAME).clearScenes();
            mRenderManager.removePreView(mRenderManager.getPreView(DIST_VP_NAME));
            mDistVP = null;
        }
        
        if(mMainVP != null)
        {
            /**Remove the Main viewport*/
            mRenderManager.getMainView(MAIN_VP_NAME).clearScenes();
            mRenderManager.removeMainView(mRenderManager.getMainView(MAIN_VP_NAME));
            mMainVP = null;
        }
        
        mIsAttached = false;
    }
}

/**
 * Creates internal viewports and attaches the cameras to the RenderManager. 
 * You dont need to call this when creating the camera system, it is called automaticaly, but you 
 * do need to call this if you manually detached the system from the manager. 
 * @param renderManager 
 */
public void attachToRenderManager()
{      
    if(mIsAttached)
    {
        detachFromRenderManager();
    }
    
    /**Create our viewports*/
    setupViewPorts();
    
    mIsAttached = true;
}

/**
 * Sets up our view ports
 */
private void setupViewPorts()
{
    if(mCamType == CameraType.MULTIPASS_LONGDIST)
    {
        mDistVP = mRenderManager.createPreView(DIST_VP_NAME, mDistCam);
        mDistVP.setClearFlags(true, true, true); 
        
        mMainVP = mRenderManager.createMainView(MAIN_VP_NAME, this);    
        mMainVP.setBackgroundColor(new ColorRGBA(0.0f, 0f, 0f, 0f));
        mMainVP.setClearFlags(false, true, true);
                  
    }
    else
    {
        mMainVP = mRenderManager.createMainView(MAIN_VP_NAME, this);    
        mMainVP.setBackgroundColor(new ColorRGBA(0.0f, 0f, 0f, 0f));
        mMainVP.setClearFlags(true, true, true);           
    } 
 }  
 
/**
 * Recomputed the Middle Clipping plane from MAX_CLIP_RATIO and the near clipping
 * plane of the main Camera
 */
private float computeMidCPlane()
{
    return mNearCPlane * MAX_CLIP_RATIO;
}

/**
 * Computes Distance Cameras near Clipping Plane accroding to CLIP_OVERLAP_PERCENT and mMidCPlane
 * @return 
 */
private float computeDistCamNearCP()
{
    return mMidCPlane - (mMidCPlane * CLIP_OVERLAP_PERCENT);
}

/**
 * Returns the Type of the camera this system is being used as. Single or Multipass.
 * @return 
 */
public CameraType getCameraType()
{
    return mCamType;
}


/**
 * Attaches the scene to internal viewports
 * @param scene 
 */
public void attachScene(Spatial scene)
{
    mMainVP.attachScene(scene);
    if(mDistVP != null)
    {
        mDistVP.attachScene(scene);
    }
}

/**
 * Returns the Main Camera of the system
 * @return 
 */
public Camera getMainCamera()
{
    return this;
}

/**
 * Returns the Distance Camera of the system
 * @return 
 */
public Camera getDistCamera()
{
    return mDistCam;
}

/**
 * Returns the Main View Port
 * @return 
 */
public ViewPort getMainViewPort()
{
    return mMainVP;
}

/**
 * Returns the Distance View Port
 * @return 
 */
public ViewPort getDistViewPort()
{
    return mDistVP;
}

    /**
 * <code>setFrustumPerspective</code> defines the frustum for the camera.  This
 * frustum is defined by a viewing angle, aspect ratio, and internal near/far planes
 *
 * @param fovY   Frame of view angle along the Y in degrees.
 * @param aspect Width:Height ratio
 */
public void setFrustumPerspective(float fovY, float aspect) 
{
    if(mCamType == CameraType.MULTIPASS_LONGDIST)
    {
        super.setFrustumPerspective(fovY, aspect, this.mNearCPlane, computeMidCPlane());
        mDistCam.setFrustumPerspective(fovY, aspect, computeDistCamNearCP(), this.mFarCPlane);
    }
    else
    {
        super.setFrustumPerspective(fovY, aspect, this.mNearCPlane, this.mFarCPlane);
    }
    
}

////
//
//
////
//
//
////
//
//
//Genera Overriden function*//
//****************************************************************************************************//

/**
 * <code>setLocation</code> sets the position of the camera.
 *
 * @param location the position of the camera.
 */
@Override
public void setLocation(Vector3f location) 
{
    super.setLocation(location);
    
    mCamNode.setLocalTranslation(location);
            
    if(mDistCam != null)
    {
        mDistCam.setLocation(location);
    }
}

/**
 * <code>setRotation</code> sets the orientation of this camera. 
 * This will be equivelant to setting each of the axes:
 * <code>&lt;br&gt;
 * cam.setLeft(rotation.getRotationColumn(0));&lt;br&gt;
 * cam.setUp(rotation.getRotationColumn(1));&lt;br&gt;
 * cam.setDirection(rotation.getRotationColumn(2));&lt;br&gt;
 * </code>
 *
 * @param rotation the rotation of this camera
 */
@Override
public void setRotation(Quaternion rotation) 
{
    super.setRotation(rotation);

    mCamNode.setLocalRotation(rotation);
    
    if(mDistCam != null)
    {
        mDistCam.setRotation(rotation);
    }
}

/**
 * <code>lookAtDirection</code> sets the direction the camera is facing
 * given a direction and an up vector.
 *
 * @param direction the direction this camera is facing.
 */
@Override
public void lookAtDirection(Vector3f direction, Vector3f up) 
{
    super.lookAtDirection(direction, up);
    
    mCamNode.setLocalRotation(rotation);
    
    if(mDistCam != null)
    {
        mDistCam.lookAtDirection(direction, up);
    }
}

/**
 * <code>setAxes</code> sets the axes (left, up and direction) for this
 * camera.
 *
 * @param left      the left axis of the camera.
 * @param up        the up axis of the camera.
 * @param direction the direction the camera is facing.
 * 
 * @see Camera#setAxes(com.jme3.math.Quaternion) 
 */
@Override
public void setAxes(Vector3f left, Vector3f up, Vector3f direction) 
{
    super.setAxes(left, up, direction);
    
    mCamNode.setLocalRotation(super.getRotation());
    
    if(mDistCam != null)
    {
        mDistCam.setAxes(left, up, direction);
    }
}

/**
 * <code>setAxes</code> uses a rotational matrix to set the axes of the
 * camera.
 *
 * @param axes the matrix that defines the orientation of the camera.
 */
@Override
public void setAxes(Quaternion axes) 
{
    super.setAxes(axes);
    
    mCamNode.setLocalRotation(super.getRotation());
    
    if(mDistCam != null)
    {
        mDistCam.setAxes(axes);
    }
}

/**
 * normalize normalizes the camera vectors.
 */
@Override
public void normalize() 
{
    super.normalize();
    
    if(mDistCam != null)
    {
        mDistCam.normalize();
    }
}

/**
 * <code>setFrame</code> sets the orientation and location of the camera.
 *
 * @param location  the point position of the camera.
 * @param left      the left axis of the camera.
 * @param up        the up axis of the camera.
 * @param direction the facing of the camera.
 * @see Camera#setFrame(com.jme3.math.Vector3f,
 *      com.jme3.math.Vector3f, com.jme3.math.Vector3f, com.jme3.math.Vector3f)
 */
@Override
public void setFrame(Vector3f location, Vector3f left, Vector3f up, Vector3f direction) 
{
    super.setFrame(location, left, up, direction);
    
    if(mDistCam != null)
    {
        mDistCam.setFrame(location, left, up, direction);
    }
}

/**
 * <code>lookAt</code> is a convienence method for auto-setting the frame
 * based on a world position the user desires the camera to look at. It
 * repoints the camera towards the given position using the difference
 * between the position and the current camera location as a direction
 * vector and the worldUpVector to compute up and left camera vectors.
 *
 * @param pos           where to look at in terms of world coordinates
 * @param worldUpVector a normalized vector indicating the up direction of the world.
 *                      (typically {0, 1, 0} in jME.)
 */
@Override
public void lookAt(Vector3f pos, Vector3f worldUpVector) 
{
    super.lookAt(pos, worldUpVector);
    
    mCamNode.setLocalRotation(super.getRotation());
    
    if(mDistCam != null)
    {
        mDistCam.lookAt(pos, worldUpVector);
    }
}

/**
 * <code>setFrame</code> sets the orientation and location of the camera.
 * 
 * @param location
 *            the point position of the camera.
 * @param axes
 *            the orientation of the camera.
 */
@Override
public void setFrame(Vector3f location, Quaternion axes) 
{
    super.setFrame(location, axes);
    
    if(mDistCam != null)
    {
        mDistCam.setFrame(location, axes);
    }
}

/**
 * <code>update</code> updates the camera parameters by calling
 * <code>onFrustumChange</code>,<code>onViewPortChange</code> and
 * <code>onFrameChange</code>.
 *
 * @see Camera#update()
 */
@Override
public void update() 
{
    super.update();
    
    if(mDistCam != null)
    {
        mDistCam.update();
    }
}

/**
 * <code>setViewPortLeft</code> sets the left boundary of the viewport
 *
 * @param left the left boundary of the viewport
 */
@Override
public void setViewPortLeft(float left) 
{
    super.setViewPortLeft(left);
    
    if(mDistCam != null)
    {
        mDistCam.setViewPortLeft(left);
    }
}

 /**
 * <code>setViewPortRight</code> sets the right boundary of the viewport
 *
 * @param right the right boundary of the viewport
 */
@Override
public void setViewPortRight(float right) 
{
    super.setViewPortRight(right);
    
    if(mDistCam != null)
    {
        mDistCam.setViewPortRight(right);
    }
}

 /**
 * <code>setViewPortTop</code> sets the top boundary of the viewport
 *
 * @param top the top boundary of the viewport
 */
@Override
public void setViewPortTop(float top) 
{
    super.setViewPortTop(top);
    
    if(mDistCam != null)
    {
        mDistCam.setViewPortTop(top);
    }
}

 /**
 * <code>setViewPortBottom</code> sets the bottom boundary of the viewport
 *
 * @param bottom the bottom boundary of the viewport
 */
@Override
public void setViewPortBottom(float bottom) 
{
    super.setViewPortBottom(bottom);
    
    if(mDistCam != null)
    {
        mDistCam.setViewPortBottom(bottom);
    }
}

/**
 * <code>setViewPort</code> sets the boundaries of the viewport
 *
 * @param left   the left boundary of the viewport (default: 0)
 * @param right  the right boundary of the viewport (default: 1)
 * @param bottom the bottom boundary of the viewport (default: 0)
 * @param top    the top boundary of the viewport (default: 1)
 */
@Override
public void setViewPort(float left, float right, float bottom, float top) 
{
    super.setViewPort(left, right, bottom, top);
    
    if(mDistCam != null)
    {
        mDistCam.setViewPort(left, right, bottom, top);
    }
}

/**
 * Overrides the projection matrix used by the camera. Will
 * use the matrix for computing the view projection matrix as well.
 * Use null argument to return to normal functionality.
 *
 * @param projMatrix
 */
@Override
public void setProjectionMatrix(Matrix4f projMatrix) 
{
    super.setProjectionMatrix(projMatrix);
    
    if(mDistCam != null)
    {
        mDistCam.setProjectionMatrix(projMatrix);
    }
}

/**
 * Updates the view projection matrix.
 */
@Override
public void updateViewProjection() 
{
    super.updateViewProjection();
    
    if(mDistCam != null)
    {
        mDistCam.updateViewProjection();
    }
}

/**
 * Clears the viewport changed flag once it has been updated inside
 * the renderer.
 */
@Override
public void clearViewportChanged() 
{
    super.clearViewportChanged();
    
    if(mDistCam != null)
    {
        mDistCam.clearViewportChanged();
    }
}

/**
 * Called when the viewport has been changed.
 */
@Override
public void onViewPortChange() 
{
    super.onViewPortChange();
    
    if(mDistCam != null)
    {
        mDistCam.onViewPortChange();
    }
}


/**
 * <code>onFrustumChange</code> updates the frustum to reflect any changes
 * made to the planes. The new frustum values are kept in a temporary
 * location for use when calculating the new frame. The projection
 * matrix is updated to reflect the current values of the frustum.
 */
@Override
public void onFrustumChange() 
{
    super.onFrustumChange();
    
    if(mDistCam != null)
    {
        mDistCam.onFrustumChange();
    }
}

/**
 * <code>onFrameChange</code> updates the view frame of the camera.
 */
@Override
public void onFrameChange() 
{
    super.onFrameChange();
    
    if(mDistCam != null)
    {
        mDistCam.onFrameChange();
    }
}

    /**
 * Enable/disable parallel projection.
 *
 * @param value true to set up this camera for parallel projection is enable, false to enter normal perspective mode
 */
@Override
public void setParallelProjection(final boolean value) 
{
    super.setParallelProjection(value);
    
    if(mDistCam != null)
    {
        mDistCam.setParallelProjection(value);
    }
}

    /**
 * Computes the z value in projection space from the z value in view space 
 * Note that the returned value is going non linearly from 0 to 1.
 * for more explanations on non linear z buffer see
 * http://www.sjbaker.org/steve/omniv/love_your_z_buffer.html
 * @param viewZPos the z value in view space.
 * @return the z value in projection space.
 */
@Override
public float getViewToProjectionZ(float viewZPos) 

{
    float far = getFrustumFar();
    float near = getFrustumNear();
    if(this.mCamType == CameraType.MULTIPASS_LONGDIST)
    {
        near = mDistCam.getFrustumFar();
    }
    float a = far / (far - near);
    float b = far * near / (near - far);
    return a + b / viewZPos;
}

/**
 * Updates the cameras location and rotation from the internal node
 */
public void updateFromNode()
{
    super.setLocation(mCamNode.getWorldTranslation());
    super.setRotation(mCamNode.getWorldRotation());
    
    if(mDistCam != null)
    {
        mDistCam.setLocation(mCamNode.getWorldTranslation());
        mDistCam.setRotation(mCamNode.getWorldRotation());
    }
}

/**
 * Returns the internal camera Node
 * @return 
 */
public Node getCameraNode()
{
    return mCamNode;
}

}

[/java]

Any help would be appreciated. :slight_smile:

You probably want to reduce the issue down to a simple test case. Bonus points if it is trivial to see the gap in the test case.

I tried to follow the code but my brain shut down at “extends Camera” so it was tougher to read after that. Probably better to find a design that doesn’t require you to extend this class. Seems overall like more of a “has-a” relationship than an “is-a” relationship but I don’t know exactly what is being attempted.

The fact that you needed to intercept all of those camera methods is pretty scary, though. Like something else is upside down.

@pspeed

The camera gets extended to create a camera with multiple passes. So this camera internally uses two cameras. Which doesn’t have anything at all to do with it’s position and orientation. Those two items are actually propagated equally to both passes.
It needs to be extended to be easily used instead of the original camera in the Application class. The “has-a” design was tried first…and wasnt very successful, management and reusability became a bit too hard. And it broke a lot of things in the App class.

Anyway, I agree about having a simple test case. I will post here again in a day or so with something smaller and more readable. Most likely with a regular camera, not a long distance one.

Looks like I need to retract this Ticket :frowning:

I made a simple example that follows the basic principles of what is going on in my very very long code, and everything lines up to the pixel. So the problem is somewhere in the code. Maybe a roundoff error or an int being cut off from a float.

here is the example just in case someone wants to see how to set up cameras to follow each other.

[java]
/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.
    */
    package com.metronaviation.mavre.examples.multiscreentest;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.renderer.Camera;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.metronaviation.mavre.base.util.logging.Log;

/**
*

  • @author Maxim Serebrennik
    */
    public class multiscreentest extends SimpleApplication
    {

    Node mMainNode = new Node();
    Node mSlaveNode = new Node();
    Camera cam2 = null;

    /**

    • @param args the command line arguments
      */
      public static void main(String[] args)
      {
      Log LOGGER = Log.getInstance();

      multiscreentest app = new multiscreentest();
      app.start();
      }

    @Override
    public void simpleInitApp()
    {
    float angle = 90;

     cam.setFrustumPerspective(angle/(settings.getWidth()/settings.getHeight()), (settings.getWidth()/settings.getHeight()), 1f, 1000f);
     
     cam2 = new Camera(640,480);
     cam2.copyFrom(cam);
             
     ViewPort vp = renderManager.createMainView("vp", cam2);    
     vp.setBackgroundColor(new ColorRGBA(0.0f, 0f, 0f, 0f));
     vp.setClearFlags(true, true, true);
     vp.attachScene(rootNode);
    
     cam.setViewPort(0f, 0.5f, 0f, 1f);
     cam2.setViewPort(0.5f, 1f, 0f, 1f);   
     
     float[] angles = {0f,-angle*FastMath.DEG_TO_RAD,0f};
     mSlaveNode.getLocalRotation().fromAngles(angles);
     mMainNode.attachChild(mSlaveNode);
     
     Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
     mat.setTexture("ColorMap", getAssetManager().loadTexture("Textures/Construct/grid.jpg"));
     
     Spatial Construct = assetManager.loadModel("Models/Construct/Construct.j3o");
     Construct.setMaterial(mat);
    
     
     getRootNode().attachChild(Construct);
    

    }

    @Override
    public void simpleUpdate(float tpf)
    {
    mMainNode.setLocalTranslation(cam.getLocation());
    mMainNode.setLocalRotation(cam.getRotation());

     cam2.setLocation(mSlaveNode.getWorldTranslation());
     cam2.setRotation(mSlaveNode.getWorldRotation());          
    

    }
    }

[/java]

Glad it’s working for you.

Just an aside:
angle/(settings.getWidth()/settings.getHeight())

angle * settings.getHeight() / settings.getWidth()

…and avoid the extra divide (and avoid dependency on parenthesis to force operator order). It may also cause less rounding problems but I’d have to think about it harder than I’m willing to right now. :slight_smile:

@pspeed said: Glad it's working for you.

Just an aside:
angle/(settings.getWidth()/settings.getHeight())

angle * settings.getHeight() / settings.getWidth()

…and avoid the extra divide (and avoid dependency on parenthesis to force operator order). It may also cause less rounding problems but I’d have to think about it harder than I’m willing to right now. :slight_smile:

If the jit is not reordering it anyway automatically at least.

@pspeed

Actually I was wrong…the simple example also does not work.
It looked like it worked because I was missing the (float) casts on the settings.getWidth(), and the ratio went to 1 instead of 1.7777778. then if the FOV is 90, each screen becomes 90x90 and it all aligns. But if we put the floats back…it stops aligning again.

Any ideas what else could be wrong?

[java]

public class MultiCameraExample extends SimpleApplication
{

Node mMainNode = new Node();
Node mSlaveNode = new Node();
Camera cam2 = null;

/**
 * @param args the command line arguments
 */
public static void main(String[] args) 
{
    Log LOGGER = Log.getInstance();
    
    MultiCameraExample app = new MultiCameraExample();
    app.start();
}

@Override
public void simpleInitApp() 
{   
    float angle = 90;

// cam.setFrustumPerspective(angle/((float)settings.getWidth()/(float)settings.getHeight()), ((float)settings.getWidth()/(float)settings.getHeight()), 1f, 1000f);
cam.setFrustumPerspective(angle*(float)settings.getHeight()/(float)settings.getWidth(), ((float)settings.getWidth()/(float)settings.getHeight()), 1f, 1000f);

    cam2 = new Camera(640,480);
    cam2.copyFrom(cam);
          
    ViewPort vp = renderManager.createMainView("vp", cam2);    
    vp.setBackgroundColor(new ColorRGBA(0.0f, 0f, 0f, 0f));
    vp.setClearFlags(true, true, true);
    vp.attachScene(rootNode);

    cam.setViewPort(0f, 0.5f, 0f, 1f);
    cam2.setViewPort(0.5f, 1f, 0f, 1f);   
    
    float[] angles = {0f,-angle*FastMath.DEG_TO_RAD,0f};
    mSlaveNode.getLocalRotation().fromAngles(angles);
    mMainNode.attachChild(mSlaveNode);
    
    Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
    mat.setTexture("ColorMap", getAssetManager().loadTexture("Textures/Construct/grid.jpg"));
    
    Spatial Construct = assetManager.loadModel("Models/Construct/Construct.j3o");
    Construct.setMaterial(mat);

    
    getRootNode().attachChild(Construct);
    
}

@Override
public void simpleUpdate(float tpf) 
{
    mMainNode.setLocalTranslation(cam.getLocation());
    mMainNode.setLocalRotation(cam.getRotation());

    cam2.setLocation(mSlaveNode.getWorldTranslation());
    cam2.setRotation(mSlaveNode.getWorldRotation());          
}

}

[/java]

here is the image of what I’m seeing. This is a grid that is all around the camera, and everything on it should allight. Left screen is looking forward, right is to the right, 90 degrees…supposedly.

This isn’t your problem but don’t ever do this:
mSlaveNode.getLocalRotation().fromAngles(angles);

…just saw it and thought I’d point it out. It potentially leaves the spatial in a state where it’s rotation has changed but it doesn’t know it.

So, what I’m hearing in the above is “when the math makes sure the horizontal angle is 90 degrees then everything works but when the math is more convoluted with potential rounding errors then it doesn’t”, ie: when aspect was 1 it worked.

As I suggested before, you might want to actually look at the Camera.java source code to see what it is doing. You could just be setting the frustum parameters directly instead of relying on two intermediate conversions to get what you want.

@pspeed

Thanks, i’ll make sure not to do getLocalRotation().fromAngles(angles)

That is correct, seems either the math is a bit off, or some large rounding error…
I will look into manualy setting the planes, and post what I find. Never did that before…What do the frustum values represent? Angle from the origin/center in radians?

@DieSlower said: @pspeed I will look into manualy setting the planes, and post what I find. Never did that before...What do the frustum values represent? Angle from the origin/center in radians?

Well, the source code is only a few clicks away but I will save you the strain. :wink:

[java]
public void setFrustumPerspective(float fovY, float aspect, float near,
float far) {
if (Float.isNaN(aspect) || Float.isInfinite(aspect)) {
// ignore.
logger.log(Level.WARNING, “Invalid aspect given to setFrustumPerspective: {0}”, aspect);
return;
}

    float h = FastMath.tan(fovY * FastMath.DEG_TO_RAD * .5f) * near;
    float w = h * aspect;
    frustumLeft = -w;
    frustumRight = w;
    frustumBottom = -h;
    frustumTop = h;
    frustumNear = near;
    frustumFar = far;

    // Camera is no longer parallel projection even if it was before
    parallelProjection = false;

    onFrustumChange();
}

[/java]

@pspeed

Thanks, I took a look at that, and the projection matrix, and a few other items. The Math looks to be right…at least from what I saw, maybe you will notice something off?

I’m not sure what this is used for in the Camera class:
coeffLeft = new float[2];
coeffRight = new float[2];
coeffBottom = new float[2];
coeffTop = new float[2];

But the projection matrix setup looked right.
I even placed in my own makePerspective function, and the results came out exactly the same.

I replaced the getLocalRotation() but of course that had no effect.

My code looks like this now:

[java]
public class MultiCameraExample extends SimpleApplication
{

Node mMainNode = new Node();
Node mSlaveNode = new Node();
Camera cam2 = null;

/**
 * @param args the command line arguments
 */
public static void main(String[] args) 
{
    Log LOGGER = Log.getInstance();
    
    MultiCameraExample app = new MultiCameraExample();
    app.start();
}

@Override
public void simpleInitApp() 
{           
    float fovX = 90.0f;
    
    float[] anglesNull = {0f,0f,0f};
    Quaternion qt = new Quaternion();
    cam.setRotation(qt.fromAngles(anglesNull));
    cam.setLocation(Vector3f.ZERO);
    
    makePerspective(fovX*(float)settings.getHeight()/(float)settings.getWidth(), ((float)settings.getWidth()/(float)settings.getHeight()), 1f, 1000f, cam);
    //makePerspective(90f, ((float)settings.getWidth()/(float)settings.getHeight()), 1f, 1000f, cam);
    //makePerspective(90f, 1f, 1f, 1000f, cam);
    
    //cam.setFrustumPerspective(fovX*(float)settings.getHeight()/(float)settings.getWidth(), ((float)settings.getWidth()/(float)settings.getHeight()), 1f, 1000f);
    //cam.setFrustumPerspective(90f, ((float)settings.getWidth()/(float)settings.getHeight()), 1f, 1000f);
    //cam.setFrustumPerspective(90f, 1f, 1f, 1000f);
          
    cam2 = new Camera(640,480);
    cam2.copyFrom(cam);
          
    ViewPort vp = renderManager.createMainView("vp", cam2);    
    vp.setBackgroundColor(new ColorRGBA(0.0f, 0f, 0f, 0f));
    vp.setClearFlags(true, true, true);
    vp.attachScene(rootNode);

    cam.setViewPort(0f, 0.5f, 0f, 1f);
    cam2.setViewPort(0.5f, 1f, 0f, 1f);   
    
    float[] angles = {0f,-fovX*FastMath.DEG_TO_RAD,0f};
    Quaternion q = new Quaternion();
    mSlaveNode.setLocalRotation(q.fromAngles(angles));
    mSlaveNode.setLocalTranslation(Vector3f.ZERO);
    mMainNode.attachChild(mSlaveNode);
    
    Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
    mat.setTexture("ColorMap", getAssetManager().loadTexture("Textures/Construct/grid.jpg"));
    
    Spatial Construct = assetManager.loadModel("Models/Construct/Construct.j3o");
    Construct.setMaterial(mat);
    
    getRootNode().attachChild(Construct);        
}

@Override
public void simpleUpdate(float tpf) 
{
    mMainNode.setLocalTranslation(cam.getLocation());
    mMainNode.setLocalRotation(cam.getRotation());

    cam2.setLocation(mSlaveNode.getWorldTranslation());
    cam2.setRotation(mSlaveNode.getWorldRotation());          
}

void makePerspective(float fovy, float aspectRatio, float zNear, float zFar, Camera camRef)
{
    // calculate the appropriate left, right etc. taken from OSG
    float tan_fovy = FastMath.tan(FastMath.DEG_TO_RAD*(fovy*0.5f));
    float right  =  tan_fovy * aspectRatio * zNear;
    float left   = -right;
    float top    =  tan_fovy * zNear;
    float bottom =  -top;
    
    //float near, float far, float left, float right,float top, float bottom
    camRef.setFrustum(zNear,zFar, left, right, top, bottom);
}

}
[/java]

What else could be wrong? I doubt making all the matrix multiplication in double vs float make that much of a difference, just in case…i looked into that…and to change that…a lot of parts of JME need to be rewritten. But somehow I doubt that float values would cause a loss of 10 degrees in some cases. If its not Double vs Float…what else could be wrong?

What would make such a difference when the ratio != 1
I checked with >1 and <1 and in every case when I set the FOVY = 90, I can see that it is exactly 90 degrees top to bottom, but left to right its always off unless the ratio is = 1

Well, the point of looking at the code was to stop doing it fovY based and change to use fovX since you know what it is already… or just manually set them up separately.

Given your description, it’s probably not a rounding error but a logic error. I’m not sure what it is, though.

@DieSlower said: I'm not sure what this is used for in the Camera class: coeffLeft = new float[2]; coeffRight = new float[2]; coeffBottom = new float[2]; coeffTop = new float[2];

Those are used to define the frustum planes. They are not plane normals but you can think of them “as if” they are - sort of :slight_smile:
Other than that I don’t know why your cameras don’t line up, visually it looks like the FOV is wrong but I can’t spot the logic error in the code.

@jmaasing

Ah, thanks! That makes a little more sense. Most camera implementations i wrote and used did have plane normal defined for culling.

@pspeed

Thanks for being patient with me…and this…

So, setting them manually? How so? Unless I’m misunderstanding something.

I tried setting them by actually hard coding the values…
Ratio 1920/1080 = 1.777777777777778
Angle 90/1.777777777777778 = 50.625

So if I set 50.625f for the FOVY and just plug in 1.777777777777778f it should get me close to 90 in FOVX…but i get the same result.

It could be a logic error…or something changing the camera when it gets activated?
Does anything influence the camera at all in the engine when it gets initialized?

I made the example even simpler…and still I see the error:

[java]
public class MultiCameraExample extends SimpleApplication
{

/**
 * @param args the command line arguments
 */
public static void main(String[] args)
{
    Log LOGGER = Log.getInstance();
   
    MultiCameraExample app = new MultiCameraExample();
    app.start();
}

@Override
public void simpleInitApp()
{  
    
    cam.setLocation(Vector3f.ZERO);
    cam.setFrustumPerspective(50.625f, 1.777777777778f, 1f, 1000f);
   
    Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
    mat.setTexture("ColorMap", getAssetManager().loadTexture("Textures/Construct/grid.jpg"));
   
    Spatial Construct = assetManager.loadModel("Models/Construct/Construct.j3o");
    Construct.setMaterial(mat);

    getRootNode().attachChild(Construct);
   
}

}
[/java]

No matter what screen resolution, no how the view port is defined, this should give me in theory a 90 degree angle on the Y…but I see the same problem.

Construct.j3o is a cube with a special grid texture that is centered around 0,0,0. If my camera starts at 0,0,0 and the FOV is 90 then I see the special edge markings of the texture. FOVX and FOVY. But it seems that every time I need to turn about 5-10 degrees to even get the corners on the screen. again, everything looks right if FOVY is 90 and ratio is 1.0, but any deviation from that…breaks everything.

Notice in this code:
[java]
float h = FastMath.tan(fovY * FastMath.DEG_TO_RAD * .5f) * near;
float w = h * aspect;
frustumLeft = -w;
frustumRight = w;
frustumBottom = -h;
frustumTop = h;
frustumNear = near;
frustumFar = far;
[/java]

How everything is based on the y FOV. It should be trivial to make it based on the x FOV, though.

[java]
float w = FastMath.tan(fovX * FastMath.DEG_TO_RAD * 0.5f) * near;
float h = w / aspect;
…etc…
[/java]

But since you already know fovX is going to be 90 you can reduce that all greatly to
float w = FastMath.tan(FastMath.QUARTER_PI) * near;

And since the tan() of 45 degrees should be 1:
float w = near;
float h = w / aspect;

I haven’t done the math so I don’t know how that compares to the numbers you are using manually… but it gets rid of a lot of intermediate garbage.

@pspeed

Right…but that still assumes that the problem is a roundoff error. Which…apparently it’s not! I can’t believe you and I both missed it…

I haven’t tried your code yet…but it seems like a shortening for the way things are usually done. Which is always good when you need more fps.

So… over and over on the post I’ve been calculating the FOVX and FOVY angles using the ratio…

Ratio 1920/1080 = 1.777777777777778
Angle 90/1.777777777777778 = 50.625

Which seems fine…if the ratio was actually linear. While the ratio of the screen sides is linear, the ratio of the angles is not since it depends on trig functions…right triangles, half the side distance, equal height on both sides, etc etc etc. I’m not going to draw picture because I’m sure you know what im talking about.

Basically to find FOVY you need to do…

fovY = 2 * arctan(tan(fovX/2)*h/w)

[java]
private float getFOVY(float fovX, float invRatio)
{
return (FastMath.RAD_TO_DEG * 2f * FastMath.atan(FastMath.tan(fovX*FastMath.DEG_TO_RAD/2f) * invRatio));
}
[/java]

This is what happens when you don’t do graphics for a while… I had a 4 year break…and lost the basics…hopefully I will get everything back faster then I lost it.

Thanks for all your help!!!

Well, my code should have also worked because it was applying the aspect ratio to the result (back to linear space) and not the angle. The point of basing it on fovX was to simplify since 90 degrees is a pretty special thing in trig and all of the other math drops away.

The fovY you were passing always looked wrong to me but I couldn’t pinpoint why. I’m glad you figured it out. (step 2 of my simplified version would have been to reverse engineer back to the angle to see that it was different.)