From 96f78f8b39a6ffe7371b6f3ee54ab061d28695fc Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Tue, 24 Dec 2024 22:29:05 +0100 Subject: [PATCH 01/20] Changed lerp() to lerpUnclamped() --- src/main/java/math/Vector4f.java | 2 +- src/test/java/math/Vector4fTest.java | 34 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/math/Vector4f.java b/src/main/java/math/Vector4f.java index ce065e19..25c896cf 100644 --- a/src/main/java/math/Vector4f.java +++ b/src/main/java/math/Vector4f.java @@ -270,7 +270,7 @@ public boolean isEqual(Vector4f other, float tolerance) { * @return A new vector representing the result of the interpolation. * @throws IllegalArgumentException If the given vector is null. */ - public Vector4f lerp(Vector4f other, float t) { + public Vector4f lerpUnclamped(Vector4f other, float t) { if (other == null) { throw new IllegalArgumentException("Other vector cannot be null."); } diff --git a/src/test/java/math/Vector4fTest.java b/src/test/java/math/Vector4fTest.java index f236d609..4a253b97 100644 --- a/src/test/java/math/Vector4fTest.java +++ b/src/test/java/math/Vector4fTest.java @@ -1668,17 +1668,17 @@ public void testIsEqualNegativeInfinity() { } // ---------------------------------------------------------------------------------------------- - // Lerp + // Lerp Unclamped // ---------------------------------------------------------------------------------------------- @Test - public void testLerpWithTZero() { + public void testLerpUnclampedWithTZero() { // Arrange Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); // Act - Vector4f result = start.lerp(end, 0.0f); + Vector4f result = start.lerpUnclamped(end, 0.0f); // Assert assertEquals(start.getX(), result.getX(), 0.0001f); @@ -1688,13 +1688,13 @@ public void testLerpWithTZero() { } @Test - public void testLerpWithTOne() { + public void testLerpUnclampedWithTOne() { // Arrange Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); // Act - Vector4f result = start.lerp(end, 1.0f); + Vector4f result = start.lerpUnclamped(end, 1.0f); // Assert assertEquals(end.getX(), result.getX(), 0.0001f); @@ -1704,13 +1704,13 @@ public void testLerpWithTOne() { } @Test - public void testLerpWithHalfwayT() { + public void testLerpUnclampedWithHalfwayT() { // Arrange Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); // Act - Vector4f result = start.lerp(end, 0.5f); + Vector4f result = start.lerpUnclamped(end, 0.5f); // Assert assertEquals(3.0f, result.getX(), 0.0001f); @@ -1720,13 +1720,13 @@ public void testLerpWithHalfwayT() { } @Test - public void testLerpWithNegativeT() { + public void testLerpUnclampedWithNegativeT() { // Arrange Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); // Act - Vector4f result = start.lerp(end, -0.5f); + Vector4f result = start.lerpUnclamped(end, -0.5f); // Assert assertEquals(-1.0f, result.getX(), 0.0001f); @@ -1736,13 +1736,13 @@ public void testLerpWithNegativeT() { } @Test - public void testLerpWithOverOneT() { + public void testLerpUnclampedWithOverOneT() { // Arrange Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); // Act - Vector4f result = start.lerp(end, 1.5f); + Vector4f result = start.lerpUnclamped(end, 1.5f); // Assert assertEquals(7.0f, result.getX(), 0.0001f); @@ -1752,13 +1752,13 @@ public void testLerpWithOverOneT() { } @Test - public void testLerpWithIdenticalStartAndEnd() { + public void testLerpUnclampedWithIdenticalStartAndEnd() { // Arrange Vector4f start = new Vector4f(3.0f, 3.0f, 3.0f, 3.0f); Vector4f end = new Vector4f(3.0f, 3.0f, 3.0f, 3.0f); // Act - Vector4f result = start.lerp(end, 0.5f); + Vector4f result = start.lerpUnclamped(end, 0.5f); // Assert assertEquals(3.0f, result.getX(), 0.0001f); @@ -1768,7 +1768,7 @@ public void testLerpWithIdenticalStartAndEnd() { } @Test - public void testLerpReturnsNewInstanceAndOriginalVectorsUntouched() { + public void testLerpUnclampedReturnsNewInstanceAndOriginalVectorsUntouched() { // Arrange Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); @@ -1776,7 +1776,7 @@ public void testLerpReturnsNewInstanceAndOriginalVectorsUntouched() { Vector4f endCopy = end.clone(); // Act - Vector4f result = start.lerp(end, 0.5f); + Vector4f result = start.lerpUnclamped(end, 0.5f); // Assert // Verify that a new instance is returned @@ -1797,7 +1797,7 @@ public void testLerpReturnsNewInstanceAndOriginalVectorsUntouched() { } @Test - public void testLerpWithNullThrowsException() { + public void testLerpUnclampedWithNullThrowsException() { // Arrange Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); Vector4f other = null; @@ -1806,7 +1806,7 @@ public void testLerpWithNullThrowsException() { assertThrows( IllegalArgumentException.class, () -> { - start.lerp(other, t); + start.lerpUnclamped(other, t); }); } From 613e50021d4340bc8ce96f0e7b83c9c7b2d24f62 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Tue, 24 Dec 2024 23:05:28 +0100 Subject: [PATCH 02/20] Added clamped lerp --- src/main/java/math/Vector4f.java | 30 +++++- src/test/java/math/Vector4fTest.java | 143 +++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) diff --git a/src/main/java/math/Vector4f.java b/src/main/java/math/Vector4f.java index 25c896cf..61d72252 100644 --- a/src/main/java/math/Vector4f.java +++ b/src/main/java/math/Vector4f.java @@ -263,13 +263,41 @@ public boolean isEqual(Vector4f other, float tolerance) { } /** - * Linearly interpolates between this vector and another vector by a factor t. + * Linearly interpolates between this vector and another vector by a factor t. The parameter t is + * clamped between [0...1]. * * @param other The other vector. * @param t The interpolation factor (0.0 <= t <= 1.0). * @return A new vector representing the result of the interpolation. * @throws IllegalArgumentException If the given vector is null. */ + public Vector4f lerp(Vector4f other, float t) { + if (other == null) { + throw new IllegalArgumentException("Other vector cannot be null."); + } + float lerpedX = Mathf.lerp(x, other.x, t); + float lerpedY = Mathf.lerp(y, other.y, t); + float lerpedZ = Mathf.lerp(z, other.z, t); + float lerpedW = Mathf.lerp(w, other.w, t); + return new Vector4f(lerpedX, lerpedY, lerpedZ, lerpedW); + } + + /** + * Performs linear interpolation between this vector and the specified vector using the given + * factor {@code t}. Unlike clamped interpolation, {@code t} is not restricted to the range [0, + * 1], allowing extrapolation. + * + * + * + * @param other the target vector to interpolate towards. + * @param t the interpolation factor, which is not clamped. + * @return a new vector representing the interpolated or extrapolated result. + * @throws IllegalArgumentException if the {@code other} vector is {@code null}. + */ public Vector4f lerpUnclamped(Vector4f other, float t) { if (other == null) { throw new IllegalArgumentException("Other vector cannot be null."); diff --git a/src/test/java/math/Vector4fTest.java b/src/test/java/math/Vector4fTest.java index 4a253b97..99179b1a 100644 --- a/src/test/java/math/Vector4fTest.java +++ b/src/test/java/math/Vector4fTest.java @@ -1810,6 +1810,149 @@ public void testLerpUnclampedWithNullThrowsException() { }); } + // ---------------------------------------------------------------------------------------------- + // Lerp (Clamped) + // ---------------------------------------------------------------------------------------------- + + @Test + public void testLerpClampedWithTZero() { + // Arrange + Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); + + // Act + Vector4f result = start.lerp(end, 0.0f); + + // Assert + assertEquals(start.getX(), result.getX(), 0.0001f); + assertEquals(start.getY(), result.getY(), 0.0001f); + assertEquals(start.getZ(), result.getZ(), 0.0001f); + assertEquals(start.getW(), result.getW(), 0.0001f); + } + + @Test + public void testLerpClampedWithTOne() { + // Arrange + Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); + + // Act + Vector4f result = start.lerp(end, 1.0f); + + // Assert + assertEquals(end.getX(), result.getX(), 0.0001f); + assertEquals(end.getY(), result.getY(), 0.0001f); + assertEquals(end.getZ(), result.getZ(), 0.0001f); + assertEquals(end.getW(), result.getW(), 0.0001f); + } + + @Test + public void testLerpClampedWithHalfwayT() { + // Arrange + Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); + + // Act + Vector4f result = start.lerp(end, 0.5f); + + // Assert + assertEquals(3.0f, result.getX(), 0.0001f); + assertEquals(4.0f, result.getY(), 0.0001f); + assertEquals(5.0f, result.getZ(), 0.0001f); + assertEquals(6.0f, result.getW(), 0.0001f); + } + + @Test + public void testLerpClampedWithNegativeT() { + // Arrange + Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); + + // Act + Vector4f result = start.lerp(end, -0.5f); + + // Assert + assertEquals(start.getX(), result.getX(), 0.0001f); // Clamped to 0 + assertEquals(start.getY(), result.getY(), 0.0001f); + assertEquals(start.getZ(), result.getZ(), 0.0001f); + assertEquals(start.getW(), result.getW(), 0.0001f); + } + + @Test + public void testLerpClampedWithOverOneT() { + // Arrange + Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); + + // Act + Vector4f result = start.lerp(end, 1.5f); + + // Assert + assertEquals(end.getX(), result.getX(), 0.0001f); // Clamped to 1 + assertEquals(end.getY(), result.getY(), 0.0001f); + assertEquals(end.getZ(), result.getZ(), 0.0001f); + assertEquals(end.getW(), result.getW(), 0.0001f); + } + + @Test + public void testLerpClampedWithIdenticalStartAndEnd() { + // Arrange + Vector4f start = new Vector4f(3.0f, 3.0f, 3.0f, 3.0f); + Vector4f end = new Vector4f(3.0f, 3.0f, 3.0f, 3.0f); + + // Act + Vector4f result = start.lerp(end, 0.5f); + + // Assert + assertEquals(3.0f, result.getX(), 0.0001f); + assertEquals(3.0f, result.getY(), 0.0001f); + assertEquals(3.0f, result.getZ(), 0.0001f); + assertEquals(3.0f, result.getW(), 0.0001f); + } + + @Test + public void testLerpClampedReturnsNewInstanceAndOriginalVectorsUntouched() { + // Arrange + Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f end = new Vector4f(5.0f, 6.0f, 7.0f, 8.0f); + Vector4f startCopy = start.clone(); + Vector4f endCopy = end.clone(); + + // Act + Vector4f result = start.lerp(end, 0.5f); + + // Assert + // Verify that a new instance is returned + assertNotSame(start, result); + assertNotSame(end, result); + + // Verify that the original start vector is untouched + assertEquals(startCopy.getX(), start.getX(), 0.0001f); + assertEquals(startCopy.getY(), start.getY(), 0.0001f); + assertEquals(startCopy.getZ(), start.getZ(), 0.0001f); + assertEquals(startCopy.getW(), start.getW(), 0.0001f); + + // Verify that the original end vector is untouched + assertEquals(endCopy.getX(), end.getX(), 0.0001f); + assertEquals(endCopy.getY(), end.getY(), 0.0001f); + assertEquals(endCopy.getZ(), end.getZ(), 0.0001f); + assertEquals(endCopy.getW(), end.getW(), 0.0001f); + } + + @Test + public void testLerpClampedWithNullThrowsException() { + // Arrange + Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f other = null; + float t = 0.5f; + + assertThrows( + IllegalArgumentException.class, + () -> { + start.lerp(other, t); + }); + } + // ---------------------------------------------------------------------------------------------- // Distance To Squared // ---------------------------------------------------------------------------------------------- From 64942f0b2f34c9d76efe8c25e8d7d7c88bdd3463 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Wed, 25 Dec 2024 08:36:53 +0100 Subject: [PATCH 03/20] Changed negateLocal to a more readable an potentially faster implementation. --- src/main/java/math/Vector4f.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/math/Vector4f.java b/src/main/java/math/Vector4f.java index 61d72252..4e2dc86e 100644 --- a/src/main/java/math/Vector4f.java +++ b/src/main/java/math/Vector4f.java @@ -173,10 +173,10 @@ public Vector4f negate() { * @return The current vector after negation. */ public Vector4f negateLocal() { - x = x == 0 ? 0 : -x; - y = y == 0 ? 0 : -y; - z = z == 0 ? 0 : -z; - w = w == 0 ? 0 : -w; + x *= -1; + y *= -1; + z *= -1; + w *= -1; return this; } From e0e87eb44b9f32dba7c282723821c959fd7030fd Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Wed, 25 Dec 2024 08:38:51 +0100 Subject: [PATCH 04/20] Changed negate back to old version. New version didn't pass tests. --- src/main/java/math/Vector4f.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/math/Vector4f.java b/src/main/java/math/Vector4f.java index 4e2dc86e..61d72252 100644 --- a/src/main/java/math/Vector4f.java +++ b/src/main/java/math/Vector4f.java @@ -173,10 +173,10 @@ public Vector4f negate() { * @return The current vector after negation. */ public Vector4f negateLocal() { - x *= -1; - y *= -1; - z *= -1; - w *= -1; + x = x == 0 ? 0 : -x; + y = y == 0 ? 0 : -y; + z = z == 0 ? 0 : -z; + w = w == 0 ? 0 : -w; return this; } From ba586f0d654949388a2369f6bec37dd79c3be6e3 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Wed, 25 Dec 2024 12:25:09 +0100 Subject: [PATCH 05/20] feat(math): Add Vector4f.multiply(Matrix4) method with unit tests - Implemented the `multiply(Matrix4)` method in the `Vector4f` class to perform matrix-vector multiplication. - Added JavaDoc to describe the method's behavior, mathematical operation, and usage. - Created comprehensive unit tests to verify the correctness of the method using various transformation matrices and vectors. - Initialized matrices using one-dimensional float arrays for consistency and readability in the tests. --- src/main/java/math/Vector4f.java | 31 ++++++++ src/test/java/math/Vector4fTest.java | 104 +++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/src/main/java/math/Vector4f.java b/src/main/java/math/Vector4f.java index 61d72252..941653fd 100644 --- a/src/main/java/math/Vector4f.java +++ b/src/main/java/math/Vector4f.java @@ -432,6 +432,37 @@ public Vector4f reflect(Vector4f normal) { return this.subtract(normal.multiply(2 * this.dot(normal))); } + /** + * Multiplies this vector by a 4x4 matrix and returns the resulting vector. + * + *

This method performs a matrix-vector multiplication, treating this vector as a column vector + * and applying the transformation defined by the given 4x4 matrix. The resulting vector + * represents the transformed coordinates. + * + *

The operation is mathematically equivalent to: + * + *

+   * [ newX ]   [ m00 m01 m02 m03 ]   [ x ]
+   * [ newY ] = [ m10 m11 m12 m13 ] * [ y ]
+   * [ newZ ]   [ m20 m21 m22 m23 ]   [ z ]
+   * [ newW ]   [ m30 m31 m32 m33 ]   [ w ]
+   * 
+ * + * @param m the 4x4 matrix to multiply with. This matrix defines the transformation (e.g., + * scaling, translation, rotation) to be applied to this vector. The matrix should be in + * row-major order for correct computation. + * @return a new {@code Vector4f} instance representing the transformed vector. + * @throws NullPointerException if {@code m} is {@code null}. + */ + public Vector4f multiply(Matrix4 m) { + float newX = m.get(0, 0) * x + m.get(0, 1) * y + m.get(0, 2) * z + m.get(0, 3) * w; + float newY = m.get(1, 0) * x + m.get(1, 1) * y + m.get(1, 2) * z + m.get(1, 3) * w; + float newZ = m.get(2, 0) * x + m.get(2, 1) * y + m.get(2, 2) * z + m.get(2, 3) * w; + float newW = m.get(3, 0) * x + m.get(3, 1) * y + m.get(3, 2) * z + m.get(3, 3) * w; + + return new Vector4f(newX, newY, newZ, newW); + } + /** * Returns the x component of the vector. * diff --git a/src/test/java/math/Vector4fTest.java b/src/test/java/math/Vector4fTest.java index 99179b1a..ae0a05a8 100644 --- a/src/test/java/math/Vector4fTest.java +++ b/src/test/java/math/Vector4fTest.java @@ -2817,4 +2817,108 @@ public void testSetAndGetW() { vector.setW(8.0f); assertEquals(8.0f, vector.getW(), 0.0001); } + + // ---------------------------------------------------------------------------------------------- + // Multiply Matrix4 + // ---------------------------------------------------------------------------------------------- + + @Test + void testMultiplyWithIdentityMatrix() { + Vector4f vector = new Vector4f(1.0f, 2.0f, 3.0f, 1.0f); + Matrix4 identityMatrix = + new Matrix4( + new float[] { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }); + + Vector4f result = vector.multiply(identityMatrix); + + assertEquals(1.0f, result.getX(), 1e-6, "X component should remain unchanged"); + assertEquals(2.0f, result.getY(), 1e-6, "Y component should remain unchanged"); + assertEquals(3.0f, result.getZ(), 1e-6, "Z component should remain unchanged"); + assertEquals(1.0f, result.getW(), 1e-6, "W component should remain unchanged"); + } + + @Test + void testMultiplyWithZeroMatrix() { + Vector4f vector = new Vector4f(1.0f, 2.0f, 3.0f, 1.0f); + Matrix4 zeroMatrix = + new Matrix4( + new float[] { + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f + }); + + Vector4f result = vector.multiply(zeroMatrix); + + assertEquals(0.0f, result.getX(), 1e-6, "X component should be 0"); + assertEquals(0.0f, result.getY(), 1e-6, "Y component should be 0"); + assertEquals(0.0f, result.getZ(), 1e-6, "Z component should be 0"); + assertEquals(0.0f, result.getW(), 1e-6, "W component should be 0"); + } + + @Test + void testMultiplyWithTranslationMatrix() { + Vector4f vector = new Vector4f(1.0f, 2.0f, 3.0f, 1.0f); + Matrix4 translationMatrix = + new Matrix4( + new float[] { + 1.0f, 0.0f, 0.0f, 5.0f, + 0.0f, 1.0f, 0.0f, 10.0f, + 0.0f, 0.0f, 1.0f, 15.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }); + + Vector4f result = vector.multiply(translationMatrix); + + assertEquals(6.0f, result.getX(), 1e-6, "X component should include translation"); + assertEquals(12.0f, result.getY(), 1e-6, "Y component should include translation"); + assertEquals(18.0f, result.getZ(), 1e-6, "Z component should include translation"); + assertEquals(1.0f, result.getW(), 1e-6, "W component should remain unchanged"); + } + + @Test + void testMultiplyWithScalingMatrix() { + Vector4f vector = new Vector4f(1.0f, 2.0f, 3.0f, 1.0f); + Matrix4 scalingMatrix = + new Matrix4( + new float[] { + 2.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 3.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 4.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }); + + Vector4f result = vector.multiply(scalingMatrix); + + assertEquals(2.0f, result.getX(), 1e-6, "X component should be scaled"); + assertEquals(6.0f, result.getY(), 1e-6, "Y component should be scaled"); + assertEquals(12.0f, result.getZ(), 1e-6, "Z component should be scaled"); + assertEquals(1.0f, result.getW(), 1e-6, "W component should remain unchanged"); + } + + @Test + void testMultiplyWithRotationMatrix() { + Vector4f vector = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); + Matrix4 rotationMatrix = + new Matrix4( + new float[] { + 0.0f, -1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }); + + Vector4f result = vector.multiply(rotationMatrix); + + assertEquals(0.0f, result.getX(), 1e-6, "X component after rotation"); + assertEquals(1.0f, result.getY(), 1e-6, "Y component after rotation"); + assertEquals(0.0f, result.getZ(), 1e-6, "Z component should remain unchanged"); + assertEquals(1.0f, result.getW(), 1e-6, "W component should remain unchanged"); + } } From 8c6012dd400f9d2ab3ec8a14dccc056006a5c9e6 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Wed, 25 Dec 2024 12:34:04 +0100 Subject: [PATCH 06/20] feat(math): Add Vector4f.toVector3f() method with unit tests - Implemented `toVector3f()` in `Vector4f` to convert a 4D vector into a 3D vector by discarding the w-component. - Included detailed JavaDoc explaining the conversion logic. - Added unit tests to verify correctness with positive, negative, and zero values. --- src/main/java/math/Vector4f.java | 9 ++++++ src/test/java/math/Vector4fTest.java | 46 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/main/java/math/Vector4f.java b/src/main/java/math/Vector4f.java index 941653fd..c208efce 100644 --- a/src/main/java/math/Vector4f.java +++ b/src/main/java/math/Vector4f.java @@ -463,6 +463,15 @@ public Vector4f multiply(Matrix4 m) { return new Vector4f(newX, newY, newZ, newW); } + /** + * Converts this 4D vector to a 3D vector by dropping the w-component. + * + * @return a new Vector3f instance containing the x, y, and z components of this Vector4f. + */ + public Vector3f toVector3f() { + return new Vector3f(x, y, z); + } + /** * Returns the x component of the vector. * diff --git a/src/test/java/math/Vector4fTest.java b/src/test/java/math/Vector4fTest.java index ae0a05a8..5726d593 100644 --- a/src/test/java/math/Vector4fTest.java +++ b/src/test/java/math/Vector4fTest.java @@ -2921,4 +2921,50 @@ void testMultiplyWithRotationMatrix() { assertEquals(0.0f, result.getZ(), 1e-6, "Z component should remain unchanged"); assertEquals(1.0f, result.getW(), 1e-6, "W component should remain unchanged"); } + + // ---------------------------------------------------------------------------------------------- + // To Vector3f + // ---------------------------------------------------------------------------------------------- + + @Test + public void testToVector3f() { + // Create a Vector4f instance + Vector4f vector4f = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + + // Convert to Vector3f + Vector3f result = vector4f.toVector3f(); + + // Assert the components are correctly converted + assertEquals(1.0f, result.getX(), 1e-6, "X component should match"); + assertEquals(2.0f, result.getY(), 1e-6, "Y component should match"); + assertEquals(3.0f, result.getZ(), 1e-6, "Z component should match"); + } + + @Test + public void testToVector3fNegativeValues() { + // Create a Vector4f instance with negative values + Vector4f vector4f = new Vector4f(-5.5f, -6.6f, -7.7f, -8.8f); + + // Convert to Vector3f + Vector3f result = vector4f.toVector3f(); + + // Assert the components are correctly converted + assertEquals(-5.5f, result.getX(), 1e-6, "X component should match"); + assertEquals(-6.6f, result.getY(), 1e-6, "Y component should match"); + assertEquals(-7.7f, result.getZ(), 1e-6, "Z component should match"); + } + + @Test + public void testToVector3fZeroValues() { + // Create a Vector4f instance with zero values + Vector4f vector4f = new Vector4f(0.0f, 0.0f, 0.0f, 0.0f); + + // Convert to Vector3f + Vector3f result = vector4f.toVector3f(); + + // Assert the components are correctly converted + assertEquals(0.0f, result.getX(), 1e-6, "X component should match"); + assertEquals(0.0f, result.getY(), 1e-6, "Y component should match"); + assertEquals(0.0f, result.getZ(), 1e-6, "Z component should match"); + } } From 5e155c43532a0cb43b01f0ce6a13c89706b8b370 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Wed, 25 Dec 2024 12:55:33 +0100 Subject: [PATCH 07/20] Add divideByW and divideByWLocal methods to Vector4f with unit tests - Implemented divideByW() method to create and return a new Vector4f by dividing the vector components by 'w' and setting 'w' to 1.0f, with exception handling for division by zero. - Implemented divideByWLocal() method to modify the current vector in place by dividing the components by 'w' and setting 'w' to 1.0f, with exception handling for division by zero. - Added unit tests to ensure correct functionality: - Test for divideByW() that verifies the original vector is untouched and a new instance is returned. - Test for divideByWLocal() that checks the modification of the original vector in place. - Test for division by zero that asserts an exception is thrown in both methods when 'w' is zero. These methods improve handling of homogeneous coordinates in 3D vectors. --- src/main/java/math/Vector4f.java | 36 ++++++++ src/test/java/math/Vector4fTest.java | 119 +++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/src/main/java/math/Vector4f.java b/src/main/java/math/Vector4f.java index c208efce..0c833911 100644 --- a/src/main/java/math/Vector4f.java +++ b/src/main/java/math/Vector4f.java @@ -158,6 +158,42 @@ public Vector4f divideLocal(float scalar) { return this; } + /** + * Divides the components of this vector by its w component to convert it from homogeneous + * coordinates to Euclidean coordinates. This operation returns a new vector with the transformed + * coordinates. If the w component is zero, an {@link ArithmeticException} is thrown as division + * by zero is not allowed. + * + * @return A new {@link Vector4f} with the components divided by w and w set to 1.0f. + * @throws ArithmeticException If the w component is zero. + */ + public Vector4f divideByW() { + if (w == 0.0f) { + throw new ArithmeticException("Division by zero."); + } + return new Vector4f(x / w, y / w, z / w, 1.0f); + } + + /** + * Divides the components of this vector by its w component to convert it from homogeneous + * coordinates to Euclidean coordinates. This operation modifies the current vector's components + * and sets w to 1.0f. If the w component is zero, an {@link ArithmeticException} is thrown as + * division by zero is not allowed. + * + * @return This {@link Vector4f} object, with its components modified and w set to 1.0f. + * @throws ArithmeticException If the w component is zero. + */ + public Vector4f divideByWLocal() { + if (w == 0.0f) { + throw new ArithmeticException("Division by zero."); + } + x /= w; + y /= w; + z /= w; + w = 1.0f; + return this; + } + /** * Negates this vector and returns the result. * diff --git a/src/test/java/math/Vector4fTest.java b/src/test/java/math/Vector4fTest.java index 5726d593..90233941 100644 --- a/src/test/java/math/Vector4fTest.java +++ b/src/test/java/math/Vector4fTest.java @@ -2967,4 +2967,123 @@ public void testToVector3fZeroValues() { assertEquals(0.0f, result.getY(), 1e-6, "Y component should match"); assertEquals(0.0f, result.getZ(), 1e-6, "Z component should match"); } + + // ---------------------------------------------------------------------------------------------- + // Divide By W + // ---------------------------------------------------------------------------------------------- + + @Test + public void testDivideByW_validW() { + // Test case where w is non-zero + Vector4f vector = new Vector4f(4.0f, 8.0f, 12.0f, 2.0f); + + Vector4f result = vector.divideByW(); + + // Expected result after division by w = 2.0f + Vector4f expected = new Vector4f(2.0f, 4.0f, 6.0f, 1.0f); + + assertEquals(expected, result, "The vector should be correctly divided by w."); + } + + @Test + public void testDivideByW_wIsZero() { + // Test case where w is zero, expecting an ArithmeticException + Vector4f vector = new Vector4f(4.0f, 8.0f, 12.0f, 0.0f); + + ArithmeticException exception = + assertThrows( + ArithmeticException.class, + () -> { + vector.divideByW(); + }); + + assertEquals( + "Division by zero.", exception.getMessage(), "Exception message should be correct."); + } + + @Test + public void testDivideByW_wIsNegative() { + // Test case where w is negative + Vector4f vector = new Vector4f(4.0f, 8.0f, 12.0f, -2.0f); + + Vector4f result = vector.divideByW(); + + // Expected result after division by w = -2.0f + Vector4f expected = new Vector4f(-2.0f, -4.0f, -6.0f, 1.0f); + + assertEquals(expected, result, "The vector should be correctly divided by w."); + } + + @Test + public void testDivideByW_createsNewInstance() { + // Create an original vector + Vector4f originalVector = new Vector4f(4.0f, 8.0f, 12.0f, 2.0f); + + // Call divideByW on the original vector + Vector4f newVector = originalVector.divideByW(); + + // Check that the original vector is not modified + assertEquals( + new Vector4f(4.0f, 8.0f, 12.0f, 2.0f), + originalVector, + "The original vector should remain unchanged."); + + // Check that a new vector is returned with the correct values + Vector4f expectedNewVector = new Vector4f(2.0f, 4.0f, 6.0f, 1.0f); + assertEquals( + expectedNewVector, + newVector, + "The returned vector should be a new instance with the expected values."); + + // Check that the original and new vectors are different instances + assertNotSame(originalVector, newVector); + } + + // ---------------------------------------------------------------------------------------------- + // Divide By W Local + // ---------------------------------------------------------------------------------------------- + + @Test + public void testDivideByWLocal_updatesOriginalVector() { + // Create a vector + Vector4f vector = new Vector4f(4.0f, 8.0f, 12.0f, 2.0f); + + // Call divideByWLocal on the vector + Vector4f result = vector.divideByWLocal(); + + // Check that the original vector has been modified + assertEquals( + new Vector4f(2.0f, 4.0f, 6.0f, 1.0f), + vector, + "The original vector should be modified in place."); + + // Check that the method returns the same instance (this) with the modified values + assertSame(vector, result); + } + + @Test + public void testDivideByWLocal_throwsArithmeticException_whenWIsZero() { + // Create a vector with w = 0 + Vector4f vector = new Vector4f(4.0f, 8.0f, 12.0f, 0.0f); + + // Assert that calling divideByWLocal throws ArithmeticException + assertThrows( + ArithmeticException.class, + () -> { + vector.divideByWLocal(); + }, + "Division by zero should throw an ArithmeticException."); + } + + @Test + public void testDivideByWLocal_doesNotCreateNewInstance() { + // Create a vector + Vector4f vector = new Vector4f(4.0f, 8.0f, 12.0f, 2.0f); + + // Call divideByWLocal on the vector + vector.divideByWLocal(); + + // Check that the method does not create a new instance, but modifies the original instance + assertSame(vector, vector.divideByWLocal()); + } } From a14a555f705413e0788303ddae0ee279408a93b9 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Thu, 26 Dec 2024 14:23:18 +0100 Subject: [PATCH 08/20] Refactor: - Replaced deprecated methods - Changed format - Renamed snapToGround / snapToGroundAndCenterZ --- .../java/mesh/creator/assets/ArchCreator.java | 355 +++++++++--------- 1 file changed, 176 insertions(+), 179 deletions(-) diff --git a/src/main/java/mesh/creator/assets/ArchCreator.java b/src/main/java/mesh/creator/assets/ArchCreator.java index a865597e..047603e7 100644 --- a/src/main/java/mesh/creator/assets/ArchCreator.java +++ b/src/main/java/mesh/creator/assets/ArchCreator.java @@ -5,194 +5,191 @@ import mesh.Mesh3D; import mesh.creator.IMeshCreator; import mesh.modifier.SolidifyModifier; +import mesh.modifier.TranslateModifier; public class ArchCreator implements IMeshCreator { - private int segments; + private int segments; - private float radius; + private float radius; - private float extendTop; + private float extendTop; - private float extendBottom; + private float extendBottom; - private float extendLeft; + private float extendLeft; - private float extendRight; + private float extendRight; - private float depth; + private float depth; - private Mesh3D mesh; - - public ArchCreator() { - segments = 15; - radius = 1; - extendTop = 0.5f; - extendBottom = 2; - extendLeft = 1; - extendRight = 1; - depth = 1; - } - - @Override - public Mesh3D create() { - initializeMesh(); - createVertices(); - createFaces(); - createArc(); - solidify(); - snapToGround(); - return mesh; - } - - private void createVertices() { - createLeftVertices(); - createRightVertices(); - } - - private void createFaces() { - createLeftFace(); - createRightFace(); - } - - private void createArc() { - float extendStep = calculateWidth() / segments; - float offsetLeft = -radius - extendLeft; - - for (int i = 0; i <= segments; i++) { - float x = offsetLeft + (i * extendStep); - Vector3f v1 = new Vector3f(x, -radius - extendTop, 0); - Vector3f v0 = createPointOnCircleAt(i); - - if (i > 0 && i < segments) - v1.setX(v0.getX()); - - addFaceAt(i); - mesh.add(v0); - mesh.add(v1); - } - } - - private Vector3f createPointOnCircleAt(int i) { - float angle = Mathf.PI + (i * (Mathf.PI / segments)); - return pointOnCircle(angle); - } - - private Vector3f pointOnCircle(float angrad) { - float x = radius * Mathf.cos(angrad); - float y = radius * Mathf.sin(angrad); - return new Vector3f(x, y, 0); - } - - private float calculateWidth() { - return radius + radius + extendLeft + extendRight; - } - - private void snapToGround() { - mesh.translateY(-extendBottom); - mesh.translateZ(depth / 2f); - } - - private void initializeMesh() { - mesh = new Mesh3D(); - } - - private void solidify() { - new SolidifyModifier(depth).modify(mesh); - } - - private void createLeftFace() { - mesh.addFace(0, 1, 5, 4); - } - - private void createRightFace() { - int a = 2 * (segments + 1) + 4; - addFace(3, 2, a - 2, a - 1); - } - - private void addFace(int... indices) { - mesh.addFace(indices); - } - - private void createLeftVertices() { - addVertex(-radius, extendBottom, 0); - addVertex(-radius - extendLeft, extendBottom, 0); - } - - private void createRightVertices() { - addVertex(radius, extendBottom, 0); - addVertex(radius + extendRight, extendBottom, 0); - } - - private void addFaceAt(int i) { - if (i >= segments) - return; - int index = (i * 2) + 4; - int index0 = index; - int index1 = index + 1; - int index2 = index + 3; - int index3 = index + 2; - mesh.addFace(index0, index1, index2, index3); - } - - private void addVertex(float x, float y, float z) { - mesh.addVertex(x, y, z); - } - - public int getSegments() { - return segments; - } - - public void setSegments(int segments) { - this.segments = segments; - } - - public float getRadius() { - return radius; - } - - public void setRadius(float radius) { - this.radius = radius; - } - - public float getExtendTop() { - return extendTop; - } - - public void setExtendTop(float extendTop) { - this.extendTop = extendTop; - } - - public float getExtendBottom() { - return extendBottom; - } - - public void setExtendBottom(float extendBottom) { - this.extendBottom = extendBottom; - } - - public float getExtendLeft() { - return extendLeft; - } - - public void setExtendLeft(float extendLeft) { - this.extendLeft = extendLeft; - } - - public float getExtendRight() { - return extendRight; - } - - public void setExtendRight(float extendRight) { - this.extendRight = extendRight; - } - - public float getDepth() { - return depth; - } - - public void setDepth(float depth) { - this.depth = depth; - } + private Mesh3D mesh; + public ArchCreator() { + segments = 15; + radius = 1; + extendTop = 0.5f; + extendBottom = 2; + extendLeft = 1; + extendRight = 1; + depth = 1; + } + + @Override + public Mesh3D create() { + initializeMesh(); + createVertices(); + createFaces(); + createArc(); + solidify(); + snapToGroundAndCenterZ(); + return mesh; + } + + private void createVertices() { + createLeftVertices(); + createRightVertices(); + } + + private void createFaces() { + createLeftFace(); + createRightFace(); + } + + private void createArc() { + float extendStep = calculateWidth() / segments; + float offsetLeft = -radius - extendLeft; + + for (int i = 0; i <= segments; i++) { + float x = offsetLeft + (i * extendStep); + Vector3f v1 = new Vector3f(x, -radius - extendTop, 0); + Vector3f v0 = createPointOnCircleAt(i); + + if (i > 0 && i < segments) v1.setX(v0.getX()); + + addFaceAt(i); + mesh.add(v0); + mesh.add(v1); + } + } + + private Vector3f createPointOnCircleAt(int i) { + float angle = Mathf.PI + (i * (Mathf.PI / segments)); + return pointOnCircle(angle); + } + + private Vector3f pointOnCircle(float angrad) { + float x = radius * Mathf.cos(angrad); + float y = radius * Mathf.sin(angrad); + return new Vector3f(x, y, 0); + } + + private float calculateWidth() { + return radius + radius + extendLeft + extendRight; + } + + private void snapToGroundAndCenterZ() { + mesh.apply(new TranslateModifier(0, -extendBottom, depth / 2f)); + } + + private void initializeMesh() { + mesh = new Mesh3D(); + } + + private void solidify() { + new SolidifyModifier(depth).modify(mesh); + } + + private void createLeftFace() { + mesh.addFace(0, 1, 5, 4); + } + + private void createRightFace() { + int a = 2 * (segments + 1) + 4; + addFace(3, 2, a - 2, a - 1); + } + + private void addFace(int... indices) { + mesh.addFace(indices); + } + + private void createLeftVertices() { + addVertex(-radius, extendBottom, 0); + addVertex(-radius - extendLeft, extendBottom, 0); + } + + private void createRightVertices() { + addVertex(radius, extendBottom, 0); + addVertex(radius + extendRight, extendBottom, 0); + } + + private void addFaceAt(int i) { + if (i >= segments) return; + int index = (i * 2) + 4; + int index0 = index; + int index1 = index + 1; + int index2 = index + 3; + int index3 = index + 2; + mesh.addFace(index0, index1, index2, index3); + } + + private void addVertex(float x, float y, float z) { + mesh.addVertex(x, y, z); + } + + public int getSegments() { + return segments; + } + + public void setSegments(int segments) { + this.segments = segments; + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } + + public float getExtendTop() { + return extendTop; + } + + public void setExtendTop(float extendTop) { + this.extendTop = extendTop; + } + + public float getExtendBottom() { + return extendBottom; + } + + public void setExtendBottom(float extendBottom) { + this.extendBottom = extendBottom; + } + + public float getExtendLeft() { + return extendLeft; + } + + public void setExtendLeft(float extendLeft) { + this.extendLeft = extendLeft; + } + + public float getExtendRight() { + return extendRight; + } + + public void setExtendRight(float extendRight) { + this.extendRight = extendRight; + } + + public float getDepth() { + return depth; + } + + public void setDepth(float depth) { + this.depth = depth; + } } From 56011d10cf0471c181646db86a5494009d5928b2 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Thu, 26 Dec 2024 14:50:00 +0100 Subject: [PATCH 09/20] Feat: Added boolean equals(float a, float b, float epsilon) --- src/main/java/math/Mathf.java | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/math/Mathf.java b/src/main/java/math/Mathf.java index 2099fb22..f1cda93e 100644 --- a/src/main/java/math/Mathf.java +++ b/src/main/java/math/Mathf.java @@ -94,9 +94,15 @@ public class Mathf { * `numberOfColumns` is less than or equal to zero. */ public static int toOneDimensionalIndex(int rowIndex, int colIndex, int numberOfColumns) { - if (rowIndex < 0 || colIndex < 0) throw new IllegalArgumentException(); - - if (numberOfColumns <= 0) throw new IllegalArgumentException(); + if (numberOfColumns <= 0) { + throw new IllegalArgumentException("NumberOfColumns must be greater than zero."); + } + if (rowIndex < 0) { + throw new IllegalArgumentException("rowIndex must be non-negative"); + } + if (colIndex < 0 || colIndex >= numberOfColumns) { + throw new IllegalArgumentException("colIndex is out of bounds"); + } return rowIndex * numberOfColumns + colIndex; } @@ -890,4 +896,17 @@ public static float normalizeAngle(float angle) { return angle; } + + /** + * Compares two floating-point numbers for equality within a given tolerance (epsilon). + * + * @param a The first number. + * @param b The second number. + * @param epsilon The tolerance within which the two numbers are considered equal. + * @return True if the absolute difference between the two numbers is less than or equal to + * epsilon, otherwise false. + */ + public static boolean equals(float a, float b, float epsilon) { + return Math.abs(a - b) <= epsilon; + } } From 53c9625ea2cd5568d4e0bb31e90b6587d8c82eb3 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Fri, 27 Dec 2024 13:02:08 +0100 Subject: [PATCH 10/20] Format changes. --- .../creator/primitives/SolidArcCreator.java | 345 +++++++++--------- 1 file changed, 171 insertions(+), 174 deletions(-) diff --git a/src/main/java/mesh/creator/primitives/SolidArcCreator.java b/src/main/java/mesh/creator/primitives/SolidArcCreator.java index 8dc09317..11744fa6 100644 --- a/src/main/java/mesh/creator/primitives/SolidArcCreator.java +++ b/src/main/java/mesh/creator/primitives/SolidArcCreator.java @@ -7,187 +7,184 @@ public class SolidArcCreator implements IMeshCreator { - private int index; + private int index; - private int vertices; + private int vertices; + + private float angle; - private float angle; + private float outerRadius; + + private float innerRadius; - private float outerRadius; + private float height; + + private boolean capStart; - private float innerRadius; + private boolean capEnd; + + private Mesh3D mesh; - private float height; + private ArcCreator creator; + + private Mesh3D outerArc; - private boolean capStart; + private Mesh3D innerArc; + + public SolidArcCreator() { + vertices = 17; + angle = Mathf.HALF_PI; + outerRadius = 3; + innerRadius = 2; + height = 1; + } + + @Override + public Mesh3D create() { + initializeMesh(); + initializeCreator(); + createArcs(); + createVertices(); + createFaces(); + capStart(); + capEnd(); + return mesh; + } + + private void initializeMesh() { + mesh = new Mesh3D(); + } + + private void initializeCreator() { + creator = new ArcCreator(); + creator.setStartAngle(0); + creator.setEndAngle(angle); + creator.setVertices(vertices); + } - private boolean capEnd; + private void createArcs() { + creator.setRadius(outerRadius); + outerArc = creator.create(); + creator.setRadius(innerRadius); + innerArc = creator.create(); + } - private Mesh3D mesh; - - private ArcCreator creator; - - private Mesh3D outerArc; - - private Mesh3D innerArc; - - public SolidArcCreator() { - vertices = 17; - angle = Mathf.HALF_PI; - outerRadius = 3; - innerRadius = 2; - height = 1; - } - - @Override - public Mesh3D create() { - initializeMesh(); - initializeCreator(); - createArcs(); - createVertices(); - createFaces(); - capStart(); - capEnd(); - return mesh; - } - - private void initializeMesh() { - mesh = new Mesh3D(); - } - - private void initializeCreator() { - creator = new ArcCreator(); - creator.setStartAngle(0); - creator.setEndAngle(angle); - creator.setVertices(vertices); - } - - private void createArcs() { - creator.setRadius(outerRadius); - outerArc = creator.create(); - creator.setRadius(innerRadius); - innerArc = creator.create(); - } - - private void createVertices() { - float halfHeight = height / 2f; - for (int i = 0; i < vertices; i++) { - addVertex(outerArc.getVertexAt(i).add(0, -halfHeight, 0)); - addVertex(innerArc.getVertexAt(i).add(0, -halfHeight, 0)); - addVertex(innerArc.getVertexAt(i).add(0, +halfHeight, 0)); - addVertex(outerArc.getVertexAt(i).add(0, +halfHeight, 0)); - } - } - - private void addVertex(Vector3f v) { - mesh.add(v); - } - - private void capStart() { - if (!capStart) - return; - mesh.addFace(0, 1, 2, 3); - } - - private void capEnd() { - if (!capEnd) - return; - int index = mesh.vertices.size(); - mesh.addFace(index - 1, index - 2, index - 3, index - 4); - } - - private void createFaces() { - for (int i = 0; i < (vertices) * 4 - 4; i += 4) { - setIndex(i); - addTopFaceAtIndex(); - addOuterFaceAtIndex(); - addBottomFaceAtIndex(); - addInnerFaceAtIndex(); - } - } - - private void addTopFaceAtIndex() { - addFaceAtIndex(1, 0, 4, 5); - } - - private void addOuterFaceAtIndex() { - addFaceAtIndex(4, 0, 3, 7); - } - - private void addBottomFaceAtIndex() { - addFaceAtIndex(2, 6, 7, 3); - } - - private void addInnerFaceAtIndex() { - addFaceAtIndex(6, 2, 1, 5); - } - - private void addFaceAtIndex(int... indices) { - int[] indices1 = new int[indices.length]; - for (int i = 0; i < indices.length; i++) { - indices1[i] = index + indices[i]; - } - mesh.addFace(indices1); + private void createVertices() { + float halfHeight = height / 2f; + for (int i = 0; i < vertices; i++) { + addVertex(outerArc.getVertexAt(i).add(0, -halfHeight, 0)); + addVertex(innerArc.getVertexAt(i).add(0, -halfHeight, 0)); + addVertex(innerArc.getVertexAt(i).add(0, +halfHeight, 0)); + addVertex(outerArc.getVertexAt(i).add(0, +halfHeight, 0)); } - - private void setIndex(int index) { - this.index = index; - } - - public float getAngle() { - return angle; - } - - public void setAngle(float angle) { - this.angle = angle; - } - - public float getInnerRadius() { - return innerRadius; - } - - public void setInnerRadius(float innerRadius) { - this.innerRadius = innerRadius; - } - - public float getOuterRadius() { - return outerRadius; - } - - public void setOuterRadius(float outerRadius) { - this.outerRadius = outerRadius; - } - - public int getRotationSegments() { - return vertices - 1; - } - - public void setRotationSegments(int rotationSegments) { - this.vertices = rotationSegments + 1; - } - - public float getHeight() { - return height; - } - - public void setHeight(float height) { - this.height = height; - } - - public boolean isCapStart() { - return capStart; - } - - public void setCapStart(boolean capStart) { - this.capStart = capStart; - } - - public boolean isCapEnd() { - return capEnd; - } - - public void setCapEnd(boolean capEnd) { - this.capEnd = capEnd; - } - + } + + private void addVertex(Vector3f v) { + mesh.add(v); + } + + private void capStart() { + if (!capStart) return; + mesh.addFace(0, 1, 2, 3); + } + + private void capEnd() { + if (!capEnd) return; + int index = mesh.vertices.size(); + mesh.addFace(index - 1, index - 2, index - 3, index - 4); + } + + private void createFaces() { + for (int i = 0; i < (vertices) * 4 - 4; i += 4) { + setIndex(i); + addTopFaceAtIndex(); + addOuterFaceAtIndex(); + addBottomFaceAtIndex(); + addInnerFaceAtIndex(); + } + } + + private void addTopFaceAtIndex() { + addFaceAtIndex(1, 0, 4, 5); + } + + private void addOuterFaceAtIndex() { + addFaceAtIndex(4, 0, 3, 7); + } + + private void addBottomFaceAtIndex() { + addFaceAtIndex(2, 6, 7, 3); + } + + private void addInnerFaceAtIndex() { + addFaceAtIndex(6, 2, 1, 5); + } + + private void addFaceAtIndex(int... indices) { + int[] indices1 = new int[indices.length]; + for (int i = 0; i < indices.length; i++) { + indices1[i] = index + indices[i]; + } + mesh.addFace(indices1); + } + + private void setIndex(int index) { + this.index = index; + } + + public float getAngle() { + return angle; + } + + public void setAngle(float angle) { + this.angle = angle; + } + + public float getInnerRadius() { + return innerRadius; + } + + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } + + public float getOuterRadius() { + return outerRadius; + } + + public void setOuterRadius(float outerRadius) { + this.outerRadius = outerRadius; + } + + public int getRotationSegments() { + return vertices - 1; + } + + public void setRotationSegments(int rotationSegments) { + this.vertices = rotationSegments + 1; + } + + public float getHeight() { + return height; + } + + public void setHeight(float height) { + this.height = height; + } + + public boolean isCapStart() { + return capStart; + } + + public void setCapStart(boolean capStart) { + this.capStart = capStart; + } + + public boolean isCapEnd() { + return capEnd; + } + + public void setCapEnd(boolean capEnd) { + this.capEnd = capEnd; + } } From f8cb6c0e3aefe8799c280dfd0c50db9d5c62eb9a Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Fri, 27 Dec 2024 13:10:42 +0100 Subject: [PATCH 11/20] Format changes. --- .../mesh/creator/primitives/ArcCreator.java | 117 +++++++++--------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/src/main/java/mesh/creator/primitives/ArcCreator.java b/src/main/java/mesh/creator/primitives/ArcCreator.java index 43c372a8..1a25f3b5 100644 --- a/src/main/java/mesh/creator/primitives/ArcCreator.java +++ b/src/main/java/mesh/creator/primitives/ArcCreator.java @@ -6,82 +6,81 @@ public class ArcCreator implements IMeshCreator { - private float startAngle; + private float startAngle; - private float endAngle; + private float endAngle; - private float radius; + private float radius; - private int vertices; + private int vertices; - private Mesh3D mesh; + private Mesh3D mesh; - public ArcCreator() { - startAngle = 0; - endAngle = Mathf.TWO_PI; - radius = 1; - vertices = 32; - } + public ArcCreator() { + startAngle = 0; + endAngle = Mathf.TWO_PI; + radius = 1; + vertices = 32; + } - @Override - public Mesh3D create() { - initializeMesh(); - createVertices(); - return mesh; - } + @Override + public Mesh3D create() { + initializeMesh(); + createVertices(); + return mesh; + } - private void initializeMesh() { - mesh = new Mesh3D(); - } + private void initializeMesh() { + mesh = new Mesh3D(); + } - private void createVertices() { - float angleBetweenPoints = calculateAngleBetweenPoints(); - for (int i = 0; i < vertices; i++) { - float currentAngle = angleBetweenPoints * i; - float x = radius * Mathf.cos(currentAngle); - float z = radius * Mathf.sin(currentAngle); - addVertex(x, 0, z); - } + private void createVertices() { + float angleBetweenPoints = calculateAngleBetweenPoints(); + for (int i = 0; i < vertices; i++) { + float currentAngle = angleBetweenPoints * i; + float x = radius * Mathf.cos(currentAngle); + float z = radius * Mathf.sin(currentAngle); + addVertex(x, 0, z); } + } - private void addVertex(float x, float y, float z) { - mesh.addVertex(x, y, z); - } + private void addVertex(float x, float y, float z) { + mesh.addVertex(x, y, z); + } - private float calculateAngleBetweenPoints() { - return startAngle + ((endAngle - startAngle) / ((float) vertices - 1)); - } - - public float getStartAngle() { - return startAngle; - } + private float calculateAngleBetweenPoints() { + return startAngle + ((endAngle - startAngle) / ((float) vertices - 1)); + } - public void setStartAngle(float startAngle) { - this.startAngle = startAngle; - } + public float getStartAngle() { + return startAngle; + } - public float getEndAngle() { - return endAngle; - } + public void setStartAngle(float startAngle) { + this.startAngle = startAngle; + } - public void setEndAngle(float endAngle) { - this.endAngle = endAngle; - } + public float getEndAngle() { + return endAngle; + } - public float getRadius() { - return radius; - } + public void setEndAngle(float endAngle) { + this.endAngle = endAngle; + } - public void setRadius(float radius) { - this.radius = radius; - } + public float getRadius() { + return radius; + } - public int getVertices() { - return vertices; - } + public void setRadius(float radius) { + this.radius = radius; + } - public void setVertices(int vertices) { - this.vertices = vertices; - } + public int getVertices() { + return vertices; + } + public void setVertices(int vertices) { + this.vertices = vertices; + } } From bda6c0ac634483115f722aa6e9758f60b70cf8a2 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Fri, 27 Dec 2024 13:17:47 +0100 Subject: [PATCH 12/20] Initial implementation of the ProArchCreator class for generating 3D arch shapes. --- .../mesh/creator/assets/ProArchCreator.java | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 src/main/java/mesh/creator/assets/ProArchCreator.java diff --git a/src/main/java/mesh/creator/assets/ProArchCreator.java b/src/main/java/mesh/creator/assets/ProArchCreator.java new file mode 100644 index 00000000..a78617ec --- /dev/null +++ b/src/main/java/mesh/creator/assets/ProArchCreator.java @@ -0,0 +1,259 @@ +package mesh.creator.assets; + +import math.Mathf; +import mesh.Mesh3D; +import mesh.creator.IMeshCreator; +import mesh.creator.primitives.SolidArcCreator; +import mesh.creator.primitives.TubeCreator; +import mesh.modifier.RotateXModifier; + +/** + * Creates a 3D arch shape, inspired by Unity's ProBuilder. + * + *

This class provides methods to configure and generate a 3D arch with customizable parameters + * such as radius, thickness, arch angle, depth, and capping options. + */ +public class ProArchCreator implements IMeshCreator { + + /** The number of sides for the arch's cross-section. Must be at least 3. */ + private int sidesCount = 16; + + /** The outer radius of the arch. */ + private float radius = 1; + + /** The thickness of the arch's wall. Must be greater than 0. */ + private float thickness = 0.1f; + + /** The angle of the arch in radians. Must be between 0 and 2*PI. */ + private float archAngle = Mathf.PI; + + /** The depth of the arch. */ + private float depth = 0.5f; + + /** Whether to cap the start of the arch. */ + private boolean capStart = true; + + /** Whether to cap the end of the arch. */ + private boolean capEnd = true; + + /** + * Creates a new 3D mesh representing the configured arch. + * + * @return The generated 3D mesh. + * @throws IllegalArgumentException if any of the parameters are invalid. + */ + @Override + public Mesh3D create() { + validateParameters(); + return (archAngle == Mathf.TWO_PI) ? createTube() : createArch(); + } + + /** + * Validates the current parameter values to ensure they are within acceptable ranges. + * + * @throws IllegalArgumentException if any of the parameters are invalid. + */ + private void validateParameters() { + if (sidesCount < 3) { + throw new IllegalArgumentException("sidesCount must be at least 3"); + } + if (thickness <= 0) { + throw new IllegalArgumentException("thickness must be greater than 0"); + } + if (archAngle <= 0 || archAngle > Mathf.TWO_PI) { + throw new IllegalArgumentException("archAngle must be between 0 and 2*PI"); + } + } + + /** + * Creates a 3D arch shape with a specific angle. + * + * @return The generated 3D arch mesh. + */ + private Mesh3D createArch() { + float innerRadius = calculateInnerRadius(); + float outerRadius = calculateOuterRadius(); + + SolidArcCreator creator = new SolidArcCreator(); + + creator.setRotationSegments(sidesCount); + creator.setInnerRadius(innerRadius); + creator.setOuterRadius(outerRadius); + creator.setAngle(archAngle); + creator.setHeight(depth); + creator.setCapStart(capStart); + creator.setCapEnd(capEnd); + + Mesh3D mesh = creator.create(); + mesh.apply(new RotateXModifier(Mathf.HALF_PI)); + + return mesh; + } + + /** + * Creates a 3D tube shape (full circle). + * + * @return The generated 3D tube mesh. + */ + private Mesh3D createTube() { + float innerRadius = calculateInnerRadius(); + float outerRadius = calculateOuterRadius(); + + TubeCreator creator = new TubeCreator(); + creator.setVertices(sidesCount); + creator.setHeight(depth); + creator.setBottomOuterRadius(outerRadius); + creator.setTopOuterRadius(outerRadius); + creator.setBottomInnerRadius(innerRadius); + creator.setTopInnerRadius(innerRadius); + + Mesh3D mesh = creator.create(); + mesh.apply(new RotateXModifier(Mathf.HALF_PI)); + + return mesh; + } + + /** + * Calculates the inner radius of the arch. + * + * @return The inner radius. + */ + private float calculateInnerRadius() { + return radius - thickness; + } + + /** + * Calculates the outer radius of the arch. + * + * @return The outer radius. + */ + private float calculateOuterRadius() { + return radius; + } + + /** + * Gets the number of sides for the arch's cross-section. + * + * @return The number of sides. + */ + public int getSidesCount() { + return sidesCount; + } + + /** + * Sets the number of sides for the arch's cross-section. + * + * @param sidesCount The new number of sides. Must be at least 3. + */ + public void setSidesCount(int sidesCount) { + this.sidesCount = sidesCount; + } + + /** + * Gets the outer radius of the arch. + * + * @return The outer radius. + */ + public float getRadius() { + return radius; + } + + /** + * Sets the outer radius of the arch. + * + * @param radius The new outer radius. + */ + public void setRadius(float radius) { + this.radius = radius; + } + + /** + * Gets the thickness of the arch's wall. + * + * @return The thickness. + */ + public float getThickness() { + return thickness; + } + + /** + * Sets the thickness of the arch's wall. + * + * @param thickness The new thickness. Must be greater than 0. + */ + public void setThickness(float thickness) { + this.thickness = thickness; + } + + /** + * Gets the angle of the arch in radians. + * + * @return The arch angle. + */ + public float getArchAngle() { + return archAngle; + } + + /** + * Sets the angle of the arch in radians. + * + * @param archAngle The new arch angle. Must be between 0 and 2*PI. + */ + public void setArchAngle(float archAngle) { + this.archAngle = archAngle; + } + + /** + * Gets the depth of the arch. + * + * @return The depth. + */ + public float getDepth() { + return depth; + } + + /** + * Sets the depth of the arch. + * + * @param depth The new depth. + */ + public void setDepth(float depth) { + this.depth = depth; + } + + /** + * Checks if the start of the arch is capped. + * + * @return True if the start is capped, false otherwise. + */ + public boolean isCapStart() { + return capStart; + } + + /** + * Sets whether to cap the start of the arch. + * + * @param capStart True to cap the start, false otherwise. + */ + public void setCapStart(boolean capStart) { + this.capStart = capStart; + } + + /** + * Checks if the end of the arch is capped. + * + * @return True if the end is capped, false otherwise. + */ + public boolean isCapEnd() { + return capEnd; + } + + /** + * Sets whether to cap the end of the arch. + * + * @param capEnd True to cap the end, false otherwise. + */ + public void setCapEnd(boolean capEnd) { + this.capEnd = capEnd; + } +} From fa292d1f948d6b464757d5381e84e2803aa36bfa Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Sat, 28 Dec 2024 12:26:29 +0100 Subject: [PATCH 13/20] Fix incorrect angle calculation in ArcCreator - Corrected the `calculateAngleBetweenPoints()` method to properly compute the incremental angle step between vertices. - Adjusted the vertex creation loop to apply the `startAngle` offset during vertex placement. - Ensured accurate vertex placement along the arc, fixing unexpected results for non-default angles. - Added a validation check to ensure the vertex count is at least 2. This fix resolves issues with improper vertex placement when creating arcs with custom start and end angles. --- src/main/java/mesh/creator/primitives/ArcCreator.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/mesh/creator/primitives/ArcCreator.java b/src/main/java/mesh/creator/primitives/ArcCreator.java index 1a25f3b5..da89febc 100644 --- a/src/main/java/mesh/creator/primitives/ArcCreator.java +++ b/src/main/java/mesh/creator/primitives/ArcCreator.java @@ -37,7 +37,7 @@ private void initializeMesh() { private void createVertices() { float angleBetweenPoints = calculateAngleBetweenPoints(); for (int i = 0; i < vertices; i++) { - float currentAngle = angleBetweenPoints * i; + float currentAngle = startAngle + angleBetweenPoints * i; float x = radius * Mathf.cos(currentAngle); float z = radius * Mathf.sin(currentAngle); addVertex(x, 0, z); @@ -49,7 +49,7 @@ private void addVertex(float x, float y, float z) { } private float calculateAngleBetweenPoints() { - return startAngle + ((endAngle - startAngle) / ((float) vertices - 1)); + return (endAngle - startAngle) / ((float) vertices - 1); } public float getStartAngle() { @@ -81,6 +81,9 @@ public int getVertices() { } public void setVertices(int vertices) { + if (vertices < 2) { + throw new IllegalArgumentException("Vertex count must be at least 2"); + } this.vertices = vertices; } } From 096553c0e154b02e110dba752d567f12ba809cb1 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Sat, 28 Dec 2024 15:11:01 +0100 Subject: [PATCH 14/20] Added center property to ArcCreator to allow specifying the center point of the generated arc. --- .../mesh/creator/primitives/ArcCreator.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/mesh/creator/primitives/ArcCreator.java b/src/main/java/mesh/creator/primitives/ArcCreator.java index da89febc..35d7d965 100644 --- a/src/main/java/mesh/creator/primitives/ArcCreator.java +++ b/src/main/java/mesh/creator/primitives/ArcCreator.java @@ -1,6 +1,7 @@ package mesh.creator.primitives; import math.Mathf; +import math.Vector3f; import mesh.Mesh3D; import mesh.creator.IMeshCreator; @@ -14,6 +15,8 @@ public class ArcCreator implements IMeshCreator { private int vertices; + private Vector3f center; + private Mesh3D mesh; public ArcCreator() { @@ -21,6 +24,7 @@ public ArcCreator() { endAngle = Mathf.TWO_PI; radius = 1; vertices = 32; + center = new Vector3f(); } @Override @@ -38,9 +42,9 @@ private void createVertices() { float angleBetweenPoints = calculateAngleBetweenPoints(); for (int i = 0; i < vertices; i++) { float currentAngle = startAngle + angleBetweenPoints * i; - float x = radius * Mathf.cos(currentAngle); - float z = radius * Mathf.sin(currentAngle); - addVertex(x, 0, z); + float x = center.x + radius * Mathf.cos(currentAngle); + float z = center.z + radius * Mathf.sin(currentAngle); + addVertex(x, center.y, z); } } @@ -82,8 +86,19 @@ public int getVertices() { public void setVertices(int vertices) { if (vertices < 2) { - throw new IllegalArgumentException("Vertex count must be at least 2"); + throw new IllegalArgumentException("Vertex count must be at least 2."); } this.vertices = vertices; } + + public Vector3f getCenter() { + return new Vector3f(center); + } + + public void setCenter(Vector3f center) { + if (center == null) { + throw new IllegalArgumentException("Center cannot be null."); + } + this.center.set(center); + } } From 95080d063ddaade29909a6d7dc13d628422a24c4 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Sat, 28 Dec 2024 15:13:40 +0100 Subject: [PATCH 15/20] Added java doc. --- .../mesh/creator/primitives/ArcCreator.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/main/java/mesh/creator/primitives/ArcCreator.java b/src/main/java/mesh/creator/primitives/ArcCreator.java index 35d7d965..e43b088e 100644 --- a/src/main/java/mesh/creator/primitives/ArcCreator.java +++ b/src/main/java/mesh/creator/primitives/ArcCreator.java @@ -5,20 +5,47 @@ import mesh.Mesh3D; import mesh.creator.IMeshCreator; +/** + * Creates a 3D arc mesh composed of a series of vertices arranged in a circular segment. The arc is + * defined by a start angle, end angle, radius, center, and the number of vertices. + * + *

This class implements the {@link IMeshCreator} interface, providing a modular way to create + * arcs for use in 3D mesh generation. It can be customized to create arcs of different sizes, + * orientations, and resolutions. + * + *

Example usage: + * + *

{@code
+ * ArcCreator arcCreator = new ArcCreator();
+ * arcCreator.setStartAngle(0);
+ * arcCreator.setEndAngle(Mathf.HALF_PI); // 90 degrees
+ * arcCreator.setRadius(2.0f);
+ * arcCreator.setVertices(16);
+ * arcCreator.setCenter(new Vector3f(1, 0, 1));
+ * Mesh3D arcMesh = arcCreator.create();
+ * }
+ */ public class ArcCreator implements IMeshCreator { + /** The starting angle of the arc in radians. Defaults to 0. */ private float startAngle; + /** The ending angle of the arc in radians. Defaults to {@link Mathf#TWO_PI}. */ private float endAngle; + /** The radius of the arc. Defaults to 1. */ private float radius; + /** The number of vertices that make up the arc. Defaults to 32. */ private int vertices; + /** The center position of the arc. Defaults to the origin (0, 0, 0). */ private Vector3f center; + /** The generated 3D mesh representing the arc. */ private Mesh3D mesh; + /** Constructs a new {@code ArcCreator} with default parameters. */ public ArcCreator() { startAngle = 0; endAngle = Mathf.TWO_PI; @@ -27,6 +54,11 @@ public ArcCreator() { center = new Vector3f(); } + /** + * Creates the arc mesh based on the current configuration. + * + * @return A {@link Mesh3D} object representing the generated arc. + */ @Override public Mesh3D create() { initializeMesh(); @@ -34,10 +66,12 @@ public Mesh3D create() { return mesh; } + /** Initializes the mesh object to store the arc's vertices. */ private void initializeMesh() { mesh = new Mesh3D(); } + /** Generates the vertices of the arc based on the radius, angles, and center. */ private void createVertices() { float angleBetweenPoints = calculateAngleBetweenPoints(); for (int i = 0; i < vertices; i++) { @@ -48,42 +82,95 @@ private void createVertices() { } } + /** + * Adds a vertex to the mesh at the specified position. + * + * @param x The x-coordinate of the vertex. + * @param y The y-coordinate of the vertex. + * @param z The z-coordinate of the vertex. + */ private void addVertex(float x, float y, float z) { mesh.addVertex(x, y, z); } + /** + * Calculates the angle between each pair of adjacent vertices. + * + * @return The angle between adjacent vertices in radians. + */ private float calculateAngleBetweenPoints() { return (endAngle - startAngle) / ((float) vertices - 1); } + /** + * Gets the starting angle of the arc in radians. + * + * @return The starting angle. + */ public float getStartAngle() { return startAngle; } + /** + * Sets the starting angle of the arc in radians. + * + * @param startAngle The starting angle. + */ public void setStartAngle(float startAngle) { this.startAngle = startAngle; } + /** + * Gets the ending angle of the arc in radians. + * + * @return The ending angle. + */ public float getEndAngle() { return endAngle; } + /** + * Sets the ending angle of the arc in radians. + * + * @param endAngle The ending angle. + */ public void setEndAngle(float endAngle) { this.endAngle = endAngle; } + /** + * Gets the radius of the arc. + * + * @return The radius. + */ public float getRadius() { return radius; } + /** + * Sets the radius of the arc. + * + * @param radius The radius. + */ public void setRadius(float radius) { this.radius = radius; } + /** + * Gets the number of vertices that make up the arc. + * + * @return The number of vertices. + */ public int getVertices() { return vertices; } + /** + * Sets the number of vertices that make up the arc. Must be at least 2. + * + * @param vertices The number of vertices. + * @throws IllegalArgumentException if {@code vertices} is less than 2. + */ public void setVertices(int vertices) { if (vertices < 2) { throw new IllegalArgumentException("Vertex count must be at least 2."); @@ -91,10 +178,21 @@ public void setVertices(int vertices) { this.vertices = vertices; } + /** + * Gets the center position of the arc. + * + * @return A {@link Vector3f} representing the center position. + */ public Vector3f getCenter() { return new Vector3f(center); } + /** + * Sets the center position of the arc. + * + * @param center A {@link Vector3f} representing the new center position. + * @throws IllegalArgumentException if {@code center} is {@code null}. + */ public void setCenter(Vector3f center) { if (center == null) { throw new IllegalArgumentException("Center cannot be null."); From 7f3acc83358f6a72a9753d2642e9f622faa36511 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Sat, 28 Dec 2024 17:02:49 +0100 Subject: [PATCH 16/20] Feat: Implemented RoundCornerPlaneCreator --- .../assets/RoundCornerPlaneCreator.java | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 src/main/java/mesh/creator/assets/RoundCornerPlaneCreator.java diff --git a/src/main/java/mesh/creator/assets/RoundCornerPlaneCreator.java b/src/main/java/mesh/creator/assets/RoundCornerPlaneCreator.java new file mode 100644 index 00000000..02a4e8e8 --- /dev/null +++ b/src/main/java/mesh/creator/assets/RoundCornerPlaneCreator.java @@ -0,0 +1,203 @@ +package mesh.creator.assets; + +import math.Mathf; +import math.Vector3f; +import mesh.Mesh3D; +import mesh.creator.IMeshCreator; +import mesh.creator.primitives.ArcCreator; +import mesh.modifier.CenterAtModifier; +import mesh.modifier.SolidifyModifier; +import mesh.modifier.subdivision.QuadsToTrianglesModifier; + +public class RoundCornerPlaneCreator implements IMeshCreator { + + private float width = 3; + + private float height = 0; + + private float depth = 6; + + private float radius = 1f; + + private int segments = 16; + + private boolean triangulateFaces = true; + + private Mesh3D mesh; + + @Override + public Mesh3D create() { + mesh = new Mesh3D(); + createVertices(); + createAllSideFaces(); + createAllCornerFaces(); + createCenterFace(); + solidify(); + centerAtOrigin(); + triangulateQuads(); + return mesh; + } + + private Vector3f createCornerVertex(float xSign, float zSign) { + float x = (xSign * (width / 2)) + (xSign * -radius); + float z = (zSign * (depth / 2)) + (zSign * -radius); + + return new Vector3f(x, 0, z); + } + + private void createVertices() { + Vector3f topLeft = createCornerVertex(-1, -1); + Vector3f topRight = createCornerVertex(1, -1); + Vector3f bottomRight = createCornerVertex(1, 1); + Vector3f bottomLeft = createCornerVertex(-1, 1); + + createCornerVertices(topLeft, Mathf.PI, Mathf.PI + Mathf.HALF_PI); + createCornerVertices(topRight, Mathf.PI + Mathf.HALF_PI, Mathf.TWO_PI); + createCornerVertices(bottomRight, 0, Mathf.HALF_PI); + createCornerVertices(bottomLeft, Mathf.HALF_PI, Mathf.PI); + + mesh.add(topLeft); + mesh.add(topRight); + mesh.add(bottomRight); + mesh.add(bottomLeft); + } + + private void solidify() { + if (height == 0) return; + mesh.apply(new SolidifyModifier(height)); + } + + private void centerAtOrigin() { + mesh.apply(new CenterAtModifier()); + } + + private void triangulateQuads() { + if (!triangulateFaces) return; + mesh.apply(new QuadsToTrianglesModifier()); + } + + private void createAllCornerFaces() { + if (triangulateFaces) { + createAllCornerTriangles(); + } else { + createAllCornerNGons(); + } + } + + private void createAllCornerTriangles() { + for (int i = 0; i < 4; i++) { + createCornerTriangles(i); + } + } + + private void createAllCornerNGons() { + for (int i = 0; i < 4; i++) { + createCornerNGons(i); + } + } + + private void createAllSideFaces() { + for (int i = 0; i < 4; i++) { + createSideFace(i); + } + } + + private void createSideFace(int index) { + int index0 = calculateTotalVertexCount() - (4 - index); + int index1 = (((index + 1) * segments) + index) % (4 * segments + 4); + int index2 = (index1 + 1) % (4 * segments + 4); + int index3 = calculateTotalVertexCount() - (3 - index); + index3 = index3 == calculateTotalVertexCount() ? index3 - 4 : index3; + + mesh.addFace(index0, index1, index2, index3); + } + + private void createCornerNGons(int index) { + int[] indices = new int[segments + 2]; + indices[0] = calculateTotalVertexCount() - (4 - index); + for (int i = 0; i < segments + 1; i++) { + indices[i + 1] = (index * (segments + 1)) + i; + } + mesh.addFace(indices); + } + + private void createCornerTriangles(int index) { + int centerIndex = calculateTotalVertexCount() - (4 - index); + int baseIndex = index * (segments + 1); + + for (int i = 0; i < segments; i++) { + int index1 = baseIndex + i; + int index2 = baseIndex + i + 1; + mesh.addFace(centerIndex, index1, index2); + } + } + + private void createCenterFace() { + int index = calculateTotalVertexCount(); + mesh.addFace(index - 4, index - 3, index - 2, index - 1); + } + + private void createCornerVertices(Vector3f center, float startAngle, float endAngle) { + ArcCreator creator = new ArcCreator(); + creator.setRadius(radius); + creator.setVertices(segments + 1); + creator.setStartAngle(startAngle); + creator.setEndAngle(endAngle); + creator.setCenter(center); + + Mesh3D arc = creator.create(); + mesh.addVertices(arc.getVertices()); + } + + private int calculateTotalVertexCount() { + return 4 + (4 * (segments + 1)); + } + + public float getWidth() { + return width; + } + + public void setWidth(float width) { + this.width = width; + } + + public float getHeight() { + return height; + } + + public void setHeight(float height) { + this.height = height; + } + + public float getDepth() { + return depth; + } + + public void setDepth(float depth) { + this.depth = depth; + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } + + public int getSegments() { + return segments; + } + + public void setSegments(int segments) { + this.segments = segments; + } + + public boolean isTriangulateFaces() { + return triangulateFaces; + } + + public void setTriangulateFaces(boolean triangulateFaces) { + this.triangulateFaces = triangulateFaces; + } +} From 3721fc2c5d3c0a0da64e263d245aeb5e8c9455f8 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Sun, 29 Dec 2024 03:01:40 +0100 Subject: [PATCH 17/20] Add getter and setter for camera movement speed in FlyByCameraControl - Added `getMoveSpeed()` to retrieve the current movement speed. - Added `setMoveSpeed(float moveSpeed)` to update the movement speed dynamically. - Enables runtime customization of camera movement speed. --- .../engine/components/FlyByCameraControl.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/engine/components/FlyByCameraControl.java b/src/main/java/engine/components/FlyByCameraControl.java index d96751bd..3aa6da3a 100644 --- a/src/main/java/engine/components/FlyByCameraControl.java +++ b/src/main/java/engine/components/FlyByCameraControl.java @@ -180,4 +180,22 @@ public void onAttach() { public void onDetach() { // Not used yet } + + /** + * Returns the current movement speed of the camera. + * + * @return The movement speed in units per second. + */ + public float getMoveSpeed() { + return moveSpeed; + } + + /** + * Sets the movement speed of the camera. + * + * @param moveSpeed The new movement speed in units per second. + */ + public void setMoveSpeed(float moveSpeed) { + this.moveSpeed = moveSpeed; + } } From 71d8a03ed47d34f6526475fdae151757fb9d2b4b Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Mon, 30 Dec 2024 11:02:13 +0100 Subject: [PATCH 18/20] Added getMouseSensitivity() and setMouseSensitivity() methods with JavaDoc to the FlyByCameraControl class. These methods provide access to the mouseSensitivity field, allowing for dynamic adjustment of camera rotation speed. This is useful for user preference settings or in-game adjustments. --- .../engine/components/FlyByCameraControl.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/engine/components/FlyByCameraControl.java b/src/main/java/engine/components/FlyByCameraControl.java index 3aa6da3a..077e4a3e 100644 --- a/src/main/java/engine/components/FlyByCameraControl.java +++ b/src/main/java/engine/components/FlyByCameraControl.java @@ -198,4 +198,28 @@ public float getMoveSpeed() { public void setMoveSpeed(float moveSpeed) { this.moveSpeed = moveSpeed; } + + /** + * Returns the current mouse sensitivity used for camera rotation. + * + *

The mouse sensitivity determines how much the camera rotates based on mouse movement. Higher + * sensitivity values result in larger rotations for smaller mouse movements. + * + * @return The current mouse sensitivity. + */ + public float getMouseSensitivity() { + return mouseSensitivity; + } + + /** + * Sets the mouse sensitivity used for camera rotation. + * + *

The mouse sensitivity determines how much the camera rotates based on mouse movement. Higher + * sensitivity values result in larger rotations for smaller mouse movements. + * + * @param mouseSensitivity The new mouse sensitivity value. + */ + public void setMouseSensitivity(float mouseSensitivity) { + this.mouseSensitivity = mouseSensitivity; + } } From 0eee805f0a08d805873251fbdf0f57ad32c99e82 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Mon, 30 Dec 2024 11:05:34 +0100 Subject: [PATCH 19/20] Refactor: Optimized FlyByCameraControl update by calling updateTarget only once at the end. This improves efficiency by reducing redundant calculations after camera and transform updates. --- src/main/java/engine/components/FlyByCameraControl.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/engine/components/FlyByCameraControl.java b/src/main/java/engine/components/FlyByCameraControl.java index 077e4a3e..e4ce9aa5 100644 --- a/src/main/java/engine/components/FlyByCameraControl.java +++ b/src/main/java/engine/components/FlyByCameraControl.java @@ -78,13 +78,12 @@ public void update(float tpf) { float mouseY = input.getMouseDeltaY() * mouseSensitivity * tpf; handleRotation(mouseX, mouseY); - updateTarget(); Vector3f velocity = calculateVelocity(); if (velocity.length() > 0) { applyMovement(velocity, tpf); } - + updateTarget(); input.center(); } @@ -147,7 +146,6 @@ private void applyMovement(Vector3f velocity, float tpf) { Vector3f position = camera.getTransform().getPosition(); position.addLocal(velocity.mult(moveSpeed * tpf)); camera.getTransform().setPosition(position); - updateTarget(); } /** From aae734faf1532ff12e2dcafbe1aad6cacf91f620 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Tue, 31 Dec 2024 01:39:31 +0100 Subject: [PATCH 20/20] Feat: Added Uv and texture support. --- .../processing/ProcessingApplication.java | 2 + .../engine/processing/ProcessingTexture.java | 43 +++++++++++++++ .../processing/ProcessingTextureLoader.java | 27 ++++++++++ src/main/java/engine/render/Material.java | 52 ++++++++++++++++++- src/main/java/engine/resources/Texture.java | 14 +++++ .../java/engine/resources/TextureLoader.java | 6 +++ .../java/engine/resources/TextureManager.java | 45 ++++++++++++++++ src/main/java/mesh/Face3D.java | 19 ++++++- src/main/java/mesh/Mesh3D.java | 46 ++++++++++++++++ src/main/java/workspace/GraphicsPImpl.java | 49 ++++++++++++++--- src/main/java/workspace/ui/Graphics3D.java | 7 ++- 11 files changed, 299 insertions(+), 11 deletions(-) create mode 100644 src/main/java/engine/processing/ProcessingTexture.java create mode 100644 src/main/java/engine/processing/ProcessingTextureLoader.java create mode 100644 src/main/java/engine/resources/Texture.java create mode 100644 src/main/java/engine/resources/TextureLoader.java create mode 100644 src/main/java/engine/resources/TextureManager.java diff --git a/src/main/java/engine/processing/ProcessingApplication.java b/src/main/java/engine/processing/ProcessingApplication.java index 8ba85e30..2b755112 100644 --- a/src/main/java/engine/processing/ProcessingApplication.java +++ b/src/main/java/engine/processing/ProcessingApplication.java @@ -6,6 +6,7 @@ import engine.input.KeyInput; import engine.input.MouseInput; import engine.resources.ResourceManager; +import engine.resources.TextureManager; import processing.core.PApplet; import workspace.GraphicsPImpl; import workspace.ui.Graphics; @@ -31,6 +32,7 @@ public void settings() { public void setup() { Graphics g = new GraphicsPImpl(this); ResourceManager.getInstance().setImageLoader(new ProcessingImageLoader(this)); + TextureManager.getInstance().setTextureLoader(new ProcessingTextureLoader(this)); container.setGraphics(g); getSurface().setTitle(settings.getTitle()); setupInput(); diff --git a/src/main/java/engine/processing/ProcessingTexture.java b/src/main/java/engine/processing/ProcessingTexture.java new file mode 100644 index 00000000..b350bc2a --- /dev/null +++ b/src/main/java/engine/processing/ProcessingTexture.java @@ -0,0 +1,43 @@ +package engine.processing; + +import engine.resources.Texture; +import processing.core.PImage; + +public class ProcessingTexture implements Texture { + + private final PImage image; + + public ProcessingTexture(PImage image) { + this.image = image; + } + + @Override + public int getWidth() { + return image.width; + } + + @Override + public int getHeight() { + return image.height; + } + + @Override + public void bind(int unit) { + // Processing doesn't use texture units in the same way, just bind globally + image.loadPixels(); + } + + @Override + public void unbind() { + // No specific unbind operation for Processing + } + + @Override + public void delete() { + // Processing handles memory management automatically + } + + public PImage getImage() { + return image; + } +} diff --git a/src/main/java/engine/processing/ProcessingTextureLoader.java b/src/main/java/engine/processing/ProcessingTextureLoader.java new file mode 100644 index 00000000..b1daa208 --- /dev/null +++ b/src/main/java/engine/processing/ProcessingTextureLoader.java @@ -0,0 +1,27 @@ +package engine.processing; + +import engine.resources.Texture; +import engine.resources.TextureLoader; +import processing.core.PApplet; +import processing.core.PImage; + +public class ProcessingTextureLoader implements TextureLoader { + + private final PApplet parent; + + public ProcessingTextureLoader(PApplet parent) { + this.parent = parent; + } + + @Override + public Texture loadTexture(String filePath) { + PImage image = + parent.loadImage( + ProcessingTextureLoader.class + .getClassLoader() + .getResource("images/" + filePath) + .getPath()); + ProcessingTexture texture = new ProcessingTexture(image); + return texture; + } +} diff --git a/src/main/java/engine/render/Material.java b/src/main/java/engine/render/Material.java index 3793d9d1..40debe56 100644 --- a/src/main/java/engine/render/Material.java +++ b/src/main/java/engine/render/Material.java @@ -1,5 +1,6 @@ package engine.render; +import engine.resources.Texture; import math.Color; import workspace.ui.Graphics; @@ -67,6 +68,10 @@ public class Material { /** Shininess factor for specular highlights. */ private final float shininess; + private Texture normalTexture; + + private Texture diffuseTexture; + /** * Constructor to set the base color of the material. * @@ -83,11 +88,13 @@ private Material(Builder builder) { this.diffuse = builder.diffuse; this.specular = builder.specular; this.shininess = builder.shininess; + this.normalTexture = builder.normalTexture; + this.diffuseTexture = builder.diffuseTexture; } /** - * Builder class to facilitate the creation of custom materials with specific lighting and shader - * properties. + * Builder class to facilitate the creation of custom materials with specific lighting, shader and + * texture properties. */ public static class Builder { @@ -103,6 +110,10 @@ public static class Builder { private float shininess = 10.0f; + private Texture diffuseTexture = null; + + private Texture normalTexture = null; + /** * Sets the base color of the material. * @@ -163,6 +174,28 @@ public Builder setUseLighting(boolean useLighting) { return this; } + /** + * Sets the normal texture of the material. + * + * @param normalTexture The normal texture, can be null + * @return The builder instance for chaining + */ + public Builder setNormalTexture(Texture normalTexture) { + this.normalTexture = normalTexture; + return this; + } + + /** + * Sets the diffuse texture of the material. + * + * @param diffuseTexture The diffuse texture, can be null. + * @return The builder instance for chaining + */ + public Builder setDiffuseTexture(Texture diffuseTexture) { + this.diffuseTexture = diffuseTexture; + return this; + } + /** * Builds and returns the Material instance with the set properties. * @@ -180,6 +213,13 @@ public Material build() { */ public void apply(Graphics g) { g.setMaterial(this); + + if (diffuseTexture != null) { + g.bindTexture(diffuseTexture, 0); // Bind to texture unit 0 + } + if (normalTexture != null) { + g.bindTexture(normalTexture, 1); // Bind to texture unit 1 + } } /** @@ -240,4 +280,12 @@ public float[] getSpecular() { public float getShininess() { return shininess; } + + public Texture getDiffuseTexture() { + return diffuseTexture; + } + + public Texture getNormalTexture() { + return normalTexture; + } } diff --git a/src/main/java/engine/resources/Texture.java b/src/main/java/engine/resources/Texture.java new file mode 100644 index 00000000..53857bc4 --- /dev/null +++ b/src/main/java/engine/resources/Texture.java @@ -0,0 +1,14 @@ +package engine.resources; + +public interface Texture { + + int getWidth(); + + int getHeight(); + + void bind(int unit); // Bind to a specific texture unit + + void unbind(); + + void delete(); +} diff --git a/src/main/java/engine/resources/TextureLoader.java b/src/main/java/engine/resources/TextureLoader.java new file mode 100644 index 00000000..6ea10108 --- /dev/null +++ b/src/main/java/engine/resources/TextureLoader.java @@ -0,0 +1,6 @@ +package engine.resources; + +public interface TextureLoader { + + Texture loadTexture(String filePath); +} diff --git a/src/main/java/engine/resources/TextureManager.java b/src/main/java/engine/resources/TextureManager.java new file mode 100644 index 00000000..bc70210e --- /dev/null +++ b/src/main/java/engine/resources/TextureManager.java @@ -0,0 +1,45 @@ +package engine.resources; + +import java.util.HashMap; +import java.util.Map; + +public class TextureManager { + + private static TextureManager instance; + + private TextureLoader imageLoader; + + private final Map resourceCache = new HashMap<>(); + + private TextureManager() {} + + public static TextureManager getInstance() { + if (instance == null) { + instance = new TextureManager(); + } + return instance; + } + + public void setTextureLoader(TextureLoader loader) { + this.imageLoader = loader; + } + + public Texture loadTexture(String path) { + if (resourceCache.containsKey(path)) { + return resourceCache.get(path); // Return cached resource + } + + if (imageLoader == null) { + throw new IllegalStateException("ImageLoader is not set!"); + } + + Texture texture = imageLoader.loadTexture(path); + resourceCache.put(path, texture); + + return texture; + } + + public void unloadImage(String path) { + resourceCache.remove(path); // Optionally handle cleanup for backend-specific resources + } +} diff --git a/src/main/java/mesh/Face3D.java b/src/main/java/mesh/Face3D.java index 46a00af0..cd38bdfd 100644 --- a/src/main/java/mesh/Face3D.java +++ b/src/main/java/mesh/Face3D.java @@ -11,20 +11,27 @@ public class Face3D { public int[] indices; + private int[] uvIndices; + public Vector3f normal; public String tag; public Face3D() { - this(new int[0]); + this(new int[0], new int[0]); } public Face3D(int... indices) { + this(indices, new int[0]); + } + + public Face3D(int[] indices, int[] uvIndices) { this.color = new Color(); this.indices = new int[indices.length]; this.normal = new Vector3f(); this.tag = ""; - for (int i = 0; i < indices.length; i++) this.indices[i] = indices[i]; + this.indices = Arrays.copyOf(indices, indices.length); + this.uvIndices = Arrays.copyOf(uvIndices, uvIndices.length); } public boolean sharesSameIndices(Face3D face) { @@ -39,6 +46,14 @@ public int getIndexAt(int index) { return indices[index % indices.length]; } + // Get UV index, return -1 if no UVs are available + public int getUvIndexAt(int index) { + if (uvIndices == null || uvIndices.length == 0) { + return -1; // No UVs available + } + return uvIndices[index % uvIndices.length]; + } + public int getVertexCount() { return indices.length; } diff --git a/src/main/java/mesh/Mesh3D.java b/src/main/java/mesh/Mesh3D.java index 914b0f4a..ede95ebd 100644 --- a/src/main/java/mesh/Mesh3D.java +++ b/src/main/java/mesh/Mesh3D.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.List; +import math.Vector2f; import math.Vector3f; import mesh.modifier.IMeshModifier; import mesh.modifier.RemoveDoubleVerticesModifier; @@ -16,11 +17,18 @@ public class Mesh3D { public ArrayList vertices; + public ArrayList faces; + private ArrayList vertexNormals; + + private ArrayList uvs; + public Mesh3D() { vertices = new ArrayList(); faces = new ArrayList(); + vertexNormals = new ArrayList(); + uvs = new ArrayList(); } /** @@ -263,4 +271,42 @@ public Vector3f getVertexAt(int index) { public Face3D getFaceAt(int index) { return faces.get(index); } + + /** + * Sets the UV coordinates for this mesh. + * + *

This method sets the list of UV coordinates that will be used for the mesh. The provided + * list of UV coordinates will replace any existing UVs. The list must be a valid {@link + * ArrayList} of {@link Vector2f} objects. If the provided list is {@code null}, an {@link + * IllegalArgumentException} will be thrown. + * + * @param uvs The list of UV coordinates to be set. It must not be {@code null} and should contain + * {@link Vector2f} objects representing the UV mapping for the mesh. + * @throws IllegalArgumentException if the provided {@code uvs} list is {@code null}. + */ + public void setUvs(ArrayList uvs) { + if (uvs == null) { + throw new IllegalArgumentException("The list of UV coordinates cannot be null."); + } + this.uvs = uvs; + } + + /** + * Retrieves the UV coordinates at the specified index. + * + *

This method returns the UV coordinates associated with the given index. If the index is out + * of bounds (either negative or beyond the size of the list), a default UV coordinate (0, 0) is + * returned to avoid potential errors or exceptions. The method does not throw an exception when + * an invalid index is provided, ensuring that the calling code can proceed without disruption. + * + * @param index The index of the UV coordinate to retrieve. + * @return The UV coordinates as a {@link Vector2f}. If the index is out of bounds, returns {@code + * new Vector2f(0, 0)}. The return value will never be {@code null}. + */ + public Vector2f getUvAt(int index) { + if (index < 0 || index >= uvs.size()) { + return new Vector2f(0, 0); + } + return uvs.get(index); + } } diff --git a/src/main/java/workspace/GraphicsPImpl.java b/src/main/java/workspace/GraphicsPImpl.java index 3d7d28da..e0ec545f 100644 --- a/src/main/java/workspace/GraphicsPImpl.java +++ b/src/main/java/workspace/GraphicsPImpl.java @@ -4,12 +4,15 @@ import engine.processing.LightGizmoRenderer; import engine.processing.LightRendererImpl; +import engine.processing.ProcessingTexture; import engine.render.Material; import engine.resources.Image; +import engine.resources.Texture; import engine.scene.camera.Camera; import engine.scene.light.Light; import engine.scene.light.LightRenderer; import math.Matrix4f; +import math.Vector2f; import math.Vector3f; import mesh.Face3D; import mesh.Mesh3D; @@ -32,6 +35,8 @@ public class GraphicsPImpl implements Graphics { private PGraphics g; + private PApplet p; + private Mesh3DRenderer renderer; private LightRenderer lightRenderer; @@ -42,6 +47,8 @@ public class GraphicsPImpl implements Graphics { public static int vertexCount = 0; + private PImage texture; + @Override public void setAmbientColor(math.Color ambientColor) { this.ambientColor = ambientColor; @@ -59,6 +66,7 @@ public void setWireframeMode(boolean wireframeMode) { public GraphicsPImpl(PApplet p) { this.g = p.g; + this.p = p; renderer = new Mesh3DRenderer(p); lightRenderer = new LightRendererImpl(p); @@ -78,11 +86,12 @@ public void fillFaces(Mesh3D mesh) { if (wireframeMode) { g.noFill(); stroke(); - renderer.drawFaces(mesh); + // renderer.drawFaces(mesh); + drawMeshFaces(mesh); } else { g.noStroke(); fill(); - renderer.drawFaces(mesh); + drawMeshFaces(mesh); } } @@ -133,11 +142,24 @@ private void drawMeshFaces(Mesh3D mesh) { } else { g.beginShape(PApplet.POLYGON); } - for (int index : f.indices) { - Vector3f v = mesh.vertices.get(index); - g.vertex(v.getX(), v.getY(), v.getZ()); + + if (texture != null) { + g.texture(texture); + g.textureMode(PApplet.NORMAL); + } + + int[] indices = f.indices; + for (int i = 0; i < indices.length; i++) { + Vector3f v = mesh.vertices.get(f.indices[i]); + int uvIndex = f.getUvIndexAt(i); + if (uvIndex != -1) { + Vector2f uv = mesh.getUvAt(uvIndex); + g.vertex(v.getX(), v.getY(), v.getZ(), uv.getX(), 1 - uv.getY()); + } else { + g.vertex(v.getX(), v.getY(), v.getZ()); + } } - g.endShape(PApplet.CLOSE); + g.endShape(); } } @@ -367,6 +389,8 @@ public void setMaterial(Material material) { return; } + this.texture = null; + // Extract material properties math.Color color = material.getColor(); float[] ambient = material.getAmbient(); @@ -411,6 +435,19 @@ public void setMaterial(Material material) { g.shininess(shininess); } + @Override + public void bindTexture(Texture texture, int unit) { // TODO Auto-generated method stub + // if (unit == 1) { + // g.textureMode(PApplet.NORMAL); + // } + ProcessingTexture texture2 = (ProcessingTexture) texture; + this.texture = texture2.getImage(); + } + + @Override + public void unbindTexture(int unit) { // TODO Auto-generated method stub + } + @Override public void setShader(String vertexShaderName, String fragmentShaderName) { try { diff --git a/src/main/java/workspace/ui/Graphics3D.java b/src/main/java/workspace/ui/Graphics3D.java index 878eb07c..c0c0621e 100644 --- a/src/main/java/workspace/ui/Graphics3D.java +++ b/src/main/java/workspace/ui/Graphics3D.java @@ -3,6 +3,7 @@ import java.util.List; import engine.render.Material; +import engine.resources.Texture; import engine.scene.camera.Camera; import engine.scene.light.Light; import math.Matrix4f; @@ -19,7 +20,7 @@ public interface Graphics3D extends Graphics2D { void rotateY(float angle); void rotateZ(float angle); - + void rotate(float rx, float ry, float rz); void render(Light light); @@ -44,6 +45,10 @@ public interface Graphics3D extends Graphics2D { void setWireframeMode(boolean wireframeMode); + void bindTexture(Texture texture, int unit); + + void unbindTexture(int unit); + /** * Sets the global ambient light color for the scene. *