[solved] Upgrade lemur gem with CharacterControl?

I want to abuse the CameraMovementState for moving a CharacterControl: as such, instead of moving the Camera, I want to move a Vector3f walkDirection which in turn get processed somewhere else. So I changed this

protected void updateFacing() {
    cameraFacing.fromAngles( (float)pitch, (float)yaw, 0 );
    camera.setRotation(cameraFacing);
}

to this:

protected void updateFacing() {
    cameraFacing.fromAngles( (float)pitch, (float)yaw, 0 );
    walkDirection.setRotation(cameraFacing);
}

But behold! Apparently, a Vector3f cannot be rotated around a Quaternion like it were a Camera…

You nee to use Quaternion.mult(Vector3f). You even have one with a store parameter to avoid creating a new Vector3f each time.

1 Like

This is music to my ears :wink:

Yeah, of course not. A camera has a position and a rotation (among about a dozen other things). A vector3f is just a vector3f… three float values. When you call cam.setRotation() you aren’t really “rotating around a Quaternion” you are setting the camera’s rotation that is used to build the camera view matrix.

@nehon’s advice is right. It might also help for your to read this tutoriral:
http://wiki.jmonkeyengine.org/doku.php/jme3:math_for_dummies

1 Like

I’ve made progress and (almost) everything works, but there is something I can’t understand: moving/strafing works and is fluid, but looking around works but is really choppy.

This is my code:

    @Override
    public void update( float tpf ) {
    
        walkDirection.set(0, 0, 0);
        // 'integrate' camera position based on the current move, strafe,
        // and elevation speeds.
        if( forward != 0 || side != 0 || elevation != 0 ) {
            Vector3f loc = camera.getLocation();
            
            Quaternion rot = camera.getRotation();
            Vector3f move = rot.mult(Vector3f.UNIT_Z).multLocal((float)(forward * speed * tpf)); 
            walkDirection.addLocal(camera.getDirection().multLocal((float)(forward * speed )));
            walkDirection.addLocal(camera.getLeft().multLocal((float)(side * speed )));
            Vector3f strafe = rot.mult(Vector3f.UNIT_X).multLocal((float)(side * speed * tpf));
            
            // Note: this camera moves 'elevation' along the camera's current up
            // vector because I find it more intuitive in free flight.
            Vector3f elev = rot.mult(Vector3f.UNIT_Y).multLocal((float)(elevation * speed * tpf));
                        
            loc = loc.add(move).add(strafe).add(elev);
        }
        if (gameLogicAppState!=null){
            gameLogicAppState.player.setWalkDirection(walkDirection);
            camera.setLocation(gameLogicAppState.player.getPhysicsLocation());
           gameLogicAppState.player.setPhysicsLocation(gameLogicAppState.player.getPhysicsLocation());
            gameLogicAppState.pPlayer.getSpatial().setLocalTranslation(camera.getLocation());
     
        }
    }
    /**
     *  Implementation of the AnalogFunctionListener interface.
     */
    @Override
    public void valueActive( FunctionId func, double value, double tpf ) {
 
        // Setup rotations and movements speeds based on current
        // axes states.    
        if( func == CameraMovementFunctions.F_Y_LOOK ) {
            pitch += -value * tpf * turnSpeed;
            if( pitch < minPitch )
                pitch = minPitch;
            if( pitch > maxPitch )
                pitch = maxPitch;
        } else if( func == CameraMovementFunctions.F_X_LOOK ) {
            yaw += -value * tpf * turnSpeed;
            if( yaw < 0 )
                yaw += Math.PI * 2;
            if( yaw > Math.PI * 2 )
                yaw -= Math.PI * 2;
        } else if( func == CameraMovementFunctions.F_MOVE ) {
            this.forward = value;
            return;
        } else if( func == CameraMovementFunctions.F_STRAFE ) {
            this.side = -value;
            return;
        } else if( func == CameraMovementFunctions.F_ELEVATE ) {
            this.elevation = value;
            return;
        } else {
            return;
        }
        updateFacing();        
    }

    protected void updateFacing() {
        cameraFacing.fromAngles( (float)pitch, (float)yaw, 0 );
        camera.setRotation(cameraFacing);
    }

