Navigator / Compass

Season's Greetings!



This is my first chance to give back to the community. I hope some people find it useful.



As I'm extremely new to jME I would appreciate an code review to see what I've done wrong or poorly and to get it into shape for sharing.



Navigator class:

The Navigator is a combination compass and waypoint display. The North indicator 'card' rotates with changes in the camera's heading (the direction that it's pointing) relative to North; the outer waypoint ring rotates with changes in the camera's location realtive to the waypoint (the bearing from camera to waypoint). Both North and the waypoint are user-settable. If no waypoint is set (null) then the outer waypoint ring is not visible.



see attached screenshot.





Revised code can be found in later post





You'll also need the two Compass images: CompassNorth.png and CompassWaypoint.png, attached below



Here is the screenshot:

nymon said:

Thanks for updating this! You should also remove the code from the first post, save people some time by not grabbing the wrong one.

roger wilco!

Really nice job!  8) The perfect place for this is the code snippets section of the wiki, please add it there.

nymon said:

Really nice job!  8) The perfect place for this is the code snippets section of the wiki, please add it there.

nymon, thanks for the compliment!
I'll put in the snippets page after someone finds and I fix the defects that *must* be lurking there.  :D

Will someone please test this and tell me if compass textures are blending correctly?



I'm trying to make a HUD and I can't get the alpha channel of my textures to blend, so I tried this class to see if it works and I'm seeing the same problem. What I see is the waypoint circle, but no 'N' in the middle, and if you move the camera in the test so that the compass is covering the sphere, you will see that the alpha channel of the textures is black, meaning its not blending -OR- you may not see this and it turns out to be a driver/card problem on my end.



But I need a sanity check here please…

Sorry I didn't see this til now nymon:



Looks like the blend states are a little off here, when I both to this it works fine:


        final BlendState blendState = getRenderer().createBlendState();
        blendState.setSourceFunctionAlpha( SourceFunction.SourceAlpha );
        blendState.setDestinationFunctionAlpha( DestinationFunction.OneMinusSourceAlpha );
        blendState.setEnabled( true );
        blendState.setTestEnabled( true );
        blendState.setBlendEnabled( true );
        blendState.setReference( 0.1f );
        blendState.setTestFunction( BlendState.TestFunction.GreaterThanOrEqualTo );

Thanks!

nymon said:

Will someone please test this and tell me if compass textures are blending correctly?

I'm trying to make a HUD and I can't get the alpha channel of my textures to blend, so I tried this class to see if it works and I'm seeing the same problem. What I see is the waypoint circle, but no 'N' in the middle, and if you move the camera in the test so that the compass is covering the sphere, you will see that the alpha channel of the textures is black, meaning its not blending -OR- you may not see this and it turns out to be a driver/card problem on my end.

But I need a sanity check here please...

Sorry for the delay! I don't recall getting an email notification about a new post.
I've seen what you're seeing but not in the code I posted.
Nevertheless, I took basixs' code and it works for me, too, so I will defer to his greater experience and revise the navigator code.
It is found below. Images are found in the first post.

Navigator class


//~--- non-JDK imports



import com.jme.image.Texture;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.TexCoords;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jme.util.geom.BufferUtils;

//~--- JDK imports

import java.nio.FloatBuffer;

/**
 * The Navigator is a combination compass and waypoint display.
 * The North indicator 'card' rotates with changes in the camera's heading
 * (the direction that it's pointing) relative to North; the outer waypoint
 * ring rotates with changes in the camera's location relative to the waypoint
 * (the bearing from camera to waypoint). Both North and the waypoint are
 * user-settable. If no waypoint is set (null) then the outer waypoint
 * ring is not visible.
 * * Note: Don't forget to call simpleUpdate from the game simpleUpdate()
 * @author Kropotkin (with lots of code and comments from HUD tutorial)
 */
public class Navigator {
    protected static DisplaySystem __display = DisplaySystem.getDisplaySystem();

    // in the case of the compass, 'up' is along the Z axis.
    protected Vector3f _upVector         = new Vector3f(0, 0, 1);
    // for the waypoint, it's along the Y axis
    protected Vector3f _waypointUpVector = new Vector3f(0, 1, 0);

