Camera.lookAt() can't look at origin?

I know this sounds a little crazy but it seems the Camera.lookAt(Vector3f,Vector3f) method doesn't work if you're trying to look at spatial that's at the origin. Here is my very simple test case:



import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingSphere;
import com.jme.math.Vector3f;
import com.jme.scene.shape.Sphere;
import com.jme.util.LoggingSystem;

public class LookAtTest extends SimpleGame {
 
  private Sphere s;
 
  /**
   * Entry point for the test,
   * @param args
   */
  public static void main(String[] args) {
    LoggingSystem.getLogger().setLevel(java.util.logging.Level.OFF);
    LookAtTest app = new LookAtTest();
    app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);
    app.start();
  }
 
  protected void simpleInitGame() {
    display.setTitle("LookAt a Sphere");
 
    s = new Sphere("Sphere", 10, 10, 10);
    s.setModelBound(new BoundingSphere());
    s.updateModelBound();
    //s.setLocalTranslation(1, 0, 0);
    rootNode.attachChild(s);
   
    cam.setLocation(new Vector3f(0f,200f,0f));
    cam.lookAt(s.getLocalTranslation(), Vector3f.UNIT_Y);
  }
}



The camera will only face the sphere in my example if I set a local translation on the sphere (other than 0,0,0). You can see this by simply uncommenting the line:

//s.setLocalTranslation(1, 0, 0);



Any idea why this is happening? And is this normal?

confirmed. and it's not normal. it's because of the vector operations in lookAt().

Quote:
confirmed. and it's not normal. it's because of the vector operations in lookAt().

Thank-you for the confirmation. So this problem has been around since the conception of Camera.lookAt() then? Should I attempt to find the problem myself or are there others responsible for doing that? Not sure how things work around here.
Mr.Marbles said:

...Not sure how things work around here.


if you find a problem make a test case (a bug report in the issue tracker is also very useful).
if you can fix it, post the test case and the fix. usually the fix is then merged into jme.
if you can't fix it post the test case. usually someone (mos probably the devs) will come up with a fix which will be merged in jme.

i'm at work right now and my vector math is not the best, so i won't be able to fix it ;)

Well, if you ask me, the cote of lookAt seems a little strange, but it may be very well that I don't know enough about jME yet.  :slight_smile:


    public void lookAt( Vector3f pos, Vector3f worldUpVector ) {
        newDirection.set( pos ).subtractLocal( location ).normalizeLocal();

        // check to see if we haven't really updated camera -- no need to call
        // sets.
        if ( newDirection.equals( direction ) ) {
            return;
        }
        direction.set( newDirection );

        up.set( worldUpVector );
        left.set( up ).crossLocal( direction ).normalizeLocal();
        up.set( direction ).crossLocal( left ).normalizeLocal();
        onFrameChange();
    }



In particular, the first line is assuming that the position of the camera is absolute (world coordinates) while doing the difference, because if it were local (and most likely local means it is centered in (0,0,0)) then attempting to look at the origin would result in a zero vector, and hence, an impossible to normalize one. That would cause the rest of the calls to also fail.

So I would ask: Is your camera position really different than (0,0,0)?
Quote:
So I would ask: Is your camera position really different than (0,0,0)?

I don't know if you're asking me this question, but anyway, the camera position in my test case is (0,200,0) as you can see from the code. So it's not at the origin. If you run the test case you'll see that the location of the sphere is actually what makes the difference. That is, if the sphere is located at (0,0,0) then the lookAt will fail, otherwise it'll pass.

