Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion jme3-core/src/main/java/com/jme3/renderer/Camera.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.jme3.math.Matrix4f;
import com.jme3.math.Plane;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.math.Vector4f;
Expand Down Expand Up @@ -1570,6 +1571,37 @@ public Vector3f getScreenCoordinates(Vector3f worldPosition, Vector3f store) {
return store;
}

/**
* Returns a ray going from camera through a screen point.
* <p>
* Resulting ray is in world space, starting on the near plane
* of the camera and going through position's (x,y) pixel coordinates on the screen.
*
* @param pos A {@link Vector2f} representing the 2D screen coordinates (in pixels)
* @return A {@link Ray} object representing the picking ray in world coordinates.
*
* <pre>{@code
* // Usage Example:
* Ray pickingRay = cam.screenPointToRay(inputManager.getCursorPosition());
* }</pre>
*/
public Ray screenPointToRay(Vector2f pos) {
TempVars vars = TempVars.get();
Vector3f nearPoint = vars.vect1;
Vector3f farPoint = vars.vect2;

// Get the world coordinates for the near and far points
getWorldCoordinates(pos, 0, nearPoint);
getWorldCoordinates(pos, 1, farPoint);

// Calculate direction and normalize
Vector3f direction = farPoint.subtractLocal(nearPoint).normalizeLocal();
Ray ray = new Ray(nearPoint, direction);

vars.release();
return ray;
}

/**
* Returns the display width.
*
Expand All @@ -1590,7 +1622,8 @@ public int getHeight() {

@Override
public String toString() {
return "Camera[location=" + location + "\n, direction=" + getDirection() + "\n"
return "Camera[location=" + location + "\n"
+ "direction=" + getDirection() + "\n"
+ "res=" + width + "x" + height + ", parallel=" + parallelProjection + "\n"
+ "near=" + frustumNear + ", far=" + frustumFar + "]";
}
Expand Down
178 changes: 115 additions & 63 deletions jme3-examples/src/main/java/jme3test/collision/TestMousePick.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2009-2021 jMonkeyEngine
* Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -29,125 +29,177 @@
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package jme3test.collision;

import com.jme3.app.SimpleApplication;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapText;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.Materials;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
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.Cylinder;
import com.jme3.scene.shape.Sphere;
import com.jme3.scene.shape.Torus;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.shadow.EdgeFilteringMode;

/**
* The primary purpose of TestMousePick is to illustrate how to detect intersections
* between a ray (originating from the camera's cursor position) and 3D objects in the scene.
* <p>
* When an intersection occurs, a visual marker (a red arrow)
* is placed at the collision point, and the name of the
* intersected object is displayed on the HUD.
*
* @author capdevon
*/
public class TestMousePick extends SimpleApplication {

public static void main(String[] args) {
TestMousePick app = new TestMousePick();
app.start();
}


private BitmapText hud;
private Node shootables;
private Geometry mark;

@Override
public void simpleInitApp() {
flyCam.setEnabled(false);
hud = createLabel(10, 10, "Text");
configureCamera();
initMark();
setupScene();
setupLights();
}

private void configureCamera() {
flyCam.setMoveSpeed(15f);
flyCam.setDragToRotate(true);

cam.setLocation(Vector3f.UNIT_XYZ.mult(6));
cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
}

private void setupScene() {
/* Create four colored boxes and a floor to shoot at: */
shootables = new Node("Shootables");
shootables.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
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());

Geometry sphere = makeShape("Sphere", new Sphere(32, 32, 1f), ColorRGBA.randomColor());
sphere.setLocalTranslation(-2f, 0f, 1f);
shootables.attachChild(sphere);

Geometry box = makeShape("Box", new Box(1, 1, 1), ColorRGBA.randomColor());
box.setLocalTranslation(1f, -2f, 0f);
shootables.attachChild(box);

Geometry cylinder = makeShape("Cylinder", new Cylinder(16, 16, 1.0f, 1.0f, true), ColorRGBA.randomColor());
cylinder.setLocalTranslation(0f, 1f, -2f);
cylinder.rotate(90 * FastMath.DEG_TO_RAD, 0, 0);
shootables.attachChild(cylinder);

Geometry torus = makeShape("Torus", new Torus(16, 16, 0.15f, 0.5f), ColorRGBA.randomColor());
torus.setLocalTranslation(1f, 0f, -4f);
shootables.attachChild(torus);

// load a character from jme3-testdata
Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
golem.scale(0.5f);
golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);
shootables.attachChild(golem);