    // default values
    protected Vector3f   _northVec     = new Vector3f(0, 0, -10).normalizeLocal();
    protected Vector2f   _north2f      = new Vector2f(0, -10).normalizeLocal();
    protected Quaternion _compassQuat  = new Quaternion();
    protected Quaternion _waypointQuat = new Quaternion();
    protected Vector2f   _camDirection2f;
    protected Vector2f   _camLocation2f;
    protected Vector3f   _cameraDirectionVec;
    protected Vector3f   _cameraLocationVec;
    protected float      _compassLocalScale;
    protected float      _compassLocationX;
    protected float      _compassLocationY;

    // inner compass
    protected Node _compassNode;
    protected Quad _compassQuad;
    protected int  _compassTextureHeight;
    protected int  _compassTextureWidth;
    private Node   _parentNode;

    // outer waypoint
    protected Vector3f _waypoint;
    protected Vector2f _waypoint2f;
    protected Node     _waypointNode;
    protected Quad     _waypointQuad;
    protected int      _waypointTextureHeight;
    protected int      _waypointTextureWidth;

    public Navigator(float localScale, Node parentNode) {
        _compassLocalScale = localScale;
        _parentNode        = parentNode;
        _compassNode       = new Node("_compassNode");
        _compassQuad       = new Quad("_compassQuad", 512f, 512f);

        // create the texture state to handle the texture
        final TextureState ts = __display.getRenderer().createTextureState();

        // load the image bs a texture (the image should be placed in the same directory bs this class)
        final Texture texture = TextureManager.loadTexture(
                getClass().getResource("/data/CompassNorth.png"),
                Texture.MinificationFilter.Trilinear, // of no use for the quad
                Texture.MagnificationFilter.Bilinear, // of no use for the quad
                1.0f, true);

        // set the texture for this texture state
        ts.setTexture(texture);

        // initialize texture width
        _compassTextureWidth = ts.getTexture().getImage().getWidth();

        // initialize texture height
        _compassTextureHeight = ts.getTexture().getImage().getHeight();

        // activate the texture state
        ts.setEnabled(true);

        // correct texture application:
        final FloatBuffer texCoords = BufferUtils.createVector2Buffer(4);

        // coordinate (0,0) for vertex 0
        texCoords.put(getUForPixel(0)).put(getVForPixel(0));

        // coordinate (0,40) for vertex 1
        texCoords.put(getUForPixel(0)).put(getVForPixel(512));

        // coordinate (40,40) for vertex 2
        texCoords.put(getUForPixel(512)).put(getVForPixel(512));

        // coordinate (40,0) for vertex 3
        texCoords.put(getUForPixel(512)).put(getVForPixel(0));

        // assign texture coordinates to the quad
        _compassQuad.setTextureCoords(new TexCoords(texCoords));

        // apply the texture state to the quad
        _compassQuad.setRenderState(ts);

        // to handle texture transparency:
        // create a blend state
        final BlendState bs = __display.getRenderer().createBlendState();
        bs.setSourceFunctionAlpha( BlendState.SourceFunction.SourceAlpha );
        bs.setDestinationFunctionAlpha( BlendState.DestinationFunction.OneMinusSourceAlpha );
        bs.setEnabled( true );
        bs.setTestEnabled( true );
        bs.setBlendEnabled( true );
        bs.setReference( 0.1f );
        bs.setTestFunction( BlendState.TestFunction.GreaterThanOrEqualTo );

        // assign the blender state to the quad
        _compassQuad.setRenderState(bs);
        _compassQuad.setRenderQueueMode(Renderer.QUEUE_ORTHO);

        /* does not work to disable light under v0.10 */
        LightState ls = __display.getRenderer().createLightState();

        ls.setEnabled(false);
        _compassQuad.setRenderState(ls);
        _compassQuad.setLightCombineMode(Spatial.LightCombineMode.Off);
        _compassQuad.updateRenderState();
        _compassQuad.setLocalScale(new Vector3f(_compassLocalScale, _compassLocalScale, _compassLocalScale));
        _compassNode.attachChild(_compassQuad);
        _parentNode.attachChild(_compassNode);
    }

