diff --git a/jme3-core/src/main/java/com/jme3/collision/CollisionResult.java b/jme3-core/src/main/java/com/jme3/collision/CollisionResult.java index 95739f5a1c..380a5c6b95 100644 --- a/jme3-core/src/main/java/com/jme3/collision/CollisionResult.java +++ b/jme3-core/src/main/java/com/jme3/collision/CollisionResult.java @@ -32,6 +32,7 @@ package com.jme3.collision; import com.jme3.math.Triangle; +import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; import com.jme3.scene.Mesh; @@ -50,6 +51,7 @@ public class CollisionResult implements Comparable { private Vector3f contactNormal; private float distance; private int triangleIndex; + private Vector2f contactBaryCoords; public CollisionResult(Geometry geometry, Vector3f contactPoint, float distance, int triangleIndex) { this.geometry = geometry; @@ -86,6 +88,10 @@ public void setTriangleIndex(int index) { this.triangleIndex = index; } + public void setContactBaryCoords(Vector2f baryCoords) { + this.contactBaryCoords = baryCoords; + } + public Triangle getTriangle(Triangle store) { if (store == null) store = new Triangle(); @@ -135,6 +141,20 @@ public int getTriangleIndex() { return triangleIndex; } + /** + * Returns the barycentric coordinates of the contact point within the + * hit triangle, or null if barycentric coordinates are not available. + * + *

The returned vector stores (u, v) where u is the weight of the + * second triangle vertex, v is the weight of the third triangle vertex, + * and the weight of the first triangle vertex is (1 - u - v). + * + * @return the barycentric (u, v) coordinates, or null + */ + public Vector2f getContactBaryCoords() { + return contactBaryCoords; + } + @Override public String toString() { return "CollisionResult[geometry=" + geometry @@ -142,6 +162,7 @@ public String toString() { + ", contactNormal=" + contactNormal + ", distance=" + distance + ", triangleIndex=" + triangleIndex + + ", contactBaryCoords=" + contactBaryCoords + "]"; } } diff --git a/jme3-core/src/main/java/com/jme3/collision/CollisionResults.java b/jme3-core/src/main/java/com/jme3/collision/CollisionResults.java index 032e464307..95041c9363 100644 --- a/jme3-core/src/main/java/com/jme3/collision/CollisionResults.java +++ b/jme3-core/src/main/java/com/jme3/collision/CollisionResults.java @@ -46,6 +46,38 @@ public class CollisionResults implements Iterable { private ArrayList results = null; private boolean sorted = true; + private boolean requiresBaryCoords = false; + + /** + * Returns whether collision queries against this instance will compute and + * store barycentric coordinates in each {@link CollisionResult}. + * + *

Barycentric coordinate computation is disabled by default. + * Enable it when you need the (u, v) weights for texture-coordinate + * interpolation or other per-hit surface lookups. + * + * @return true if barycentric coordinates will be computed + * @see #setRequiresBaryCoords(boolean) + */ + public boolean isRequiresBaryCoords() { + return requiresBaryCoords; + } + + /** + * Controls whether collision queries against this instance should compute + * and store barycentric coordinates in each {@link CollisionResult}. + * + *

Barycentric coordinate computation is disabled by default. + * Enable it when you need the (u, v) weights for texture-coordinate + * interpolation or other per-hit surface lookups. + * + * @param requiresBaryCoords true to enable barycentric coordinate + * computation, false (the default) to skip it + * @see #isRequiresBaryCoords() + */ + public void setRequiresBaryCoords(boolean requiresBaryCoords) { + this.requiresBaryCoords = requiresBaryCoords; + } /** * Clears all collision results added to this list diff --git a/jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java b/jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java index 12bf88b23a..36326255b1 100644 --- a/jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java +++ b/jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java @@ -39,6 +39,7 @@ import com.jme3.math.Matrix4f; import com.jme3.math.Ray; import com.jme3.math.Triangle; +import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.util.TempVars; import java.io.IOException; @@ -401,13 +402,14 @@ public final int intersectWhere(Ray r, for (int i = node.leftIndex; i <= node.rightIndex; i++) { tree.getTriangle(i, v1, v2, v3); - float t = r.intersects(v1, v2, v3); + Vector2f baryCoords = results.isRequiresBaryCoords() ? new Vector2f() : null; + float t = r.intersects(v1, v2, v3, baryCoords); if (!Float.isInfinite(t)) { if (worldMatrix != null) { worldMatrix.mult(v1, v1); worldMatrix.mult(v2, v2); worldMatrix.mult(v3, v3); - float t_world = new Ray(o, d).intersects(v1, v2, v3); + float t_world = new Ray(o, d).intersects(v1, v2, v3, baryCoords); t = t_world; } @@ -422,6 +424,9 @@ public final int intersectWhere(Ray r, CollisionResult cr = new CollisionResult(contactPoint, worldSpaceDist); cr.setContactNormal(contactNormal); cr.setTriangleIndex(tree.getTriangleIndex(i)); + if (baryCoords != null) { + cr.setContactBaryCoords(baryCoords); + } results.addCollision(cr); cols++; } diff --git a/jme3-core/src/main/java/com/jme3/math/Ray.java b/jme3-core/src/main/java/com/jme3/math/Ray.java index 7b42102fef..9c78686c88 100644 --- a/jme3-core/src/main/java/com/jme3/math/Ray.java +++ b/jme3-core/src/main/java/com/jme3/math/Ray.java @@ -336,6 +336,90 @@ public float intersects(Vector3f v0, Vector3f v1, Vector3f v2) { return Float.POSITIVE_INFINITY; } + /** + * Test for an intersection between the ray and the given triangle and, + * if an intersection exists, store the barycentric coordinates of the + * contact point in baryCoords. + * + *

The stored (u, v) barycentric coordinates mean: u is the weight + * of v1, v is the weight of v2, and + * (1 - u - v) is the weight of v0. + * + * @param v0 first vertex of the triangle (not null, unaffected) + * @param v1 second vertex of the triangle (not null, unaffected) + * @param v2 third vertex of the triangle (not null, unaffected) + * @param baryCoords storage for the barycentric (u, v) coordinates of the + * contact point (modified on hit, may be null) + * @return the distance along the ray to the intersection, or + * {@link Float#POSITIVE_INFINITY} if there is no intersection + */ + public float intersects(Vector3f v0, Vector3f v1, Vector3f v2, Vector2f baryCoords) { + float edge1X = v1.x - v0.x; + float edge1Y = v1.y - v0.y; + float edge1Z = v1.z - v0.z; + + float edge2X = v2.x - v0.x; + float edge2Y = v2.y - v0.y; + float edge2Z = v2.z - v0.z; + + float normX = ((edge1Y * edge2Z) - (edge1Z * edge2Y)); + float normY = ((edge1Z * edge2X) - (edge1X * edge2Z)); + float normZ = ((edge1X * edge2Y) - (edge1Y * edge2X)); + + float dirDotNorm = direction.x * normX + direction.y * normY + direction.z * normZ; + + float diffX = origin.x - v0.x; + float diffY = origin.y - v0.y; + float diffZ = origin.z - v0.z; + + float sign; + if (dirDotNorm > FastMath.FLT_EPSILON) { + sign = 1; + } else if (dirDotNorm < -FastMath.FLT_EPSILON) { + sign = -1f; + dirDotNorm = -dirDotNorm; + } else { + // ray and triangle/quad are parallel + return Float.POSITIVE_INFINITY; + } + + float diffEdge2X = ((diffY * edge2Z) - (diffZ * edge2Y)); + float diffEdge2Y = ((diffZ * edge2X) - (diffX * edge2Z)); + float diffEdge2Z = ((diffX * edge2Y) - (diffY * edge2X)); + + float dirDotDiffxEdge2 = sign * (direction.x * diffEdge2X + + direction.y * diffEdge2Y + + direction.z * diffEdge2Z); + + if (dirDotDiffxEdge2 >= 0.0f) { + diffEdge2X = ((edge1Y * diffZ) - (edge1Z * diffY)); + diffEdge2Y = ((edge1Z * diffX) - (edge1X * diffZ)); + diffEdge2Z = ((edge1X * diffY) - (edge1Y * diffX)); + + float dirDotEdge1xDiff = sign * (direction.x * diffEdge2X + + direction.y * diffEdge2Y + + direction.z * diffEdge2Z); + + if (dirDotEdge1xDiff >= 0.0f) { + if (dirDotDiffxEdge2 + dirDotEdge1xDiff <= dirDotNorm) { + float diffDotNorm = -sign * (diffX * normX + diffY * normY + diffZ * normZ); + if (diffDotNorm >= 0.0f) { + // ray intersects triangle + float inv = 1f / dirDotNorm; + float t = diffDotNorm * inv; + if (baryCoords != null) { + baryCoords.set(dirDotDiffxEdge2 * inv, + dirDotEdge1xDiff * inv); + } + return t; + } + } + } + } + + return Float.POSITIVE_INFINITY; + } + /** * intersectWherePlanar determines if the Ray intersects a * quad defined by the specified points and if so it stores the point of @@ -393,13 +477,18 @@ public int collideWith(Collidable other, CollisionResults results) { return bv.collideWith(this, results); } else if (other instanceof AbstractTriangle) { AbstractTriangle tri = (AbstractTriangle) other; - float d = intersects(tri.get1(), tri.get2(), tri.get3()); + Vector2f baryCoords = results.isRequiresBaryCoords() ? new Vector2f() : null; + float d = intersects(tri.get1(), tri.get2(), tri.get3(), baryCoords); if (Float.isInfinite(d) || Float.isNaN(d)) { return 0; } Vector3f point = new Vector3f(direction).multLocal(d).addLocal(origin); - results.addCollision(new CollisionResult(point, d)); + CollisionResult cr = new CollisionResult(point, d); + if (baryCoords != null) { + cr.setContactBaryCoords(baryCoords); + } + results.addCollision(cr); return 1; } else { throw new UnsupportedCollisionException(); diff --git a/jme3-core/src/test/java/com/jme3/collision/RayCollisionTest.java b/jme3-core/src/test/java/com/jme3/collision/RayCollisionTest.java new file mode 100644 index 0000000000..66b84149b9 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/collision/RayCollisionTest.java @@ -0,0 +1,262 @@ +/* + * 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.collision; + +import com.jme3.math.Ray; +import com.jme3.math.Triangle; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests barycentric coordinate computation in ray vs. triangle collision. + */ +public class RayCollisionTest { + + private static final float DELTA = 1e-5f; + + /** + * A ray hitting exactly at v0 should give barycoords (0, 0), + * meaning the weight of v0 is 1 and the weights of v1 and v2 are 0. + */ + @Test + public void testBarycentricAtV0() { + // Triangle in the XY plane + Vector3f v0 = new Vector3f(0f, 0f, 0f); + Vector3f v1 = new Vector3f(1f, 0f, 0f); + Vector3f v2 = new Vector3f(0f, 1f, 0f); + + // Ray aimed straight at v0 + Ray ray = new Ray(new Vector3f(0f, 0f, 5f), new Vector3f(0f, 0f, -1f)); + + Vector2f bary = new Vector2f(); + float t = ray.intersects(v0, v1, v2, bary); + + Assert.assertFalse("Expected intersection", Float.isInfinite(t)); + Assert.assertEquals("u should be 0 at v0", 0f, bary.x, DELTA); + Assert.assertEquals("v should be 0 at v0", 0f, bary.y, DELTA); + // w0 = 1 - u - v = 1 + } + + /** + * A ray hitting exactly at v1 should give barycoords (1, 0). + */ + @Test + public void testBarycentricAtV1() { + Vector3f v0 = new Vector3f(0f, 0f, 0f); + Vector3f v1 = new Vector3f(1f, 0f, 0f); + Vector3f v2 = new Vector3f(0f, 1f, 0f); + + // Ray aimed straight at v1 + Ray ray = new Ray(new Vector3f(1f, 0f, 5f), new Vector3f(0f, 0f, -1f)); + + Vector2f bary = new Vector2f(); + float t = ray.intersects(v0, v1, v2, bary); + + Assert.assertFalse("Expected intersection", Float.isInfinite(t)); + Assert.assertEquals("u should be 1 at v1", 1f, bary.x, DELTA); + Assert.assertEquals("v should be 0 at v1", 0f, bary.y, DELTA); + // w0 = 1 - 1 - 0 = 0 + } + + /** + * A ray hitting exactly at v2 should give barycoords (0, 1). + */ + @Test + public void testBarycentricAtV2() { + Vector3f v0 = new Vector3f(0f, 0f, 0f); + Vector3f v1 = new Vector3f(1f, 0f, 0f); + Vector3f v2 = new Vector3f(0f, 1f, 0f); + + // Ray aimed straight at v2 + Ray ray = new Ray(new Vector3f(0f, 1f, 5f), new Vector3f(0f, 0f, -1f)); + + Vector2f bary = new Vector2f(); + float t = ray.intersects(v0, v1, v2, bary); + + Assert.assertFalse("Expected intersection", Float.isInfinite(t)); + Assert.assertEquals("u should be 0 at v2", 0f, bary.x, DELTA); + Assert.assertEquals("v should be 1 at v2", 1f, bary.y, DELTA); + // w0 = 1 - 0 - 1 = 0 + } + + /** + * A ray hitting the centroid should give barycoords (1/3, 1/3). + */ + @Test + public void testBarycentricAtCentroid() { + Vector3f v0 = new Vector3f(0f, 0f, 0f); + Vector3f v1 = new Vector3f(1f, 0f, 0f); + Vector3f v2 = new Vector3f(0f, 1f, 0f); + + // Centroid = (v0 + v1 + v2) / 3 = (1/3, 1/3, 0) + float cx = (v0.x + v1.x + v2.x) / 3f; + float cy = (v0.y + v1.y + v2.y) / 3f; + Ray ray = new Ray(new Vector3f(cx, cy, 5f), new Vector3f(0f, 0f, -1f)); + + Vector2f bary = new Vector2f(); + float t = ray.intersects(v0, v1, v2, bary); + + Assert.assertFalse("Expected intersection", Float.isInfinite(t)); + Assert.assertEquals("u should be 1/3 at centroid", 1f / 3f, bary.x, DELTA); + Assert.assertEquals("v should be 1/3 at centroid", 1f / 3f, bary.y, DELTA); + // w0 = 1 - 1/3 - 1/3 = 1/3 + } + + /** + * Verifies that the barycentric coordinates reconstruct the contact point. + */ + @Test + public void testBarycentricReconstructsContactPoint() { + Vector3f v0 = new Vector3f(2f, 0f, 0f); + Vector3f v1 = new Vector3f(0f, 3f, 0f); + Vector3f v2 = new Vector3f(-1f, -1f, 0f); + + // Hit point: midpoint of edge v0-v1 (weight v0=0.5, v1=0.5, v2=0) + float hitX = (v0.x + v1.x) / 2f; + float hitY = (v0.y + v1.y) / 2f; + Ray ray = new Ray(new Vector3f(hitX, hitY, 10f), new Vector3f(0f, 0f, -1f)); + + Vector2f bary = new Vector2f(); + float t = ray.intersects(v0, v1, v2, bary); + + Assert.assertFalse("Expected intersection", Float.isInfinite(t)); + + float u = bary.x; // weight of v1 + float v = bary.y; // weight of v2 + float w = 1f - u - v; // weight of v0 + + // Reconstruct hit point from barycentric coords + float recX = w * v0.x + u * v1.x + v * v2.x; + float recY = w * v0.y + u * v1.y + v * v2.y; + + Assert.assertEquals("Reconstructed X matches hit point", hitX, recX, DELTA); + Assert.assertEquals("Reconstructed Y matches hit point", hitY, recY, DELTA); + } + + /** + * Verifies that CollisionResult contains barycentric coords when a ray + * collides with an AbstractTriangle and the flag is enabled. + */ + @Test + public void testCollisionResultContainsBaryCoords() { + Vector3f v0 = new Vector3f(0f, 0f, 0f); + Vector3f v1 = new Vector3f(1f, 0f, 0f); + Vector3f v2 = new Vector3f(0f, 1f, 0f); + + // Ray aimed at centroid + float cx = (v0.x + v1.x + v2.x) / 3f; + float cy = (v0.y + v1.y + v2.y) / 3f; + Ray ray = new Ray(new Vector3f(cx, cy, 5f), new Vector3f(0f, 0f, -1f)); + + Triangle tri = new Triangle(v0, v1, v2); + CollisionResults results = new CollisionResults(); + results.setRequiresBaryCoords(true); + int count = ray.collideWith(tri, results); + + Assert.assertEquals("Expected exactly 1 collision", 1, count); + + CollisionResult cr = results.getClosestCollision(); + Assert.assertNotNull("CollisionResult should not be null", cr); + + Vector2f bary = cr.getContactBaryCoords(); + Assert.assertNotNull("Barycentric coords should be set when flag is enabled", bary); + Assert.assertEquals("u should be 1/3", 1f / 3f, bary.x, DELTA); + Assert.assertEquals("v should be 1/3", 1f / 3f, bary.y, DELTA); + } + + /** + * Verifies that barycentric coords are NOT computed when the flag is + * disabled (the default). + */ + @Test + public void testCollisionResultNoBaryCoordsByDefault() { + Vector3f v0 = new Vector3f(0f, 0f, 0f); + Vector3f v1 = new Vector3f(1f, 0f, 0f); + Vector3f v2 = new Vector3f(0f, 1f, 0f); + + float cx = (v0.x + v1.x + v2.x) / 3f; + float cy = (v0.y + v1.y + v2.y) / 3f; + Ray ray = new Ray(new Vector3f(cx, cy, 5f), new Vector3f(0f, 0f, -1f)); + + Triangle tri = new Triangle(v0, v1, v2); + CollisionResults results = new CollisionResults(); // flag defaults to false + int count = ray.collideWith(tri, results); + + Assert.assertEquals("Expected exactly 1 collision", 1, count); + + CollisionResult cr = results.getClosestCollision(); + Assert.assertNotNull("CollisionResult should not be null", cr); + Assert.assertNull("Barycentric coords should be null when flag is disabled", + cr.getContactBaryCoords()); + } + + /** + * Verifies that no intersection returns POSITIVE_INFINITY and bary is not + * modified. + */ + @Test + public void testNoIntersectionLeavesBaryUnchanged() { + Vector3f v0 = new Vector3f(0f, 0f, 0f); + Vector3f v1 = new Vector3f(1f, 0f, 0f); + Vector3f v2 = new Vector3f(0f, 1f, 0f); + + // Ray that misses the triangle (outside the XY square) + Ray ray = new Ray(new Vector3f(5f, 5f, 5f), new Vector3f(0f, 0f, -1f)); + + Vector2f bary = new Vector2f(99f, 99f); + float t = ray.intersects(v0, v1, v2, bary); + + Assert.assertTrue("Expected no intersection", Float.isInfinite(t)); + // bary should be untouched on miss + Assert.assertEquals("bary.x should be unchanged", 99f, bary.x, DELTA); + Assert.assertEquals("bary.y should be unchanged", 99f, bary.y, DELTA); + } + + /** + * Verifies that passing null for baryCoords still returns correct distance. + */ + @Test + public void testNullBaryCoordsDoesNotThrow() { + Vector3f v0 = new Vector3f(0f, 0f, 0f); + Vector3f v1 = new Vector3f(1f, 0f, 0f); + Vector3f v2 = new Vector3f(0f, 1f, 0f); + + Ray ray = new Ray(new Vector3f(0.25f, 0.25f, 5f), new Vector3f(0f, 0f, -1f)); + + float t = ray.intersects(v0, v1, v2, null); + + Assert.assertFalse("Expected intersection", Float.isInfinite(t)); + Assert.assertEquals("Distance should be 5", 5f, t, DELTA); + } +}