Geometry floor = makeShape("Floor", new Box(15, .2f, 15), ColorRGBA.Gray);
floor.setLocalTranslation(0, -4, -5);
shootables.attachChild(floor);
}

private void setupLights() {
// 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());
rootNode.addLight(sun);

// init shadows
FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 2048, 3);
dlsf.setLight(sun);
dlsf.setLambda(0.55f);
dlsf.setShadowIntensity(0.8f);
dlsf.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
fpp.addFilter(dlsf);
viewPort.addProcessor(fpp);
}

private final CollisionResults results = new CollisionResults();
private final Quaternion tempQuat = new Quaternion();

@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();

Ray ray = new Ray(origin, direction);
CollisionResults results = new CollisionResults();
Ray ray = cam.screenPointToRay(inputManager.getCursorPosition());
results.clear();
shootables.collideWith(ray, results);
// System.out.println("----- Collisions? " + results.size() + "-----");
// for (int i = 0; i < results.size(); i++) {
// // For each hit, we know distance, impact point, name of geometry.
// float dist = results.getCollision(i).getDistance();
// Vector3f pt = results.getCollision(i).getWorldContactPoint();
// String hit = results.getCollision(i).getGeometry().getName();
// System.out.println("* Collision #" + i);
// System.out.println(" You shot " + hit + " at " + pt + ", " + dist + " wu away.");
// }

if (results.size() > 0) {
CollisionResult closest = results.getClosestCollision();
mark.setLocalTranslation(closest.getContactPoint());
Vector3f point = closest.getContactPoint();
Vector3f normal = closest.getContactNormal();

Quaternion q = new Quaternion();
q.lookAt(closest.getContactNormal(), Vector3f.UNIT_Y);
mark.setLocalRotation(q);
tempQuat.lookAt(normal, Vector3f.UNIT_Y);
mark.setLocalRotation(tempQuat);
mark.setLocalTranslation(point);

rootNode.attachChild(mark);
hud.setText(closest.getGeometry().toString());

} else {
hud.setText("No collision");
rootNode.detachChild(mark);
}
}

/** A cube object for target practice */
private Geometry makeCube(String name, float x, float y, float z) {
Box box = new Box(1, 1, 1);
Geometry cube = new Geometry(name, box);
cube.setLocalTranslation(x, y, z);
Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat1.setColor("Color", ColorRGBA.randomColor());
cube.setMaterial(mat1);
return cube;

private BitmapText createLabel(int x, int y, String text) {
BitmapText bmp = guiFont.createLabel(text);
bmp.setLocalTranslation(x, settings.getHeight() - y, 0);
bmp.setColor(ColorRGBA.Red);
guiNode.attachChild(bmp);
return bmp;
}

/** A floor to show that the "shot" can go through several objects. */
private Geometry makeFloor() {
Box box = new Box(15, .2f, 15);
Geometry floor = new Geometry("the Floor", box);
floor.setLocalTranslation(0, -4, -5);
Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat1.setColor("Color", ColorRGBA.Gray);
floor.setMaterial(mat1);
return floor;
private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) {
Geometry geo = new Geometry(name, mesh);
Material mat = new Material(assetManager, Materials.LIGHTING);
mat.setBoolean("UseMaterialColors", true);
mat.setColor("Diffuse", color);
geo.setMaterial(mat);
return geo;
}

/**
* A red arrow to mark the spot being picked.
*/
private void initMark() {
Arrow arrow = new Arrow(Vector3f.UNIT_Z.mult(2f));
mark = new Geometry("BOOM!", arrow);
Material mark_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mark_mat.setColor("Color", ColorRGBA.Red);
mark.setMaterial(mark_mat);
mark = new Geometry("Marker", arrow);
Material mat = new Material(assetManager, Materials.UNSHADED);
mat.setColor("Color", ColorRGBA.Red);
mark.setMaterial(mat);
}

private Spatial makeCharacter() {
// load a character from jme3-testdata
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;
}
}