    public void setWaypoint(Vector3f waypoint) {
        _waypoint     = waypoint.clone();
        _waypoint2f   = new Vector2f(_waypoint.x, _waypoint.z);
        _waypointNode = new Node("_waypointNode");
        _waypointQuad = new Quad("_waypointQuad", 512f, 512f);

        // create the texture state to handle the texture
        final TextureState ts = __display.getRenderer().createTextureState();

        // load the image bs a texture (the image should be placed in the same directory bs this class)
        final Texture texture = TextureManager.loadTexture(getClass().getResource("/data/CompassWaypoint.png"),
                                    Texture.MinificationFilter.Trilinear,    // of no use for the quad
            Texture.MagnificationFilter.Bilinear,    // of no use for the quad
            1.0f, true);

        // set the texture for this texture state
        ts.setTexture(texture);

        // initialize texture width
        _waypointTextureWidth = ts.getTexture().getImage().getWidth();

        // initialize texture height
        _waypointTextureHeight = ts.getTexture().getImage().getHeight();

        // activate the texture state
        ts.setEnabled(true);

        // correct texture application:
        final FloatBuffer texCoords = BufferUtils.createVector2Buffer(4);

        // coordinate (0,0) for vertex 0
        texCoords.put(getUForPixel(0)).put(getVForPixel(0));

        // coordinate (0,40) for vertex 1
        texCoords.put(getUForPixel(0)).put(getVForPixel(512));

        // coordinate (40,40) for vertex 2
        texCoords.put(getUForPixel(512)).put(getVForPixel(512));

        // coordinate (40,0) for vertex 3
        texCoords.put(getUForPixel(512)).put(getVForPixel(0));

        // assign texture coordinates to the quad
        _waypointQuad.setTextureCoords(new TexCoords(texCoords));

        // apply the texture state to the quad
        _waypointQuad.setRenderState(ts);

        // to handle texture transparency:
        // create a blend state
        final BlendState bs = __display.getRenderer().createBlendState();
        bs.setSourceFunctionAlpha( BlendState.SourceFunction.SourceAlpha );
        bs.setDestinationFunctionAlpha( BlendState.DestinationFunction.OneMinusSourceAlpha );
        bs.setEnabled( true );
        bs.setTestEnabled( true );
        bs.setBlendEnabled( true );
        bs.setReference( 0.1f );
        bs.setTestFunction( BlendState.TestFunction.GreaterThanOrEqualTo );


        // assign the blender state to the quad
        _waypointQuad.setRenderState(bs);
        _waypointQuad.updateRenderState();
        _waypointQuad.setRenderQueueMode(Renderer.QUEUE_ORTHO);

        /* does not work to disable light under v0.10 */
        LightState ls = __display.getRenderer().createLightState();

        _waypointQuad.setRenderState(ls);
        _waypointQuad.updateRenderState();
        _waypointQuad.setLightCombineMode(Spatial.LightCombineMode.Off);
        _waypointQuad.updateRenderState();
        _waypointQuad.setLocalScale(new Vector3f(_compassLocalScale, _compassLocalScale, _compassLocalScale));
        _waypointNode.attachChild(_waypointQuad);
        _parentNode.attachChild(_waypointNode);
    }

    public int getHeight() {
        return Math.round(_compassTextureHeight * _compassLocalScale);
    }

    public int getWidth() {
        return Math.round(_compassTextureWidth * _compassLocalScale);
    }

    public void setLocation(float locX, float locY) {
        _compassLocationX = locX;
        _compassLocationY = locY;
        _compassQuad.setLocalTranslation(_compassLocationX, _compassLocationY, 0);

        if (_waypoint != null) {
            _waypointQuad.setLocalTranslation(_compassLocationX, _compassLocationY, 0);
        }
    }

    private float getUForPixel(int xPixel) {
        return (float) xPixel / _compassTextureWidth;
    }

    private float getVForPixel(int yPixel) {
        return 1f - (float) yPixel / _compassTextureHeight;
    }

    public void simpleUpdate(Camera cam) {

        // update the inner compass indicator
        _cameraDirectionVec = cam.getDirection().normalizeLocal();
        _camDirection2f     = new Vector2f(_cameraDirectionVec.x, _cameraDirectionVec.z);
        _compassQuat.fromAngleNormalAxis(_north2f.angleBetween(_camDirection2f), getUpVector());
        _compassQuad.setLocalRotation(_compassQuat);

        // update the outer waypoint
        if (_waypoint != null) {
            _cameraLocationVec = cam.getLocation();
            _camLocation2f     = new Vector2f(_cameraLocationVec.x, _cameraLocationVec.z);
            _waypointQuat.fromAngleNormalAxis(_camLocation2f.angleBetween(_waypoint2f) + FastMath.DEG_TO_RAD * 180,
                                              getUpVector());
            _waypointQuad.setLocalRotation(_waypointQuat);
        }
    }

