Skip to content

Commit f7d5b16

Browse files
authored
Merge pull request jMonkeyEngine#2479 from capdevon/capdevon-Camera
Camera: Add optimized screenPointToRay(Vector2f) for efficient raycasting
2 parents b61347a + 1af9a2b commit f7d5b16

File tree

2 files changed

+149
-64
lines changed

2 files changed

+149
-64
lines changed

jme3-core/src/main/java/com/jme3/renderer/Camera.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.jme3.math.Matrix4f;
4343
import com.jme3.math.Plane;
4444
import com.jme3.math.Quaternion;
45+
import com.jme3.math.Ray;
4546
import com.jme3.math.Vector2f;
4647
import com.jme3.math.Vector3f;
4748
import com.jme3.math.Vector4f;
@@ -1570,6 +1571,37 @@ public Vector3f getScreenCoordinates(Vector3f worldPosition, Vector3f store) {
15701571
return store;
15711572
}
15721573

1574+
/**
1575+
* Returns a ray going from camera through a screen point.
1576+
* <p>
1577+
* Resulting ray is in world space, starting on the near plane
1578+
* of the camera and going through position's (x,y) pixel coordinates on the screen.
1579+
*
1580+
* @param pos A {@link Vector2f} representing the 2D screen coordinates (in pixels)
1581+
* @return A {@link Ray} object representing the picking ray in world coordinates.
1582+
*
1583+
* <pre>{@code
1584+
* // Usage Example:
1585+
* Ray pickingRay = cam.screenPointToRay(inputManager.getCursorPosition());
1586+
* }</pre>
1587+
*/
1588+
public Ray screenPointToRay(Vector2f pos) {
1589+
TempVars vars = TempVars.get();
1590+
Vector3f nearPoint = vars.vect1;
1591+
Vector3f farPoint = vars.vect2;
1592+
1593+
// Get the world coordinates for the near and far points
1594+
getWorldCoordinates(pos, 0, nearPoint);
1595+
getWorldCoordinates(pos, 1, farPoint);
1596+
1597+
// Calculate direction and normalize
1598+
Vector3f direction = farPoint.subtractLocal(nearPoint).normalizeLocal();
1599+
Ray ray = new Ray(nearPoint, direction);
1600+
1601+
vars.release();
1602+
return ray;
1603+
}
1604+
15731605
/**
15741606
* Returns the display width.
15751607
*
@@ -1590,7 +1622,8 @@ public int getHeight() {
15901622

15911623
@Override
15921624
public String toString() {
1593-
return "Camera[location=" + location + "\n, direction=" + getDirection() + "\n"
1625+
return "Camera[location=" + location + "\n"
1626+
+ "direction=" + getDirection() + "\n"
15941627
+ "res=" + width + "x" + height + ", parallel=" + parallelProjection + "\n"
15951628
+ "near=" + frustumNear + ", far=" + frustumFar + "]";
15961629
}
Lines changed: 115 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2009-2021 jMonkeyEngine
2+
* Copyright (c) 2009-2025 jMonkeyEngine
33
* All rights reserved.
44
*
55
* Redistribution and use in source and binary forms, with or without
@@ -29,125 +29,177 @@
2929
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
3030
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3131
*/
32-
3332
package jme3test.collision;
3433

3534
import com.jme3.app.SimpleApplication;
3635
import com.jme3.collision.CollisionResult;
3736
import com.jme3.collision.CollisionResults;
37+
import com.jme3.font.BitmapText;
3838
import com.jme3.light.DirectionalLight;
3939
import com.jme3.material.Material;
40+
import com.jme3.material.Materials;
4041
import com.jme3.math.ColorRGBA;
42+
import com.jme3.math.FastMath;
4143
import com.jme3.math.Quaternion;
4244
import com.jme3.math.Ray;
4345
import com.jme3.math.Vector3f;
46+
import com.jme3.post.FilterPostProcessor;
47+
import com.jme3.renderer.queue.RenderQueue;
4448
import com.jme3.scene.Geometry;
49+
import com.jme3.scene.Mesh;
4550
import com.jme3.scene.Node;
4651
import com.jme3.scene.Spatial;
4752
import com.jme3.scene.debug.Arrow;
4853
import com.jme3.scene.shape.Box;
54+
import com.jme3.scene.shape.Cylinder;
55+
import com.jme3.scene.shape.Sphere;
56+
import com.jme3.scene.shape.Torus;
57+
import com.jme3.shadow.DirectionalLightShadowFilter;
58+
import com.jme3.shadow.EdgeFilteringMode;
4959

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

