[Solved]rotation problem in TestMousePick.java only when rootNode’s Translation isn’t 0,0,0; jme3 72

I’ve been fiddling with this for hours but I’m failing, please help…,

if I add any non Vector3f.ZERO local translation to the root node, although I am able to properly position the arrow, I cannot properly rotate it so that it shows perpendicular to the surface of collision(ie. a normal), as it does when translation of rootNode is 0,0,0

The rootNode can have any scale/rotation and it works ok, but if it has different translation => I don’t know how to rotate such that it remains the same as if it has 0,0,0 translation aka arrow is a normal on the surface

I’ve marked the line with rootNode translation with “XXX:” , set any value for x,y,z to see the problem, set to 0,0,0 to see it works well

this screenshot is when 0,0,0 thus it’s working, the arrow is a normal on the surface:

http://i.imgur.com/sBbsH.png

=====

the screenshot is when 0,0,-29 aka not working, failed to make arrow perpendicular on surface

http://i.imgur.com/c1MJa.png

====

pre type="java"
/*

  • Copyright © 2009-2010 jMonkeyEngine

  • All rights reserved.



  • Redistribution and use in source and binary forms, with or without

  • modification, are permitted provided that the following conditions are

  • met:



    • Redistributions of source code must retain the above copyright

  • notice, this list of conditions and the following disclaimer.



    • Redistributions in binary form must reproduce the above copyright

  • notice, this list of conditions and the following disclaimer in the

  • documentation and/or other materials provided with the distribution.



    • Neither the name of ‘jMonkeyEngine’ nor the names of its contributors

  • may be used to endorse or promote products derived from this software

  • without specific prior written permission.



  • THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS

  • “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED

  • TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR

  • PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR

  • CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,

  • EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,

  • PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR

  • PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF

  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING

  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

  • SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


    /





    package org.jme3.tests;





    import java.util.prefs.BackingStoreException;





    import com.jme3.app.SimpleApplication;


    import com.jme3.collision.CollisionResult;


    import com.jme3.collision.CollisionResults;


    import com.jme3.light.DirectionalLight;


    import com.jme3.material.Material;


    import com.jme3.math.ColorRGBA;


    import com.jme3.math.FastMath;


    import com.jme3.math.Quaternion;


    import com.jme3.math.Ray;


    import com.jme3.math.Transform;


    import com.jme3.math.Vector3f;


    import com.jme3.scene.Geometry;


    import com.jme3.scene.Node;


    import com.jme3.scene.Spatial;


    import com.jme3.scene.debug.Arrow;


    import com.jme3.scene.shape.Box;


    import com.jme3.system.AppSettings;





    public class TestMousePick extends SimpleApplication {





    public static void main(String[] args) throws BackingStoreException {


    TestMousePick app = new TestMousePick();


    AppSettings aps = new AppSettings(true);


    aps.load(aps.getTitle());


    aps.setVSync(true);


    app.setShowSettings(false);


    app.setSettings(aps);


    app.start();


    }





    Node shootables;


    Geometry mark;





    @Override


    public void simpleInitApp() {


    // flyCam.setEnabled(false);


    flyCam.setDragToRotate(true);


    flyCam.setMoveSpeed(20f);


    initMark(); // a red sphere to mark the hit





    /
    * create four colored boxes and a floor to shoot at: */


    shootables = new Node(“Shootables”);


    rootNode.attachChild(shootables);


    shootables.attachChild(makeCube(“a Dragon”, -2f, 0f, 1f));


    shootables.attachChild(makeCube(“a tin can”, 1f, -2f, 0f));


    shootables.attachChild(makeCube(“the Sheriff”, 0f, 1f, -2f));


    shootables.attachChild(makeCube(“the Deputy”, 1f, 0f, -4f));


    shootables.attachChild(makeFloor());


    shootables.attachChild(makeCharacter());





    rootNode.scale(3.5f, 4.2f, 4.7f);


    rootNode.rotate(FastMath.DEG_TO_RAD * 65f, FastMath.DEG_TO_RAD * 65f,


    FastMath.DEG_TO_RAD * 65f);


    rootNode.setLocalTranslation(0, 0, 0


    // -29


    );// XXX:





    cam.setLocation(new Vector3f(1.4989337f, 4.4191055f, 73.00994f));


    cam.setRotation(new Quaternion(-0.0011200219f, 0.9985451f,


    -0.022919897f, -0.048795838f));


    cam.setDirection(new Vector3f(-0.09739835f, -0.045882408f, -0.99418724f));


    }





    @Override


    public void simpleUpdate(float tpf) {


    Vector3f origin = cam.getWorldCoordinates(


    inputManager.getCursorPosition(), 0.0f);


    Vector3f direction = cam.getWorldCoordinates(


    inputManager.getCursorPosition(), 0.3f);





    direction.subtractLocal(origin).normalizeLocal();


    // System.out.println( "origin: "


    // + origin


    // + " / dir: "


    // + direction );


    // Transform tr =


    // rootNode.getWorldTransform();


    // origin =


    // tr.transformVector(


    // origin,


    // null );


    // direction =


    // tr.transformVector(


    // direction,


    // null );


    // System.out.println( "origin : "


    // + origin );


    // System.out.println( "direction: "


    // + direction );





    Ray ray = new Ray(origin, direction);


    CollisionResults results = new CollisionResults();


    shootables.collideWith(ray, results);


    if (results.size() > 0) {


    CollisionResult closest = results.getClosestCollision();


    Vector3f contact = closest.getContactPoint();


    Vector3f normal = closest.getContactNormal();


    System.out.println(normal.angleBetween(contact)

    • FastMath.RAD_TO_DEG);


      Transform tr = rootNode.getWorldTransform();


      Vector3f upVec = contact.cross(normal);


      upVec = tr.transformInverseVector(upVec, null);


      contact = tr.transformInverseVector(contact, null);


      normal = tr.transformInverseVector(normal, null);





      mark.setLocalTranslation(contact);





      Quaternion q =


      // // tr.getRotation().clone().opposite();


      new Quaternion();





      // Vector3f.UNIT_Y;


      // tr.transformInverseVector(


      // Vector3f.UNIT_Y,


      // null );


      q.lookAt(normal, upVec);


      // q.multLocal( tr.getRotation().inverse() );


      mark.setLocalRotation(q);


      // mark.lookAt(


      // normal,


      // upVec );


      // q.subtractLocal( tr.getRotation().clone() );


      // q.multLocal( tr.getRotation().inverse() );


      // System.out.println( tr.getRotation(). );


      rootNode.attachChild(mark);


      } else {


      rootNode.detachChild(mark);


      }


      }





      /** A cube object for target practice /


      protected Geometry makeCube(String name, float x, float y, float z) {


      Box box = new Box(new Vector3f(x, y, z), 1, 1, 1);


      Geometry cube = new Geometry(name, box);


      Material mat1 = new Material(assetManager,


      “Common/MatDefs/Misc/SolidColor.j3md”);


      mat1.setColor(“Color”, ColorRGBA.randomColor());


      cube.setMaterial(mat1);


      return cube;


      }





      /
      * A floor to show that the “shot” can go through several objects. /


      protected Geometry makeFloor() {


      Box box = new Box(new Vector3f(0, -4, -5), 15, .2f, 15);


      Geometry floor = new Geometry(“the Floor”, box);


      Material mat1 = new Material(assetManager,


      “Common/MatDefs/Misc/SolidColor.j3md”);


      mat1.setColor(“Color”, ColorRGBA.Gray);


      floor.setMaterial(mat1);


      return floor;


      }





      /
      * A red ball that marks the last spot that was “hit” by the “shot”. */


      protected void initMark() {


      Arrow arrow = new Arrow(Vector3f.UNIT_Z.mult(2f));


      arrow.setLineWidth(3);





      // Sphere sphere = new Sphere(30, 30, 0.2f);


      mark = new Geometry(“BOOM!”, arrow);


      // mark = new Geometry(“BOOM!”, sphere);


      Material mark_mat = new Material(assetManager,


      “Common/MatDefs/Misc/SolidColor.j3md”);


      mark_mat.setColor(“Color”, ColorRGBA.Red);


      mark.setMaterial(mark_mat);


      }





      protected Spatial makeCharacter() {


      // load a character from jme3test-test-data


      Spatial golem = assetManager.loadModel(“Models/Oto/Oto.mesh.xml”);


      golem.scale(0.5f);


      golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);





      // We must add a light to make the model visible


      DirectionalLight sun = new DirectionalLight();


      sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());


      golem.addLight(sun);


      return golem;


      }


      }



      /pre

https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:math_for_dummies

Thanks! I’ve looked on that before, several times, I guess I didn’t learn anything? since I failed to do the above rotation :slight_smile:

Let me get more specific, in my first post example:

in simpleUpdate()

1. I’m getting the mouse position on the 2D screen via

pre type="java"
inputManager.getCursorPosition()
/pre

2. I’m getting the 3D world coordinates of that position, in 3D space as origin variable, where the zPos is right on the Camera.NEAR_PLANE

pre type="java"
Vector3f origin = cam.getWorldCoordinates(


inputManager.getCursorPosition(), 0.0f);
/pre

3. I’m getting the 3D world coordinates of that same 2D mouse position, in 3D space, where the zPos represents how deep(positive zPos, 0.3f) into the screen should the 3D world coord be (as x,y,z), I imagine an imaginary ray from 2D mouse position towards the 3D space where it is seen as a dot from the current point of view, and zPos represents how deep into the screen to go on that ray and get that 3D coord from it, where the 3D coord is a dot that’s always on the ray

