diff --git a/src/main/java/engine/components/FlyByCameraControl.java b/src/main/java/engine/components/FlyByCameraControl.java index d96751bd..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(); } /** @@ -180,4 +178,46 @@ 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; + } + + /** + * 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; + } } 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/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; + } } diff --git a/src/main/java/math/Vector4f.java b/src/main/java/math/Vector4f.java index ce065e19..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. * @@ -263,7 +299,8 @@ 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). @@ -271,6 +308,33 @@ public boolean isEqual(Vector4f other, float tolerance) { * @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."); } @@ -404,6 +468,46 @@ 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); + } + + /** + * 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/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/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; + } } 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; + } +} 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; + } +} diff --git a/src/main/java/mesh/creator/primitives/ArcCreator.java b/src/main/java/mesh/creator/primitives/ArcCreator.java index 43c372a8..e43b088e 100644 --- a/src/main/java/mesh/creator/primitives/ArcCreator.java +++ b/src/main/java/mesh/creator/primitives/ArcCreator.java @@ -1,87 +1,202 @@ package mesh.creator.primitives; import math.Mathf; +import math.Vector3f; 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 { - private float startAngle; - - private float endAngle; - - private float radius; - - private int vertices; - - private Mesh3D mesh; - - public ArcCreator() { - startAngle = 0; - endAngle = Mathf.TWO_PI; - radius = 1; - vertices = 32; - } - - @Override - public Mesh3D create() { - initializeMesh(); - createVertices(); - return mesh; - } - - 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 addVertex(float x, float y, float z) { - mesh.addVertex(x, y, z); - } - - private float calculateAngleBetweenPoints() { - return startAngle + ((endAngle - startAngle) / ((float) vertices - 1)); + /** 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; + radius = 1; + vertices = 32; + 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(); + createVertices(); + 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++) { + float currentAngle = startAngle + angleBetweenPoints * i; + float x = center.x + radius * Mathf.cos(currentAngle); + float z = center.z + radius * Mathf.sin(currentAngle); + addVertex(x, center.y, z); } - - public float getStartAngle() { - return startAngle; - } - - public void setStartAngle(float startAngle) { - this.startAngle = startAngle; - } - - public float getEndAngle() { - return endAngle; - } - - public void setEndAngle(float endAngle) { - this.endAngle = endAngle; + } + + /** + * 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."); } - - public float getRadius() { - return radius; + 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."); } - - public void setRadius(float radius) { - this.radius = radius; - } - - public int getVertices() { - return vertices; - } - - public void setVertices(int vertices) { - this.vertices = vertices; - } - + this.center.set(center); + } } 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; + } } 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. * diff --git a/src/test/java/math/Vector4fTest.java b/src/test/java/math/Vector4fTest.java index f236d609..90233941 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,7 +1752,150 @@ 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.lerpUnclamped(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 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); + Vector4f startCopy = start.clone(); + Vector4f endCopy = end.clone(); + + // Act + Vector4f result = start.lerpUnclamped(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 testLerpUnclampedWithNullThrowsException() { + // Arrange + Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f other = null; + float t = 0.5f; + + assertThrows( + IllegalArgumentException.class, + () -> { + start.lerpUnclamped(other, t); + }); + } + + // ---------------------------------------------------------------------------------------------- + // 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); @@ -1768,7 +1911,7 @@ public void testLerpWithIdenticalStartAndEnd() { } @Test - public void testLerpReturnsNewInstanceAndOriginalVectorsUntouched() { + 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); @@ -1797,7 +1940,7 @@ public void testLerpReturnsNewInstanceAndOriginalVectorsUntouched() { } @Test - public void testLerpWithNullThrowsException() { + public void testLerpClampedWithNullThrowsException() { // Arrange Vector4f start = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f); Vector4f other = null; @@ -2674,4 +2817,273 @@ 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"); + } + + // ---------------------------------------------------------------------------------------------- + // 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"); + } + + // ---------------------------------------------------------------------------------------------- + // 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()); + } }