5272
public static void main(String[] args) {
5373
TestMousePick app = new TestMousePick();
5474
app.start();
5575
}
56-
76+
77+
private BitmapText hud;
5778
private Node shootables;
5879
private Geometry mark;
5980

6081
@Override
6182
public void simpleInitApp() {
62-
flyCam.setEnabled(false);
83+
hud = createLabel(10, 10, "Text");
84+
configureCamera();
6385
initMark();
86+
setupScene();
87+
setupLights();
88+
}
89+
90+
private void configureCamera() {
91+
flyCam.setMoveSpeed(15f);
92+
flyCam.setDragToRotate(true);
6493

94+
cam.setLocation(Vector3f.UNIT_XYZ.mult(6));
95+
cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
96+
}
97+
98+
private void setupScene() {
6599
/* Create four colored boxes and a floor to shoot at: */
66100
shootables = new Node("Shootables");
101+
shootables.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
67102
rootNode.attachChild(shootables);
68-
shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f));
69-
shootables.attachChild(makeCube("a tin can", 1f, -2f, 0f));
70-
shootables.attachChild(makeCube("the Sheriff", 0f, 1f, -2f));
71-
shootables.attachChild(makeCube("the Deputy", 1f, 0f, -4f));
72-
shootables.attachChild(makeFloor());
73-
shootables.attachChild(makeCharacter());
103+
104+
Geometry sphere = makeShape("Sphere", new Sphere(32, 32, 1f), ColorRGBA.randomColor());
105+
sphere.setLocalTranslation(-2f, 0f, 1f);
106+
shootables.attachChild(sphere);
107+
108+
Geometry box = makeShape("Box", new Box(1, 1, 1), ColorRGBA.randomColor());
109+
box.setLocalTranslation(1f, -2f, 0f);
110+
shootables.attachChild(box);
111+
112+
Geometry cylinder = makeShape("Cylinder", new Cylinder(16, 16, 1.0f, 1.0f, true), ColorRGBA.randomColor());
113+
cylinder.setLocalTranslation(0f, 1f, -2f);
114+
cylinder.rotate(90 * FastMath.DEG_TO_RAD, 0, 0);
115+
shootables.attachChild(cylinder);
116+
117+
Geometry torus = makeShape("Torus", new Torus(16, 16, 0.15f, 0.5f), ColorRGBA.randomColor());
118+
torus.setLocalTranslation(1f, 0f, -4f);
119+
shootables.attachChild(torus);
120+
121+
// load a character from jme3-testdata
122+
Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
123+
golem.scale(0.5f);
124+
golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);
125+
shootables.attachChild(golem);
126+
127+
Geometry floor = makeShape("Floor", new Box(15, .2f, 15), ColorRGBA.Gray);
128+
floor.setLocalTranslation(0, -4, -5);
129+
shootables.attachChild(floor);
130+
}
131+
132+
private void setupLights() {
133+
// We must add a light to make the model visible
134+
DirectionalLight sun = new DirectionalLight();
135+
sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());
136+
rootNode.addLight(sun);
137+
138+
// init shadows
139+
FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
140+
DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 2048, 3);
141+
dlsf.setLight(sun);
142+
dlsf.setLambda(0.55f);
143+
dlsf.setShadowIntensity(0.8f);
144+
dlsf.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
145+
fpp.addFilter(dlsf);
146+
viewPort.addProcessor(fpp);
74147
}
75148

