From c83f44483f21e3acb4e0716bdca196376919d32d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:51:49 +0000 Subject: [PATCH 1/3] Initial plan From 45746c99ed9fe17c30737dd8f2af7077b77b5f7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:57:54 +0000 Subject: [PATCH 2/3] Add optional collision detection to ChaseCamera Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/bbc3cb55-0143-4029-ae57-d45917e45660 Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- .../main/java/com/jme3/input/ChaseCamera.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java b/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java index fe494801b3..7c7a87719a 100644 --- a/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java +++ b/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java @@ -31,15 +31,19 @@ */ package com.jme3.input; +import com.jme3.collision.CollisionResult; +import com.jme3.collision.CollisionResults; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.input.controls.*; import com.jme3.math.FastMath; +import com.jme3.math.Ray; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; +import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.control.Control; import com.jme3.util.clone.Cloner; @@ -139,6 +143,11 @@ public class ChaseCamera implements ActionListener, AnalogListener, Control, Jme protected boolean zoomin; protected boolean hideCursorOnRotate = true; + protected boolean checkCollision = false; + protected Node collisionNode = null; + protected float collisionMinDistance = 0.1f; + private final CollisionResults collisionResults = new CollisionResults(); + private final Ray collisionRay = new Ray(); /** * Constructs the chase camera @@ -522,6 +531,10 @@ protected void updateCamera(float tpf) { } //computing the position computePosition(); + //checking for collision with the environment + if (checkCollision && collisionNode != null) { + checkCameraCollision(); + } //setting the position at last cam.setLocation(pos.addLocal(lookAtOffset)); } else { @@ -530,6 +543,10 @@ protected void updateCamera(float tpf) { rotation = targetRotation; distance = targetDistance; computePosition(); + //checking for collision with the environment + if (checkCollision && collisionNode != null) { + checkCameraCollision(); + } cam.setLocation(pos.addLocal(lookAtOffset)); } //keeping track on the previous position of the target @@ -1006,6 +1023,105 @@ public Vector3f getUpVector() { return initialUpVec; } + /** + * Checks for collisions between the camera and the collision node. + * If a collision is detected along the ray from the target to the + * computed camera position, the camera is moved to + * {@code collisionMinDistance} units in front of the collision point + * to prevent clipping through geometry. + */ + protected void checkCameraCollision() { + Vector3f targetPos = target.getWorldTranslation(); + // Direction from target to computed camera position + Vector3f camDir = pos.subtract(targetPos); + float maxDist = camDir.length(); + if (maxDist < FastMath.ZERO_TOLERANCE) { + return; + } + camDir.normalizeLocal(); + collisionRay.setOrigin(targetPos); + collisionRay.setDirection(camDir); + collisionRay.setLimit(maxDist); + + collisionResults.clear(); + collisionNode.collideWith(collisionRay, collisionResults); + + if (collisionResults.size() > 0) { + CollisionResult closest = collisionResults.getClosestCollision(); + float collisionDist = closest.getDistance(); + if (collisionDist < maxDist) { + // Place camera just in front of the collision point + float adjustedDist = Math.max(collisionDist - collisionMinDistance, 0); + pos.set(targetPos).addLocal(camDir.mult(adjustedDist)); + } + } + } + + /** + * Returns whether camera collision detection with the environment is enabled. + * + * @return true if collision detection is enabled, false otherwise (default=false) + */ + public boolean isCheckCollision() { + return checkCollision; + } + + /** + * Enables or disables camera collision detection with the environment. + * When enabled, the camera will not pass through geometry in the + * collision node. This feature is disabled by default. + * + * @param checkCollision true to enable collision detection, false to disable + * @see #setCollisionNode(Node) + */ + public void setCheckCollision(boolean checkCollision) { + this.checkCollision = checkCollision; + } + + /** + * Returns the node used for camera collision detection. + * + * @return the collision node, or null if none is set + */ + public Node getCollisionNode() { + return collisionNode; + } + + /** + * Sets the node to use for camera collision detection. + * When the chase camera has collision detection enabled, it will cast + * a ray from the target to the camera position and stop the camera + * before any geometry in this node. + * + * @param collisionNode the node to collide with (alias created), or null to disable + * @see #setCheckCollision(boolean) + */ + public void setCollisionNode(Node collisionNode) { + this.collisionNode = collisionNode; + } + + /** + * Returns the minimum distance to maintain between the camera and a + * collision point when collision detection is enabled. + * + * @return the minimum distance (in world units, default=0.1) + */ + public float getCollisionMinDistance() { + return collisionMinDistance; + } + + /** + * Sets the minimum distance to maintain between the camera and a + * collision point when collision detection is enabled. The camera will + * be placed this far in front of any detected collision. + * + * @param collisionMinDistance the desired minimum distance (in world units, + * default=0.1) + */ + public void setCollisionMinDistance(float collisionMinDistance) { + this.collisionMinDistance = collisionMinDistance; + } + public boolean isHideCursorOnRotate() { return hideCursorOnRotate; } From fb4466683c404152b240b5c2d4b3e79ddbf0a0f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:52:36 +0000 Subject: [PATCH 3/3] Refactor collision detection: CameraCollider interface + SceneCameraCollider Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/0532228e-79c6-460d-b67d-1b8d6f455024 Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- .../java/com/jme3/input/CameraCollider.java | 70 +++++ .../main/java/com/jme3/input/ChaseCamera.java | 123 ++------- .../com/jme3/input/SceneCameraCollider.java | 247 ++++++++++++++++++ 3 files changed, 339 insertions(+), 101 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/input/CameraCollider.java create mode 100644 jme3-core/src/main/java/com/jme3/input/SceneCameraCollider.java diff --git a/jme3-core/src/main/java/com/jme3/input/CameraCollider.java b/jme3-core/src/main/java/com/jme3/input/CameraCollider.java new file mode 100644 index 0000000000..3d087d4503 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/input/CameraCollider.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2009-2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input; + +import com.jme3.math.Vector3f; + +/** + * Interface for camera collision detection used by {@link ChaseCamera}. + * Implementations determine how the chase camera interacts with the scene + * when there is geometry between the target and the camera. + * + *
A {@code CameraCollider} receives the target's world position and the + * desired camera position, and may adjust the camera position in-place to + * prevent it from clipping through geometry.
+ * + *The built-in implementation is {@link SceneCameraCollider}, which uses + * ray-casting against one or more scene {@link com.jme3.scene.Node Nodes}. + * Custom implementations can integrate with physics engines or any other + * collision system.
+ * + * @see ChaseCamera#setCameraCollider(CameraCollider) + * @see SceneCameraCollider + */ +public interface CameraCollider { + + /** + * Adjusts the camera position to avoid passing through scene geometry. + * + *Implementations should test for obstructions between + * {@code targetPosition} and {@code camPosition} and, if any are found, + * update {@code camPosition} in-place so that the camera stays in front of + * the obstruction.
+ * + * @param targetPosition the world position of the chase camera's target + * (read-only) + * @param camPosition the desired camera position before collision + * adjustment; updated in-place with the adjusted + * position if a collision is detected + */ + void collide(Vector3f targetPosition, Vector3f camPosition); +} diff --git a/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java b/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java index 7c7a87719a..5aaff5d75b 100644 --- a/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java +++ b/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java @@ -31,19 +31,15 @@ */ package com.jme3.input; -import com.jme3.collision.CollisionResult; -import com.jme3.collision.CollisionResults; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.input.controls.*; import com.jme3.math.FastMath; -import com.jme3.math.Ray; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; -import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.control.Control; import com.jme3.util.clone.Cloner; @@ -143,11 +139,7 @@ public class ChaseCamera implements ActionListener, AnalogListener, Control, Jme protected boolean zoomin; protected boolean hideCursorOnRotate = true; - protected boolean checkCollision = false; - protected Node collisionNode = null; - protected float collisionMinDistance = 0.1f; - private final CollisionResults collisionResults = new CollisionResults(); - private final Ray collisionRay = new Ray(); + protected CameraCollider cameraCollider = null; /** * Constructs the chase camera @@ -532,8 +524,8 @@ protected void updateCamera(float tpf) { //computing the position computePosition(); //checking for collision with the environment - if (checkCollision && collisionNode != null) { - checkCameraCollision(); + if (cameraCollider != null) { + cameraCollider.collide(target.getWorldTranslation(), pos); } //setting the position at last cam.setLocation(pos.addLocal(lookAtOffset)); @@ -544,8 +536,8 @@ protected void updateCamera(float tpf) { distance = targetDistance; computePosition(); //checking for collision with the environment - if (checkCollision && collisionNode != null) { - checkCameraCollision(); + if (cameraCollider != null) { + cameraCollider.collide(target.getWorldTranslation(), pos); } cam.setLocation(pos.addLocal(lookAtOffset)); } @@ -1024,102 +1016,31 @@ public Vector3f getUpVector() { } /** - * Checks for collisions between the camera and the collision node. - * If a collision is detected along the ray from the target to the - * computed camera position, the camera is moved to - * {@code collisionMinDistance} units in front of the collision point - * to prevent clipping through geometry. - */ - protected void checkCameraCollision() { - Vector3f targetPos = target.getWorldTranslation(); - // Direction from target to computed camera position - Vector3f camDir = pos.subtract(targetPos); - float maxDist = camDir.length(); - if (maxDist < FastMath.ZERO_TOLERANCE) { - return; - } - camDir.normalizeLocal(); - collisionRay.setOrigin(targetPos); - collisionRay.setDirection(camDir); - collisionRay.setLimit(maxDist); - - collisionResults.clear(); - collisionNode.collideWith(collisionRay, collisionResults); - - if (collisionResults.size() > 0) { - CollisionResult closest = collisionResults.getClosestCollision(); - float collisionDist = closest.getDistance(); - if (collisionDist < maxDist) { - // Place camera just in front of the collision point - float adjustedDist = Math.max(collisionDist - collisionMinDistance, 0); - pos.set(targetPos).addLocal(camDir.mult(adjustedDist)); - } - } - } - - /** - * Returns whether camera collision detection with the environment is enabled. + * Returns the {@link CameraCollider} used to prevent the camera from + * passing through scene geometry, or {@code null} if collision detection + * is disabled (the default). * - * @return true if collision detection is enabled, false otherwise (default=false) + * @return the current camera collider, or {@code null} */ - public boolean isCheckCollision() { - return checkCollision; + public CameraCollider getCameraCollider() { + return cameraCollider; } /** - * Enables or disables camera collision detection with the environment. - * When enabled, the camera will not pass through geometry in the - * collision node. This feature is disabled by default. + * Sets the {@link CameraCollider} that will be used to prevent the camera + * from passing through scene geometry. Set to {@code null} (the default) + * to disable collision detection. * - * @param checkCollision true to enable collision detection, false to disable - * @see #setCollisionNode(Node) - */ - public void setCheckCollision(boolean checkCollision) { - this.checkCollision = checkCollision; - } - - /** - * Returns the node used for camera collision detection. - * - * @return the collision node, or null if none is set - */ - public Node getCollisionNode() { - return collisionNode; - } - - /** - * Sets the node to use for camera collision detection. - * When the chase camera has collision detection enabled, it will cast - * a ray from the target to the camera position and stop the camera - * before any geometry in this node. - * - * @param collisionNode the node to collide with (alias created), or null to disable - * @see #setCheckCollision(boolean) - */ - public void setCollisionNode(Node collisionNode) { - this.collisionNode = collisionNode; - } - - /** - * Returns the minimum distance to maintain between the camera and a - * collision point when collision detection is enabled. - * - * @return the minimum distance (in world units, default=0.1) - */ - public float getCollisionMinDistance() { - return collisionMinDistance; - } - - /** - * Sets the minimum distance to maintain between the camera and a - * collision point when collision detection is enabled. The camera will - * be placed this far in front of any detected collision. + *The built-in {@link SceneCameraCollider} supports ray-casting against + * one or more scene nodes with optional per-geometry exclusion via + * userData. Custom implementations can integrate with physics engines or + * any other collision system.
* - * @param collisionMinDistance the desired minimum distance (in world units, - * default=0.1) + * @param cameraCollider the collider to use, or {@code null} to disable + * @see SceneCameraCollider */ - public void setCollisionMinDistance(float collisionMinDistance) { - this.collisionMinDistance = collisionMinDistance; + public void setCameraCollider(CameraCollider cameraCollider) { + this.cameraCollider = cameraCollider; } public boolean isHideCursorOnRotate() { diff --git a/jme3-core/src/main/java/com/jme3/input/SceneCameraCollider.java b/jme3-core/src/main/java/com/jme3/input/SceneCameraCollider.java new file mode 100644 index 0000000000..0598eb1ce4 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/input/SceneCameraCollider.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2009-2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input; + +import com.jme3.collision.CollisionResult; +import com.jme3.collision.CollisionResults; +import com.jme3.math.FastMath; +import com.jme3.math.Ray; +import com.jme3.math.Vector3f; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link CameraCollider} that prevents the {@link ChaseCamera} from passing + * through scene geometry by casting a ray from the target to the desired camera + * position and adjusting the camera to sit just in front of the nearest hit. + * + *Multiple scene nodes can be registered via + * {@link #addNode(Node)} / {@link #setNodes(List)}, which is useful for paged + * or otherwise partitioned worlds.
+ * + *Individual geometries or entire sub-trees can be excluded from camera + * collision by setting a non-null userData value with the key returned by + * {@link #getExcludeTag()} on the spatial or any of its ancestors. By default + * the exclude tag is {@code null}, meaning no filtering is performed.
+ * + *Example usage:
+ *
+ * SceneCameraCollider collider = new SceneCameraCollider(rootNode);
+ * collider.setExcludeTag("ignoreCamera");
+ * chaseCamera.setCameraCollider(collider);
+ *
+ *
+ * @see CameraCollider
+ * @see ChaseCamera#setCameraCollider(CameraCollider)
+ */
+public class SceneCameraCollider implements CameraCollider {
+
+ private final ListAny geometry (or any of its parent nodes) that has a non-null userData + * value for this key is ignored during the collision check. This allows + * invisible barriers or other objects to block player movement without + * also affecting camera movement.
+ * + * @param excludeTag the userData key to check, or {@code null} to disable + */ + public void setExcludeTag(String excludeTag) { + this.excludeTag = excludeTag; + } + + /** + * Returns the minimum distance the camera will maintain from a detected + * collision point. + * + * @return the minimum distance (in world units, default=0.1) + */ + public float getMinDistance() { + return minDistance; + } + + /** + * Sets the minimum distance the camera will maintain from a detected + * collision point. The camera is placed this far in front of the hit. + * + * @param minDistance the desired minimum distance (in world units, + * default=0.1) + */ + public void setMinDistance(float minDistance) { + this.minDistance = minDistance; + } + + /** + * {@inheritDoc} + * + *Casts a ray from {@code targetPosition} toward {@code camPosition}. + * Iterates over all registered nodes and finds the closest collision that + * is not excluded. If any such collision is closer than the desired camera + * distance, the camera is moved to + * {@code max(collisionDistance - minDistance, 0)} units from the target.
+ */ + @Override + public void collide(Vector3f targetPosition, Vector3f camPosition) { + Vector3f camDir = camPosition.subtract(targetPosition); + float maxDist = camDir.length(); + if (maxDist < FastMath.ZERO_TOLERANCE) { + return; + } + camDir.normalizeLocal(); + ray.setOrigin(targetPosition); + ray.setDirection(camDir); + ray.setLimit(maxDist); + + float closestDist = maxDist; + for (Node node : nodes) { + collisionResults.clear(); + node.collideWith(ray, collisionResults); + if (collisionResults.size() == 0) { + continue; + } + // CollisionResults are sorted by distance (closest first) + for (CollisionResult result : collisionResults) { + float dist = result.getDistance(); + if (dist >= closestDist) { + // All remaining results are further away + break; + } + if (excludeTag != null && isExcluded(result.getGeometry())) { + continue; + } + closestDist = dist; + break; + } + } + + if (closestDist < maxDist) { + float adjustedDist = Math.max(closestDist - minDistance, 0); + // camDir is normalized and not modified by mult(float), which returns a new vector + camPosition.set(targetPosition).addLocal(camDir.mult(adjustedDist)); + } + } + + /** + * Returns {@code true} if the given spatial or any of its ancestors has a + * non-null value for the exclude tag. + * + * @param spatial the spatial to test + * @return true if the spatial should be excluded from collision + */ + private boolean isExcluded(Spatial spatial) { + Spatial current = spatial; + while (current != null) { + if (current.getUserData(excludeTag) != null) { + return true; + } + current = current.getParent(); + } + return false; + } +}