Thanks!

Walkdirection is already made framerate independent by the physics system so you should remove the tpf there.

1 Like

Thanks, and I’ve updated my code, but the problem isn’t solved: the problem is about the camera rotation.

Was the original demo choppy?

My code is basically a mix between:
a) http://wiki.jmonkeyengine.org/doku.php/jme3:beginner:hello_collision
b) Lemur Gems #1 : InputMapper based camera movement

Neither a) nor b) was choppy before my merge.

So then it’s something about your code that’s doing it… hard for me to tell on a quick read. Try commenting bits out until it gets smooth again.

This is the smallest demo that I’ve managed to make. If you run it, only Y_LOOK will work but still give choppyness…

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package mygame;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.light.AmbientLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.Texture;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.event.BaseAppState;
import com.simsilica.lemur.input.AnalogFunctionListener;
import com.simsilica.lemur.input.FunctionId;
import com.simsilica.lemur.input.InputMapper;
import com.simsilica.lemur.input.InputState;
import com.simsilica.lemur.input.StateFunctionListener;
import prototype.appstates.lemur.CameraMovementFunctions;


public class BugReportMC2 extends SimpleApplication
         {
Spatial sceneModel;
    private TerrainQuad terrain;
    Material matTerrain;
    private float grassScale = 64;
    private float dirtScale = 16;
    private float rockScale = 128;
  
    public static void main(String[] args) {
        BugReportMC2 app = new BugReportMC2();
        //app.setShowSettings(false);
        app.start();
    }

    public void simpleInitApp() {
        GuiGlobals.initialize(this);
        AmbientLight ambLight = new AmbientLight();
        ambLight.setColor(new ColorRGBA(5f, 5f, 5f, 0.8f));
        rootNode.addLight(ambLight);
        stateManager.attach(new CameraMovementState());
CameraMovementFunctions.initializeDefaultMappings(GuiGlobals.getInstance().getInputMapper());
      createTerrain2();
        
    }
 

    @Override
    public void simpleUpdate(float tpf) {
    }
    

    
    private void createTerrain2() {
        // First, we load up our textures and the heightmap texture for the terrain

        // TERRAIN TEXTURE material
        matTerrain = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
        matTerrain.setBoolean("useTriPlanarMapping", false);
        matTerrain.setBoolean("WardIso", true);
        matTerrain.setFloat("Shininess", 0);

        // ALPHA map (for splat textures)
        matTerrain.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));

        // GRASS texture
        Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
        grass.setWrap(Texture.WrapMode.Repeat);
        matTerrain.setTexture("DiffuseMap", grass);
        matTerrain.setFloat("DiffuseMap_0_scale", grassScale);

        // DIRT texture
        Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
        dirt.setWrap(Texture.WrapMode.Repeat);
        matTerrain.setTexture("DiffuseMap_1", dirt);
        matTerrain.setFloat("DiffuseMap_1_scale", dirtScale);

        // ROCK texture
        Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
        rock.setWrap(Texture.WrapMode.Repeat);
        matTerrain.setTexture("DiffuseMap_2", rock);
        matTerrain.setFloat("DiffuseMap_2_scale", rockScale);

        // HEIGHTMAP image (for the terrain heightmap)
        Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");
        AbstractHeightMap heightmap = null;
        try {
            heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 0.5f);
            heightmap.load();
            heightmap.smooth(0.9f, 1);

        } catch (Exception e) {
            e.printStackTrace();
        }
        
        // CREATE THE TERRAIN
        terrain = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap());
        TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
        control.setLodCalculator( new DistanceLodCalculator(65, 2.7f) ); // patch size, and a multiplier
        terrain.addControl(control);
        terrain.setMaterial(matTerrain);
        terrain.setLocalTranslation(0, -100, 0);
        terrain.setLocalScale(10.f, 1.f, 10.f);
        rootNode.attachChild(terrain);
    }

    public static final String GROUP = "Main";
    public static final String INGAME = "Ingame";


 class CameraMovementState extends BaseAppState
                                 implements AnalogFunctionListener, StateFunctionListener {

    private InputMapper inputMapper;
    private Camera camera;
    private double turnSpeed = .5f;//2.5;  // one half complete revolution in 2.5 seconds
    private double yaw = FastMath.PI;
    private double pitch;
    private double maxPitch = FastMath.HALF_PI;
    private double minPitch = -FastMath.HALF_PI;
    private Quaternion cameraFacing = new Quaternion().fromAngles((float)pitch, (float)yaw, 0);

    public CameraMovementState() {
    }

    public void setPitch( double pitch ) {
        this.pitch = pitch;
        updateFacing();
    }

    public double getPitch() {
        return pitch;
    }
    
    public void setYaw( double yaw ) {
        this.yaw = yaw;
        updateFacing();
    }
    
    public double getYaw() {
        return yaw;
    }

    public void setRotation( Quaternion rotation ) {
        // Do our best
        float[] angle = rotation.toAngles(null);
        this.pitch = angle[0];
        this.yaw = angle[1];
        updateFacing();
    }
    
    public Quaternion getRotation() {
        return camera.getRotation();
    }

    @Override
    protected void initialize(Application app) {
        this.camera = app.getCamera();
        
        if( inputMapper == null )
            inputMapper = GuiGlobals.getInstance().getInputMapper();
        
        // Most of the movement functions are treated as analog.        
        inputMapper.addAnalogListener(this,
                                      CameraMovementFunctions.F_Y_LOOK);

    }
    

    @Override
    protected void cleanup(Application app) {
    }

    @Override
    protected void enable() {
        // Make sure our input group is enabled
        inputMapper.activateGroup( CameraMovementFunctions.GROUP_MOVEMENT );
        
        // And kill the cursor
        GuiGlobals.getInstance().setCursorEventsEnabled(false);
        
        // A 'bug' in Lemur causes it to miss turning the cursor off if
        // we are enabled before the MouseAppState is initialized.
        getApplication().getInputManager().setCursorVisible(false);        
    }

    @Override
    protected void disable() {
        inputMapper.deactivateGroup( CameraMovementFunctions.GROUP_MOVEMENT );
        GuiGlobals.getInstance().setCursorEventsEnabled(true);        
    }

    @Override
    public void update( float tpf ) {
     }
 
    /**
     *  Implementation of the StateFunctionListener interface.
     */
    @Override
    public void valueChanged( FunctionId func, InputState value, double tpf ) {
     }

    
    /**
     *  Implementation of the AnalogFunctionListener interface.
     */
    @Override
    public void valueActive( FunctionId func, double value, double tpf ) {
 
        // Setup rotations and movements speeds based on current
        // axes states.    
        if( func == CameraMovementFunctions.F_Y_LOOK ) {
            pitch += -value * tpf * turnSpeed;
            if( pitch < minPitch )
                pitch = minPitch;
            if( pitch > maxPitch )
                pitch = maxPitch;
        } 
        updateFacing();        
    }

    protected void updateFacing() {
        cameraFacing.fromAngles( (float)pitch, (float)yaw, 0 );
        camera.setRotation(cameraFacing);
    }
}
   
}

Solved! I needed to add this:

    flyCam.setEnabled(false);

However, I can’t find this code on the lemur gems… @pspeed what do you think?

Because I never even included the FlyCamAppState to begin with.

I only start with two states in my app:

    public CameraDemo() {
        super(new StatsAppState(), new CameraMovementState());
    }

I needed some time to figure it out… SimpleApplication attaches several AppStates by default, UNLESS you use the constructor with appStates…

A really clever n00b trap :smiley:

We’re pretty sure that when I refactor this that you will explicitly have to choose your initial app states… even if it’s by using a standard constant or something. It will avoid these issues of “magic gone wrong”.

1 Like