pre type="java"
Vector3f direction = cam.getWorldCoordinates(


inputManager.getCursorPosition(), 0.3f);
/pre

4. I now need to get the direction between the two world coords, via substract() , normalize because it’s just a direction

pre type="java"
direction.subtractLocal(origin).normalizeLocal();
/pre

5. create a Ray from origin with that direction

pre type="java"
Ray ray = new Ray(origin, direction);
/pre

6. now we collide the ray with all those objects in the shootables node:

pre type="java"
CollisionResults results = new CollisionResults();


shootables.collideWith(ray, results);
/pre

7. if there are any collisions, we get the closest collision:

pre type="java"
if (results.size() > 0) {


CollisionResult closest = results.getClosestCollision();
/pre

8. here we get the position of the contact point(world coords) where the ray hit some geometry

pre type="java"
Vector3f contact = closest.getContactPoint();
/pre

9. and then we get the normal, where I understand that a normal is the perpendicular on a surface, in this case this normal is perpendicular on the collided geometry (or triangle which is part of the big geometry), but what we get here is a vector , whos x,y,z , as I see it, are somewhere on this normal, even though x,y,z represent world coords (just as contact)

pre type="java"
Vector3f normal = closest.getContactNormal();
/pre

The way I see it here, this is not the normal vector (?) , that is it doesn’t have that direction such that it’s perpendicular on the surface,

in order to get the normalVector I’d have to do

normalVector=normal.substract(contact).normalizeLocal();

where normalVector would be more like the direction from the contact point, which is perpendicular on the surface of the contact point

But I must be wrong, such that normal is already the normal vector, fine then. I stand corrected

10. I transform both contact and normal from world coords into rootNode’s local coords such that when they are a part of rootNode they will appear in the same place in the world as before this transformation occurred.

pre type="java"
contact = rootNode.worldToLocal(contact, null);


normal = rootNode.worldToLocal(normal, null);
/pre

11. I attach the mark Arrow to the rootNode and set the mark’s local position in rootNode to be at the contact point on the surface

pre type="java"
rootNode.attachChild(mark);


mark.setLocalTranslation(contact);
/pre

12. now I attempt to find the rotation of the Z axis such that Z axis points in the same direction as normal and using a Quaternion to store this relative rotation and the applying it to the mark

first, I make sure the up vector Vector3f.UNIT_Y is transformed from world coord to rootNode’s coords, since rootNode’s rotated/scaled/translated such that Vector3f.UNIT_Y it’s no longer pointing up from the rootNode’s point of view

pre type="java"
Vector3f upVec = rootNode.worldToLocal(Vector3f.UNIT_Y, null);
/pre

13. and now make the quaternion transform it’s Z axis(whichever this is, since it stores relative rotation depends on what it’s going to be applied to) such that it points in the same direction as normal vector variable, then we apply this rotation to mark which should rotate mark’s Z axis (where our Arrow is pointing at, that is, Arrow is the Z axis) such that it points in the same direction as normal

pre type="java"
Quaternion q =new Quaternion();


q.lookAt(normal, upVec);


mark.setLocalRotation(q);
/pre

All well and good, it works well if rootNode is rotated/scaled to any values, but its localTranslation must be 0,0,0

As soon as rootNode’s local translation is != 0,0,0 ie. 0,0,-29 the mark aka Arrow is no longer perpendicular on the contact surface

What am I missing really?

This is the code as it was described now (change the rootNode’s translation to 0,0,-29 to see not working):

So either:

  1. I’m not doing the rotation right for it doesn’t account rightfully rootNode’s translation, but it does it’s rotation/scale
  2. CollisionResult.getContactNormal() doesn’t return something right when rootNode is translated(I’m hoping this is the case lol?!), while CollisionResult.getContactPoint() does return right all the time
  3. something else



    pre type="java"
    /*

  • Copyright © 2009-2010 jMonkeyEngine

  • All rights reserved.



  • Redistribution and use in source and binary forms, with or without

  • modification, are permitted provided that the following conditions are

  • met:



    • Redistributions of source code must retain the above copyright

  • notice, this list of conditions and the following disclaimer.



    • Redistributions in binary form must reproduce the above copyright

  • notice, this list of conditions and the following disclaimer in the

  • documentation and/or other materials provided with the distribution.



    • Neither the name of ‘jMonkeyEngine’ nor the names of its contributors

  • may be used to endorse or promote products derived from this software

  • without specific prior written permission.



  • THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS

  • “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED

  • TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR

  • PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR

  • CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,

  • EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,

  • PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR

  • PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF

  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING

  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

  • SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


    /





    package org.jme3.tests;





    import java.util.prefs.BackingStoreException;





    import com.jme3.app.SimpleApplication;


    import com.jme3.collision.CollisionResult;


    import com.jme3.collision.CollisionResults;


    import com.jme3.light.DirectionalLight;


    import com.jme3.material.Material;


    import com.jme3.math.ColorRGBA;


    import com.jme3.math.FastMath;


    import com.jme3.math.Quaternion;


    import com.jme3.math.Ray;


    import com.jme3.math.Transform;


    import com.jme3.math.Vector3f;


    import com.jme3.scene.Geometry;


    import com.jme3.scene.Node;


    import com.jme3.scene.Spatial;


    import com.jme3.scene.debug.Arrow;


    import com.jme3.scene.shape.Box;


    import com.jme3.scene.shape.Line;


    import com.jme3.system.AppSettings;





    public class TestMousePick extends SimpleApplication {





    public static void main(String[] args) throws BackingStoreException {


    TestMousePick app = new TestMousePick();


    AppSettings aps = new AppSettings(true);


    aps.load(aps.getTitle());


    aps.setVSync(true);


    app.setShowSettings(false);


    app.setSettings(aps);


    app.start();


    }





    Node shootables;


    Geometry mark;





    @Override


    public void simpleInitApp() {


    // flyCam.setEnabled(false);


    flyCam.setDragToRotate(true);


    flyCam.setMoveSpeed(20f);


    initMark(); // a red sphere to mark the hit





    /
    * create four colored boxes and a floor to shoot at: /


    shootables = new Node(“Shootables”);


    rootNode.attachChild(shootables);


    shootables.attachChild(makeCube(“a Dragon”, -2f, 0f, 1f));


    shootables.attachChild(makeCube(“a tin can”, 1f, -2f, 0f));


    shootables.attachChild(makeCube(“the Sheriff”, 0f, 1f, -2f));


    shootables.attachChild(makeCube(“the Deputy”, 1f, 0f, -4f));


    shootables.attachChild(makeFloor());


    shootables.attachChild(makeCharacter());





    rootNode.scale(3.5f, 4.2f, 4.7f);


    rootNode.rotate(FastMath.DEG_TO_RAD * 65f, FastMath.DEG_TO_RAD * 65f,


    FastMath.DEG_TO_RAD * 65f);


    rootNode.setLocalTranslation(0, 0, 0 // - 29//


    );// XXX:





    cam.setLocation(new Vector3f(1.4989337f, 4.4191055f, 73.00994f));


    cam.setRotation(new Quaternion(-0.0011200219f, 0.9985451f,


    -0.022919897f, -0.048795838f));


    cam.setDirection(new Vector3f(-0.09739835f, -0.045882408f, -0.99418724f));


    }





    @Override


    public void simpleUpdate(float tpf) {


    Vector3f origin = cam.getWorldCoordinates(


    inputManager.getCursorPosition(), 0.0f);


    Vector3f direction = cam.getWorldCoordinates(


    inputManager.getCursorPosition(), 0.3f);





    System.out.println("origin: " + origin + " / dir: " + direction);


    direction.subtractLocal(origin).normalizeLocal();


    // direction = origin.subtract(direction).normalize();


    // Transform tr =


    // rootNode.getWorldTransform();


    // origin =


    // tr.transformVector(


    // origin,


    // null );


    // direction =


    // tr.transformVector(


    // direction,


    // null );


    // System.out.println( "origin : "


    // + origin );


    // System.out.println( "direction: "


    // + direction );





    Ray ray = new Ray(origin, direction);


    CollisionResults results = new CollisionResults();


    shootables.collideWith(ray, results);


    if (results.size() > 0) {


    CollisionResult closest = results.getClosestCollision();


    Vector3f contact = closest.getContactPoint();


    Vector3f normal = closest.getContactNormal();


    // System.out.println(normal.normalize().angleBetween(


    // contact.normalize())


    // * FastMath.RAD_TO_DEG);


    // Vector3f upVec = contact.cross(normal);


    // Vector3f normalVec = normal.subtract(contact).normalizeLocal();


    // // System.out.println(upVec.normalize().angleBetween(


    // // contact.normalize())


    // // * FastMath.RAD_TO_DEG);


    // // System.out.println(upVec.normalize().angleBetween(


    // // normal.normalize())


    // // * FastMath.RAD_TO_DEG);


    //


    contact = rootNode.worldToLocal(contact, null);


    normal = rootNode.worldToLocal(normal, null);


    // // Transform tr = rootNode.getWorldTransform();


    // // // Vector3f upVec =


    // // // contact.cross(normal)


    // // // rootNode.worldToLocal(Vector3f.UNIT_Y, null);


    // normalVec = rootNode.worldToLocal(normalVec, null);


    // normalVec.normalizeLocal();


    // // // upVec = tr.transformInverseVector(upVec, null);


    // // contact = tr.transformInverseVector(contact, null);


    // // normal = tr.transformInverseVector(normal, null);


    rootNode.attachChild(mark);


    mark.setLocalTranslation(contact);





    Vector3f upVec = rootNode.worldToLocal(Vector3f.UNIT_Y, null);


    Quaternion q =


    // // rootNode.getLocalRotation();


    // // // tr.getRotation().clone().opposite();


    new Quaternion();


    //


    // // Vector3f.UNIT_Y;


    // // tr.transformInverseVector(


    // // Vector3f.UNIT_Y,


    // // null );





    // makes Z axis be in the same direction as normal


    // (our mark arrow is on the Z axis)





    q.lookAt(normal, upVec.normalize());


    // q.multLocal(rootNode.getLocalRotation().opposite());


    mark.setLocalRotation(q);


    // mark.lookAt(normalVec, upVec);


    // mark.lookAt(


    // normal,


    // upVec );


    // q.subtractLocal( tr.getRotation().clone() );


    // q.multLocal( tr.getRotation().inverse() );


    // System.out.println( tr.getRotation(). );





    // Line line = new Line(contact, normalVec);


    // Geometry geoLine = new Geometry(“line1”, line);


    // // mark = new Geometry(“BOOM!”, sphere);


    // Material mark_mat = new Material(assetManager,


    // “Common/MatDefs/Misc/SolidColor.j3md”);


    // mark_mat.setColor(“Color”, ColorRGBA.Red);


    // geoLine.setMaterial(mark_mat);


    // geoLine.setLocalTranslation(contact);


    // // geoLine.setLocalTransform(tr);


    // // geoLine.worldToLocal(in, store)


    // rootNode.attachChild(geoLine);


    } else {


    rootNode.detachChild(mark);


    }


    }





    /
    * A cube object for target practice /


    protected Geometry makeCube(String name, float x, float y, float z) {


    Box box = new Box(new Vector3f(x, y, z), 1, 1, 1);


    Geometry cube = new Geometry(name, box);


    Material mat1 = new Material(assetManager,


    “Common/MatDefs/Misc/SolidColor.j3md”);


    mat1.setColor(“Color”, ColorRGBA.randomColor());


    cube.setMaterial(mat1);


    return cube;


    }





    /
    * A floor to show that the “shot” can go through several objects. /


    protected Geometry makeFloor() {


    Box box = new Box(new Vector3f(0, -4, -5), 15, .2f, 15);


    Geometry floor = new Geometry(“the Floor”, box);


    Material mat1 = new Material(assetManager,


    “Common/MatDefs/Misc/SolidColor.j3md”);


    mat1.setColor(“Color”, ColorRGBA.Gray);


    floor.setMaterial(mat1);


    return floor;


    }





    /
    * A red ball that marks the last spot that was “hit” by the “shot”. */


    protected void initMark() {


    Arrow arrow = new Arrow(Vector3f.UNIT_Z.mult(2f));


    arrow.setLineWidth(3);





    // Sphere sphere = new Sphere(30, 30, 0.2f);


    mark = new Geometry(“BOOM!”, arrow);


    // mark = new Geometry(“BOOM!”, sphere);


    Material mark_mat = new Material(assetManager,


    “Common/MatDefs/Misc/SolidColor.j3md”);


    mark_mat.setColor(“Color”, ColorRGBA.Red);


    mark.setMaterial(mark_mat);


    }





    protected Spatial makeCharacter() {


    // load a character from jme3test-test-data


    Spatial golem = assetManager.loadModel(“Models/Oto/Oto.mesh.xml”);


    golem.scale(0.5f);


    golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);





    // We must add a light to make the model visible


    DirectionalLight sun = new DirectionalLight();


    sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());


    golem.addLight(sun);


    return golem;


    }


    }



    /pre