149+
private final CollisionResults results = new CollisionResults();
150+
private final Quaternion tempQuat = new Quaternion();
151+
76152
@Override
77153
public void simpleUpdate(float tpf){
78-
Vector3f origin = cam.getWorldCoordinates(inputManager.getCursorPosition(), 0.0f);
79-
Vector3f direction = cam.getWorldCoordinates(inputManager.getCursorPosition(), 0.3f);
80-
direction.subtractLocal(origin).normalizeLocal();
81154

82-
Ray ray = new Ray(origin, direction);
83-
CollisionResults results = new CollisionResults();
155+
Ray ray = cam.screenPointToRay(inputManager.getCursorPosition());
156+
results.clear();
84157
shootables.collideWith(ray, results);
85-
// System.out.println("----- Collisions? " + results.size() + "-----");
86-
// for (int i = 0; i < results.size(); i++) {
87-
// // For each hit, we know distance, impact point, name of geometry.
88-
// float dist = results.getCollision(i).getDistance();
89-
// Vector3f pt = results.getCollision(i).getWorldContactPoint();
90-
// String hit = results.getCollision(i).getGeometry().getName();
91-
// System.out.println("* Collision #" + i);
92-
// System.out.println(" You shot " + hit + " at " + pt + ", " + dist + " wu away.");
93-
// }
158+
94159
if (results.size() > 0) {
95160
CollisionResult closest = results.getClosestCollision();
96-
mark.setLocalTranslation(closest.getContactPoint());
161+
Vector3f point = closest.getContactPoint();
162+
Vector3f normal = closest.getContactNormal();
97163

98-
Quaternion q = new Quaternion();
99-
q.lookAt(closest.getContactNormal(), Vector3f.UNIT_Y);
100-
mark.setLocalRotation(q);
164+
tempQuat.lookAt(normal, Vector3f.UNIT_Y);
165+
mark.setLocalRotation(tempQuat);
166+
mark.setLocalTranslation(point);
101167

102168
rootNode.attachChild(mark);
169+
hud.setText(closest.getGeometry().toString());
170+
103171
} else {
172+
hud.setText("No collision");
104173
rootNode.detachChild(mark);
105174
}
106175
}
107-
108-
/** A cube object for target practice */
109-
private Geometry makeCube(String name, float x, float y, float z) {
110-
Box box = new Box(1, 1, 1);
111-
Geometry cube = new Geometry(name, box);
112-
cube.setLocalTranslation(x, y, z);
113-
Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
114-
mat1.setColor("Color", ColorRGBA.randomColor());
115-
cube.setMaterial(mat1);
116-
return cube;
176+
177+
private BitmapText createLabel(int x, int y, String text) {
178+
BitmapText bmp = guiFont.createLabel(text);
179+
bmp.setLocalTranslation(x, settings.getHeight() - y, 0);
180+
bmp.setColor(ColorRGBA.Red);
181+
guiNode.attachChild(bmp);
182+
return bmp;
117183
}
118184

119-
/** A floor to show that the "shot" can go through several objects. */
120-
private Geometry makeFloor() {
121-
Box box = new Box(15, .2f, 15);
122-
Geometry floor = new Geometry("the Floor", box);
123-
floor.setLocalTranslation(0, -4, -5);
124-
Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
125-
mat1.setColor("Color", ColorRGBA.Gray);
126-
floor.setMaterial(mat1);
127-
return floor;
185+
private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) {
186+
Geometry geo = new Geometry(name, mesh);
187+
Material mat = new Material(assetManager, Materials.LIGHTING);
188+
mat.setBoolean("UseMaterialColors", true);
189+
mat.setColor("Diffuse", color);
190+
geo.setMaterial(mat);
191+
return geo;
128192
}
129193

130194
/**
131195
* A red arrow to mark the spot being picked.
132196
*/
133197
private void initMark() {
134198
Arrow arrow = new Arrow(Vector3f.UNIT_Z.mult(2f));
135-
mark = new Geometry("BOOM!", arrow);
136-
Material mark_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
137-
mark_mat.setColor("Color", ColorRGBA.Red);
138-
mark.setMaterial(mark_mat);
199+
mark = new Geometry("Marker", arrow);
200+
Material mat = new Material(assetManager, Materials.UNSHADED);
201+
mat.setColor("Color", ColorRGBA.Red);
202+
mark.setMaterial(mat);
139203
}
140204

141-
private Spatial makeCharacter() {
142-
// load a character from jme3-testdata
143-
Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
144-
golem.scale(0.5f);
145-
golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);
146-
147-
// We must add a light to make the model visible
148-
DirectionalLight sun = new DirectionalLight();
149-
sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());
150-
golem.addLight(sun);
151-
return golem;
152-
}
153205
}

0 commit comments

Comments
 (0)