I have a problem with terrain picking.
I can detect if I clicked on a box or some other object but I cant do it with terrain.
My code is set up like here: https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:advanced:mouse_picking
So i just get a ray and then call rootNode.colllideWith(ray, results); This works for normal object
but doesn’t seem to detect terrain :s
Terrain picking does work. I would have to see your exact code to see what is wrong with it.
For more examples of terrain picking:
TerrainTestCollision.java
TerrainTestModifyHeight.java
hmmm I solved part of the problem (normalize a vector that isn’t normalized in the page I linked to but is in normalized in the terrain test). If my terrain is flat I seems to work but as I introduce a nice curved terrain there are heap of dead spots seemingly random over the the terrain :s
Here is all the code I use (there is quite a bit of code just quickly glued in there but shouldn’t be a problem).
Game.java
[java]
package net.urban;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import net.urban.util.Interpolation;
/**
- test
*
*/
public class Game extends BaseApplication {
public static void main(String[] args) {
Game app = new Game();
app.start();
}
@Override
public void simpleInitApp() {
Box b = new Box(Vector3f.ZERO, 1, 1, 1);
Geometry geom = new Geometry("Box", b);
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.Blue);
mat.getAdditionalRenderState().setWireframe(true);
geom.setMaterial(mat);
TerrainQuad terrain = new TerrainQuad("Terrain", 65, 513, null);
changeTerrainHeight(terrain);
terrain.setMaterial(mat);
TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
terrain.addControl(control);
terrain.setLocalTranslation(new Vector3f(0,-10,0));
rootNode.attachChild(terrain);
}
@Override
public void simpleUpdate(float tpf) {
//TODO: add update code
}
@Override
public void simpleRender(RenderManager rm) {
//TODO: add render code
}
private void changeTerrainHeight(TerrainQuad terrain) {
int halfSize = (terrain.getTerrainSize()/2)+1;
for(int x = -(halfSize-1); x < halfSize; x+=16){
for(int z = -(halfSize-1); z < halfSize; z+=16){
System.out.println(x + " " + z);
terrain.setHeight(new Vector2f(x,z), (float)ImprovedNoise.noise(x, z, 2.718)*10);
}
}
for(int x = -(halfSize-1); x < halfSize; x+=16){
for(int z = -(halfSize-1); z < halfSize; z+=16){
System.out.println("Smooth:" + x + " " + z);
float h1 = terrain.getHeight(new Vector2f(x ,z));
float h2 = terrain.getHeight(new Vector2f(x+16,z));
float h3 = terrain.getHeight(new Vector2f(x ,z+16));
float h4 = terrain.getHeight(new Vector2f(x+16,z+16));
for(int x1 = 0; x1 < 16; x1++){
float xi = ((float)x1)/16;
for(int z1 = 0; z1< 16; z1++){
float zi = ((float)z1)/16;
terrain.setHeight(new Vector2f(x+x1, z+z1), (float)Interpolation.cosineInterpolate2D(h1, h2, h3, h4, xi, zi));
}
}
}
}
}
}
[/java]
BaseApplication.java
[java]
package net.urban;
import com.jme3.app.Application;
import com.jme3.app.StatsView;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.FlyByCamera;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeContext.Type;
import com.jme3.system.JmeSystem;
/
*
*/
public abstract class BaseApplication extends Application{
protected Node rootNode = new Node("Root Node");
protected Node guiNode = new Node("Gui Node");
protected float secondCounter = 0.0f;
protected int frameCounter = 0;
protected BitmapText fpsText;
protected BitmapFont guiFont;
protected StatsView statsView;
protected boolean showSettings = true;
private boolean showFps = true;
private boolean showStats = true;
private KeyActionListener keyActionListener;
private AnalogActionListener analogActionListener;
@Override
public void start() {
// set some default settings in-case
// settings dialog is not shown
boolean loadSettings = false;
if (settings == null) {
setSettings(new AppSettings(true));
loadSettings = true;
}
// show settings dialog
if (showSettings) {
if (!JmeSystem.showSettingsDialog(settings, loadSettings)) {
return;
}
}
//re-setting settings they can have been merged from the registry.
setSettings(settings);
super.start();
}
/
- Retrieves guiNode
-
@return guiNode Node object
*
*/
public Node getGuiNode() {
return guiNode;
}
/**
- Retrieves rootNode
-
@return rootNode Node object
*
*/
public Node getRootNode() {
return rootNode;
}
public boolean isShowSettings() {
return showSettings;
}
/**
- Toggles settings window to display at start-up
-
@param showSettings Sets true/false
*
*/
public void setShowSettings(boolean showSettings) {
this.showSettings = showSettings;
}
/**
- Attaches FPS statistics to guiNode and displays it on the screen.
*
*/
public void loadFPSText() {
guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
fpsText = new BitmapText(guiFont, false);
fpsText.setLocalTranslation(0, fpsText.getLineHeight(), 0);
fpsText.setText("Frames per second");
guiNode.attachChild(fpsText);
}
/**
- Attaches Statistics View to guiNode and displays it on the screen
- above FPS statistics line.
*
*/
public void loadStatsView() {
statsView = new StatsView("Statistics View", assetManager, renderer.getStatistics());
// move it up so it appears above fps text
statsView.setLocalTranslation(0, fpsText.getLineHeight(), 0);
guiNode.attachChild(statsView);
}
@Override
public void initialize() {
super.initialize();
guiNode.setQueueBucket(Bucket.Gui);
guiNode.setCullHint(CullHint.Never);
loadFPSText();
loadStatsView();
viewPort.attachScene(rootNode);
guiViewPort.attachScene(guiNode);
if (inputManager != null) {
inputManager.setCursorVisible(true);
keyActionListener = new KeyActionListener(this);
analogActionListener = new AnalogActionListener(this);
if (context.getType() == Type.Display) {
inputManager.addMapping(KeyActionListener.INPUT_MAPPING_EXIT, new KeyTrigger(KeyInput.KEY_ESCAPE));
}
inputManager.addMapping(KeyActionListener.INPUT_MAPPING_CAMERA_POS, new KeyTrigger(KeyInput.KEY_C));
inputManager.addMapping(KeyActionListener.INPUT_MAPPING_MEMORY, new KeyTrigger(KeyInput.KEY_M));
inputManager.addMapping(KeyActionListener.INPUT_MAPPING_HIDE_STATS, new KeyTrigger(KeyInput.KEY_F5));
inputManager.addListener(keyActionListener, KeyActionListener.INPUT_MAPPING_EXIT,
KeyActionListener.INPUT_MAPPING_CAMERA_POS, KeyActionListener.INPUT_MAPPING_MEMORY, KeyActionListener.INPUT_MAPPING_HIDE_STATS);
inputManager.addMapping("pick target", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
inputManager.addListener(analogActionListener, "pick target");
}
// call user code
simpleInitApp();
}
@Override
public void update() {
super.update(); // makes sure to execute AppTasks
if (speed == 0 || paused) {
return;
}
float tpf = timer.getTimePerFrame() * speed;
if (showFps) {
secondCounter += timer.getTimePerFrame();
frameCounter ++;
if (secondCounter >= 1.0f) {
int fps = (int) (frameCounter / secondCounter);
fpsText.setText("Frames per second: " + fps);
secondCounter = 0.0f;
frameCounter = 0;
}
}
// update states
stateManager.update(tpf);
// simple update and root node
simpleUpdate(tpf);
rootNode.updateLogicalState(tpf);
guiNode.updateLogicalState(tpf);
rootNode.updateGeometricState();
guiNode.updateGeometricState();
// render states
stateManager.render(renderManager);
renderManager.render(tpf, context.isRenderable());
simpleRender(renderManager);
stateManager.postRender();
}
public void setDisplayFps(boolean show) {
showFps = show;
fpsText.setCullHint(show ? CullHint.Never : CullHint.Always);
}
public void setDisplayStatView(boolean show) {
showStats = show;
statsView.setEnabled(show);
statsView.setCullHint(show ? CullHint.Never : CullHint.Always);
}
public abstract void simpleInitApp();
public abstract void simpleUpdate(float tpf);
public abstract void simpleRender(RenderManager rm);
void toggleDisplayFps() {
setDisplayFps(!showFps);
}
void toggleDisplayStats() {
setDisplayStatView(!showStats);
}
}
[/java]
AnalogActionListener
[java]
package net.urban;
import com.jme3.collision.CollisionResults;
import com.jme3.input.controls.AnalogListener;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Sphere;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.geomipmap.picking.BresenhamTerrainPicker;
import com.jme3.terrain.geomipmap.picking.TerrainPickData;
import com.jme3.terrain.geomipmap.picking.TerrainPicker;
import java.util.ArrayList;
/**
*
-
@author Jan-Pieter
*/
public class AnalogActionListener implements AnalogListener{
private final BaseApplication app;
public AnalogActionListener(BaseApplication a){
app = a;
}
public void onAnalog(String name, float intensity, float tpf) {
if (name.equals("pick target")) {
// Reset results list.
CollisionResults results = new CollisionResults();
// Convert screen click to 3d position
Vector2f click2d = app.getInputManager().getCursorPosition();
Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone();
Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal();
// Aim the ray from the clicked spot forwards.
Ray ray = new Ray(click3d, dir);
// Collect intersections between ray and all nodes in results list.
app.getRootNode().collideWith(ray, results);
// (Print the results so we see what is going on:)
System.out.println("
Collisions? " + results.size() + "
");
for (int i = 0; i < results.size(); i++) {
// (For each “hit”, we know distance, impact point, geometry.)
float dist = results.getCollision(i).getDistance();
Vector3f pt = results.getCollision(i).getContactPoint();
String target = results.getCollision(i).getGeometry().getName();
Sphere s = new Sphere(10, 10, (float)0.5);
Geometry geom = new Geometry("Sphere", s);
geom.setLocalTranslation(pt);
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.Red);
mat.getAdditionalRenderState().setWireframe(true);
geom.setMaterial(mat);
app.getRootNode().attachChild(geom);
System.out.println("Selection #" + i + ": " + target + " at " + pt + ", " + dist + " WU away.");
}
// Use the results -- we rotate the selected geometry.
if (results.size() > 0) {
}
} // else if ...
}
}
[/java]
KeyActionListener.java
[java]
package net.urban;
import com.jme3.input.controls.ActionListener;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.util.BufferUtils;
/**
*
* @author Jan-Pieter
*/
public class KeyActionListener implements ActionListener{
public static final String INPUT_MAPPING_EXIT = "SIMPLEAPP_Exit";
public static final String INPUT_MAPPING_CAMERA_POS = "SIMPLEAPP_CameraPos";
public static final String INPUT_MAPPING_MEMORY = "SIMPLEAPP_Memory";
public static final String INPUT_MAPPING_HIDE_STATS = "SIMPLEAPP_HideStats";
private final BaseApplication app;
public KeyActionListener(BaseApplication a){
app = a;
}
public void onAction(String name, boolean value, float tpf) {
if (!value) {
return;
}
if (name.equals(INPUT_MAPPING_EXIT)) {
app.stop();
} else if (name.equals(INPUT_MAPPING_CAMERA_POS)) {
if (app.getCamera() != null) {
Vector3f loc = app.getCamera().getLocation();
Quaternion rot = app.getCamera().getRotation();
System.out.println("Camera Position: ("
+ loc.x + ", " + loc.y + ", " + loc.z + ")");
System.out.println("Camera Rotation: " + rot);
System.out.println("Camera Direction: " + app.getCamera().getDirection());
}
} else if (name.equals(INPUT_MAPPING_MEMORY)) {
BufferUtils.printCurrentDirectMemory(null);
}else if (name.equals(INPUT_MAPPING_HIDE_STATS)){
app.toggleDisplayFps();
app.toggleDisplayStats();
}
}
}
[/java]
ImprovedNoise.java
[java]
package net.urban;
/**
* http://mrl.nyu.edu/%7Eperlin/noise/
* @author KEN PERLIN
*/
public final class ImprovedNoise {
static public double noise(double x, double y, double z) {
int X = (int)Math.floor(x) & 255, // FIND UNIT CUBE THAT
Y = (int)Math.floor(y) & 255, // CONTAINS POINT.
Z = (int)Math.floor(z) & 255;
x -= Math.floor(x); // FIND RELATIVE X,Y,Z
y -= Math.floor(y); // OF POINT IN CUBE.
z -= Math.floor(z);
double u = fade(x), // COMPUTE FADE CURVES
v = fade(y), // FOR EACH OF X,Y,Z.
w = fade(z);
int A = p[X ]+Y, AA = p[A]+Z, AB = p[A+1]+Z, // HASH COORDINATES OF
B = p[X+1]+Y, BA = p+Z, BB = p[B+1]+Z; // THE 8 CUBE CORNERS,
return lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), // AND ADD
grad(p[BA ], x-1, y , z )), // BLENDED
lerp(u, grad(p[AB ], x , y-1, z ), // RESULTS
grad(p[BB ], x-1, y-1, z ))),// FROM 8
lerp(v, lerp(u, grad(p[AA+1], x , y , z-1 ), // CORNERS
grad(p[BA+1], x-1, y , z-1 )), // OF CUBE
lerp(u, grad(p[AB+1], x , y-1, z-1 ),
grad(p[BB+1], x-1, y-1, z-1 ))));
}
static double fade(double t) { return t * t * t * (t * (t * 6 - 15) + 10); }
static double lerp(double t, double a, double b) { return a + t * (b - a); }
static double grad(int hash, double x, double y, double z) {
int h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE
double u = h<8 ? x : y, // INTO 12 GRADIENT DIRECTIONS.
v = h<4 ? y : h==12||h==14 ? x : z;
return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
}
static final int p[] = new int[512], permutation[] = { 151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
};
static { for (int i=0; i < 256 ; i++) p[256+i] = p = permutation; }
}
[/java]
Interpolation.java
[java]
package net.urban.util;
/**
*
* @author Jan-Pieter
*/
public class Interpolation {
public static float cosineInterpolate(float y1, float y2, float x){
float temp = ((1-(float)Math.cos(x*Math.PI))/2);
return (float)(y1*(1-temp) + y2*temp);
}
public static float cosineInterpolate2D(float y1, float y2,
float y3, float y4,
float x, float z){
float i1 = cosineInterpolate(y1, y2, x);
float i2 = cosineInterpolate(y3, y4, x);
return cosineInterpolate(i1, i2, z);
}
}
[/java]
I investigated this a bit further and the dead spots seem to be related to the curvature of the terrain. I can never get an intersection on the top of hills. It seems that when both the curvature in the X an Z direction is negative (I think negative, not sure here because I know quite a bit of math but this is a vague area) I can’t get an intersection.
If I aim around the tops of the hills I can get intersections along the contours of them and in the valleys but not around the top (top as in the dome shaped bits).
If I go over a saddles shaped bit I can still get intersection presumably because it doesn’t have negative curvature in both X and Z
I only have a little time to look into this, but so far:
a) just use SimpleApplication, you should not have to subclass Application, just subclass SimpleApplication.
b) not colliding on hills appears to just happen in your test case. The other terrain tests, and in the SDK, you can collide anywhere, hill or valley. I did launch your test and I can see what you are encountering but I cannot replicate it anywhere else.
c) most of the time you will want to use CollisionResult cr = collisionResults.getClosestCollision();
I might have more time to look at it later this weekend.
Well I quickly tried to go back to subclass SimpleApplication and that wasn’t the problem. I get the same result. I also tried the code with the mouse pointer fixed at the center and I get the same result.
Thanks for taking a look at it. I hope you can find it because I have no idea what could be wrong :s
ok I found the problem.
First, please re-write your app; start by not copy pasting and overwriting SimpleApp. It will just make things easier and SimpleApplication already works very well.
Second, your test case introduces difficulties. Since you pick from the root node, you will keep colliding with the collision spheres you are creating. So in some instances you will collide up all the way to the camera with new spheres being created in front of each other as you pick them. Add them to a different node and pick just the scene node (with the real data). I’m sure this was just to test why the collision wasn’t happening, but either way, it makes life easier.
So finally, when you modify the height of the terrain you should call terrain.updateModelBound(); Do this after your changeTerrainHeight(terrain);
That will fix it.
I will update the javadocs to make this more clear. I will see about having it do it automatically as well.
thanks Sploreg terrain.updateModelBound() works perfect.
The thing is that the SDK says it’s unnecessary so yeah it’s not really clear but glad I know this now