I think I give up, been trying this even before I posted first post, clearly I’m missing something, unless a bug somewhere in jme3

Here are some files that I tried to apply these ideas to,

a new kind of camera with SimpleApplication2 where while in the mouse cursor mode (by pressing Enter once) I attempt to show a perpendicular Arrow on the objects the mouse hovers on and upon the LMB(left mouse button) release that object is going to be followed by the camera. Anyway this perpendicular aka normal will fail to be perpendicular only when rootNode have non zero translation (no matter what rotation/scale it has)

http://i.imgur.com/Po8N0.png

pre type="java"
package org.jme3.tests;





import com.jme3.font.BitmapText;


import com.jme3.input.KeyInput;


import com.jme3.input.controls.ActionListener;


import com.jme3.input.controls.AnalogListener;


import com.jme3.input.controls.KeyTrigger;


import com.jme3.material.Material;


import com.jme3.math.ColorRGBA;


import com.jme3.math.FastMath;


import com.jme3.math.Quaternion;


import com.jme3.math.Spline;


import com.jme3.math.Vector3f;


import com.jme3.scene.Geometry;


import com.jme3.scene.Mesh;


import com.jme3.scene.Node;


import com.jme3.scene.Spatial.CullHint;


import com.jme3.scene.debug.Arrow;


import com.jme3.scene.debug.Grid;


import com.jme3.scene.shape.Box;


import com.jme3.scene.shape.Curve;