Yes, yes… Sorry about that, it was more like a rhetorical question for if it was local or world position… But I know what is the problem with the code (either yours or jME's).



The problem is that you are setting your camera exactly above the sphere in the case of lookAt(0,0,0) And unfortunately, the direction of the camera and the vector pointing up cannot be in the same line, otherwise the left direction cannot be dermined… The cross product is only defined for two vectors that are linearly independent:smiley:



To fix this, just move your camera a tiny bit in the Z direction like so:

cam.setLocation(new Vector3f(0f,200f,0.1f));



It should work like a charm. BTW, any developers around? I would suggest to change lookAt like so:

    public void lookAt( Vector3f pos, Vector3f worldUpVector ) {
        newDirection.set( pos ).subtractLocal( location ).normalizeLocal();

        // check to see if we haven't really updated camera -- no need to call
        // sets.
        if ( newDirection.equals( direction ) ) {
            return;
        }
        direction.set( newDirection );

        up.set( worldUpVector );
        left.set( up ).crossLocal( direction ).normalizeLocal();
        if( left.equals( Vector3f.ZERO ) )
            left.set( -1.0f, 0f, 0f );
        up.set( direction ).crossLocal( left ).normalizeLocal();
        onFrameChange();
    }

Hmm, but does changing left like so work if you were in a -X = up situation?  Perhaps detecting a collinear situation of direction to UP and shifting the lookat very slightly might be a better solution.

Quote:
Perhaps detecting a collinear situation of direction to UP and shifting the lookat very slightly might be a better solution

Does this imply re-positioning the camera from it's intended position? I mean, from a usage point of view, if I want to place the camera at (0,200,0) then that's exactly where I want it to be, not approximately there. Is this too demanding?

I think the better solution is (and I think thsi is what renanse is saying) to shift the "look at" vector, NOT the camera.  So your camera would be aimed slightly off from where you intended (I'm assuming this will be so slight as to be unnoticable under normal usage) but your camera itself would not shift.  So when you request to look at 0,0,0 it would be shifted to something like .001,0,0 before the calculations if a colinear situation were detected (probably a bad example).



Someone correct me if I'm getting this wrong.

Quote:
to shift the "look at" vector, NOT the camera.  So your camera would be aimed slightly off from where you intended

So you're referring to the direction vector of the camera? So the direction won't be "exactly" where you want to look but "close enough"? I wonder, is this really a limitation of vector math?

:frowning: Damn, you are absolutely right… I missed the fact that you set the up to whatever you want… Testing for collinearity is trivial, but I wanted to save the computation.



This brings another issue to the table: Which should be the left direction if up and direction are collinear? (IMHO) it should not matter, and we should be able to just pick one, and make the computation faster… Perturbing the direction a bit is exactly the same as arbitrarily setting that direction, except we have control over which orientation the camera will have.







Well, if up and direction are already collinear and normalized, then we can simply get a non-zero component of direction (x or y in this example), and then (y, -x, 0) is perpendicular to (x, y, z)… if x and y are zero, then (z, 0, -x) (or (0, z, -y) ) is perpendicular to (x, y, z)…



All and all, what I suggest would look like:


    public void lookAt( Vector3f pos, Vector3f worldUpVector ) {
        newDirection.set( pos ).subtractLocal( location ).normalizeLocal();

        // check to see if we haven't really updated camera -- no need to call
        // sets.
        if ( newDirection.equals( direction ) ) {
            return;
        }
        direction.set( newDirection );

        up.set( worldUpVector );
        left.set( up ).crossLocal( direction ).normalizeLocal();
        if( left.equals( Vector3f.ZERO ) )
            if( direction.x != 0 )
                left.set( direction.y, -direction.x, 0f );
            else
                left.set( 0f, direction.z, -direction.y );
        up.set( direction ).crossLocal( left ).normalizeLocal();
        onFrameChange();
    }



@Mr. Marbles, it is not too much to ask, it is just that if you want your camera in the same direction as your up is defined, then it is a special case.

The above code will always point to the direction in question and will be consistent within runs... It uses many if's though, and I would like to optimize it a little bit.

I like it.  Any particular reason you switch based on direction.x?

Wasn't there talks of removing the public member variables in vectors?  As such wouldn't it be a good idea to not use them directly in new code?

Gentleman Hal comes out of left field…  :P  Valid point, but I am hoping to refactor these fields with a programming tool when we get to that anyway, so as long as we are getting proper functionality at this point.

renanse said:

I like it.  Any particular reason you switch based on direction.x?


Not really, I only need ONE coordinate value to be different than zero so that the left vector (which includes a zero) does not hit (0,0,0).

Makes sense.  I'll wait to add the fix to cvs if you are planning to clean it up any more.

I'm not a Vector math expert, but I noticed in the proposed solution that only the left vector is being checked for Vector3f.ZERO, what about:


up.set( direction ).crossLocal( left ).normalizeLocal();



Is there any chance of this producing Vector3f.ZERO as well?

If we were trying to look at our own position in space.

Mr.Marbles said:

I'm not a Vector math expert, but I noticed in the proposed solution that only the left vector is being checked for Vector3f.ZERO, what about:

up.set( direction ).crossLocal( left ).normalizeLocal();



Is there any chance of this producing Vector3f.ZERO as well?


Well, I have included this case in the method... fortunately, this takes care of the last cross product which is no longer needed because up is already normalized.

The code is:

    public void lookAt( Vector3f pos, Vector3f worldUpVector ) {
        newDirection.set( pos ).subtractLocal( location ).normalizeLocal();

        // check to see if we haven't really updated camera -- no need to call
        // sets.
        if ( newDirection.equals( direction ) ) {
            return;
        }
        direction.set( newDirection );

        up.set( worldUpVector ).normalizeLocal();
        if( up.equals( Vector3f.ZERO ) )
            up.set( Vector3f.UNIT_Y );
        left.set( up ).crossLocal( direction ).normalizeLocal();
        if( left.equals( Vector3f.ZERO ) )
            if( direction.x != 0 )
                left.set( direction.y, -direction.x, 0f );
            else
                left.set( 0f, direction.z, -direction.y );
        up.set( direction ).crossLocal( left ).normalizeLocal();
        onFrameChange();
    }



Unfortunately there seems to be no elegant way to avoid the if's since we have to assume the parameters can be anything.

P.S. Sorry for the late response. I just got back from work.

[Edit] St00pid me underestimated the value of the last cross product... Now it works.