    /**
     * @return the _upVector
     */
    public Vector3f getUpVector() {
        return _upVector;
    }

    /**
     * @param _upVector the _upVector to set
     */
    public void setUpVector(Vector3f _upVector) {
        this._upVector = _upVector;
    }

    /**
     * @return the _waypointUpVector
     */
    public Vector3f getWaypointUpVector() {
        return _waypointUpVector;
    }

    /**
     * @param _waypointUpVector the _waypointUpVector to set
     */
    public void setWaypointUpVector(Vector3f _waypointUpVector) {
        this._waypointUpVector = _waypointUpVector;
    }

    /**
     * @return the _northVec
     */
    public Vector3f getNorthVec() {
        return _northVec;
    }

    /**
     * @param northVec the _northVec to set
     */
    public void setNorthVec(Vector3f northVec) {
        this._northVec = northVec;
        _north2f.x     = _northVec.x;
        _north2f.y     = _northVec.z;
    }
}



TestNavigator class


import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingBox;
import com.jme.math.Vector3f;
import com.jme.renderer.Renderer;
import com.jme.scene.VBOInfo;
import com.jme.scene.shape.Sphere;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 *
 * Test the Navigator. Ensure that it does point North and
 * that the waypoint, if set, points to the actual waypoint.
 * If the waypoint is not set (null) then the outer waypoint ring
 * is not displayed.
 * The Navigator is a combination compass and waypoint display.
 * The North indicator 'card' rotates with changes in the camera's heading
 * (the direction that it's pointing) relative to North; the outer waypoint
 * ring rotates with changes in the camera's location relative to the waypoint
 * (the bearing from camera to waypoint). Both North and the waypoint are
 * user-settable. If no waypoint is set (null) then the outer waypoint
 * ring is not visible.
 * * Note the call to _navigator.simpleUpdate in simpleUpdate()
 * @author Kropotkin
 */
public class TestNavigator extends SimpleGame {

    private static final Logger logger = Logger
            .getLogger(TestNavigator.class.getName());

    private Navigator _navigator;

    public static void main(String[] args) {

        TestNavigator app = new TestNavigator();
        app.setConfigShowMode(ConfigShowMode.AlwaysShow);
        app.start();
    }

    @Override
    protected void simpleUpdate() {
        _navigator.simpleUpdate(cam);
    }

    @Override
    protected void simpleInitGame() {

        display.setTitle("Test Navigator");

        Vector3f waypoint = new Vector3f(0, 0, -350);

        // Add a sphere - it will be our waypoint
        Sphere s = new Sphere("Sphere", 12, 12, 12);
        s.setModelBound(new BoundingBox());
        s.updateModelBound();

        // set the location of the sphere to be the waypoint
        s.setLocalTranslation(waypoint);

        rootNode.attachChild(s);
        s.setVBOInfo(new VBOInfo(true));
        s.setRenderQueueMode(Renderer.QUEUE_OPAQUE);

        // Create the navigator
        _navigator = new Navigator(0.25f, rootNode);

        // Set north
        // same dimension as skybox
        _navigator.setNorthVec(new Vector3f(0, 0, -100));

        // set the location of the waypoint
        // if the waypoint is not set (null) then
        // the outer waypoint card is not visible
        _navigator.setWaypoint(waypoint);
       
        // get the location of the upper left corner of the screen
        // in order to place the navigator on-screen
        float locX = _navigator.getWidth() - _navigator.getWidth() / 2;
        float locY = display.getHeight() - _navigator.getHeight() + _navigator.getHeight() / 2;

        // set navigator's on-screen location
        _navigator.setLocation(locX, locY);
   
    }
}



If there are no objections by the end of the weekend, I'll post this on the snippets page per nymon's suggestion.

Thanks!

Thanks for updating this! You should also remove the code from the first post, save people some time by not grabbing the wrong one.