/**

  • Sample 2 - How to use nodes as handles to manipulate objects in the scene

  • graph. You can rotate, translate, and scale objects by manipulating their

  • parent nodes. The Root Node is special: Only what is attached to the Root

  • Node appears in the scene.


    /


    public class HelloNode7 extends SimpleApplication2 {





    private static final float moveSpeed = 1f;


    private static final float rotSpeed = 2f;


    private static final float yawSpeed = 4f;


    private static final float rollSpeed = 7f;


    private static final float pitchSpeed = 11f;


    private Vector3f cornerPos;





    private Geometry geoCurve;





    private Geometry geoBox;





    private final Spline spline = new Spline();


    private Node coord;


    private Box box;


    private final Vector3f centerOfBox = new Vector3f();


    private final Quaternion qRotation = new Quaternion();


    float yaw = 0f;


    float roll = 0f;


    float pitch = 0f;


    private final static String mapIncDelay = “IncDelay”;


    private final static String mapDecDelay = “DecDelay”;


    private final static String mapPause = “mapPause”;


    private final static String mapHelp = “mapHelp”;





    long sleep = 0;


    private float fpsNow;


    private float maxSeenFps = 30;


    private static final long sleepIncrement = 10;


    private static final float lineWidth = 5f;


    private BitmapText helloText;


    private BitmapText helpText;


    private boolean pauseBox = false;


    private boolean help;





    // private final String mapRollRight =


    // “rollRight”;


    // private final String mapRollLeft =


    // “rollLeft”;





    public static void main(String[] args) {


    HelloNode7 app = new HelloNode7();


    app.setShowSettings(false);


    app.setVSync(true);


    app.start();


    }





    /


    • (non-Javadoc)



    • @see com.jme3.app.SimpleApplication#simpleUpdate(float)


      */


      @Override


      public void simpleUpdate(float tpf) {


      if (pauseBox) {


      return;


      }





      yaw = (yaw + FastMath.DEG_TO_RAD * rotSpeed * yawSpeed * tpf)


      % (FastMath.PI * 2);


      roll = (roll + FastMath.DEG_TO_RAD * rotSpeed * rollSpeed * tpf)


      % (FastMath.PI * 2);


      pitch = (pitch + FastMath.DEG_TO_RAD * rotSpeed * pitchSpeed * tpf)


      % (FastMath.PI * 2);


      qRotation.fromAngles(yaw, roll, pitch);


      // rotating the box to these absolute angles (from its origin pos)


      geoBox.setLocalRotation(qRotation);


      // move box “forward” too, that is, forward relative to itself ie.


      // spaceship moving forward


      geoBox.move(geoBox.getLocalRotation().getRotationColumn(2)


      .mult(moveSpeed * tpf));





      Vector3f clonedCornerPos = cornerPos.clone();


      // now applying the same transform (pos/rot/scale) to the corner as the


      // box has


      // clonedCornerPos =


      geoBox.getLocalTransform().transformVector(clonedCornerPos,// in,


      clonedCornerPos// store


      );


      // even if I comment the following “coord” related updates still low fps


      geoBox.getLocalTransform().transformVector(box.getCenter().clone(),


      centerOfBox);


      // same orientation as Box


      coord.setLocalTransform(geoBox.getLocalTransform());


      // move coord system at center of Box


      coord.setLocalTranslation(centerOfBox);


      // til here





      // we have the corner’s exact pos now, relative to the Node the geoBox &


      // geoCurve are both in


      // we add that pos to the curve


      spline.addControlPoint(clonedCornerPos);// must be cloned!


      // we must create new Curve object because we can’t add/update the


      // spline in existing one (?!)


      fpsNow = timer.getFrameRate();


      if (fpsNow > maxSeenFps) {


      maxSeenFps = fpsNow;


      }


      int subSegments = (int) Math.floor(maxSeenFps / fpsNow);


      geoCurve.setMesh(new Curve(spline, subSegments));


      helloText.setText("MaxSeenFps: " + (int) Math.ceil(maxSeenFps)

  • " / subSegs: " + subSegments);


    try {


    Thread.sleep(sleep);


    } catch (InterruptedException e) {


    e.printStackTrace();


    }


    }





    private void initGUI() {


    // guiFont =


    // assetManager.loadFont( “Interface/Fonts/Default.fnt” );


    helloText = new BitmapText(guiFont, false);


    helloText.setSize(guiFont.getCharSet().getRenderedSize());


    helloText.setLocalTranslation(300, helloText.getLineHeight(), 0);


    guiNode.attachChild(helloText);





    helpText = new BitmapText(guiFont, false);


    helpText.setSize(guiFont.getCharSet().getRenderedSize());


    helpText.setLocalTranslation(0, 22 * helpText.getLineHeight(), 0);


    help = false;


    updateHelp();


    guiNode.attachChild(helpText);


    }





    private void initKeys() {


    inputManager


    .addMapping(mapIncDelay, new KeyTrigger(KeyInput.KEY_LMENU));// left


    // alt


    inputManager.addMapping(mapDecDelay, new KeyTrigger(


    KeyInput.KEY_LCONTROL));


    inputManager.addMapping(mapPause, new KeyTrigger(KeyInput.KEY_RSHIFT));


    inputManager.addMapping(mapHelp, new KeyTrigger(KeyInput.KEY_H));


    // inputManager.deleteMapping( mapIncDelay );


    // inputManager.addMapping(


    // mapIncDelay,


    // new KeyTrigger(


    // KeyInput.KEY_SPACE ) );





    // inputManager.addMapping(


    // mapRollRight,


    // new KeyTrigger(


    // KeyInput.KEY_E ) );


    // inputManager.addMapping(


    // mapRollLeft,


    // new KeyTrigger(


    // KeyInput.KEY_Q ) );





    inputManager.addListener(analogListener, mapIncDelay, mapDecDelay


    // ,mapRollRight,


    // mapRollLeft


    );


    inputManager.addListener(actionListener, mapPause, mapHelp);


    // inputManager.deleteMapping( “FLYCAM_Rise” );


    }





    private final ActionListener actionListener = new ActionListener() {





    @Override


    public void onAction(String name, boolean isPressed, float tpf) {


    do {


    if ((name == mapPause) && (isPressed)) {


    pauseBox = !pauseBox;


    break;


    }





    if ((name == mapHelp) && (isPressed)) {


    help = !help;


    updateHelp();


    }


    } while (false);


    }


    };





    private void updateHelp() {


    if (help) {


    helpText.setText(“H to toggle this helpn”

    • “RShift to pause boxn”

    • “Space to toggle follow boxn”

    • “Enter to toggle mouse cursor, then hold RMB to aim, or LMB to select obj2follown”

    • “Tab move camera up (all moves relative to itself)n”

    • “LShift/CapsLock move cam downn”

    • “Q/E roll camera left/rightn”

    • “W/S move camera forwards/backwardsn”

    • “A/D strafe camera left/rightn”

    • “LAlt to increase sleep in updaten”

    • “LControl to decrease sleep in updaten”

    • “while following spatial(ie. key Space):n”

    • “tmove mouse on / diagonal to roll camera left ie. Qn”

    • “tmove mouse on \ diagonal to roll camera right ie. En”);


      // guiNode.attachChild( helpText );


      } else {


      helpText.setText(“Press H for help/keys”);


      // guiNode.detachChild( helpText );


      }


      }





      // protected


      // void


      // rotateCamera(


      // float value,


      // Vector3f axis )


      // {


      //


      // Matrix3f mat =


      // new Matrix3f();


      // mat.fromAngleNormalAxis(


      // // flyCam.rotationSpeed


      // 1f * value,


      // axis );


      //


      // Vector3f up =


      // cam.getUp();


      // Vector3f left =


      // cam.getLeft();


      // Vector3f dir =


      // cam.getDirection();


      //


      // mat.mult(


      // up,


      // up );


      // mat.mult(


      // left,


      // left );


      // mat.mult(


      // dir,


      // dir );


      //


      // Quaternion q =


      // new Quaternion();


      // q.fromAxes(


      // left,


      // up,


      // dir );


      // q.normalize();


      //


      // cam.setAxes( q );


      // }





      private final AnalogListener analogListener = new AnalogListener() {





      @Override


      public void onAnalog(String name, float value, float tpf) {


      // if ( mapRollRight == name )


      // {


      // System.out.println( mapRollRight );


      // rotateCamera(


      // value,


      // cam.getDirection() );


      // return;


      // }


      // if ( mapRollLeft == name )


      // {


      // System.out.println( mapRollLeft );


      // rotateCamera(


      // -value,


      // cam.getDirection() );


      // return;


      // }


      if (mapIncDelay == name) {


      sleep += sleepIncrement;


      // inputManager.deleteMapping( mapIncDelay );


      // inputManager.addMapping(


      // mapIncDelay,


      // new KeyTrigger(


      // KeyInput.KEY_SPACE ) );


      }


      if (mapDecDelay == name) {


      if (sleep > 0) {


      sleep -= sleepIncrement;


      }


      if (sleep < 0) {


      sleep = 0;


      }


      }


      System.out.println(“Sleep now at:” + sleep);


      }


      };





      @Override


      public void simple2InitApp() {


      getEvilFlyCam().setMoveSpeed(20f);


      // flyCam.


      // cam.setLocation( new Vector3f(


      // -7.3405366f,


      // 23.450567f,


      // -20.834333f ) );


      // cam.setRotation( new Quaternion(


      // 0.46806002f,


      // 0.095443755f,


      // -0.050935324f,


      // 0.8770495f ) );


      Node subNode = new Node();


      subNode.setLocalTranslation(new Vector3f(2, 0.5f, 1));


      subNode.rotate(1f, -2f, 4f);


      // a box with non 0,0,0 center which means a position of x,y,z relative


      // to it’s parent geoBox(below)


      box = new Box(


      // Vector3f.ZERO,


      new Vector3f(


      // this center is causing some relaxed cam-following, see diff


      // when 0,0,0


      2, 3, 4), 0.5f, 0.9f, 1.3f);


      geoBox = new Geometry(“Box”, box);


      geoBox.setLocalTranslation(1f, 2f, 3f);


      geoBox.scale(1.3f, 1.2f, 1.1f);// always ok


      geoBox.rotate(2f, -4f, 0.4f);


      Material mat2 = new Material(assetManager,


      “Common/MatDefs/Misc/WireColor.j3md”);


      mat2.setColor(“Color”, ColorRGBA.Red);


      geoBox.setMaterial(mat2);





      Material curveMat = new Material(assetManager,


      “Common/MatDefs/Misc/WireColor.j3md”);


      curveMat.setColor(“Color”, ColorRGBA.Green);


      geoCurve = new Geometry(“trails”);


      geoCurve.setMaterial(curveMat);


      subNode.attachChild(geoCurve);


      subNode.attachChild(geoBox);





      // rootNode.setLocalTranslation(


      // -1,


      // -2,


      // -4 );//XXX: bug when uncommented or I don’t know how to make a normal


      // on the collided surface


      rootNode.rotate(-1f, 2f, -4f);





      rootNode.attachChild(subNode);


      rootNode.scale(1.5f);// this is always ok


      subNode.scale(0.4f);// this is always ok too


      // subNode.scale(1.3f, 0.7f, 0.4f);// XXX: bug when uncommented,


      // Curve/Spline fail, most likely this&above bugs are connected


      // rootNode.scale(


      // 0.4f,


      // 1.4f,


      // 1.1f );// XXX: or/and this





      // doing this here only once


      cornerPos = new Vector3f();


      // calculating position of corner, first getting corner as it were if


      // box were at 0,0,0 and no rotation/scale


      cornerPos.set(





      box.getXExtent(), box.getYExtent(), box.getZExtent());


      // now considering box may have a different than 0,0,0 center


      cornerPos.addLocal(box.getCenter());


      coord = new Node();


      coord.setCullHint(CullHint.Never);


      attachCoordinateAxes(Vector3f.ZERO, coord);


      subNode.attachChild(coord);


      // coord.setLocalTranslation( geoBox.getLocalTranslation() );//


      // box.getCenter() );


      initKeys();


      initGUI();


      attachGrid(Vector3f.ZERO, 100, ColorRGBA.Yellow);


      getEvilFlyCam().setPickables(subNode);


      getEvilFlyCam().fixAt(coord);


      }





      private void attachCoordinateAxes(Vector3f pos, Node toNode) {


      Arrow arrow = new Arrow(Vector3f.UNIT_X);


      // make arrow thicker,


      arrow.setLineWidth(lineWidth);


      putShape(arrow, ColorRGBA.Cyan, toNode).setLocalTranslation(pos);





      arrow = new Arrow(Vector3f.UNIT_Y);


      arrow.setLineWidth(lineWidth); // make arrow thicker


      putShape(arrow, ColorRGBA.Green, toNode).setLocalTranslation(pos);





      arrow = new Arrow(Vector3f.UNIT_Z);


      arrow.setLineWidth(lineWidth); // make arrow thicker


      putShape(arrow, ColorRGBA.Blue, toNode).setLocalTranslation(pos);


      }





      private Geometry putShape(Mesh shape, ColorRGBA color, Node onNode) {


      Geometry g = new Geometry(“coordinate axis”, shape);


      Material mat = new Material(assetManager,


      “Common/MatDefs/Misc/Unshaded.j3md”);


      mat.getAdditionalRenderState().setWireframe(true);


      mat.setColor(“Color”, color);


      g.setMaterial(mat);


      onNode.attachChild(g);


      g.setCullHint(CullHint.Inherit);


      return g;


      }





      private Geometry attachGrid(Vector3f pos, int size, ColorRGBA color) {


      Geometry g = new Geometry(“wireframe grid”, new Grid(size, size, 10f));


      Material mat = new Material(assetManager,


      “Common/MatDefs/Misc/Unshaded.j3md”);


      mat.getAdditionalRenderState().setWireframe(true);


      mat.setColor(“Color”, color);


      g.setMaterial(mat);


      g.center().move(pos);


      rootNode.attachChild(g);


      return g;


      }


      }



      /pre

      ============





      pre type="java"
      package org.jme3.tests;





      import com.jme3.asset.AssetManager;


      import com.jme3.collision.CollisionResult;


      import com.jme3.collision.CollisionResults;


      import com.jme3.export.JmeExporter;


      import com.jme3.export.JmeImporter;


      import com.jme3.input.FlyByCamera;


      import com.jme3.input.InputManager;


      import com.jme3.input.JoyInput;


      import com.jme3.input.Joystick;


      import com.jme3.input.KeyInput;


      import com.jme3.input.MouseInput;


      import com.jme3.input.controls.ActionListener;


      import com.jme3.input.controls.AnalogListener;


      import com.jme3.input.controls.KeyTrigger;


      import com.jme3.input.controls.MouseAxisTrigger;


      import com.jme3.input.controls.MouseButtonTrigger;


      import com.jme3.material.Material;


      import com.jme3.math.ColorRGBA;


      import com.jme3.math.FastMath;


      import com.jme3.math.Matrix3f;


      import com.jme3.math.Quaternion;


      import com.jme3.math.Ray;


      import com.jme3.math.Transform;


      import com.jme3.math.Vector2f;


      import com.jme3.math.Vector3f;


      import com.jme3.renderer.Camera;


      import com.jme3.renderer.RenderManager;


      import com.jme3.renderer.ViewPort;


      import com.jme3.scene.Geometry;


      import com.jme3.scene.Node;


      import com.jme3.scene.Spatial;


      import com.jme3.scene.control.Control;


      import com.jme3.scene.debug.Arrow;





      /


       


       
      /


      public class EvilFlyByCamera {





          private static final String prefix = “EvilFlyCam_”;


          /



        


       
      /


          private static final String mapLower = prefix + “Lower”;


          /


        


       
      /


          private static final String mapRise = prefix + “Rise”;


          /



        


       
      /


          private static final String mapDTRKey = prefix + “RotateDrag/DTRKey”;


          /


        


       
      /


          private static final String mapZoomOut = prefix + “ZoomOut”;


          /



        


       
      /


          private static final String mapZoomIn = prefix + “ZoomIn”;


          /


        


       
      /


          private static final String mapBackward = prefix + “Backward”;


          /



        


       
      /


          private static final String mapForward = prefix + “Forward”;


          /


        


       
      /


          private static final String mapStrafeRight = prefix + “StrafeRight”;


          /



        


       
      /


          private static final String mapStrafeLeft = prefix + “StrafeLeft”;


          /


        


       
      /


          private static final String mapDown = prefix + “Down”;


          private static final String mapMouseDown = prefix + “MouseDown”;


          /



        


       
      /


          private static final String mapUp = prefix + “Up”;


          private static final String mapMouseUp = prefix + “MouseUp”;


          /


        


       
      /


          private static final String mapRight = prefix + “Right”;


          private static final String mapMouseRight = prefix + “MouseRight”;


          /



        


       
      /


          private static final String mapLeft = prefix + “Left”;


          private static final String mapMouseLeft = prefix + “MouseLeft”;


          private static final String mapRollRight = prefix + “rollRight”;


          private static final String mapRollLeft = prefix + “rollLeft”;





          private static final String mapToggleDragToRotate = prefix


          + “ToggleDragToRotate/toggleDTRKey”;


          private static final String mapToggleFollowSpatial = prefix


          + “mapToggleFollowSpatial”;


          private static final String mapSelectSpatial = prefix + “mapSelectSpatial”;





          private static final float zoomBump = 10f;





          private boolean lastMRIState = false;


          private boolean prevDTRKeyState = false;


          private boolean alreadyHoldingDTRKey = false;


          private boolean enableFollowSpatial = true; // follow by


      // default


          private Node pickables = null;


          private final Node rootNode;


          private Spatial tempFixAt = null;





          private boolean allowSelection = false;


          private final ActionListener actionListener = new ActionListener() {





      @SuppressWarnings(“synthetic-access”)


      @Override


      public void onAction(String name, boolean isPressed, float tpf) {


          Q.assumedTrue(enabled);





          do {





      if (name == mapSelectSpatial)


      // && ( !isPressed ) )


      {


          allowSelection = isPressed;


          // System.out.println( "onAction: "


          // + tpf );


          if (!allowSelection) {


      if (null != tempFixAt) {


          rootNode.detachChild(mark);


          fixAt(tempFixAt);


          tempFixAt = null;


          System.out


          .println(“fixAt(” + fixAt.getName() + “)”);


      } else {


          System.out.println(“nothing to fixAt”);


      }


          }


          break;


      }





      if ((name == mapToggleFollowSpatial) && (isPressed)) {


          enableFollowSpatial = !enableFollowSpatial;


          break;


      }





      if (name == mapDTRKey)// it’s mouse actually


      {


          System.out.println("mapDTRKey " + isPressed);


          alreadyHoldingDTRKey = isPressed;


          // Q.assumedTrue( isDTRModeNow );


          flipDragToRotate(false);


          // when dragging we hide cursor and can


          // rotate


          // when we stop dragging ie. mouse button


          // released, then we cannot rotate


          // anymore and also we show mouse cursor


          // inputManager.setCursorVisible( !isPressed );


          // setMouseRotationInputs( isPressed );


          break;


      }





      if ((mapToggleDragToRotate == name) && (isPressed)


      && (!alreadyHoldingDTRKey)) {


          System.out.println(“mapToggleDragToRotate”);


          // setDragToRotate( !dragToRotate );// 1st


          flipDragToRotate(true);


          // setMouseRotationInputs( !dragToRotate


          // );//


          // 2nd


          break;


      }


          } while (false);


      }// method


          };





          private Geometry mark;





          private void initMark() {


      Arrow arrow = new Arrow(Vector3f.UNIT_Z.mult(2f));


      arrow.setLineWidth(3);





      // Sphere sphere = new Sphere(30, 30, 0.2f);


      mark = new Geometry(“BOOM!”, arrow);


      // mark = new Geometry(“BOOM!”, sphere);


      Material mark_mat = new Material(assetManager,


      “Common/MatDefs/Misc/SolidColor.j3md”);


      mark_mat.setColor(“Color”, ColorRGBA.Red);


      mark.setMaterial(mark_mat);


          }





          private final AnalogListener analogListener = new AnalogListener() {





      @SuppressWarnings(“synthetic-access”)


      @Override


      public void onAnalog(String name, float value, float tpf) {


          Q.assumedTrue(enabled);





          do {





      if ((name == mapSelectSpatial) && (allowSelection)) {


          // System.out.println( “onAnalog: "


          // + tpf );


          Vector2f screenPos = inputManager.getCursorPosition();


          Vector3f origin = cam.getWorldCoordinates(screenPos, 0.0f);


          Vector3f direction = cam.getWorldCoordinates(screenPos,


          0.3f);


          direction.subtractLocal(origin).normalizeLocal();


          Ray ray = new Ray(origin, direction);


          // System.out.println( screenPos


          // + " / "


          // + cam.getWorldCoordinates(


          // screenPos,


          // 0f ) );


          CollisionResults results = new CollisionResults();


          pickables.collideWith(ray, results);


          if (results.size() > 0) {


      CollisionResult closest = results.getClosestCollision();


      // if (null != tempFixAt) {


      // tempFixAt.removeControl( keepMark );


      // }


      tempFixAt = closest.getGeometry();


      // tempFixAt.setLocalTransform( t. );


      // tempFixAt.addControl( keepMark );


      // System.out.println( “hovering: "


      // + tempFixAt.getName() );


      Vector3f contact = closest.getContactPoint();


      Vector3f normal = closest.getContactNormal()


      .normalize();


      Transform tr = rootNode.getWorldTransform();


      contact = tr.transformInverseVector(contact, null);


      normal = tr.transformInverseVector(normal, null);


      Vector3f upVec = contact.cross(normal);


      // Vector3f.UNIT_Y;


      upVec = tr.transformInverseVector(


      // Vector3f.UNIT_Y,


      upVec, null);


      mark.setLocalTranslation(contact);


      Quaternion q = new Quaternion();


      q.lookAt(normal, upVec);


      // q.subtractLocal( tr.getRotation().clone() );


      // q.inverseLocal();


      // tr.


      mark.setLocalRotation(q);


      // mark.setLocalTransform( tr );





      rootNode.attachChild(mark);


          }


          // System.out.println(cam.getWorldCoordinates(


          // screenPos, zPos ));


          break;


      }





      if (mapLeft == name) {


          rotateCamera(value, cam.getUp());


          break;


      }


      if (mapRight == name) {


          rotateCamera(-value, cam.getUp());


          break;


      }


      if (mapUp == name) {


          rotateCamera(-value, cam.getLeft());


          break;


      }


      if (mapDown == name) {


          rotateCamera(value, cam.getLeft());


          break;


      }


      if (mapForward == name) {


          moveCamera(value, false);


          break;


      }


      if (mapBackward == name) {


          moveCamera(-value, false);


          break;


      }


      if (mapStrafeLeft == name) {


          moveCamera(value, true);


          break;


      }


      if (mapStrafeRight == name) {


          moveCamera(-value, true);


          break;


      }


      if (mapRise == name) {


          riseCamera(value);


          break;


      }


      if (mapLower == name) {


          riseCamera(-value);


          break;


      }


      if (mapZoomIn == name) {


          zoomCamera(zoomBump  value);


          break;


      }


      if (mapZoomOut == name) {


          zoomCamera(zoomBump 
       -value);


          break;


      }


      if (name == mapRollRight) {


          rotateCamera(value, cam.getDirection());


          break;


      }


      if (name == mapRollLeft) {


          rotateCamera(-value, cam.getDirection());


          break;


      }


      if (mapMouseRight == name) {


          rotateCamera(-value, cam.getUp());


          break;


      }


      if (mapMouseLeft == name) {


          rotateCamera(value, cam.getUp());


          break;


      }


      if (mapMouseUp == name) {


          rotateCamera(-value, cam.getLeft());


          break;


      }


      if (mapMouseDown == name) {


          rotateCamera(value, cam.getLeft());


          break;


      }


          } while (false);// auto break if none of the above


          // if ( ( null != fixAt )


          // && ( enableFollowSpatial ) )


          // {


          // Q.assumedNotNull( fixAt );


          // lookAt( fixAt );


          // }


          // System.out.println( inputManager.isCursorVisible()


          // + " / "


          // + name );


      }// method


          }; // AnalogListener





          private final Camera cam;





          private float rotationSpeed = 1f;


          private float moveSpeed = 3f;


          // private MotionAllowedListener motionAllowed ://has useless method


          // null;


          private boolean enabled = false;


          private boolean isDTRModeNow = false;


          private boolean prevRegInputsState = false;





          private InputManager inputManager = null;


          private Spatial fixAt = null;


          private final Control control = new Control() {





      private boolean enabledControl = true;





      private Spatial followedOne = null;





      @Override


      public void write(JmeExporter ex) {


      }





      @Override


      public void read(JmeImporter im) {


      }





      @Override


      public Control cloneForSpatial(Spatial spatial) {


          Q.badCall(””);


          return null;


      }





      @Override


      public void setSpatial(Spatial spatial) {


          // Q.assumedNotNull( spatial );


          followedOne = spatial;


      }





      @Override


      public void setEnabled(boolean enabled1) {


          enabledControl = enabled1;


      }





      @Override


      public boolean isEnabled() {


          return enabledControl;


      }





      @SuppressWarnings(“synthetic-access”)


      @Override


      public void update(float tpf) {


          if ((!enabledControl) || (!enableFollowSpatial)) {


      return;


          }


          Q.assumedNotNull(followedOne);


          lookAt(followedOne);


      }





      @Override


      public void render(RenderManager rm, ViewPort vp) {


          // nothing here


      }


          };





          private final static String[] allAnalog = { mapLeft, mapRight, mapUp,


          mapDown,





          mapStrafeLeft, mapStrafeRight, mapForward, mapBackward,





          mapZoomIn, mapZoomOut,





          mapRise, mapLower,





          mapRollLeft, mapRollRight, mapSelectSpatial





          };





          private final static String[] allAction = { mapToggleDragToRotate,


          mapToggleFollowSpatial, mapSelectSpatial };


          private final static String[] mappingsMRI = { mapMouseLeft, mapMouseRight,


          mapMouseUp, mapMouseDown };





          private final AssetManager assetManager;





          /


            @param cam1


           
      /


          public EvilFlyByCamera(Camera cam1, InputManager inputManager_,


          boolean enableNow, Node rootNode1, AssetManager asm) {


      cam = cam1;


      inputManager = inputManager_;


      assetManager = asm;


      Q.assumedNotNull(asm);


      Q.assumedNotNull(cam);


      Q.assumedNotNull(inputManager);





      Q.assumedFalse(lastMRIState);


      Q.assumedFalse(prevDTRKeyState);


      Q.assumedFalse(prevRegInputsState);


      Q.assumedFalse(alreadyHoldingDTRKey);


      Q.assumedFalse(enabled);


      setEnabled(enableNow);


      Q.assumedTrue(enableNow == enabled);


      internalApplyDTRMode(isDTRModeNow);// isDTRModeNow can be any value here


      Q.assumedNotNull(rootNode1);


      rootNode = rootNode1;


      setPickables(rootNode1);


      Q.assumedFalse(allowSelection);


      initMark();


          }





          public void lookAt(Spatial followedOne) {


      Q.assumedNotNull(followedOne);


      cam.lookAt(followedOne.getWorldTranslation(), cam.getUp());


          }





          /



            @param parent


           
                  the Node that has all the children which we’re allowed to pick


                       when mouse cursor is visible, after picking with LMB that


           
                  object will be the new fixAt() aka followed


           /


          public void setPickables(Node parent) {


      pickables = parent;


      Q.assumedNotNull(pickables);


          }





          public void fixAt(Spatial spatial) {


      Q.assumedNotNull(spatial);


      if (null != fixAt) {


          // remove prev one


          fixAt.removeControl(control);


      }


      // else


      // {


      // // was null, probably first time, so we make sure it’s follow by


      // default


      // enableFollowSpatial =


      // true;


      // }


      // add new one


      fixAt = spatial;


      fixAt.addControl(control);


          }





          public void setFollowSpatial(boolean followNow) {


      enableFollowSpatial = followNow;


          }





          /**


           
       allows only alternate calls, ie. 1. false 2. true 3. false 4. true will


            throw if two consecutive calls attempt to set same value ie. 1. true 2.


           
       true


            


           
       @param enable


                       if true, prev state must’ve been false, else if false prev.


           
                  state must’ve been true


           /


          private void setMouseRotationInputs(boolean enable) {


      if (enable) {


          Q.assumedFalse(lastMRIState);


          inputManager.addMapping(mapMouseLeft, new MouseAxisTrigger(


          MouseInput.AXIS_X, true));


          inputManager.addMapping(mapMouseRight, new MouseAxisTrigger(


          MouseInput.AXIS_X, false));


          inputManager.addMapping(mapMouseUp, new MouseAxisTrigger(


          MouseInput.AXIS_Y, false));





          inputManager.addMapping(mapMouseDown, new MouseAxisTrigger(


          MouseInput.AXIS_Y, true));





          inputManager.addListener(analogListener, mappingsMRI);


      } else {


          Q.assumedTrue(lastMRIState);





          for (int i = 0; i < mappingsMRI.length; i++) {


      inputManager.deleteMapping(mappingsMRI);


      // listener auto removed


          }


      }


      lastMRIState = enable;


          }





          private void setDTRKey(boolean state) {


      if (state) {


          Q.assumedFalse(prevDTRKeyState);


          inputManager.addMapping(mapDTRKey, new MouseButtonTrigger(


          MouseInput.BUTTON_RIGHT));


          inputManager.addListener(actionListener, mapDTRKey);


      } else {


          Q.assumedTrue(prevDTRKeyState);


          inputManager.deleteMapping(mapDTRKey);// and listener


      }


      prevDTRKeyState = state;


          }





          // public


          // void


          // setMotionAllowedListener(


          // MotionAllowedListener listener )


          // {


          // motionAllowed =


          // listener;


          // }





          /**


           
       Sets the move speed. The speed is given in world units per second.


            


           
       @param moveSpeed1


           /


          public void setMoveSpeed(float moveSpeed1) {


      moveSpeed = moveSpeed1;


          }





          /**


           
       Sets the rotation speed.


            


           
       @param rotationSpeed1


           /


          public void setRotationSpeed(float rotationSpeed1) {


      rotationSpeed = rotationSpeed1;


          }





          /**


           
       @param enable


                       If false, the camera will ignore input.


           
      /


          public void setEnabled(boolean enable) {


      if (enabled ˆ enable) {


          // state change


          enabled = enable;


          registerInputs(enabled);// true when called the first time


      }





      // if ( enabled


      // && !enable )


      // {


      // if ( !allowedDTRmode


      // || ( allowedDTRmode && canRotate ) )


      // {


      // inputManager.setCursorVisible( true );


      // }


      // }


      // enabled =


      // enable;


          }





          /


            @param regNow


           
                  true means register, false means de-register<br>


                       must always alternate else throws ie. false, true, false, true


           
                  instead of true, true or false,false


           /


          private void registerInputs(boolean regNow) {


      Q.assumedNotNull(inputManager);


      if (regNow) {


          Q.assumedFalse(prevRegInputsState);


          // inputManager.setCursorVisible( isDTRModeNow );


          // setMouseRotationInputs( !isDTRModeNow );





          // both mouse and button - rotation of cam


          inputManager.addMapping(mapToggleDragToRotate, new KeyTrigger(


          KeyInput.KEY_NUMPADENTER), new KeyTrigger(


          KeyInput.KEY_RETURN));





          inputManager.addMapping(mapLeft, new KeyTrigger(KeyInput.KEY_LEFT));





          inputManager.addMapping(mapRight,


          new KeyTrigger(KeyInput.KEY_RIGHT));





          inputManager.addMapping(mapUp, new KeyTrigger(KeyInput.KEY_UP));





          inputManager.addMapping(mapDown, new KeyTrigger(KeyInput.KEY_DOWN));





          // mouse only - zoom in/out with wheel, and rotate drag


          inputManager.addMapping(mapZoomIn, new MouseAxisTrigger(


          MouseInput.AXIS_WHEEL, false));


          inputManager.addMapping(mapZoomOut, new MouseAxisTrigger(


          MouseInput.AXIS_WHEEL, true));





          // keyboard only WASD for movement and WZ for rise/lower height


          inputManager.addMapping(mapStrafeLeft, new KeyTrigger(


          KeyInput.KEY_A));


          inputManager.addMapping(mapStrafeRight, new KeyTrigger(


          KeyInput.KEY_D));


          inputManager.addMapping(mapForward, new KeyTrigger(KeyInput.KEY_W));


          inputManager


          .addMapping(mapBackward, new KeyTrigger(KeyInput.KEY_S));


          inputManager.addMapping(mapRise, new KeyTrigger(KeyInput.KEY_TAB));


          inputManager.addMapping(mapLower, new KeyTrigger(


          KeyInput.KEY_CAPITAL), new KeyTrigger(KeyInput.KEY_LSHIFT),


          new KeyTrigger(KeyInput.KEY_Z));





          inputManager.addMapping(mapRollRight,


          new KeyTrigger(KeyInput.KEY_E));


          inputManager


          .addMapping(mapRollLeft, new KeyTrigger(KeyInput.KEY_Q));





          inputManager.addMapping(mapToggleFollowSpatial, new KeyTrigger(


          KeyInput.KEY_SPACE));





          inputManager


          .addMapping(mapSelectSpatial, new MouseButtonTrigger(0));





          inputManager.addListener(analogListener, allAnalog);





          inputManager.addListener(actionListener, allAction);





          enableJoy1();// this will always remain enabled hmm


      } else {


          Q.assumedTrue(prevRegInputsState);


          // unreg


          for (int i = 0; i < allAnalog.length; i++) {


      inputManager.deleteMapping(allAnalog);


          }


          for (int i = 0; i < allAction.length; i++) {


      inputManager.deleteMapping(allAction);


          }


      }


      prevRegInputsState = regNow;


          }





          private void enableJoy1() {


      Joystick[] joysticks = inputManager.getJoysticks();


      if ((joysticks != null) && (joysticks.length > 0)) {


          Joystick joystick = joysticks[0];// only first one


          joystick.assignAxis(mapStrafeRight, mapStrafeLeft,


          JoyInput.AXIS_POV_X);


          joystick.assignAxis(mapForward, mapBackward, JoyInput.AXIS_POV_Y);


          joystick.assignAxis(mapRight, mapLeft, joystick.getXAxisIndex());


          joystick.assignAxis(mapDown, mapUp, joystick.getYAxisIndex());


      }


          }





          protected void rotateCamera(float value, Vector3f axis) {


      // if ( dragToRotate )


      // {


      // if ( !canRotate )


      // {


      // return;


      // }


      // }





      Matrix3f mat = new Matrix3f();


      mat.fromAngleNormalAxis(rotationSpeed 
       value, axis);





      Vector3f up = cam.getUp();


      Vector3f left = cam.getLeft();


      Vector3f dir = cam.getDirection();





      mat.mult(up, up);


      mat.mult(left, left);


      mat.mult(dir, dir);





      Quaternion q = new Quaternion();


      q.fromAxes(left, up, dir);


      q.normalize();





      cam.setAxes(q);


          }





          protected void zoomCamera(float value) {


      // derive fovY value


      float h = cam.getFrustumTop();


      float w = cam.getFrustumRight();


      float aspect = w / h;





      float near = cam.getFrustumNear();





      float fovY = FastMath.atan(h / near) / (FastMath.DEG_TO_RAD  .5f);


      fovY += value 
       0.1f;





      h = FastMath.tan(fovY  FastMath.DEG_TO_RAD  .5f)  near;


      w = h 
       aspect;





      cam.setFrustumTop(h);


      cam.setFrustumBottom(-h);


      cam.setFrustumLeft(-w);


      cam.setFrustumRight(w);


          }





          protected void riseCamera(float value) {


      Vector3f vel = cam.getUp();// already cloned


      vel.multLocal(value  moveSpeed);


      Vector3f pos = cam.getLocation().clone();


      // if ( motionAllowed != null )


      // {


      // motionAllowed.checkMotionAllowed(


      // pos,


      // vel );


      // }


      // else


      // {


      pos.addLocal(vel);


      // }





      cam.setLocation(pos);


          }





          protected void moveCamera(float value, boolean sideways) {


      Vector3f vel = new Vector3f();


      Vector3f pos = cam.getLocation().clone();





      if (sideways) {


          cam.getLeft(vel);


      } else {


          cam.getDirection(vel);


      }


      vel.multLocal(value 
       moveSpeed);





      // if ( motionAllowed != null )


      // {


      // motionAllowed.checkMotionAllowed(


      // pos,


      // vel );


      // }


      // else


      // {


      pos.addLocal(vel);


      // }





      cam.setLocation(pos);


          }





          /



            @return If enabled


           
       @see FlyByCamera#setEnabled(boolean)


           /


          public boolean isEnabled() {


      return enabled;


          }





          /**


           
       @return If drag to rotate feature is enabled.


            


           
       @see FlyByCamera#setDragToRotate(boolean)


           /


          public boolean isDragToRotate() {


      return isDTRModeNow;


          }





          /**


           
       @param isDTRModeNow


                       set to true to show mouse cursor and enable the DTRKey which


           
                  must be held in order to allow mouse to rotate camera<br>


                       set to false to hide mouse cursor and allow mouse to rotate


           
                  camera all the time<br>


                       you can always use the toggleDTRKey to switch between these


           
                  two modes<br>


           /


          public void setDragToRotateMode(boolean dragToRotate1) {


      if (dragToRotate1 ˆ isDTRModeNow)// xor ie. 1ˆ1=0ˆ0=0 1ˆ0=0ˆ1=1


      {


          flipDragToRotate(true);


      }


          }





          /**


           
       @param alsoAffectDTRKey


                       if false then don’t set/unset DTRKey because we called this


           
                  when DTRKey was pressed and so we want it to be able to be


                       triggered again when key released(which will cause flip again)<br>


           
                  it should be false when DTRKey is either pressed OR released<br>


                       if true, then don’t mess with reg/unreg of DTRKey<br>


           
      /


          private void flipDragToRotate(boolean alsoAffectDTRKey) {


      isDTRModeNow = !isDTRModeNow;// toggle


      internalApplyDTRMode(alsoAffectDTRKey);


          }





          private void internalApplyDTRMode(boolean alsoAffectDTRKey) {


      Q.assumedNotNull(inputManager);


      inputManager.setCursorVisible(isDTRModeNow);


      if (alsoAffectDTRKey) {


          setDTRKey(isDTRModeNow);


      }


      setMouseRotationInputs(!isDTRModeNow);


          }


      }



      /pre



      =========

      pre type="java"
      package org.jme3.tests;





      import java.util.logging.Level;


      import java.util.logging.Logger;


      import java.util.prefs.BackingStoreException;





      import com.jme3.app.Application;


      import com.jme3.app.SimpleApplication;


      import com.jme3.system.AppSettings;





      /


       


       
      /


      public abstract class SimpleApplication2 extends SimpleApplication {





          private static final Logger logger = Logger.getLogger(Application.class


          .getName());





          private boolean vSync = true;


          private EvilFlyByCamera evilFlyCam;





          public SimpleApplication2() {


      super();


          }





          // /



          //  constructor


          // 



          //  @param vSync1


          // 
      /


          // public SimpleApplication2(


          // boolean vSync1 )


          // {


          // super();


          // vSync =


          // vSync1;


          // }





          public void setVSync(boolean enabled) {


      vSync = enabled;


          }





          /


           
       (non-Javadoc)


            


           
       @see com.jme3.app.SimpleApplication#simpleInitApp()


           /


          @Override


          public final void simpleInitApp() {


      flyCam.setEnabled(false);


      flyCam = null;


      evilFlyCam = new EvilFlyByCamera(cam, inputManager, true, rootNode,


      assetManager);


      evilFlyCam.setMoveSpeed(20f);


      // evilFlyCam.registerWithInput( inputManager );


      evilFlyCam.setDragToRotateMode(false);


      simple2InitApp();


          }





          public EvilFlyByCamera getEvilFlyCam() {


      return evilFlyCam;


          }





          public abstract void simple2InitApp();





          /



            (non-Javadoc)


           
       


            @see com.jme3.app.SimpleApplication#start()


           
      /


         &nb

the post keeps getting cut off, here’s again what died:

pre type="java"
package org.jme3.tests;





import java.util.logging.Level;


import java.util.logging.Logger;


import java.util.prefs.BackingStoreException;





import com.jme3.app.Application;


import com.jme3.app.SimpleApplication;


import com.jme3.system.AppSettings;





/


 


 
/


public abstract class SimpleApplication2 extends SimpleApplication {





    private static final Logger logger = Logger.getLogger(Application.class


    .getName());





    private boolean vSync = true;


    private EvilFlyByCamera evilFlyCam;





    public SimpleApplication2() {


super();


    }





    // /



    //  constructor


    // 



    //  @param vSync1


    // 
/


    // public SimpleApplication2(


    // boolean vSync1 )


    // {


    // super();


    // vSync =


    // vSync1;


    // }





    public void setVSync(boolean enabled) {


vSync = enabled;


    }





    /


     
 (non-Javadoc)


      


     
 @see com.jme3.app.SimpleApplication#simpleInitApp()


     /


    @Override


    public final void simpleInitApp() {


flyCam.setEnabled(false);


flyCam = null;


evilFlyCam = new EvilFlyByCamera(cam, inputManager, true, rootNode,


assetManager);


evilFlyCam.setMoveSpeed(20f);


// evilFlyCam.registerWithInput( inputManager );


evilFlyCam.setDragToRotateMode(false);


simple2InitApp();


    }





    public EvilFlyByCamera getEvilFlyCam() {


return evilFlyCam;


    }





    public abstract void simple2InitApp();





    /



      (non-Javadoc)


     
 


      @see com.jme3.app.SimpleApplication#start()


     
/


    @Override


    public void start() {


if (!isShowSettings()) {


    if (null == settings) {


AppSettings settings1 = new AppSettings(true);


// settings.setTitle(“window title”);


// load the settings that were previously saved by the settings


// dialog when it was shown ie. when app.setShowSettings(true);


try {


    logger.log(


    Level.INFO,


    “Manually loading config from disk(?) aka registry, because settings dialog is now shown”);


    settings1.load(settings1.getTitle());


} catch (BackingStoreException e) {


    Q.rethrow(e);


}


if (settings1.isVSync() != vSync) {


    logger.warning("vSync overriden to " + vSync);


    settings1.setVSync(vSync);// if wasn’t already in load


      // settings


}


setSettings(settings1);


    }


}


super.start();


    }


}



/pre

===============

pre type="java"
package org.jme3.tests;





public class Q {


    public static void assumedTrue(boolean trueExpr) {


if (!trueExpr) {


    throw new AssertionError(“The expresion wasn’t true as expected!”);


}


    }





    public static void badCall(String msg) {


throw new AssertionError("BadCall: " + msg);


    }





    public static void assumedNotNull(Object obj) {


if (null == obj) {


    throw new AssertionError(“was expecting NOT null but was null”);


}


    }





    public static void assumedFalse(boolean falseExpr) {


if (falseExpr) {


    throw new AssertionError(“The expresion wasn’t false as expected!”);


}


    }





    public static void rethrow(Throwable t) {


Q.assumedNotNull(t);


throw new RuntimeException(t);


    }


}



/pre



====end

The normal vector is perpendicular to the surface, yes. So its of course rational that it does not point at the collision location. Its a vector that goes from the collision location 1 unit in the normal direction.

solved, code changes + latest jme3 r7299

I don’t know if it was just my code being bad …

[java]package org.jme3.forum;



/*

  • Copyright © 2009-2010 jMonkeyEngine
  • All rights reserved.

    *
  • Redistribution and use in source and binary forms, with or without
  • modification, are permitted provided that the following conditions are
  • met:

    *
    • Redistributions of source code must retain the above copyright
  • notice, this list of conditions and the following disclaimer.

    *
    • Redistributions in binary form must reproduce the above copyright
  • notice, this list of conditions and the following disclaimer in the
  • documentation and/or other materials provided with the distribution.

    *
    • Neither the name of ‘jMonkeyEngine’ nor the names of its contributors
  • may be used to endorse or promote products derived from this software
  • without specific prior written permission.

    *
  • THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  • "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  • TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  • PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  • CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  • EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  • PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  • PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  • SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    /

    import java.util.prefs.BackingStoreException;

    import com.jme3.app.SimpleApplication;

    import com.jme3.collision.CollisionResult;

    import com.jme3.collision.CollisionResults;

    import com.jme3.light.DirectionalLight;

    import com.jme3.material.Material;

    import com.jme3.math.ColorRGBA;

    import com.jme3.math.FastMath;

    import com.jme3.math.Quaternion;

    import com.jme3.math.Ray;

    import com.jme3.math.Transform;

    import com.jme3.math.Vector3f;

    import com.jme3.scene.Geometry;

    import com.jme3.scene.Node;

    import com.jme3.scene.Spatial;

    import com.jme3.scene.debug.Arrow;

    import com.jme3.scene.shape.Box;

    import com.jme3.scene.shape.Line;

    import com.jme3.system.AppSettings;



    public class TestMousePickAgain extends SimpleApplication {

    public static void main(String[] args) throws BackingStoreException {

    TestMousePickAgain app = new TestMousePickAgain();

    AppSettings aps = new AppSettings(true);

    aps.load(aps.getTitle());

    aps.setVSync(true);

    app.setShowSettings(false);

    app.setSettings(aps);

    app.start();

    }



    Node shootables;

    Geometry mark;

    private final Vector3f marksScale = new Vector3f(4, 4, 10f);



    @Override

    public void simpleInitApp() {

    flyCam.setDragToRotate(true);

    flyCam.setMoveSpeed(20f);

    initMark();

    /
    * create four colored boxes and a floor to shoot at: /

    shootables = new Node(“Shootables”);

    rootNode.attachChild(shootables);

    shootables.attachChild(makeCube(“a Dragon”, -2f, 0f, 1f));

    shootables.attachChild(makeCube(“a tin can”, 1f, -2f, 0f));

    shootables.attachChild(makeCube(“the Sheriff”, 0f, 1f, -2f));

    shootables.attachChild(makeCube(“the Deputy”, 1f, 0f, -4f));

    shootables.attachChild(makeFloor());

    shootables.attachChild(makeCharacter());

    rootNode.scale(3.5f, 4.2f, 4.7f);

    rootNode.rotate(FastMath.DEG_TO_RAD * 65f, FastMath.DEG_TO_RAD * 65f,

    FastMath.DEG_TO_RAD * 65f);

    rootNode.setLocalTranslation(0, 0, 0 - 29

    //

    );// XXX:

    cam.setLocation(new Vector3f(1.4989337f, 4.4191055f, 73.00994f));

    cam.setRotation(new Quaternion(-0.0011200219f, 0.9985451f,

    -0.022919897f, -0.048795838f));

    }



    @Override

    public void simpleUpdate(float tpf) {

    Vector3f origin = cam.getWorldCoordinates(

    inputManager.getCursorPosition(), 0.0f);

    Vector3f direction = cam.getWorldCoordinates(

    inputManager.getCursorPosition(), 0.3f);

    // System.out.println("origin: " + origin + " / dir: " + direction);

    direction.subtractLocal(origin).normalizeLocal();

    Ray ray = new Ray(origin, direction);

    CollisionResults results = new CollisionResults();

    shootables.collideWith(ray, results);

    if (results.size() > 0) {

    CollisionResult closest = results.getClosestCollision();

    Vector3f contact = closest.getContactPoint();

    Vector3f normal = closest.getContactNormal();

    contact = rootNode.worldToLocal(contact, null);

    rootNode.attachChild(mark);

    mark.setLocalTranslation(contact);

    Vector3f upVec = rootNode.worldToLocal(Vector3f.UNIT_Y, null);

    Quaternion q = new Quaternion();

    // makes Z axis be in the same direction as normal

    // (our mark arrow is on the Z axis)

    q.lookAt(normal, upVec.normalize());

    q = mark.getParent().getWorldRotation().inverse().mult(q);

    mark.setLocalRotation(q);

    // if I want to keep mark’s scale to 1, regardless of parents’

    // transforms:

    mark.setLocalScale(marksScale.divide(mark.getParent()

    .getWorldScale()));

    } else {

    rootNode.detachChild(mark);

    }

    }



    /
    * A cube object for target practice /

    protected Geometry makeCube(String name, float x, float y, float z) {

    Box box = new Box(new Vector3f(x, y, z), 1, 1, 1);

    Geometry cube = new Geometry(name, box);

    Material mat1 = new Material(assetManager,

    “Common/MatDefs/Misc/SolidColor.j3md”);

    mat1.setColor(“Color”, ColorRGBA.randomColor());

    cube.setMaterial(mat1);

    return cube;

    }



    /
    * A floor to show that the “shot” can go through several objects. /

    protected Geometry makeFloor() {

    Box box = new Box(new Vector3f(0, -4, -5), 15, .2f, 15);

    Geometry floor = new Geometry(“the Floor”, box);

    Material mat1 = new Material(assetManager,

    “Common/MatDefs/Misc/SolidColor.j3md”);

    mat1.setColor(“Color”, ColorRGBA.Gray);

    floor.setMaterial(mat1);

    return floor;

    }



    /
    * A red ball that marks the last spot that was “hit” by the “shot”. */

    protected void initMark() {

    Arrow arrow = new Arrow(Vector3f.UNIT_Z.mult(2f));

    arrow.setLineWidth(3);

    mark = new Geometry(“markGeo”, arrow);

    Material mark_mat = new Material(assetManager,

    “Common/MatDefs/Misc/SolidColor.j3md”);

    mark_mat.setColor(“Color”, ColorRGBA.Red);

    mark.setMaterial(mark_mat);

    }



    protected Spatial makeCharacter() {

    // load a character from jme3test-test-data

    Spatial golem = assetManager.loadModel(“Models/Oto/Oto.mesh.xml”);

    golem.scale(0.5f);

    golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);

    // We must add a light to make the model visible

    DirectionalLight sun = new DirectionalLight();

    sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());

    golem.addLight(sun);

    return golem;

    }

    }[/java]