diff --git a/src/main/java/engine/Timer.java b/src/main/java/engine/Timer.java new file mode 100644 index 00000000..d3572e22 --- /dev/null +++ b/src/main/java/engine/Timer.java @@ -0,0 +1,272 @@ +package engine; + +/** + * The {@code Timer} class provides a utility for tracking elapsed time, frames per second (FPS), + * and time scaling for games or applications. It uses nanosecond precision for timekeeping and + * offers features such as formatted time representation, time-per-frame calculation, and + * slow-motion or speed-up effects via time scaling. + * + *

Key features include: + * + *

+ * + * This class is designed to be updated on every frame of an application or game. + */ +public class Timer { + + /** Real world time when the timer started. */ + private long startTime; + + /** The last recorded time in nanoseconds. */ + private long lastTime; + + /** The time in milliseconds taken for the last frame. */ + private float time; + + /** Accumulates milliseconds for FPS calculation. */ + private long millisecondCounter; + + /** The frame count during the last FPS update. */ + private int lastFrameCount; + + /** The calculated frames per second (FPS). */ + private int fps; + + /** Total elapsed time in milliseconds. */ + private long totalTime; + + /** Scaling factor for time (default is 1.0 for real-time). */ + private float timeScale; + + /** Total number of frames since the Timer started. */ + private int frameCount; + + /** Constructs a {@code Timer} with a default time scale of 1.0. */ + public Timer() { + this.startTime = System.nanoTime(); + this.lastTime = System.nanoTime(); + this.time = 0; + this.totalTime = 0; + this.timeScale = 1f; + this.frameCount = 0; + } + + /** + * Constructs a {@code Timer} with the specified initial time scale. + * + * @param initialTimeScale the initial time scaling factor + */ + public Timer(float initialTimeScale) { + this.timeScale = initialTimeScale; + } + + /** + * Returns the current frames per second (FPS). + * + * @return the frames per second + */ + public float getFrameRate() { + return fps; + } + + /** Updates the FPS calculation based on the accumulated milliseconds. */ + private void updateFPS() { + millisecondCounter += time; + if (millisecondCounter >= 1000) { + millisecondCounter = 0; + fps = frameCount - lastFrameCount; + lastFrameCount = frameCount; + } + } + + /** + * Updates the Timer. This method must be called once per frame to ensure accurate time tracking. + */ + public void update() { + long currentTime = System.nanoTime(); + time = (currentTime - lastTime) / 1_000_000.0f; // Convert to milliseconds + lastTime = currentTime; + totalTime += time; + frameCount++; + updateFPS(); + } + + /** + * Resets the {@code Timer} to its initial state, clearing all accumulated time and frame count + * values. This includes resetting the following: + * + * + * + *

This method can be used when you need to restart the timer, such as for restarting the game + * / application or resetting the simulation state. + */ + public void reset() { + this.startTime = System.nanoTime(); + this.lastTime = startTime; + this.time = 0; + this.totalTime = 0; + this.frameCount = 0; + this.fps = 0; + this.millisecondCounter = 0; + this.lastFrameCount = 0; + } + + /** + * Returns the total elapsed time in seconds, scaled by the current time scale. + * + * @return the scaled total elapsed time in seconds + */ + public float getTotalTime() { + return totalTime / 1000.0f * timeScale; + } + + /** + * Returns the total elapsed time in seconds, independent of the time scale. + * + * @return the unscaled total elapsed time in seconds + */ + public float getUnscaledTotalTime() { + return totalTime / 1000.0f; + } + + /** + * Returns a formatted string representing the scaled total time in the format HH:MM:SS. + * + * @return the formatted scaled total time + */ + public String getFormattedTotalTime() { + return formatTime(getTotalTime()); + } + + /** + * Returns a formatted string representing the unscaled total time in the format HH:MM:SS. + * + * @return the formatted unscaled total time + */ + public String getUnscaledFormattedTotalTime() { + return formatTime(getUnscaledTotalTime()); + } + + /** + * Returns the time it took to complete the last frame in seconds, scaled by the current time + * scale. + * + * @return the scaled time per frame in seconds + */ + public float getTimePerFrame() { + return time / 1000.0f * timeScale; + } + + /** + * Returns the time it took to complete the last frame in seconds, independent of the time scale. + * + * @return the unscaled time per frame in seconds + */ + public float getUnscaledTimePerFrame() { + return time / 1000.0f; + } + + /** + * Returns the current time scaling factor. + * + * @return the time scale + */ + public float getTimeScale() { + return timeScale; + } + + /** + * Sets the time scaling factor. A value of 1.0 represents real-time, values less than 1.0 slow + * down time, and values greater than 1.0 speed up time. + * + * @param timeScale the new time scaling factor + */ + public void setTimeScale(float timeScale) { + this.timeScale = timeScale; + } + + /** + * Returns the real-time elapsed since the game / application started, measured in seconds. + * + *

This method uses `System.nanoTime()` to obtain a high-precision timestamp. The returned + * value is a `float` representing the elapsed time in seconds. + * + * @return The real-time elapsed since the game started, in seconds. + */ + public float getRealtimeSinceStartup() { + return (System.nanoTime() - startTime) / 1_000_000_000.0f; + } + + /** + * Returns the real-time elapsed since the game / application started, measured in seconds, as a + * `double` value. + * + *

This method uses `System.nanoTime()` to obtain a high-precision timestamp. The returned + * value is a `double` representing the elapsed time in seconds, providing higher precision than + * the `float` version. + * + * @return The real-time elapsed since the game started, in seconds, as a `double`. + */ + public double getRealtimeSinceStartupAsDouble() { + return (System.nanoTime() - startTime) / 1_000_000_000.0; + } + + /** + * Returns the total number of frames that have passed since the Timer started. + * + * @return the total frame count + */ + public int getFrameCount() { + return frameCount; + } + + /** + * Formats a time value in seconds into a string in the format HH:MM:SS. + * + * @param timeInSeconds the time in seconds to format + * @return the formatted time string + */ + private String formatTime(float timeInSeconds) { + int s = (int) timeInSeconds; + return String.format("%d:%02d:%02d", s / 3600, (s % 3600) / 60, s % 60); + } + + /** + * Returns a string representation of this Timer, showing its current state. + * + * @return a string representation of the Timer + */ + @Override + public String toString() { + return "Timer [millisecondCounter=" + + millisecondCounter + + ", lastFrameCount=" + + lastFrameCount + + ", fps=" + + fps + + ", lastTime=" + + lastTime + + ", time=" + + time + + ", totalTime=" + + totalTime + + ", timeScale=" + + timeScale + + ", frameCount=" + + frameCount + + "]"; + } +} diff --git a/src/main/java/engine/components/AbstractComponent.java b/src/main/java/engine/components/AbstractComponent.java new file mode 100644 index 00000000..669586e0 --- /dev/null +++ b/src/main/java/engine/components/AbstractComponent.java @@ -0,0 +1,41 @@ +package engine.components; + +import engine.scene.SceneNode; + +/** + * Abstract base class for all components in the scene graph. + *

+ * This class provides a shared implementation of common functionality across + * all components, reducing boilerplate and centralizing shared logic for ease + * of maintenance. + *

+ */ +public abstract class AbstractComponent implements Component { + + /** Reference to the owning SceneNode */ + protected SceneNode owner; + + /** + * Sets the owner (parent node) of this component. + *

+ * This is common logic provided by the abstract class to ensure consistency + * among all components. + *

+ * + * @param owner The SceneNode that owns this component. + */ + @Override + public void setOwner(SceneNode owner) { + this.owner = owner; + } + + /** + * Retrieves the owning node for convenience. + * + * @return The owning SceneNode instance. + */ + public SceneNode getOwner() { + return owner; + } + +} \ No newline at end of file diff --git a/src/main/java/engine/components/CinematicBlackBarsRenderer.java b/src/main/java/engine/components/CinematicBlackBarsRenderer.java new file mode 100644 index 00000000..7a397239 --- /dev/null +++ b/src/main/java/engine/components/CinematicBlackBarsRenderer.java @@ -0,0 +1,203 @@ +package engine.components; + +import math.Color; +import workspace.ui.Graphics; + +/** + * CinematicBlackBarsRenderer is responsible for rendering cinematic black bars + * on the top and bottom of the screen. It supports smooth fade-in and fade-out + * animations for cinematic effects during transitions. + *

+ * This component renders black bars with configurable speed and handles dynamic + * animations like appearing or disappearing smoothly over time. The fade + * effects are controlled by the speed of transition and a constant bar size (in + * pixels). + *

+ */ +public class CinematicBlackBarsRenderer extends AbstractComponent + implements RenderableComponent { + + /** Default height (in pixels) of the cinematic bars at full fade-in. */ + private static final int DEFAULT_TARGET_BAR_HEIGHT = 200; + + /** + * Default speed at which the fade-in and fade-out effects occur (in pixels + * per second). + */ + private static final float DEFAULT_FADE_SPEED = 70.0f; + + /** Indicates whether the black bars are currently fading in. */ + private boolean fadingIn; + + /** Indicates whether the black bars are currently fading out. */ + private boolean fadingOut; + + /** Current visual size of the black bars (interpolated over time). */ + private float currentSize; + + /** The speed at which fade animations are applied (in pixels/second). */ + private float fadeSpeed; + + /** The target bar height at full fade-in (in pixels). */ + private float targetBarHeight; + + /** + * Default constructor for initializing the renderer instance. + *

+ * This sets the default fade speed and target bar height to predefined + * constants. + *

+ */ + public CinematicBlackBarsRenderer() { + this.fadeSpeed = DEFAULT_FADE_SPEED; + this.targetBarHeight = DEFAULT_TARGET_BAR_HEIGHT; + } + + /** + * Constructor with configurable fade speed and target bar height. + * + * @param fadeSpeed Speed at which fade animations will play (in + * pixels/second). + * @param targetBarHeight The target height of the cinematic bars at full + * fade-in. + */ + public CinematicBlackBarsRenderer(float fadeSpeed, float targetBarHeight) { + this.fadeSpeed = fadeSpeed; + this.targetBarHeight = targetBarHeight; + } + + /** + * Updates the animation state over time. + *

+ * This method advances the fade-in and fade-out animations by incrementally + * updating the `currentSize` value based on elapsed time and speed. Stops + * animations when they reach their final states. + *

+ * + * @param tpf Time per frame (time elapsed since the last frame in seconds). + */ + @Override + public void update(float tpf) { + if (!isFading()) { + return; + } + if (fadingIn) { + animateFadeIn(tpf); + } + if (fadingOut) { + animateFaceOut(tpf); + } + } + + /** + * Handles the fade-in animation by incrementally increasing the size of the + * black bars over time until the target bar height is reached. + * + * @param tpf Time per frame (time elapsed since the last frame in seconds). + */ + private void animateFadeIn(float tpf) { + currentSize += fadeSpeed * tpf; + if (currentSize >= targetBarHeight) { + currentSize = targetBarHeight; + fadingIn = false; + } + } + + /** + * Handles the fade-out animation by decrementally decreasing the size of the + * black bars over time until their visual size is zero. + * + * @param tpf Time per frame (time elapsed since the last frame in seconds). + */ + private void animateFaceOut(float tpf) { + currentSize -= fadeSpeed * tpf; + if (currentSize <= 0) { + currentSize = 0; + fadingOut = false; + } + } + + /** + * Determines if a fade animation (either in or out) is currently active. + * + * @return {@code true} if either fading in or fading out is active, + * {@code false} otherwise. + */ + public boolean isFading() { + return fadingIn || fadingOut; + } + + /** + * Starts the fade-in animation. + *

+ * This stops any ongoing fade-out animation and sets the internal state to + * begin the fade-in effect from the current position. + *

+ */ + public void fadeIn() { + if (fadingIn) + return; + fadingIn = true; + fadingOut = false; + } + + /** + * Starts the fade-out animation. + *

+ * This stops any ongoing fade-in animation and sets the internal state to + * begin the fade-out effect from the current position. + *

+ */ + public void fadeOut() { + if (fadingOut) + return; + fadingOut = true; + fadingIn = false; + } + + /** + * Renders the cinematic black bars at the top and bottom of the screen. + *

+ * The rendering uses the current interpolated size value (`currentSize`) to + * draw smooth black bars transitioning in and out. This rendering respects + * the screen's current dimensions and is dynamically updated during + * animation. + *

+ * + * @param g The graphics context used to perform the rendering. + */ + @Override + public void render(Graphics g) { + g.setColor(Color.BLACK); + renderTopBar(g); + renderBottomBar(g); + } + + /** + * Renders the top cinematic black bar using the current animation state. + * + * @param g The graphics context used for rendering. + */ + private void renderTopBar(Graphics g) { + g.fillRect(0, 0, g.getWidth(), (int) currentSize); + } + + /** + * Renders the bottom cinematic black bar using the current animation state. + * + * @param g The graphics context used for rendering. + */ + private void renderBottomBar(Graphics g) { + g.fillRect(0, g.getHeight() - (int) currentSize, g.getWidth(), + (int) currentSize); + } + + @Override + public void onAttach() { + } + + @Override + public void onDetach() { + } + +} \ No newline at end of file diff --git a/src/main/java/engine/components/CircularAnimationComponent.java b/src/main/java/engine/components/CircularAnimationComponent.java new file mode 100644 index 00000000..4e4aa193 --- /dev/null +++ b/src/main/java/engine/components/CircularAnimationComponent.java @@ -0,0 +1,110 @@ +package engine.components; + +import engine.scene.SceneNode; +import math.Mathf; + +/** + * A simple animation component that makes the owner SceneNode move in circular + * motion over the XZ plane. + *

+ * This component calculates circular motion based on a defined radius and + * angular speed, updating the position of the owning SceneNode in real-time. + * The motion occurs on the horizontal plane (XZ plane) while keeping the + * Y-coordinate constant. + *

+ *

+ * Properties: + *

+ *

+ *

+ * This component updates the position of the owning SceneNode every frame, + * creating smooth circular motion over time. + *

+ * + * @see SceneNode + * @see Transform + */ +public class CircularAnimationComponent extends AbstractComponent { + + /** + * Radius of circular motion. Determines how far from the center the object + * orbits. + */ + private float radius; + + /** + * Angular speed in radians per second that determines how fast the object + * orbits. + */ + private float angularSpeed; + + /** Tracks the total elapsed time since the start of the animation. */ + private float timeElapsed; + + /** + * Default constructor that initializes with default values: - radius = 30.0f + * - angularSpeed = 1.0f + */ + public CircularAnimationComponent() { + this.radius = 30.0f; + this.angularSpeed = 1.0f; + this.timeElapsed = 0.0f; + } + + /** + * Creates a CircularAnimationComponent with specified radius and angular + * speed. + * + * @param radius The distance from the center of circular motion to the + * object. + * @param angularSpeed The speed at which the object orbits, in radians per + * second. + */ + public CircularAnimationComponent(float radius, float angularSpeed) { + this.radius = radius; + this.angularSpeed = angularSpeed; + } + + /** + * Updates the animation logic each frame. + *

+ * This method calculates the new position of the owning SceneNode in a + * circular path using trigonometric functions. The position is updated on the + * XZ plane with constant Y-coordinate to simulate planar circular motion. + *

+ * + * @param tpf Time per frame (time in seconds since the last frame). + */ + @Override + public void update(float tpf) { + // Update the elapsed time based on the time per frame + timeElapsed += tpf; + + // Calculate the new X and Z positions using circular motion formulas + float x = radius * Mathf.cos(angularSpeed * timeElapsed); + float z = radius * Mathf.sin(angularSpeed * timeElapsed); + + // Retrieve the transform of the owning SceneNode + SceneNode node = getOwner(); + Transform transform = node.getTransform(); + + // Set the new position while maintaining the current Y-coordinate + transform.setPosition(x, transform.getPosition().y, z); + } + + @Override + public void onAttach() { + } + + @Override + public void onDetach() { + } + +} \ No newline at end of file diff --git a/src/main/java/engine/components/Component.java b/src/main/java/engine/components/Component.java new file mode 100644 index 00000000..9cd5fc4c --- /dev/null +++ b/src/main/java/engine/components/Component.java @@ -0,0 +1,65 @@ +package engine.components; + +import engine.scene.SceneNode; + +/** + * Represents a generic component within the scene graph architecture. + *

+ * Components are modular pieces of behavior or functionality that can be added + * to {@link SceneNode} instances. They encapsulate logic, rendering, or other + * behaviors, following a component-based design pattern. + *

+ *

+ * Each component should manage its lifecycle, with {@code onAttach()}, + * {@code update()}, and {@code onDetach()} methods, allowing nodes to manage + * their behavior lifecycle cleanly. + *

+ */ +public interface Component { + + /** + * Sets the owning {@link SceneNode} for this component. + *

+ * This is called when the component is added to a {@link SceneNode}. The + * owning node serves as the context within which this component operates, + * allowing it to interact with other components or node transformations. + *

+ * + * @param owner The {@link SceneNode} that owns this component; cannot be + * null. + */ + void setOwner(SceneNode owner); + + /** + * Updates the component's logic every frame. + *

+ * Called once per frame during the scene's update cycle. The time-per-frame + * (tpf) is passed in to allow for time-based animations or logic that depends + * on frame timing. + *

+ * + * @param tpf The time per frame in seconds (time delta) since the last + * update. + */ + void update(float tpf); + + /** + * Called when the component is attached to a {@link SceneNode}. + *

+ * This allows the component to set up necessary resources, state, or perform + * other preparatory work specific to being added to a node. + *

+ */ + void onAttach(); + + /** + * Called when the component is detached from a {@link SceneNode}. + *

+ * This ensures no memory is leaked, threads are terminated, or other + * resources are left hanging by cleaning up internal state and releasing + * references. + *

+ */ + void onDetach(); + +} \ No newline at end of file diff --git a/src/main/java/engine/components/ControlWASD.java b/src/main/java/engine/components/ControlWASD.java new file mode 100644 index 00000000..87b2d42e --- /dev/null +++ b/src/main/java/engine/components/ControlWASD.java @@ -0,0 +1,163 @@ +package engine.components; + +import engine.input.Input; +import engine.input.Key; +import engine.scene.SceneNode; +import math.Vector3f; + +/** + * ControlWASD is a movement component that allows moving a node in the scene + * graph using keyboard input (W/A/S/D). It integrates with the scene's + * transformation system to control position changes, allowing basic 3D + * navigation in a scene graph. + * + *

+ * Movement is normalized to ensure consistent speed, even when moving + * diagonally. This class supports velocity adjustments via the speed multiplier + * and works by translating the owning node within the 3D world space. + *

+ * + *

+ * Example usage: Attach this component to a {@link SceneNode} to allow movement + * using WASD keyboard inputs. The position updates depend on the elapsed time + * per frame to ensure smooth and consistent movement over varying frame rates. + *

+ */ +public class ControlWASD extends AbstractComponent { + + /** + * The speed multiplier determines how fast the node moves per second. + */ + private float speed; + + /** + * The Input instance to monitor keyboard events (injected dependency). + */ + private Input input; + + /** + * Constructs a new ControlWASD component, injecting the Input logic needed + * for detecting key presses. + * + * @param input The Input system used to check for keyboard input events. Must + * not be null. + * @throws IllegalArgumentException if input is null. + */ + public ControlWASD(Input input) { + this(input, 1); + } + + /** + * Constructs a new ControlWASD component, injecting the Input logic needed + * for detecting key presses and allowing a custom movement speed multiplier. + * + *

+ * This constructor allows developers to specify custom speeds for different + * movement styles, debugging scenarios, or game mechanics adjustments. + *

+ * + * @param input The Input system used to monitor keyboard input events. Must + * not be null. + * @param speed The speed multiplier determining how fast the node moves per + * second. Must be non-negative. + * @throws IllegalArgumentException if input is null or speed is less than 0. + */ + public ControlWASD(Input input, float speed) { + if (input == null) { + throw new IllegalArgumentException("Input cannot be null."); + } + if (speed < 0) { + throw new IllegalArgumentException("Speed must be non-negative."); + } + this.input = input; + this.speed = speed; + } + + /** + * Updates the movement of the owning node based on keyboard input. + *

+ * This method calculates the velocity vector by processing the WASD input and + * applies it to move the node smoothly in the 3D space by adjusting its + * position over time with respect to `tpf` (time per frame). + *

+ * + * @param tpf Time per frame, used to ensure frame-rate-independent movement. + */ + @Override + public void update(float tpf) { + SceneNode node = getOwner(); + Vector3f velocity = handleInput(); + + Transform transform = node.getTransform(); + Vector3f position = transform.getPosition(); + + transform.setPosition(position.add(velocity.mult(tpf))); + } + + /** + * Processes keyboard input to determine velocity. Handles the W/A/S/D keys to + * allow movement in the 3D plane. Normalizes the vector to ensure diagonal + * movement doesn't lead to faster speeds. + * + * @return A velocity vector representing the computed movement direction and + * speed. + */ + private Vector3f handleInput() { + Vector3f velocity = new Vector3f(); + + // Check for movement inputs + if (input.isKeyPressed(Key.W)) + velocity.addLocal(0, 0, -1); + if (input.isKeyPressed(Key.A)) + velocity.addLocal(-1, 0, 0); + if (input.isKeyPressed(Key.S)) + velocity.addLocal(0, 0, 1); + if (input.isKeyPressed(Key.D)) + velocity.addLocal(1, 0, 0); + + // Normalize diagonal movement to prevent unintended speed boosts + if (velocity.length() > 0) { + velocity.normalizeLocal().multLocal(speed); + } + return velocity; + } + + /** + * Retrieves the current movement speed multiplier. + * + * @return The current speed value. + */ + public float getSpeed() { + return speed; + } + + /** + * Sets the movement speed multiplier for controlling how fast the node moves + * through the scene. + * + *

+ * A speed of 1.0 represents the default unit speed. Increasing this value + * will make the node move faster, while decreasing it will slow it down. + * Movement speed is scaled by elapsed time per frame to ensure + * frame-rate-independent movement. + *

+ * + * @param speed The new speed value to set. Must be a non-negative number. + * @throws IllegalArgumentException if the speed is less than 0. + */ + public void setSpeed(float speed) { + if (speed < 0) { + throw new IllegalArgumentException("Speed must be non-negative."); + } + this.speed = speed; + } + + @Override + public void onAttach() { + } + + @Override + public void onDetach() { + } + +} \ No newline at end of file diff --git a/src/main/java/engine/components/Geometry.java b/src/main/java/engine/components/Geometry.java new file mode 100644 index 00000000..eb658fc6 --- /dev/null +++ b/src/main/java/engine/components/Geometry.java @@ -0,0 +1,164 @@ +package engine.components; + +import engine.render.Material; +import math.Bounds; +import math.Color; +import mesh.Mesh3D; +import mesh.util.MeshBoundsCalculator; +import workspace.ui.Graphics; + +/** + * The {@code Geometry} class represents a 3D object in a scene with a mesh and + * material applied to it. It is responsible for rendering the mesh and applying + * the appropriate material to it. The class also provides access to the mesh's + * bounding box, which is useful for purposes like culling, spatial + * partitioning, and debugging. + * + * This class implements the {@link RenderableComponent} interface, indicating + * that it has a render method to be invoked during the render loop of the + * engine. + * + * @see RenderableComponent + * @see Material + * @see Mesh3D + * @see Bounds + */ +public class Geometry extends AbstractComponent implements RenderableComponent { + + /** The mesh representing the geometry of the object. */ + private Mesh3D mesh; + + /** The material applied to the mesh for rendering. */ + private Material material; + + /** + * The bounding box of the mesh used for culling, spatial partitioning, and + * debugging. + */ + private Bounds bounds; + + /** + * Constructs a {@code Geometry} with the specified mesh and a default + * material. + * + * @param mesh The {@link Mesh3D} object representing the geometry of the + * object. + * @throws IllegalArgumentException If the mesh is {@code null}. + */ + public Geometry(Mesh3D mesh) { + this(mesh, Material.DEFAULT_WHITE); + } + + /** + * Constructs a {@code Geometry} with the specified mesh and material. + * + * @param mesh The {@link Mesh3D} object representing the geometry of the + * object. + * @param material The {@link Material} to be applied to the mesh. + * @throws IllegalArgumentException If the mesh or material is {@code null}. + */ + public Geometry(Mesh3D mesh, Material material) { + validate(mesh, material); + this.mesh = mesh; + this.material = material; + this.bounds = MeshBoundsCalculator.calculateBounds(mesh); + } + + /** + * Validates the mesh and material to ensure they are not {@code null}. + * + * @param mesh The {@link Mesh3D} object to validate. + * @param material The {@link Material} to validate. + * @throws IllegalArgumentException If the mesh or material is {@code null}. + */ + private void validate(Mesh3D mesh, Material material) { + if (mesh == null) { + throw new IllegalArgumentException("Mesh cannot be null."); + } + if (material == null) { + throw new IllegalArgumentException("Material cannot be null."); + } + } + + /** + * Renders the geometry by applying the material and drawing the mesh using + * the specified graphics context. + * + * @param g The {@link Graphics} context used for rendering. + */ + @Override + public void render(Graphics g) { + material.apply(g); + g.fillFaces(mesh); + material.release(g); + debugRenderBounds(g); + } + + /** + * Debugs the rendering by drawing the bounding box of the mesh using the + * specified graphics context. The bounding box is rendered in red to help + * visualize the mesh's extents. This method can be used for debugging + * purposes to ensure the mesh is properly positioned and scaled in the scene. + * + *

+ * Beyond debugging, the bounding box is useful for spatial partitioning + * techniques like frustum culling, as well as for determining the overall + * size and position of the mesh in the 3D world. + *

+ * + * @param g The {@link Graphics} context used for rendering the debug bounding + * box. + */ + public void debugRenderBounds(Graphics g) { + if (bounds == null) { + return; + } + + g.setColor(Color.RED); + + // Extract corner points for readability + float minX = bounds.getMin().x; + float minY = bounds.getMin().y; + float minZ = bounds.getMin().z; + float maxX = bounds.getMax().x; + float maxY = bounds.getMax().y; + float maxZ = bounds.getMax().z; + + // Draw lines for each edge of the bounding box + g.drawLine(minX, minY, minZ, maxX, minY, minZ); + g.drawLine(minX, minY, minZ, minX, maxY, minZ); + g.drawLine(minX, minY, minZ, minX, minY, maxZ); + + g.drawLine(maxX, maxY, maxZ, minX, maxY, maxZ); + g.drawLine(maxX, maxY, maxZ, maxX, minY, maxZ); + g.drawLine(maxX, maxY, maxZ, maxX, maxY, minZ); + + g.drawLine(minX, maxY, minZ, maxX, maxY, minZ); + g.drawLine(maxX, minY, minZ, maxX, maxY, minZ); + g.drawLine(maxX, minY, minZ, maxX, minY, maxZ); + + g.drawLine(minX, maxY, maxZ, minX, minY, maxZ); + g.drawLine(maxX, minY, maxZ, minX, minY, maxZ); + g.drawLine(minX, maxY, maxZ, minX, maxY, minZ); + } + + /** + * Updates the state of the geometry. This method is a placeholder for + * potential updates to the mesh state over time. + * + * @param tpf The time per frame used for the update (in seconds). + */ + @Override + public void update(float tpf) { + // Placeholder for potential mesh state updates + } + + @Override + public void onAttach() { + } + + @Override + public void onDetach() { + } + +} \ No newline at end of file diff --git a/src/main/java/engine/components/RenderableComponent.java b/src/main/java/engine/components/RenderableComponent.java new file mode 100644 index 00000000..b4a66416 --- /dev/null +++ b/src/main/java/engine/components/RenderableComponent.java @@ -0,0 +1,45 @@ +package engine.components; + +import engine.scene.SceneNode; +import workspace.ui.Graphics; + +/** + * Represents a renderable component within the scene graph architecture. + *

+ * RenderComponents are modular pieces of rendering behavior that can be added + * to {@link SceneNode} instances. They follow the component-based design + * pattern, encapsulating rendering logic and enabling flexible and reusable + * visual behavior within a scene. + *

+ *

+ * A RenderComponent should implement the rendering logic in its + * {@code render()} method, which is invoked during the rendering pass of a + * scene graph. It is designed to work with a graphics context (such as that + * represented by the {@link Graphics} class) to issue drawing commands. + *

+ *

+ * Example use cases include rendering meshes, particles, UI overlays, or other + * visual elements as part of a node's rendering pipeline. + *

+ * + * @see Component + * @see SceneNode + */ +public interface RenderableComponent extends Component { + + /** + * Renders this component using the provided graphics context. + *

+ * This method is called during the rendering phase of the scene graph + * traversal. Implementations of this method should issue drawing commands + * using the provided {@link Graphics} instance. Components should encapsulate + * the logic necessary to visualize themselves or their state. + *

+ * + * @param g The graphics context to use for rendering. This context provides + * methods for transformations, shading, drawing primitives, and + * other rendering operations. + */ + void render(Graphics g); + +} \ No newline at end of file diff --git a/src/main/java/engine/components/RotationComponent.java b/src/main/java/engine/components/RotationComponent.java new file mode 100644 index 00000000..dbdab16f --- /dev/null +++ b/src/main/java/engine/components/RotationComponent.java @@ -0,0 +1,122 @@ +package engine.components; + +import engine.scene.SceneNode; +import math.Vector3f; + +/** + * A simple rotation component that rotates the owning SceneNode around a + * specified axis at a constant angular speed. + * + *

+ * Properties: + *

+ *

+ * + *

+ * The rotation is applied incrementally to the Transform component of the + * owning SceneNode during each frame update. + *

+ */ +public class RotationComponent extends AbstractComponent { + + /** The axis around which the node will rotate. */ + private Vector3f axis; + + /** The angular speed of the rotation in radians per second. */ + private float angularSpeed; + + /** + * Constructs a RotationComponent with default axis (0, 1, 0) and angular + * speed. Default settings will rotate around the Y-axis. + */ + public RotationComponent() { + this.axis = new Vector3f(0, 1, 0); // Default rotation axis: Y-axis + this.angularSpeed = 1.0f; // Default angular speed: 1 radian/second + } + + /** + * Constructs a RotationComponent with a specified axis and angular speed. + * + * @param axis The axis of rotation (must not be null). + * @param angularSpeed The angular speed in radians per second. + */ + public RotationComponent(Vector3f axis, float angularSpeed) { + if (axis == null || axis.length() == 0) { + throw new IllegalArgumentException( + "Rotation axis cannot be null or zero-length."); + } + this.axis = axis.normalize(); // Normalize the axis to ensure proper + // rotation + this.angularSpeed = angularSpeed; + } + + /** + * Updates the rotation logic each frame. + * + * @param tpf Time per frame (in seconds since the last frame). + */ + @Override + public void update(float tpf) { + SceneNode node = getOwner(); + if (node == null) + return; + + // Calculate the incremental rotation angle + float angleIncrement = angularSpeed * tpf; + + // Apply rotation to the owning SceneNode's Transform + node.getTransform().rotate(axis.mult(angleIncrement)); + } + + /** + * Sets a new rotation axis for the component. + * + * @param axis The new axis of rotation. + */ + public void setAxis(Vector3f axis) { + if (axis == null || axis.length() == 0) { + throw new IllegalArgumentException( + "Rotation axis cannot be null or zero-length."); + } + this.axis = axis.normalize(); + } + + /** + * Sets the angular speed of the rotation. + * + * @param angularSpeed The new angular speed in radians per second. + */ + public void setAngularSpeed(float angularSpeed) { + this.angularSpeed = angularSpeed; + } + + /** + * Gets the current rotation axis. + * + * @return The axis of rotation. + */ + public Vector3f getAxis() { + return axis; + } + + /** + * Gets the current angular speed. + * + * @return The angular speed in radians per second. + */ + public float getAngularSpeed() { + return angularSpeed; + } + + @Override + public void onAttach() { + } + + @Override + public void onDetach() { + } + +} \ No newline at end of file diff --git a/src/main/java/engine/components/Transform.java b/src/main/java/engine/components/Transform.java new file mode 100644 index 00000000..dd4a0d84 --- /dev/null +++ b/src/main/java/engine/components/Transform.java @@ -0,0 +1,245 @@ +package engine.components; + +import math.Vector3f; +import workspace.ui.Graphics; + +/** + * Represents a transformation in 3D space, encapsulating position, rotation, + * and scale. Implements the Component interface. + *

+ * The {@code Transform} class provides functionality to manipulate and apply + * transformations such as translation, rotation, and scaling to objects or + * nodes in a 3D scene. It acts as a helper utility to represent and modify the + * spatial properties of a 3D object or a scene node. + *

+ *

+ * The transformations are defined by: + *

+ *

+ *

+ * This class provides methods for applying transformations to a rendering + * context, modifying transformations incrementally, and setting transformation + * properties explicitly. + *

+ * + * @see Vector3f + * @see Graphics + */ +public class Transform extends AbstractComponent { + + /** The position of this transform in 3D space. */ + private Vector3f position; + + /** + * The rotation (in radians) of this transform around the X, Y, and Z axes. + */ + private Vector3f rotation; + + /** The scaling factors along the X, Y, and Z axes. */ + private Vector3f scale; + + /** + * Constructs a new {@code Transform} with default position, rotation, and + * scale values. + *

+ * The default position is set to (0, 0, 0), the default rotation is (0, 0, + * 0), and the default scale is (1, 1, 1). + *

+ */ + public Transform() { + this.position = new Vector3f(); + this.rotation = new Vector3f(); + this.scale = new Vector3f(1, 1, 1); + } + + /** + * Applies this transformation to the given graphics context. + *

+ * This method translates the context to the object's position, applies + * rotations around the X, Y, and Z axes, and scales the object using the + * defined scale values. + *

+ * + * @param g The graphics context to which this transformation is applied. + */ + public void apply(Graphics g) { + g.translate(position.x, position.y, position.z); + g.rotateX(rotation.x); + g.rotateY(rotation.y); + g.rotateZ(rotation.z); + g.scale(scale.x, scale.y, scale.z); + } + + /** + * Translates this transformation by the given delta vector. + *

+ * This modifies the position of the transform by adding the provided vector + * to the current position. + *

+ * + * @param delta The vector representing the change in position. + */ + public void translate(Vector3f delta) { + this.position.addLocal(delta); + } + + /** + * Rotates this transformation by the given delta vector (in radians). + *

+ * This modifies the rotation of the transform by adding the provided vector's + * values to the current rotation. + *

+ * + * @param delta The vector representing the change in rotation (in radians). + */ + public void rotate(Vector3f delta) { + this.rotation.addLocal(delta); + } + + /** + * Scales this transformation by the provided scaling factors. + *

+ * This modifies the scale of the transform by multiplying the current scale + * values by the provided factors. + *

+ * + * @param factor The vector representing the scale factors to apply along each + * axis. + */ + public void scale(Vector3f factor) { + this.scale.multLocal(factor); + } + + /** + * Retrieves a copy of the position vector. + * + * @return A new Vector3f instance representing the current position. + */ + public Vector3f getPosition() { + return new Vector3f(position); + } + + /** + * Sets the position of this transform to a specific value. + * + * @param position The new position vector to set. Must not be null. + * @throws IllegalArgumentException if the provided vector is null. + */ + public void setPosition(Vector3f position) { + if (position == null) { + throw new IllegalArgumentException("Position cannot be null."); + } + this.position.set(position); + } + + /** + * Sets the position of this transform to the specified coordinates. + *

+ * This method updates the position vector to the provided (x, y, z) values, + * effectively moving the object to the given location in 3D space. + *

+ * + * @param x The X-coordinate of the new position. + * @param y The Y-coordinate of the new position. + * @param z The Z-coordinate of the new position. + */ + public void setPosition(float x, float y, float z) { + this.position.set(x, y, z); + } + + /** + * Retrieves a copy of the rotation vector. + * + * @return A new Vector3f instance representing the current rotation. + */ + public Vector3f getRotation() { + return new Vector3f(rotation); + } + + /** + * Sets the rotation of this transform to a specific value. + * + * @param rotation The new rotation vector (in radians) to set. Must not be + * null. + * @throws IllegalArgumentException if the provided vector is null. + */ + public void setRotation(Vector3f rotation) { + if (rotation == null) { + throw new IllegalArgumentException("Rotation cannot be null."); + } + this.rotation.set(rotation); + } + + /** + * Sets the rotation of this transform to the specified angles in radians. + *

+ * This method updates the rotation vector to the provided (rx, ry, rz) + * values, which represent the rotation of the object around the X, Y, and Z + * axes, respectively. + *

+ * + * @param rx The rotation angle around the X-axis, in radians. + * @param ry The rotation angle around the Y-axis, in radians. + * @param rz The rotation angle around the Z-axis, in radians. + */ + public void setRotation(float rx, float ry, float rz) { + this.rotation.set(rx, ry, rz); + } + + /** + * Retrieves a copy of the scale vector. + * + * @return A new Vector3f instance representing the current scale. + */ + public Vector3f getScale() { + return new Vector3f(scale); + } + + /** + * Sets the scale of this transform to a specific value. + * + * @param scale The new scale vector to set. Must not be null. + * @throws IllegalArgumentException if the provided vector is null. + */ + public void setScale(Vector3f scale) { + if (scale == null) { + throw new IllegalArgumentException("Scale cannot be null."); + } + this.scale.set(scale); + } + + /** + * Sets the scale of this transform to the specified scale factors along each + * axis. + *

+ * This method updates the scale vector to the provided (sx, sy, sz) values, + * allowing the object to be scaled uniformly or non-uniformly along the X, Y, + * and Z axes. + *

+ * + * @param sx The scale factor along the X-axis. + * @param sy The scale factor along the Y-axis. + * @param sz The scale factor along the Z-axis. + */ + public void setScale(float sx, float sy, float sz) { + this.scale.set(sx, sy, sz); + } + + @Override + public void update(float tpf) { + } + + @Override + public void onAttach() { + } + + @Override + public void onDetach() { + } + +} \ No newline at end of file diff --git a/src/main/java/engine/render/Material.java b/src/main/java/engine/render/Material.java new file mode 100644 index 00000000..af8206f7 --- /dev/null +++ b/src/main/java/engine/render/Material.java @@ -0,0 +1,288 @@ +package engine.render; + +import math.Color; +import workspace.ui.Graphics; + +/** + * Represents a material with lighting and shader properties for 3D rendering. + *

+ * A Material defines how a 3D object interacts with light, determining its + * visual appearance under various lighting conditions. It encapsulates + * attributes such as ambient, diffuse, and specular light coefficients, + * shininess, and base color, which collectively define how light reflects off + * the object's surface. + *

+ *

+ * This class includes predefined materials for common use cases (e.g., default + * white, black, or other colors) and supports the creation of custom materials + * with specific properties through the use of {@link Builder}. + *

+ *

+ * The material properties can control effects like reflectivity, surface + * roughness, and color, enabling diverse visual representations for 3D meshes + * in a rendering engine. + *

+ */ +public class Material { + + /** + * Default white material with a base white color and standard light + * coefficients. + */ + public static final Material DEFAULT_WHITE = new Material(Color.WHITE); + + /** + * Default black material with a base black color and standard light + * coefficients. + */ + public static final Material DEFAULT_BLACK = new Material(Color.BLACK); + + /** + * Default red material with a base red color and standard light coefficients. + */ + public static final Material DEFAULT_RED = new Material(Color.RED); + + /** + * Default green material with a base green color and standard light + * coefficients. + */ + public static final Material DEFAULT_GREEN = new Material(Color.GREEN); + + /** + * Default blue material with a base blue color and standard light + * coefficients. + */ + public static final Material DEFAULT_BLUE = new Material(Color.BLUE); + + /** + * Metallic silver material with a shiny silver appearance. + */ + public static final Material METALLIC_SILVER_MATERIAL = MaterialFactory + .createMetallicSilver(); + + /** + * Metallic gold material with a shiny gold appearance. + */ + public static final Material GOLDEN_METALLIC_MATERIAL = MaterialFactory + .createGoldenMetallic(); + + /** + * Clear glass material with a transparent appearance. + */ + public static final Material GLASS_CLEAR_MATERIAL = MaterialFactory + .createGlassClear(); + + /** + * Stone grey material with a matte grey appearance. + */ + public static final Material STONE_GREY_MATERIAL = MaterialFactory + .createStoneGrey(); + + /** + * Water material with a reflective blue appearance. + */ + public static final Material WATER_MATERIAL = MaterialFactory.createWater(); + + private boolean useLighting; + + /** + * Base color for the material. + */ + private final Color color; + + /** + * Ambient light coefficient (R, G, B). + */ + private final float[] ambient; + + /** + * Diffuse light coefficient (R, G, B). + */ + private final float[] diffuse; + + /** + * Specular light coefficient (R, G, B). + */ + private final float[] specular; + + /** + * Shininess factor for specular highlights. + */ + private final float shininess; + + /** + * Constructor to set the base color of the material. + * + * @param color The base color of the material. + */ + public Material(Color color) { + this(new Builder().setColor(color)); + } + + private Material(Builder builder) { + this.useLighting = builder.useLighting; + this.color = builder.color; + this.ambient = builder.ambient; + this.diffuse = builder.diffuse; + this.specular = builder.specular; + this.shininess = builder.shininess; + } + + /** + * Builder class to facilitate the creation of custom materials with specific + * lighting and shader properties. + */ + public static class Builder { + + private boolean useLighting = true; + + private Color color = new Color(1, 1, 1); // Default color is white + + private float[] ambient = new float[] { 0.2f, 0.2f, 0.2f }; + + private float[] diffuse = new float[] { 1.0f, 1.0f, 1.0f }; + + private float[] specular = new float[] { 1.0f, 1.0f, 1.0f }; + + private float shininess = 10.0f; + + /** + * Sets the base color of the material. + * + * @param color The desired base color. + * @return The builder instance for chaining. + */ + public Builder setColor(Color color) { + this.color = color; + return this; + } + + /** + * Sets the ambient light coefficient of the material. + * + * @param ambient The desired ambient light coefficient (R, G, B). + * @return The builder instance for chaining. + */ + public Builder setAmbient(float[] ambient) { + this.ambient = ambient; + return this; + } + + /** + * Sets the diffuse light coefficient of the material. + * + * @param diffuse The desired diffuse light coefficient (R, G, B). + * @return The builder instance for chaining. + */ + public Builder setDiffuse(float[] diffuse) { + this.diffuse = diffuse; + return this; + } + + /** + * Sets the specular light coefficient of the material. + * + * @param specular The desired specular light coefficient (R, G, B). + * @return The builder instance for chaining. + */ + public Builder setSpecular(float[] specular) { + this.specular = specular; + return this; + } + + /** + * Sets the shininess value of the material. + * + * @param shininess The shininess factor for specular highlights. + * @return The builder instance for chaining. + */ + public Builder setShininess(float shininess) { + this.shininess = shininess; + return this; + } + + public Builder setUseLighting(boolean useLighting) { + this.useLighting = useLighting; + return this; + } + + /** + * Builds and returns the Material instance with the set properties. + * + * @return A new instance of {@link Material}. + */ + public Material build() { + return new Material(this); + } + } + + /** + * Applies this material's properties to the provided rendering context. + * + * @param g The {@link Graphics} instance to apply this material to. + */ + public void apply(Graphics g) { + g.setMaterial(this); + } + + /** + * Releases this material's properties from the rendering context, useful for + * cleaning up shaders or material-specific settings. + * + * @param g The {@link Graphics} instance from which this material will be + * unbound. + */ + public void release(Graphics g) { + // Logic for releasing or resetting rendering context goes here + } + + public boolean isUseLighting() { + return useLighting; + } + + /** + * Retrieves the base color of the material. + * + * @return The {@link Color} representing the base color of the material. + */ + public Color getColor() { + return color; + } + + /** + * Retrieves the ambient light coefficient of the material. + * + * @return An array representing the ambient light coefficient (R, G, B). + */ + public float[] getAmbient() { + return ambient; + } + + /** + * Retrieves the diffuse light coefficient of the material. + * + * @return An array representing the diffuse light coefficient (R, G, B). + */ + public float[] getDiffuse() { + return diffuse; + } + + /** + * Retrieves the specular light coefficient of the material. + * + * @return An array representing the specular light coefficient (R, G, B). + */ + public float[] getSpecular() { + return specular; + } + + /** + * Retrieves the shininess factor of the material. + * + * @return The shininess factor that controls specular highlights. + */ + public float getShininess() { + return shininess; + } + +} \ No newline at end of file diff --git a/src/main/java/engine/render/MaterialFactory.java b/src/main/java/engine/render/MaterialFactory.java new file mode 100644 index 00000000..0360b0a0 --- /dev/null +++ b/src/main/java/engine/render/MaterialFactory.java @@ -0,0 +1,121 @@ +package engine.render; + +import math.Color; + +/** + * Factory class for creating predefined Material instances using a builder + * pattern. + *

+ * This class provides various predefined material configurations, such as + * metallic silver, golden metallic, clear glass, stone grey, and water effects. + * Each material can be created through the builder pattern to encapsulate + * lighting properties like ambient, diffuse, specular colors, and shininess + * levels. + *

+ *

+ * The goal of these predefined materials is to simplify common rendering use + * cases, such as simulating metals, glass, stone, and water effects, by + * abstracting their creation and configuration logic. + *

+ *

+ * Each static method in this class returns a fully built Material object, ready + * for use in rendering pipelines or graphics engines. + *

+ */ +public class MaterialFactory { + + /** + * Creates a metallic silver material with preset properties. + *

+ * This material has a diffuse color representing silver, with ambient, + * diffuse, and specular properties tuned for a metallic effect. + *

+ * + * @return A {@link Material} instance configured as metallic silver. + */ + public static Material createMetallicSilver() { + return new Material.Builder() + .setColor(new Color(0.75f, 0.75f, 0.75f)) + .setAmbient(new float[] { 0.2f, 0.2f, 0.2f }) + .setDiffuse(new float[] { 1.0f, 1.0f, 1.0f }) + .setSpecular(new float[] { 1.0f, 1.0f, 1.0f }) + .setShininess(50.0f) + .build(); + } + + /** + * Creates a golden metallic material with preset properties. + *

+ * This material mimics the reflective, shiny properties of gold. + *

+ * + * @return A {@link Material} instance configured as golden metallic. + */ + public static Material createGoldenMetallic() { + return new Material.Builder() + .setColor(new Color(1.0f, 0.84f, 0.0f)) + .setAmbient(new float[] { 0.3f, 0.3f, 0.3f }) + .setDiffuse(new float[] { 1.0f, 0.84f, 0.0f }) + .setSpecular(new float[] { 1.0f, 1.0f, 0.5f }) + .setShininess(100.0f) + .build(); + } + + /** + * Creates a clear glass-like material with preset properties. + *

+ * This material simulates the transparency and reflection characteristics of + * clear glass. + *

+ * + * @return A {@link Material} instance configured as clear glass. + */ + public static Material createGlassClear() { + return new Material.Builder() + .setColor(new Color(0.7f, 0.9f, 1.0f)) + .setAmbient(new float[] { 0.3f, 0.3f, 0.3f }) + .setDiffuse(new float[] { 0.8f, 0.8f, 0.8f }) + .setSpecular(new float[] { 1.0f, 1.0f, 1.0f }) + .setShininess(5.0f) + .build(); + } + + /** + * Creates a stone grey material with preset properties. + *

+ * This material has a matte stone-like appearance, suitable for terrain, + * rocky surfaces, or architectural models. + *

+ * + * @return A {@link Material} instance configured as stone grey. + */ + public static Material createStoneGrey() { + return new Material.Builder() + .setColor(new Color(0.5f, 0.5f, 0.5f)) + .setAmbient(new float[] { 0.4f, 0.4f, 0.4f }) + .setDiffuse(new float[] { 0.6f, 0.6f, 0.6f }) + .setSpecular(new float[] { 0.2f, 0.2f, 0.2f }) + .setShininess(10.0f) + .build(); + } + + /** + * Creates a water-like material with preset properties. + *

+ * This material simulates the transparency and light-reflective properties of + * water, incorporating a mix of blue and subtle reflective colors. + *

+ * + * @return A {@link Material} instance configured as water. + */ + public static Material createWater() { + return new Material.Builder() + .setColor(new Color(0.0f, 0.5f, 1.0f)) + .setAmbient(new float[] { 0.1f, 0.3f, 0.5f }) + .setDiffuse(new float[] { 0.3f, 0.5f, 0.7f }) + .setSpecular(new float[] { 0.2f, 0.2f, 0.6f }) + .setShininess(2.0f) + .build(); + } + +} diff --git a/src/main/java/engine/render/effects/Particle.java b/src/main/java/engine/render/effects/Particle.java new file mode 100644 index 00000000..7e413908 --- /dev/null +++ b/src/main/java/engine/render/effects/Particle.java @@ -0,0 +1,168 @@ +package engine.render.effects; + +import math.Vector3f; + +/** + * Represents a single particle with physical properties and lifecycle tracking. + * This class models particles' motion using physics principles like velocity, + * acceleration, damping (drag), and lifetime constraints. + *

+ * Designed to support effects like trails, smoke, explosions, or other particle + * effects by integrating old and new positions for rendering purposes. + *

+ * + * @author Simon Dietz + */ +public class Particle { + + /** The current position of the particle in world-space coordinates. */ + private Vector3f position; + + /** + * The previous position of the particle; used for effects like trails or + * motion blur. + */ + private Vector3f oldPosition; + + /** + * The velocity vector of the particle, representing how it moves over time. + */ + private Vector3f velocity; + + /** The acceleration vector affecting the particle's motion. */ + private Vector3f acceleration; + + /** Total duration (in seconds) for which the particle will live. */ + private float lifetime; + + /** Tracks how much time has elapsed since the particle was created. */ + private float elapsedTime; + + /** The damping factor simulates air resistance or drag on the particle. */ + private float dampingFactor = 0.98f; + + /** + * Constructs a new particle with the specified initial position, velocity, + * acceleration, and lifetime. Initializes the old position to match the + * initial position. + * + * @param position Initial position of the particle in 3D space. + * @param velocity Initial velocity vector of the particle. + * @param acceleration Acceleration vector affecting the particle. + * @param lifetime Total time (seconds) this particle will live. + */ + public Particle(Vector3f position, Vector3f velocity, Vector3f acceleration, + float lifetime) { + this.position = new Vector3f(position); + this.oldPosition = new Vector3f(position); + this.velocity = velocity; + this.acceleration = acceleration; + this.lifetime = lifetime; + this.elapsedTime = 0f; + } + + /** + * Applies a force to the particle by adding it to the acceleration vector. + * Useful for simulating environmental effects like wind or gravity. + *

+ * Assumes that mass is constant and equals 1 for simplicity. + *

+ * + * @param force The force vector to apply to the particle's acceleration. + */ + public void applyForce(Vector3f force) { + acceleration.addLocal(force); + } + + /** + * Applies damping to simulate drag or resistance, slowing down the particle's + * motion over time. + */ + public void applyDamping() { + velocity.multLocal(dampingFactor); + } + + /** + * Updates the particle's position, velocity, and applies damping effects over + * a given time step. Resets acceleration after each update to ensure isolated + * force application each frame. + * + * @param deltaTime The time elapsed since the last frame (in seconds). + */ + public void update(float deltaTime) { + elapsedTime += deltaTime; + + oldPosition.set(position); + + // Apply physics: velocity changes due to acceleration + velocity.addLocal(acceleration.mult(deltaTime)); + + // Apply environmental drag/damping + applyDamping(); + + // Update position based on the new velocity + position.addLocal(velocity.mult(deltaTime)); + + // Reset acceleration for the next simulation step + acceleration.set(0, 0, 0); + } + + /** + * Checks if the particle is still alive (i.e., has not exceeded its + * lifetime). + * + * @return {@code true} if the particle's elapsed time is less than its total + * lifetime, otherwise {@code false}. + */ + public boolean isAlive() { + return elapsedTime < lifetime; + } + + /** + * Gets the current position of the particle in 3D space. + * + * @return The current position vector of the particle. + */ + public Vector3f getPosition() { + return position; + } + + /** + * Gets the previous position of the particle. Useful for rendering trails or + * other visual effects. + * + * @return The old position vector of the particle. + */ + public Vector3f getOldPosition() { + return oldPosition; + } + + /** + * Gets the amount of time that has elapsed since the particle was created. + * + * @return The elapsed time in seconds. + */ + public float getElapsedTime() { + return elapsedTime; + } + + /** + * Gets the total lifetime of the particle. + * + * @return The total lifetime of the particle in seconds. + */ + public float getLifetime() { + return lifetime; + } + + /** + * Sets the particle's lifetime to a new value. This can extend or shorten how + * long the particle will exist. + * + * @param lifetime New lifetime value in seconds. + */ + public void setLifetime(float lifetime) { + this.lifetime = lifetime; + } + +} diff --git a/src/main/java/engine/render/effects/ParticleComponent.java b/src/main/java/engine/render/effects/ParticleComponent.java new file mode 100644 index 00000000..991d164a --- /dev/null +++ b/src/main/java/engine/render/effects/ParticleComponent.java @@ -0,0 +1,119 @@ +package engine.render.effects; + +import engine.components.AbstractComponent; +import engine.components.RenderableComponent; +import workspace.ui.Graphics; + +/** + * A component responsible for managing and rendering particles using a + * specified particle emitter and renderer. + *

+ * This class implements the {@link RenderableComponent} interface, allowing it to + * integrate seamlessly with the rendering system. It uses a + * {@link ParticleEmitter} to handle the logic of particle spawning and updates, + * while delegating rendering operations to a {@link ParticleRenderer}. + *

+ *

+ * The ParticleComponent ensures proper lifecycle management by handling + * initialization, updates, rendering, and cleanup for both the emitter and + * renderer components. + *

+ * + * @author Simon Dietz + */ +public class ParticleComponent extends AbstractComponent + implements RenderableComponent { + + private ParticleEmitter emitter; + + private ParticleRenderer renderer; + + /** + * Creates a new ParticleComponent with the given particle emitter and + * renderer. + * + * @param emitter The particle emitter responsible for spawning and managing + * particle lifecycles. + * @param renderer The particle renderer responsible for drawing particles on + * the provided graphics context. + */ + public ParticleComponent(ParticleEmitter emitter, ParticleRenderer renderer) { + this.emitter = emitter; + this.renderer = renderer; + } + + /** + * Initializes the renderer resources necessary for drawing particles. + */ + @Override + public void initialize() { + renderer.initialize(); + } + + /** + * Updates the particle emitter with the time-per-frame value to spawn and + * manage particles over time. + */ + @Override + public void update(float tpf) { + emitter.update(tpf); + } + + /** + * Delegates the rendering of particles to the renderer, passing the current + * particles to visualize. + */ + @Override + public void render(Graphics g) { + renderer.render(g, emitter.getParticles()); + } + + /** + * Cleans up any resources used by the particle renderer. + */ + @Override + public void cleanup() { + renderer.cleanup(); + } + + /** + * Retrieves the particle emitter associated with this component. + * + * @return The ParticleEmitter instance used for spawning and updating + * particles. + */ + public ParticleEmitter getEmitter() { + return emitter; + } + + /** + * Sets a new particle emitter for this component. This can be used to + * dynamically change the emitter's behavior or particle spawning logic at + * runtime. + * + * @param emitter The new ParticleEmitter instance. + */ + public void setEmitter(ParticleEmitter emitter) { + this.emitter = emitter; + } + + /** + * Retrieves the particle renderer associated with this component. + * + * @return The ParticleRenderer responsible for drawing particles. + */ + public ParticleRenderer getRenderer() { + return renderer; + } + + /** + * Sets a new particle renderer for this component. This allows for swapping + * rendering strategies or visualizations dynamically at runtime. + * + * @param renderer The new ParticleRenderer instance. + */ + public void setRenderer(ParticleRenderer renderer) { + this.renderer = renderer; + } + +} diff --git a/src/main/java/engine/render/effects/ParticleEmitter.java b/src/main/java/engine/render/effects/ParticleEmitter.java new file mode 100644 index 00000000..b6aaf710 --- /dev/null +++ b/src/main/java/engine/render/effects/ParticleEmitter.java @@ -0,0 +1,210 @@ +package engine.render.effects; + +import java.util.concurrent.ConcurrentLinkedQueue; + +import math.Vector3f; + +/** + * Represents a particle emitter responsible for generating and managing + * particles based on defined parameters like velocity, acceleration, and + * lifetime ranges. This class supports both continuous particle emission and + * burst-based emission modes. + *

+ * The emitter allows for dynamic configuration of properties such as initial + * velocity ranges, acceleration ranges, particle lifetime ranges, and the + * emission rate. It uses a thread-safe queue to manage particles for efficient + * access and updates. + *

+ * + *

+ * Key Features: + *

+ * + * + * @author Simon Dietz + */ +public class ParticleEmitter { + + /** The world-space origin of the particle emitter. */ + private Vector3f position; + + /** The range for randomizing initial particle velocities. */ + private Vector3f velocityRange; + + /** The range for randomizing initial particle accelerations. */ + private Vector3f accelerationRange; + + /** The range of possible particle lifetimes. */ + private float lifetimeRange; + + /** The rate at which particles are emitted (particles per second). */ + private int particlesPerSecond; + + /** Whether the emitter is currently configured for burst emission mode. */ + private boolean burstMode; + + /** Number of particles to emit during each burst. */ + private int burstCount; + + /** + * Tracks elapsed time to determine when particles should be emitted during + * continuous mode. + */ + private float timeSinceLastEmission = 0f; + + /** A thread-safe queue storing active particles. */ + private ConcurrentLinkedQueue particles; + + /** + * Constructs a new ParticleEmitter with a specified position and emission + * rate. + * + * @param position The initial world-space position of the emitter. + * @param particlesPerSecond The rate at which particles are emitted in + * continuous mode (particles per second). + */ + public ParticleEmitter(Vector3f position, int particlesPerSecond) { + this.position = position; + this.particlesPerSecond = particlesPerSecond; + this.velocityRange = new Vector3f(1f, 1f, 1f); + this.accelerationRange = new Vector3f(0f, 0f, 0f); + this.lifetimeRange = 5f; // Default particle lifetime of 5 seconds + this.particles = new ConcurrentLinkedQueue<>(); + this.burstMode = false; // Default mode is continuous particle emission + this.burstCount = 0; + } + + /** + * Updates particles and performs emission logic based on elapsed time. + * Handles both continuous emission and burst emission logic. Cleans up + * expired particles from the particle queue. + * + * @param deltaTime Time elapsed since the last frame, in seconds. + */ + public void update(float deltaTime) { + if (burstMode) { + emitBurst(); + } else { + timeSinceLastEmission += deltaTime; + float emissionInterval = 1f / particlesPerSecond; + + // Emit particles continuously based on elapsed time + while (timeSinceLastEmission >= emissionInterval) { + emitParticle(); + timeSinceLastEmission -= emissionInterval; + } + } + + // Update and clean expired particles + for (Particle particle : particles) { + particle.update(deltaTime); + if (!particle.isAlive()) { + particles.remove(particle); + } + } + } + + /** + * Emits a single particle with randomized properties (velocity, acceleration, + * and lifetime) within their configured ranges. + */ + private void emitParticle() { + Vector3f initialPosition = new Vector3f(position); + Vector3f initialVelocity = randomizeVector(velocityRange); + Vector3f initialAcceleration = randomizeVector(accelerationRange); + float lifetime = randomizeFloat(lifetimeRange); + + Particle particle = new Particle(initialPosition, initialVelocity, + initialAcceleration, lifetime); + particles.add(particle); + } + + /** + * Emits a burst of particles, the number of which is defined by the + * `burstCount`. After completing a burst, burst mode is disabled + * automatically. + */ + private void emitBurst() { + for (int i = 0; i < burstCount; i++) { + emitParticle(); + } + burstMode = false; // Disable burst mode after the burst is emitted. + } + + /** + * Randomizes a vector's x, y, z components within their respective ranges. + * + * @param range The range to randomize values within. + * @return A new randomized vector. + */ + private Vector3f randomizeVector(Vector3f range) { + return new Vector3f((float) (Math.random() * range.x * 2 - range.x), + (float) (Math.random() * range.y * 2 - range.y), + (float) (Math.random() * range.z * 2 - range.z)); + } + + /** + * Randomizes a float value within a range [0, range). + * + * @param range The range to randomize values within. + * @return A randomized float value. + */ + private float randomizeFloat(float range) { + return (float) (Math.random() * range); + } + + /** + * Configures the emitter to use burst mode with a specified number of + * particles to emit. After enabling, particles will only emit in bursts until + * reset. + * + * @param burstCount Number of particles to emit during each burst. + */ + public void setBurstMode(int burstCount) { + this.burstMode = true; + this.burstCount = burstCount; + } + + /** + * Updates the range for randomizing initial particle velocities. + * + * @param velocityRange The new velocity range. + */ + public void setVelocityRange(Vector3f velocityRange) { + this.velocityRange = velocityRange; + } + + /** + * Updates the range for randomizing initial particle accelerations. + * + * @param accelerationRange The new acceleration range. + */ + public void setAccelerationRange(Vector3f accelerationRange) { + this.accelerationRange = accelerationRange; + } + + /** + * Updates the range of particle lifetimes. + * + * @param lifetimeRange The new lifetime range. + */ + public void setLifetimeRange(float lifetimeRange) { + this.lifetimeRange = lifetimeRange; + } + + /** + * Retrieves all currently active particles managed by this emitter. + * + * @return A concurrent queue containing the currently active particles. + */ + public ConcurrentLinkedQueue getParticles() { + return particles; + } + +} \ No newline at end of file diff --git a/src/main/java/engine/render/effects/ParticleRenderer.java b/src/main/java/engine/render/effects/ParticleRenderer.java new file mode 100644 index 00000000..90b6cfea --- /dev/null +++ b/src/main/java/engine/render/effects/ParticleRenderer.java @@ -0,0 +1,36 @@ +package engine.render.effects; + +import java.util.Collection; + +import workspace.ui.Graphics; + +/** + * Interface for rendering particles in a particle system. Implementations of + * this interface define how particles are visually represented, such as using + * sprites, points, or other rendering techniques. + * + * @author Simon Dietz + */ +public interface ParticleRenderer { + + /** + * Renders a batch of particles using the provided graphics context. + * + * @param g The graphics context used for rendering. + * @param particles The collection of particles to render. + */ + void render(Graphics g, Collection particles); + + /** + * Initializes any resources or setup required for rendering particles. This + * could include shaders, textures, or other rendering assets. + */ + void initialize(); + + /** + * Cleans up resources used by the renderer when it is no longer needed. This + * ensures efficient memory and resource management. + */ + void cleanup(); + +} \ No newline at end of file diff --git a/src/main/java/engine/scene/Scene.java b/src/main/java/engine/scene/Scene.java new file mode 100644 index 00000000..af0935e4 --- /dev/null +++ b/src/main/java/engine/scene/Scene.java @@ -0,0 +1,321 @@ +package engine.scene; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import engine.scene.camera.Camera; +import engine.scene.light.Light; +import workspace.ui.Graphics; + +/** + * The {@code Scene} class manages a hierarchy of {@code SceneNode}s for + * rendering and updating. It handles root-level scene nodes, lighting, and a + * thread pool for parallel updates, offering a high-level interface for + * managing and rendering complex 3D scenes. + */ +public class Scene { + + /** Default name assigned to a newly created scene if no name is provided. */ + private static final String DEFAULT_NAME = "Untitled-Scene"; + + /** + * List of root-level nodes in the scene hierarchy for rendering and updates. + */ + private final List rootNodes = new ArrayList<>(); + + /** List of lights in the scene that are used for lighting calculations. */ + private final List lights = new ArrayList<>(); + + /** Thread pool used to parallelize updates for performance optimization. */ + private final ExecutorService updateExecutor = Executors + .newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + + /** Flag indicating whether the scene is rendered in wireframe mode. */ + private boolean wireframeMode; + + /** Name of the scene. Used for identification or debugging purposes. */ + private final String name; + + /** + * The currently active camera that determines the scene's view + * transformation. + */ + private Camera activeCamera; + + /** + * Constructs a {@code Scene} with a default name. + */ + public Scene() { + this(DEFAULT_NAME); + } + + /** + * Constructs a {@code Scene} with the specified name. + * + * @param name The name of the scene. + * @throws IllegalArgumentException if the name is {@code null}. + */ + public Scene(String name) { + if (name == null) { + throw new IllegalArgumentException("Name cannot be null."); + } + this.name = name; + } + + /** + * Adds a SceneNode to the root level of the scene graph. + * + * @param node The node to add to the root level. + */ + public void addNode(SceneNode node) { + if (node == null) { + throw new IllegalArgumentException("Node cannot be null."); + } + synchronized (rootNodes) { + rootNodes.add(node); + } + } + + /** + * Adds a light to the scene's list of lights for rendering and lighting + * calculations. Ensures thread-safe addition by synchronizing on the `lights` + * list. + * + * @param light The Light instance to be added to the scene. + * @throws IllegalArgumentException if the provided light is null. + */ + public void addLight(Light light) { + if (light == null) { + throw new IllegalArgumentException("Light cannot be null."); + } + synchronized (lights) { + lights.add(light); + } + } + + /** + * Removes a SceneNode from the root level. + * + * @param node The node to remove from the root level. + */ + public void removeNode(SceneNode node) { + if (node == null) + return; + synchronized (rootNodes) { + rootNodes.remove(node); + } + } + + /** + * Perform parallel updates on all nodes in the scene graph. + * + * @param deltaTime The time step for simulation logic updates. + */ + public void update(float deltaTime) { + synchronized (rootNodes) { + for (SceneNode node : Collections.unmodifiableList(rootNodes)) { + // Submit updates to worker threads + updateExecutor.submit(() -> node.update(deltaTime)); + } + } + } + + /** + * Render lights and nodes concurrently. However, rendering must still run on + * the main thread for compatibility with most rendering APIs. + */ + public void render(Graphics g) { + if (activeCamera != null) { + g.applyCamera(activeCamera); + } + + g.setWireframeMode(wireframeMode); + renderLights(g); + + synchronized (rootNodes) { + + for (SceneNode node : rootNodes) { + node.render(g); + } + } + + } + + /** + * Renders all lights in the scene safely by synchronizing access to the + * lights list. This ensures thread-safe iteration and rendering, especially + * when lights are added or removed concurrently. + * + * @param g The graphics context used for rendering the lights. + */ + private void renderLights(Graphics g) { + synchronized (lights) { + for (Light light : lights) { + g.render(light); + } + } + } + + /** + * Cleans up resources and shuts down the executor safely. + */ + public void cleanup() { + // Shutdown thread pool properly + updateExecutor.shutdown(); + synchronized (rootNodes) { + for (SceneNode node : rootNodes) { + node.cleanup(); + } + rootNodes.clear(); + } + } + + /** + * Retrieves the number of root nodes in the scene. + * + * @return The count of root nodes currently present in the scene graph. + */ + public int getRootCount() { + synchronized (rootNodes) { + return rootNodes.size(); + } + } + + /** + * Performs a complete cleanup of all lights and nodes in the scene, in + * addition to shutting down worker threads. This ensures no memory leaks or + * dangling references persist after the scene is no longer needed. + */ + public void cleanupAllResources() { + synchronized (lights) { + lights.clear(); + } + synchronized (rootNodes) { + for (SceneNode node : rootNodes) { + node.cleanup(); + } + rootNodes.clear(); + } + updateExecutor.shutdown(); + } + + /** + * Retrieves all lights that are currently active in the scene. This allows + * querying of lights for dynamic light management features. + * + * @return A thread-safe copy of the current lights list. + */ + public List getAllLights() { + synchronized (lights) { + return new ArrayList<>(lights); + } + } + + /** + * Fetches all the nodes at the root level in a thread-safe way. Useful for + * debugging, visualization, or debugging purposes to monitor scene nodes in + * real-time. + * + * @return A list of root SceneNodes currently in the scene graph. + */ + public List getRootNodes() { + synchronized (rootNodes) { + return new ArrayList<>(rootNodes); + } + } + + /** + * Finds and removes all SceneNodes matching a particular condition. For + * example, it can remove nodes based on type or other predicates. + * + * @param predicate The condition to test nodes against. + * @return The number of nodes removed. + */ + public int removeNodesIf(java.util.function.Predicate predicate) { + int count = 0; + synchronized (rootNodes) { + for (SceneNode node : new ArrayList<>(rootNodes)) { + if (predicate.test(node)) { + node.cleanup(); + rootNodes.remove(node); + count++; + } + } + } + return count; + } + + /** + * Sets the currently active camera for the scene. The active camera + * determines the view and projection matrices used during rendering. If no + * active camera is set, rendering will proceed without camera + * transformations. + * + * @param camera The camera to set as the active camera. May be null to + * disable camera-based rendering logic. + */ + public void setActiveCamera(Camera camera) { + this.activeCamera = camera; + } + + /** + * Retrieves the currently active camera used for rendering the scene. The + * active camera's view and projection matrices define the perspective and + * viewport used during rendering. + * + * @return The currently active camera, or {@code null} if no active camera + * has been set. + */ + public Camera getActiveCamera() { + return this.activeCamera; + } + + /** + * Retrieves the number of lights currently managed by the scene. + *

+ * This method provides the total count of lights that are part of the scene's + * lighting system. It operates in a thread-safe manner by synchronizing + * access to the shared lights list to ensure accurate and safe read + * operations, even when lights are being added or removed concurrently. + *

+ * + * @return The total number of lights currently in the scene. + */ + public int getLightCount() { + synchronized (lights) { + return lights.size(); + } + } + + /** + * Retrieves the name of the scene. + * + * @return The name of the scene. + */ + public String getName() { + return name; + } + + /** + * Checks if the scene is in wireframe rendering mode. + * + * @return {@code true} if wireframe mode is enabled, {@code false} otherwise. + */ + public boolean isWireframeMode() { + return wireframeMode; + } + + /** + * Enables or disables wireframe rendering mode for the scene. + * + * @param wireframeMode {@code true} to enable wireframe mode, {@code false} + * to disable it. + */ + public void setWireframeMode(boolean wireframeMode) { + this.wireframeMode = wireframeMode; + } + +} \ No newline at end of file diff --git a/src/main/java/engine/scene/SceneNode.java b/src/main/java/engine/scene/SceneNode.java new file mode 100644 index 00000000..5a5a6550 --- /dev/null +++ b/src/main/java/engine/scene/SceneNode.java @@ -0,0 +1,375 @@ +package engine.scene; + +import java.util.ArrayList; +import java.util.List; + +import engine.components.Component; +import engine.components.RenderableComponent; +import engine.components.Transform; +import workspace.ui.Graphics; + +/** + * Represents a single node within the scene graph. + *

+ * A {@code SceneNode} is a fundamental building block in a hierarchical scene + * graph structure, managing child nodes, components, and transformations. Scene + * nodes are organized in a parent-child relationship, where a parent node can + * have multiple children, and each child can have its own components and + * transformations. + *

+ *

+ * The {@code SceneNode} manages its own transformation through a + * {@link Transform} object, handles rendering, updates logic for itself and its + * children, and provides methods for managing components like + * {@link RenderableComponent}. + *

+ *

+ * Example use cases include: + *

    + *
  • Modeling a hierarchy of objects (e.g., parts of a character or modular + * environment pieces).
  • + *
  • Managing rendering logic and transformations in a scene graph.
  • + *
  • Composing behavior with reusable components for modular design.
  • + *
+ *

+ * + * @see Transform + * @see Component + * @see RenderComponents + */ +public class SceneNode { + + /** + * The default name assigned to a scene node if no name is provided. + */ + private static final String DEFAULT_NAME = "Untitled-Node"; + + /** + * The name of this node, primarily intended for debugging and identification + * purposes. + */ + private String name; + + /** The parent node in the scene graph hierarchy. */ + private SceneNode parent; + + /** List of child nodes attached to this node. */ + private List children; + + /** List of components (logic/rendering behavior) attached to this node. */ + private List components; + + /** + * Creates a new {@code SceneNode} with the specified name. + * + * @param name The name to assign to the scene node. + * @throws IllegalArgumentException if the name is {@code null}. + */ + public SceneNode(String name) { + if (name == null) { + throw new IllegalArgumentException("Name cannot be null."); + } + this.name = name; + this.children = new ArrayList(); + this.components = new ArrayList(); + // Add a default Transform component + this.components.add(new Transform()); + } + + /** + * Constructs a new, empty {@code SceneNode} with default transformations, an + * empty list of children, and an empty list of components. + */ + public SceneNode() { + this(DEFAULT_NAME); + } + + /** + * Renders this node and all its children recursively. + *

+ * This method applies the local transformation, renders components, and + * traverses through all child nodes to render them as well. This ensures the + * entire subtree rooted at this node is rendered properly. + *

+ * + * @param g The graphics context used for rendering this node and its + * children. + */ + public void render(Graphics g) { + g.pushMatrix(); + + applyLocalTransform(g); + renderComponents(g); + + for (SceneNode child : children) { + child.render(g); + } + + g.popMatrix(); + } + + /** + * Applies the local transformation to the graphics context. + */ + private void applyLocalTransform(Graphics g) { + getTransform().apply(g); + } + + /** + * Renders all associated {@link RenderableComponent} instances attached to this + * node. + *

+ * This method iterates through all render components and calls their + * respective rendering logic. + *

+ * + * @param g The graphics context used for rendering. + */ + protected void renderComponents(Graphics g) { + for (RenderableComponent renderer : getRenderComponents()) { + renderer.render(g); + } + } + + /** + * Updates this node's logic and propagates updates to children nodes. + * + * @param tpf The time per frame in seconds (delta time). + */ + public void update(float tpf) { + updateComponents(tpf); + updateChildren(tpf); + } + + /** + * Updates all components attached to this node. + * + * @param tpf The time per frame in seconds. + */ + protected void updateComponents(float tpf) { + for (Component component : components) { + component.update(tpf); + } + } + + /** + * Updates all child nodes of this node recursively. + * + * @param tpf The time per frame in seconds. + */ + protected void updateChildren(float tpf) { + for (SceneNode child : children) { + child.update(tpf); + } + } + + /** + * Cleans up this node's resources, components, and children recursively. + *

+ * Each component and child is cleaned up to ensure no resources are left + * hanging, preventing memory leaks or unwanted behavior. + *

+ */ + public void cleanup() { + for (Component component : components) { + try { + component.cleanup(); + } catch (Exception e) { + System.err.println("Error cleaning up component: " + e.getMessage()); + } + } + + for (SceneNode child : children) { + child.cleanup(); + } + + components.clear(); + children.clear(); + } + + /** + * Adds a child node to this node's hierarchy. + *

+ * Prevents the addition of null nodes and ensures no duplicate child is + * added. + *

+ * + * @param child The child {@code SceneNode} to add. + */ + public void addChild(SceneNode child) { + if (child == null) { + throw new IllegalArgumentException("Child node cannot be null."); + } + if (children.contains(child)) { + return; + } + child.parent = this; + children.add(child); + } + + /** + * Removes a child node from this node's hierarchy. + *

+ * Cleans up the child before removing it to ensure no resources are leaked. + *

+ * + * @param child The child {@code SceneNode} to remove. + */ + public void removeChild(SceneNode child) { + if (child == null) { + return; + } + if (!children.contains(child)) { + return; + } + child.cleanup(); + child.parent = null; + children.remove(child); + } + + /** + * Adds a component to this node. + *

+ * Ensures that duplicate components of the same instance are not added. + *

+ * + * @param component The {@link Component} to add. + * @throws IllegalArgumentException if the component is null. + */ + public void addComponent(Component component) { + if (component == null) { + throw new IllegalArgumentException("Component cannot be null."); + } + if (!components.contains(component)) { + components.add(component); + component.setOwner(this); + component.initialize(); + } + } + + /** + * Removes a component from this node. + *

+ * If the component is found, it is cleaned up before removal. + *

+ * + * @param component The {@link Component} to remove. + * @throws IllegalArgumentException if the component is null. + */ + public void removeComponent(Component component) { + if (component == null) { + throw new IllegalArgumentException("Component cannot be null."); + } + if (components.contains(component)) { + components.remove(component); + component.cleanup(); + component.setOwner(null); + } + } + + /** + * Retrieves the first component of the specified type attached to this node. + * + * @param componentClass The class type of the component to retrieve. + * @param The type of the component. + * @return The first matching component, or {@code null} if none exists. + */ + public T getComponent(Class componentClass) { + for (Component component : components) { + if (componentClass.isInstance(component)) { + return componentClass.cast(component); + } + } + return null; + } + + /** + * Retrieves a list of components of a specific type attached to this node. + *

+ * Enables querying for specific types of behavior or functionality attached + * to a node. + *

+ * + * @param componentClass The class type of the component to retrieve. + * @param The type of component to search for. + * @return A list of components matching the specified type. + */ + public List getComponents(Class componentClass) { + List result = new ArrayList<>(); + for (Component component : components) { + if (componentClass.isInstance(component)) { + result.add(componentClass.cast(component)); + } + } + return result; + } + + /** + * Retrieves all render components for this node. + * + * @return A list of {@link RenderableComponent} instances associated with this + * node. + */ + public List getRenderComponents() { + return getComponents(RenderableComponent.class); + } + + /** + * Retrieves the root node in the scene graph hierarchy. + * + * @return The root {@code SceneNode} in the hierarchy. + */ + public SceneNode getRoot() { + if (parent == null) { + return this; + } + return parent.getRoot(); + } + + /** + * Checks whether this node is the root node in the hierarchy. + * + * @return {@code true} if this node is the root; {@code false} otherwise. + */ + public boolean isRoot() { + return parent == null; + } + + /** + * Checks whether this node is a leaf node (has no children). + * + * @return {@code true} if this node has no children; {@code false} otherwise. + */ + public boolean isLeaf() { + return children.isEmpty(); + } + + /** Retrieves the Transform component associated with this node. */ + public Transform getTransform() { + return getComponents(Transform.class).stream().findFirst().orElseThrow( + () -> new IllegalStateException("Transform component is missing.")); + } + + /** + * Retrieves the name of this {@code SceneNode}. + * + * @return The name of the node. + */ + public String getName() { + return name; + } + + /** + * Sets the name of this {@code SceneNode}. + * + * @param name The new name to assign to the node. + * @throws IllegalArgumentException if the name is {@code null}. + */ + public void setName(String name) { + if (name == null) { + throw new IllegalArgumentException("Name cannot be null."); + } + this.name = name; + } + +} \ No newline at end of file diff --git a/src/main/java/engine/scene/camera/Camera.java b/src/main/java/engine/scene/camera/Camera.java new file mode 100644 index 00000000..2c1e14a8 --- /dev/null +++ b/src/main/java/engine/scene/camera/Camera.java @@ -0,0 +1,202 @@ +package engine.scene.camera; + +import engine.components.Transform; +import math.Matrix4f; +import math.Ray3f; +import math.Vector3f; + +/** + * Represents a generic camera within a 3D scene. A camera defines the view and + * projection settings necessary for rendering a 3D scene from a specific + * perspective. This interface abstracts common properties and functionalities + * that all camera types share, including transformation, view matrices, + * projection matrices, and interaction with screen-space coordinates. + *

+ * Cameras are responsible for defining how 3D scenes are projected onto a 2D + * screen and can be configured for various projection modes, such as + * perspective and orthographic projections. Implementations should define their + * specific logic for projection and view matrix handling, field of view + * adjustments, clipping plane configuration, and conversion between screen and + * world coordinates. + *

+ */ +public interface Camera { + + Vector3f getTarget(); + + /** + * Retrieves the camera's transformation. + *

+ * The transformation defines the position, rotation, and scaling of the + * camera in the 3D world. This transformation is used to compute the camera's + * position and orientation in world space. + *

+ * + * @return The {@link Transform} representing the camera's position, rotation, + * and scaling. + */ + Transform getTransform(); + + /** + * Retrieves the current view matrix of the camera. + *

+ * The view matrix is used to transform world-space coordinates into camera + * (view) space. It is typically derived from the camera's position and + * orientation in the 3D world. + *

+ * + * @return The view matrix as a {@link Matrix4f}. + */ + Matrix4f getViewMatrix(); + + /** + * Retrieves the current projection matrix of the camera. + *

+ * The projection matrix defines how a 3D scene is projected onto a 2D + * viewport, depending on the camera's projection settings (perspective or + * orthographic). + *

+ * + * @return The projection matrix as a {@link Matrix4f}. + */ + Matrix4f getProjectionMatrix(); + + /** + * Updates the view matrix based on the current transformation. + *

+ * This method should recalculate the view matrix whenever the camera's + * position or orientation has changed. + *

+ */ + void updateViewMatrix(); + + /** + * Updates the projection matrix based on camera-specific settings. + *

+ * This method should be called whenever changes are made to parameters like + * the field of view, near or far clipping planes, or aspect ratio. + *

+ */ + void updateProjectionMatrix(); + + /** + * Retrieves the field of view (FOV) for perspective cameras. + *

+ * The field of view determines how wide or narrow the camera's view is and + * only applies to perspective projections. + *

+ * + * @return The current field of view in degrees. + */ + float getFieldOfView(); + + /** + * Sets the field of view (FOV) for perspective cameras. + *

+ * This only has an effect on cameras configured for perspective projection. + *

+ * + * @param fov The desired field of view in degrees. + */ + void setFieldOfView(float fov); + + /** + * Retrieves the near clipping plane distance. + *

+ * The near clipping plane defines the closest distance from the camera at + * which objects are rendered. Objects closer than this distance will not be + * visible. + *

+ * + * @return The near clipping plane distance. + */ + float getNearPlane(); + + /** + * Sets the near clipping plane distance. + *

+ * This modifies how close an object must be to the camera to be visible. + *

+ * + * @param nearPlane The desired near clipping plane distance. + */ + void setNearPlane(float nearPlane); + + /** + * Retrieves the far clipping plane distance. + *

+ * The far clipping plane defines the furthest distance from the camera at + * which objects are rendered. Objects farther away than this distance will + * not be visible. + *

+ * + * @return The far clipping plane distance. + */ + float getFarPlane(); + + /** + * Sets the far clipping plane distance. + *

+ * This modifies how far objects can be from the camera to remain visible. + *

+ * + * @param farPlane The desired far clipping plane distance. + */ + void setFarPlane(float farPlane); + + /** + * Retrieves the current aspect ratio of the camera's viewport. + *

+ * The aspect ratio is defined as the ratio of the viewport's width to its + * height and is used to adjust the projection matrix accordingly. + *

+ * + * @return The aspect ratio of the viewport. + */ + float getAspectRatio(); + + /** + * Sets the aspect ratio for the camera's viewport. + *

+ * Changing the aspect ratio should trigger an update to the projection + * matrix. + *

+ * + * @param aspectRatio The desired aspect ratio. + */ + void setAspectRatio(float aspectRatio); + + /** + * Converts 2D screen coordinates to a 3D ray in world space. + *

+ * This method is essential for raycasting operations, such as determining + * which objects in the scene correspond to a given 2D screen-space click. + *

+ * + * @param screenX The x-coordinate on the screen. + * @param screenY The y-coordinate on the screen. + * @param viewportWidth The width of the viewport in pixels. + * @param viewportHeight The height of the viewport in pixels. + * @return A {@link Ray3f} representing the computed ray in 3D world space. + */ + Ray3f createRay(float screenX, float screenY, int viewportWidth, + int viewportHeight); + + /** + * Converts 2D screen-space coordinates to their corresponding world-space + * coordinates. + *

+ * This is the inverse of projection and can be used for operations like + * object picking or determining intersections between screen-space inputs and + * 3D objects in the world. + *

+ * + * @param screenCoords The 2D screen-space coordinates to unproject. + * @param viewportWidth The width of the viewport in pixels. + * @param viewportHeight The height of the viewport in pixels. + * @return The corresponding 3D world-space coordinates as a {@link Vector3f}. + */ + Vector3f unproject(Vector3f screenCoords, int viewportWidth, + int viewportHeight); + +} \ No newline at end of file diff --git a/src/main/java/engine/scene/light/AmbientLight.java b/src/main/java/engine/scene/light/AmbientLight.java new file mode 100644 index 00000000..b6e52e9b --- /dev/null +++ b/src/main/java/engine/scene/light/AmbientLight.java @@ -0,0 +1,99 @@ +package engine.scene.light; + +import math.Color; + +/** + * Represents an ambient light source in a 3D scene. + * + *

+ * Ambient light provides uniform, non-directional illumination throughout the + * entire 3D scene. Unlike other types of lights, such as point lights or + * directional lights, ambient light does not have a specific position or + * direction, and it affects all objects equally, regardless of their location + * or orientation. This is ideal for simulating indirect or global lighting + * effects that ensure objects are visible even when not directly illuminated by + * other light sources. + *

+ * + *
+ * Key Characteristics of an ambient light:
+ * - Color: Defined by an instance of {@link math.Color},
+ *   representing the light's hue.
+ * - No position: Ambient light is global, meaning it affects all parts of the
+ *   scene uniformly.
+ * - Non-directional: Ambient light lacks a specific directional focus and has
+ *   equal influence everywhere.
+ * 
+ * + * Usage: This class allows you to dynamically configure the ambient light's + * color during runtime. Integration with rendering systems can be achieved via + * the {@link LightRenderer}. + */ +public class AmbientLight implements Light { + + /** + * Ambient color. + */ + private Color color; + + /** + * Constructs a new AmbientLight with a default color of white. + */ + public AmbientLight() { + this(Color.WHITE); + } + + /** + * Constructs a new AmbientLight with the specified color. + * + * @param color the color of the ambient light. Must not be null. + * @throws IllegalArgumentException if the color is null. + */ + public AmbientLight(Color color) { + setColor(color); + } + + /** + * Gets the type of the light. + * + * @return the light type, which is {@link LightType#AMBIENT}. + */ + @Override + public LightType getType() { + return LightType.AMBIENT; + } + + /** + * Renders the ambient light using the specified {@link LightRenderer}. + * + * @param renderer the renderer to use for rendering the light. + */ + @Override + public void render(LightRenderer renderer) { + renderer.render(this); + } + + /** + * Gets the color of the ambient light. + * + * @return the color of the light. Never null. + */ + @Override + public Color getColor() { + return color; + } + + /** + * Sets the color of the ambient light. + * + * @param color the new color for the ambient light. Must not be null. + * @throws IllegalArgumentException if the color is null. + */ + public void setColor(Color color) { + if (color == null) { + throw new IllegalArgumentException("Color cannot be null."); + } + this.color = color; + } + +} \ No newline at end of file diff --git a/src/main/java/scene/light/DirectionalLight.java b/src/main/java/engine/scene/light/DirectionalLight.java similarity index 99% rename from src/main/java/scene/light/DirectionalLight.java rename to src/main/java/engine/scene/light/DirectionalLight.java index 29ed5bf6..ccfd8661 100644 --- a/src/main/java/scene/light/DirectionalLight.java +++ b/src/main/java/engine/scene/light/DirectionalLight.java @@ -1,4 +1,4 @@ -package scene.light; +package engine.scene.light; import math.Color; import math.Vector3f; diff --git a/src/main/java/scene/light/Light.java b/src/main/java/engine/scene/light/Light.java similarity index 98% rename from src/main/java/scene/light/Light.java rename to src/main/java/engine/scene/light/Light.java index 002ffddd..18765910 100644 --- a/src/main/java/scene/light/Light.java +++ b/src/main/java/engine/scene/light/Light.java @@ -1,4 +1,4 @@ -package scene.light; +package engine.scene.light; import math.Color; diff --git a/src/main/java/scene/light/LightRenderer.java b/src/main/java/engine/scene/light/LightRenderer.java similarity index 65% rename from src/main/java/scene/light/LightRenderer.java rename to src/main/java/engine/scene/light/LightRenderer.java index 3490d213..4e519fef 100644 --- a/src/main/java/scene/light/LightRenderer.java +++ b/src/main/java/engine/scene/light/LightRenderer.java @@ -1,4 +1,6 @@ -package scene.light; +package engine.scene.light; + +import workspace.ui.Graphics; /** * Interface for rendering various light sources in a 3D scene. @@ -12,6 +14,22 @@ */ public interface LightRenderer { + /** + * Sets the graphics context for the light renderer. + *

+ * This method initializes the rendering environment by associating the given + * {@link Graphics} instance with the light renderer. The graphics context is + * responsible for rendering commands, shader bindings, and light + * computations. Implementations can use this context to issue rendering + * commands for different light types or configure the rendering pipeline as + * needed. + *

+ * + * @param g The {@link Graphics} instance to be used by the light renderer. + * Must not be null. + */ + void setGraphics(Graphics g); + /** * Renders a generic light source. *

@@ -63,4 +81,17 @@ public interface LightRenderer { */ void render(DirectionalLight light); + /** + * Renders an ambient light source. + *

+ * This method handles the rendering of ambient light, which provides uniform + * illumination across the entire scene without directionality or position. + * Ambient light is used to simulate indirect lighting and ensures that + * objects are visible even when not directly lit by other light sources. + *

+ * + * @param light The ambient light source to render. Must not be null. + */ + void render(AmbientLight light); + } \ No newline at end of file diff --git a/src/main/java/scene/light/LightType.java b/src/main/java/engine/scene/light/LightType.java similarity index 55% rename from src/main/java/scene/light/LightType.java rename to src/main/java/engine/scene/light/LightType.java index 051300a9..8d641565 100644 --- a/src/main/java/scene/light/LightType.java +++ b/src/main/java/engine/scene/light/LightType.java @@ -1,9 +1,9 @@ -package scene.light; +package engine.scene.light; /** * Enum representing different types of lights. * - * This enum defines the three primary types of lights commonly used in 3D + * This enum defines the four primary types of lights commonly used in 3D * graphics: * *
@@ -12,10 +12,13 @@
  *   a specific direction.
  * - SPOT: A spotlight emits light in a cone shape, with a specific 
  *   direction and angle.
+ * - AMBIENT: An ambient light provides uniform illumination across the 
+ *   entire scene, simulating indirect lighting with no specific direction 
+ *   or position.
  * 
*/ public enum LightType { - POINT, DIRECTIONAL, SPOT + POINT, DIRECTIONAL, SPOT, AMBIENT -} +} \ No newline at end of file diff --git a/src/main/java/scene/light/PointLight.java b/src/main/java/engine/scene/light/PointLight.java similarity index 99% rename from src/main/java/scene/light/PointLight.java rename to src/main/java/engine/scene/light/PointLight.java index f37a81df..6262f402 100644 --- a/src/main/java/scene/light/PointLight.java +++ b/src/main/java/engine/scene/light/PointLight.java @@ -1,4 +1,4 @@ -package scene.light; +package engine.scene.light; import math.Color; import math.Vector3f; diff --git a/src/main/java/scene/light/SpotLight.java b/src/main/java/engine/scene/light/SpotLight.java similarity index 99% rename from src/main/java/scene/light/SpotLight.java rename to src/main/java/engine/scene/light/SpotLight.java index c6d17e16..72bc0cf1 100644 --- a/src/main/java/scene/light/SpotLight.java +++ b/src/main/java/engine/scene/light/SpotLight.java @@ -1,4 +1,4 @@ -package scene.light; +package engine.scene.light; import math.Color; import math.Vector3f; diff --git a/src/main/java/math/Bounds.java b/src/main/java/math/Bounds.java new file mode 100644 index 00000000..57f62990 --- /dev/null +++ b/src/main/java/math/Bounds.java @@ -0,0 +1,229 @@ +package math; + +/** + * The {@code Bounds} class represents a 3D axis-aligned bounding box (AABB), + * defined by two {@link Vector3f} points: the minimum and maximum corners. This + * class provides various utility methods for manipulating and querying the + * bounding box, such as checking if a point is contained within the bounds, + * expanding the bounds, and testing for intersections with rays or other + * bounds. + * + *

+ * A bounding box is often used in 3D graphics for collision detection, frustum + * culling, and other spatial queries. + *

+ */ +public class Bounds { + + /** + * The minimum corner of the bounding box. + */ + private Vector3f min; + + /** + * The maximum corner of the bounding box. + */ + private Vector3f max; + + /** + * Constructs a new {@code Bounds} object with the specified minimum and + * maximum corners. + * + * @param min the minimum corner of the bounding box + * @param max the maximum corner of the bounding box + * @throws IllegalArgumentException if either {@code min} or {@code max} is + * {@code null} + */ + public Bounds(Vector3f min, Vector3f max) { + if (min == null) { + throw new IllegalArgumentException("Min cannot be null."); + } + if (max == null) { + throw new IllegalArgumentException("Max cannot be null."); + } + this.min = new Vector3f(min); + this.max = new Vector3f(max); + } + + /** + * Returns the closest point on the bounding box to the given {@code point}. + * The closest point is determined by clamping each coordinate of the point + * between the minimum and maximum bounds of the box. + * + * @param point the point to clamp to the bounding box + * @return a new {@code Vector3f} representing the closest point on the + * bounding box + */ + public Vector3f closestPoint(Vector3f point) { + float x = Math.max(min.x, Math.min(max.x, point.x)); + float y = Math.max(min.y, Math.min(max.y, point.y)); + float z = Math.max(min.z, Math.min(max.z, point.z)); + return new Vector3f(x, y, z); + } + + /** + * Checks if the given {@code point} is inside the bounding box. The point is + * considered inside if all of its coordinates are between the minimum and + * maximum coordinates of the box. + * + * @param p the point to check + * @return {@code true} if the point is inside the bounding box, {@code false} + * otherwise + */ + public boolean contains(Vector3f p) { + return p.x >= min.x && p.x <= max.x && p.y >= min.y + && p.y <= max.y && p.z >= min.z && p.z <= max.z; + } + + /** + * Expands the bounding box to encompass the given {@code point}. If the point + * is outside the current bounds, the box will be enlarged to include it. + * + * @param p the point to include in the bounding box + */ + public void encapsulate(Vector3f p) { + min.set(Math.min(min.x, p.x), Math.min(min.y, p.y), Math.min(min.z, p.z)); + max.set(Math.max(max.x, p.x), Math.max(max.y, p.y), Math.max(max.z, p.z)); + } + + /** + * Expands the bounding box by the given {@code amount}. The expansion is done + * uniformly along all axes, increasing the size of the bounding box by the + * specified amount. + * + * @param amount the amount to expand the bounding box by + */ + public void expand(float amount) { + float halfAmount = amount / 2; + min.subtractLocal(halfAmount, halfAmount, halfAmount); + max.addLocal(halfAmount, halfAmount, halfAmount); + } + + /** + * Tests whether the given ray intersects this axis-aligned bounding box + * (AABB). + *

+ * The method uses the slab method to compute the intersection by checking the + * ray's position and direction relative to the box's bounds in each axis (x, + * y, z). It accounts for parallel rays and updates intersection intervals to + * determine if there is an overlap. + *

+ * + * @param ray the {@link Ray3f} to test for intersection with this AABB. The + * ray must have its inverse direction precomputed and accessible + * via {@code ray.getDirectionInv()} for optimal performance. + * @return {@code true} if the ray intersects the AABB, {@code false} + * otherwise. + */ + public boolean intersectsRay(Ray3f ray) { + if (ray.getDirection().isZero()) { + return false; // A ray with zero direction cannot intersect anything + } + + if (min.equals(max)) { + return ray.getOrigin().equals(min); + } + + float tmin = 0.0f; + float tmax = Float.POSITIVE_INFINITY; + + for (int d = 0; d < 3; ++d) { + float invDir = ray.getDirectionInv().get(d); + + // Handle zero direction component + if (invDir == 0.0f) { + if (ray.getOrigin().get(d) < min.get(d) + || ray.getOrigin().get(d) > max.get(d)) { + return false; + } + continue; + } + + float bmin, bmax; + if (invDir < 0.0f) { + bmin = max.get(d); + bmax = min.get(d); + } else { + bmin = min.get(d); + bmax = max.get(d); + } + + float dmin = (bmin - ray.getOrigin().get(d)) * invDir; + float dmax = (bmax - ray.getOrigin().get(d)) * invDir; + + tmin = Math.max(tmin, dmin); + tmax = Math.min(tmax, dmax); + + if (tmin > tmax) { + return false; + } + } + + return true; + } + + /** + * Tests if this bounding box intersects another {@code Bounds}. The + * intersection is checked by comparing the min and max coordinates of both + * boxes. + * + * @param other the other bounding box to check for intersection + * @return {@code true} if the bounding boxes intersect, {@code false} + * otherwise + */ + public boolean intersects(Bounds other) { + return min.x <= other.max.x && max.x >= other.min.x && min.y <= other.max.y + && max.y >= other.min.y && min.z <= other.max.z && max.z >= other.min.z; + } + + /** + * Calculates the squared distance from the given {@code point} to the closest + * point on the bounding box. This method avoids calculating the square root + * for performance reasons, returning the squared distance instead. + * + * @param point the point to calculate the squared distance from + * @return the squared distance from the point to the closest point on the + * bounding box + */ + public float sqrDistance(Vector3f point) { + float dx = Math.max(0, Math.max(min.x - point.x, point.x - max.x)); + float dy = Math.max(0, Math.max(min.y - point.y, point.y - max.y)); + float dz = Math.max(0, Math.max(min.z - point.z, point.z - max.z)); + return dx * dx + dy * dy + dz * dz; + } + + /** + * Sets the minimum and maximum corners of the bounding box to the specified + * values. + * + * @param min the new minimum corner + * @param max the new maximum corner + * @throws IllegalArgumentException if min or max is null. + */ + public void setMinMax(Vector3f min, Vector3f max) { + if (min == null || max == null) { + throw new IllegalArgumentException("Min and Max cannot be null."); + } + this.min = new Vector3f(min); + this.max = new Vector3f(max); + } + + /** + * Returns the minimum corner of the bounding box. + * + * @return the minimum corner of the bounding box + */ + public Vector3f getMin() { + return min; + } + + /** + * Returns the maximum corner of the bounding box. + * + * @return the maximum corner of the bounding box + */ + public Vector3f getMax() { + return max; + } + +} \ No newline at end of file diff --git a/src/main/java/math/Color.java b/src/main/java/math/Color.java index cc1613bf..8af38db0 100644 --- a/src/main/java/math/Color.java +++ b/src/main/java/math/Color.java @@ -1,564 +1,512 @@ package math; -/** - * Representation of RGBA colors. The components (r,g,b) define a color in RGB - * color space. - * - * @author Simon - * @version 0.3, 11 December 2017 - * - */ +/** Representation of RGBA colors. The components (r,g,b) define a color in RGB color space. */ public class Color { - /** - * Solid black. RGBA is (0, 0, 0, 1). - */ - public static final Color BLACK = new Color(0f, 0f, 0f, 1f); - - /** - * Solid blue. RGBA is (0, 0, 1, 1). - */ - public static final Color BLUE = new Color(0f, 0f, 1f, 1f); - - /** - * Completely transparent. RGBA is (0, 0, 0, 0). - */ - public static final Color CLEAR = new Color(0f, 0f, 0f, 0f); - - /** - * Cyan. RGBA is (0, 1, 1, 1). - */ - public static final Color CYAN = new Color(0f, 1f, 1f, 1f); - - /** - * Dark gray. RGBA is (0.25, 0.25, 0.25, 1). - */ - public static final Color DARK_GRAY = new Color(0.25f, 0.25f, 0.25f, 1.0f); - - /** - * Gray. RGBA is (0.5, 0.5, 0.5, 1). - */ - public static final Color GRAY = new Color(0.5f, 0.5f, 0.5f, 1f); - - /** - * Solid green. RGBA is (0, 1, 0, 1). - */ - public static final Color GREEN = new Color(0f, 1f, 0f, 1f); - - /** - * English spelling for gray. RGBA is the same (0.5, 0.5, 0.5, 1). - */ - public static final Color GREY = GRAY; - - /** - * Light gray. RGBA is (0.75f, 0.75f, 0.75f, 1f). - */ - public static final Color LIGHT_GRAY = new Color(0.75f, 0.75f, 0.75f, 1f); - - /** - * Magenta. RGBA is (1, 0, 1, 1). - */ - public static final Color MAGENTA = new Color(1f, 0f, 1f, 1f); - - /** - * Solid red. RGBA is (1, 0, 0, 1). - */ - public static final Color RED = new Color(1f, 0f, 0f, 1f); - - /** - * Solid white. RGBA is (1, 1, 1, 1). - */ - public static final Color WHITE = new Color(1f, 1f, 1f, 1f); - - /** - * Yellow. RGBA is (1, 1, 0, 1). - */ - public static final Color YELLOW = new Color(1f, 1f, 0f, 1f); - - /** - * The red component of the color. - */ - private float r; - - /** - * The green component of the color. - */ - private float g; - - /** - * The blue component of the color. - */ - private float b; - - /** - * The alpha component of the color. - */ - private float a; - - /** - * Constructs a new instance of this color with r,g,b,a components set to 0. - */ - public Color() { - this(0, 0, 0, 0); - } - - /** - * Constructs a new instance of this color with r,g,b,a components set to the - * values provided by color c. - * - * @param c the color to copy from - */ - public Color(Color c) { - this(c.r, c.g, c.b, c.a); - } - - /** - * Constructs a new instance of this {@link Color} with the given r,g,b - * components and a alpha value set to 1f. - * - * @param r the red component of this color - * @param g the green component of this color - * @param b the blue component of this color - */ - public Color(float r, float g, float b) { - this(r, g, b, 1.0f); - } - - /** - * Constructs a new instance of this {@link Color} with the given r,g,b,a - * components. - * - * - * @param r the red component of this color - * @param g the green component of this color - * @param b the blue component of this color - * @param a the alpha component of this color - */ - public Color(float r, float g, float b, float a) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } - - /** - * Returns a new color instance with the specified integer r,g,b values. - * - * @param r the red component for the color (0 to 255) - * @param g the green component for the color (0 to 255) - * @param b the blue component for the color (0 to 255) - * - * @return the newly created color - */ - public static Color getColorFromInt(int r, int g, int b) { - r = Mathf.clampInt(r, 0, 255); - g = Mathf.clampInt(g, 0, 255); - b = Mathf.clampInt(b, 0, 255); - return new Color(r / 255f, g / 255f, b / 255f); - } - - /** - * Adds the components of a given color to those of this color creating a new - * color object. Each component is added separately. If the provided color is - * null, an exception is thrown. - * - * @param color the color to add to this color - * @return the resultant color - */ - public Color add(Color color) { - return subtract(color, null); - } - - /** - * Adds the components of a given color to those of this color storing the - * result in the given result color. Each component is added separately. If - * the provided color c is null, an exception is thrown. If the provided - * result color is null, a new color is created. - * - * @param color the color to add to this color - * @param result the color to store the result in - * @return the resultant color - */ - public Color add(Color color, Color result) { - if (result == null) - result = new Color(); - result.r = this.r + color.r; - result.g = this.g + color.g; - result.b = this.b + color.b; - result.a = this.a + color.a; - return result; - } - - /** - * Adds the given r,g,b,a components to those of this color creating a new - * color object. Each component is added separately. - * - * @param r the red component to add - * @param g the green component to add - * @param b the blue component to add - * @param a the alpha component to add - * @return the resultant color - */ - public Color add(float r, float g, float b, float a) { - return new Color(this.r + r, this.g + g, this.b + b, this.a + a); - } - - /** - * Adds the color c to this color internally, and returns a handle to this - * color for easy chaining of calls. Each component is added separately. - * - * @param color the color to add to this color - * @return this - */ - public Color addLocal(Color color) { - this.r += color.r; - this.g += color.g; - this.b += color.b; - this.a += color.a; - return this; - } - - /** - * Adds the provided components to this color internally, and returns a handle - * to this color for easy chaining of calls. - * - * @param r the red component to add - * @param g the green component to add - * @param b the blue component to add - * @param a the alpha component to add - * - * @return this - */ - public Color addLocal(float r, float g, float b, float a) { - this.r += r; - this.g += g; - this.b += b; - this.a += a; - return this; - } - - /** - * Subtracts the components of a given color from those of this color creating - * a new color object. Each component is subtracted separately. If the - * provided color is null, an exception is thrown. - * - * @param color the color to subtract from this color - * @return the resultant color - */ - public Color subtract(Color color) { - return subtract(color, null); - } - - /** - * Subtracts the values of a given color from those of this color storing the - * result in the given color. Each component is subtracted separately. If the - * provided color c is null, an exception is thrown. If the provided result - * color is null, a new color is created. - * - * @param color the color to subtract from this color - * @param result the color to store the result in - * @return the resultant color - */ - public Color subtract(Color color, Color result) { - if (result == null) - result = new Color(); - result.r = this.r - color.r; - result.g = this.g - color.g; - result.b = this.b - color.b; - result.a = this.a - color.a; - return result; - } - - /** - * * Subtracts the given r,g,b,a components from those of this color creating - * a new color object. Each component is subtracted separately. - * - * @param r the red component to subtract - * @param g the green component to subtract - * @param b the blue component to subtract - * @param a the alpha component to subtract - * @return the resultant color - */ - public Color subtract(float r, float g, float b, float a) { - return new Color(this.r - r, this.g - g, this.b - b, this.a - a); - } - - /** - * Subtracts the color c from this color internally, and returns a handle to - * this color for easy chaining of calls. Each component is subtracted - * separately. - * - * @param color the color to subtract from this color - * @return this - */ - public Color subtractLocal(Color color) { - this.r -= color.r; - this.g -= color.g; - this.b -= color.b; - this.a -= color.a; - return this; - } - - /** - * Subtracts the provided components from this color internally, and returns a - * handle to this color for easy chaining of calls. - * - * @param r the red component to subtract - * @param g the green component to subtract - * @param b the blue component to subtract - * @param a the alpha component to subtract - * - * @return this - */ - public Color subtractLocal(float r, float g, float b, float a) { - this.r -= r; - this.g -= g; - this.b -= b; - this.a -= a; - return this; - } - - /** - * Divides this color by a internally. Each component is scaled - * separately - * - * @return this - */ - public Color divideLocal(float a) { - this.r /= a; - this.g /= a; - this.b /= a; - this.a /= a; - return this; - } - - /** - * Clamps the components of this color between 0.0f and 1.0f internally, and - * returns a handle to this color for easy chaining of calls. - * - * @return this - */ - public Color clampLocal() { - r = Mathf.clamp01(r); - g = Mathf.clamp01(g); - b = Mathf.clamp01(b); - a = Mathf.clamp01(a); - return this; - } - - /** - * Sets all components of this color to 0.0f internally, and returns a handle - * to this color for easy chaining of calls. - * - * @return this - */ - public Color clearLocal() { - r = g = b = a = 0.0f; - return this; - } - - /** - * Returns the maximum color component value: Max(r,g,b). This method does not - * consider the alpha component. - * - * @return the maximum color component - */ - public float maxComponent() { - return Mathf.max(new float[] { r, g, b }); - } - - /** - * Returns a new float array containing the r,g,b,a components of this color - * in that order. - * - * @return the components of this color as array - */ - public float[] toArray() { - return new float[] { r, g, b, a }; - } - - /** - * Stores the r,g,b,a components in the given array. If the provided store - * array is null a new array is created to store the components in. - * - * @param store the array to store the components into - * @return store - * @throws IndexOutOfBoundsException if store.length < 4 - */ - public float[] toArray(float[] store) { - if (store == null) - store = new float[4]; - store[0] = r; - store[1] = g; - store[2] = b; - store[3] = a; - return store; - } - - /** - * Sets the r,g,b,a components of this color to the specified new values. - * - * @param r the new red component for this color - * @param g the new green component for this color - * @param b the new blue component for this color - * @param a the new alpha component for this color - */ - public void set(float r, float g, float b, float a) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } - - /** - * Sets the r,g,b components of this color to the specified new values. The - * alpha component is set to 1.0f. - * - * @param r the new red component for this color - * @param g the new green component for this color - * @param b the new blue component for this color - */ - public void set(float r, float g, float b) { - this.r = r; - this.g = g; - this.b = b; - this.a = 1.0f; - } - - /** - * Returns the red component of this color as float value (0f to 1f). - * - * @return the red component of this color (0f to 1f) - */ - public float getRed() { - return r; - } - - /** - * Returns the green component of this color as float value (0f to 1f). - * - * @return the green component of this color (0f to 1f) - */ - public float getGreen() { - return g; - } - - /** - * Returns the blue component of this color as float value (0f to 1f). - * - * @return the blue component of this color (0f to 1f) - */ - public float getBlue() { - return b; - } - - /** - * Returns the alpha component of this color as float value (0f to 1f). - * - * @return the alpha component of this color (0f to 1f) - */ - public float getAlpha() { - return a; - } - - /** - * Returns the red component of this color as an integer value (0 to 255). - * - * @return the red component of this color (0 to 255) - */ - public int getRedInt() { - return (int) (r * 255 + 0.5); - } - - /** - * Returns the green component of this color as an integer value (0 to 255). - * - * @return the green component of this color (0 to 255) - */ - public int getGreenInt() { - return (int) (g * 255 + 0.5); - } - - /** - * Returns the blue component of this color as an integer value (0 to 255). - * - * @return the blue component of this color (0 to 255) - */ - public int getBlueInt() { - return (int) (b * 255 + 0.5); - } - - /** - * Returns the alpha component of this color as an integer value (0 to 255). - * - * @return the alpha component of this color (0 to 255) - */ - public int getAlphaInt() { - return (int) (a * 255 + 0.5); - } - - /** - * Returns the RGBA value representing the color. (Bits 24-31 are alpha, 16-23 - * are red, 8-15 are green, 0-7 are blue). - * - * @return the RGBA value of this color as integer - */ - public int getRGBA() { - int r = getRedInt(); - int g = getGreenInt(); - int b = getBlueInt(); - int a = getAlphaInt(); - return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) - | ((b & 0xFF) << 0); - } - - /** - * Returns a unique hash code for this color object based on it's values. If - * two colors are logically equivalent, they will return the same hash code - * value. - * - * @return the hash code value of this color - */ - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Float.floatToIntBits(a); - result = prime * result + Float.floatToIntBits(b); - result = prime * result + Float.floatToIntBits(g); - result = prime * result + Float.floatToIntBits(r); - return result; - } - - /** - * Determines if this color is equals to the given object obj. - * - * @param obj the object to compare for equality - * @return true if they are equal - */ - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Color other = (Color) obj; - return Float.floatToIntBits(r) == Float.floatToIntBits(other.r) - && Float.floatToIntBits(g) == Float.floatToIntBits(other.g) - && Float.floatToIntBits(b) == Float.floatToIntBits(other.b) - && Float.floatToIntBits(a) == Float.floatToIntBits(other.a); - } - - /** - * Returns a string representation of this color. - * - * @return a string representation of this color - */ - @Override - public String toString() { - return "Color [r=" + r + ", g=" + g + ", b=" + b + ", a=" + a + "]"; - } - + /** Solid black. RGBA is (0, 0, 0, 1). */ + public static final Color BLACK = new Color(0f, 0f, 0f, 1f); + + /** Solid blue. RGBA is (0, 0, 1, 1). */ + public static final Color BLUE = new Color(0f, 0f, 1f, 1f); + + /** Completely transparent. RGBA is (0, 0, 0, 0). */ + public static final Color CLEAR = new Color(0f, 0f, 0f, 0f); + + /** Cyan. RGBA is (0, 1, 1, 1). */ + public static final Color CYAN = new Color(0f, 1f, 1f, 1f); + + /** Dark gray. RGBA is (0.25, 0.25, 0.25, 1). */ + public static final Color DARK_GRAY = new Color(0.25f, 0.25f, 0.25f, 1.0f); + + /** Gray. RGBA is (0.5, 0.5, 0.5, 1). */ + public static final Color GRAY = new Color(0.5f, 0.5f, 0.5f, 1f); + + /** Solid green. RGBA is (0, 1, 0, 1). */ + public static final Color GREEN = new Color(0f, 1f, 0f, 1f); + + /** English spelling for gray. RGBA is the same (0.5, 0.5, 0.5, 1). */ + public static final Color GREY = GRAY; + + /** Light gray. RGBA is (0.75f, 0.75f, 0.75f, 1f). */ + public static final Color LIGHT_GRAY = new Color(0.75f, 0.75f, 0.75f, 1f); + + /** Magenta. RGBA is (1, 0, 1, 1). */ + public static final Color MAGENTA = new Color(1f, 0f, 1f, 1f); + + /** Solid red. RGBA is (1, 0, 0, 1). */ + public static final Color RED = new Color(1f, 0f, 0f, 1f); + + /** Solid white. RGBA is (1, 1, 1, 1). */ + public static final Color WHITE = new Color(1f, 1f, 1f, 1f); + + /** Yellow. RGBA is (1, 1, 0, 1). */ + public static final Color YELLOW = new Color(1f, 1f, 0f, 1f); + + /** The red component of the color. */ + private float r; + + /** The green component of the color. */ + private float g; + + /** The blue component of the color. */ + private float b; + + /** The alpha component of the color. */ + private float a; + + /** Constructs a new instance of this color with r,g,b,a components set to 0. */ + public Color() { + this(0, 0, 0, 0); + } + + /** + * Constructs a new instance of this color with r,g,b,a components set to the values provided by + * color c. + * + * @param c the color to copy from + */ + public Color(Color c) { + this(c.r, c.g, c.b, c.a); + } + + /** + * Constructs a new instance of this {@link Color} with the given r,g,b components and a alpha + * value set to 1f. + * + * @param r the red component of this color + * @param g the green component of this color + * @param b the blue component of this color + */ + public Color(float r, float g, float b) { + this(r, g, b, 1.0f); + } + + /** + * Constructs a new instance of this {@link Color} with the given r,g,b,a components. + * + * @param r the red component of this color + * @param g the green component of this color + * @param b the blue component of this color + * @param a the alpha component of this color + */ + public Color(float r, float g, float b, float a) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + /** + * Returns a new color instance with the specified integer r,g,b values. + * + * @param r the red component for the color (0 to 255) + * @param g the green component for the color (0 to 255) + * @param b the blue component for the color (0 to 255) + * @return the newly created color + */ + public static Color getColorFromInt(int r, int g, int b) { + int clampR = Mathf.clampInt(r, 0, 255); + int clampG = Mathf.clampInt(g, 0, 255); + int clampB = Mathf.clampInt(b, 0, 255); + return new Color(clampR / 255f, clampG / 255f, clampB / 255f); + } + + /** + * Adds the components of a given color to those of this color creating a new color object. Each + * component is added separately. If the provided color is null, an exception is thrown. + * + * @param color the color to add to this color + * @return the resultant color + */ + public Color add(Color color) { + return add(color, null); + } + + /** + * Adds the components of a given color to those of this color storing the result in the given + * result color. Each component is added separately. If the provided color c is null, an exception + * is thrown. If the provided result color is null, a new color is created. + * + * @param color the color to add to this color + * @param result the color to store the result in + * @return the resultant color + */ + public Color add(Color color, Color result) { + if (result == null) result = new Color(); + result.r = this.r + color.r; + result.g = this.g + color.g; + result.b = this.b + color.b; + result.a = this.a + color.a; + return result; + } + + /** + * Adds the given r,g,b,a components to those of this color creating a new color object. Each + * component is added separately. + * + * @param r the red component to add + * @param g the green component to add + * @param b the blue component to add + * @param a the alpha component to add + * @return the resultant color + */ + public Color add(float r, float g, float b, float a) { + return new Color(this.r + r, this.g + g, this.b + b, this.a + a); + } + + /** + * Adds the color c to this color internally, and returns a handle to this color for easy chaining + * of calls. Each component is added separately. + * + * @param color the color to add to this color + * @return this + */ + public Color addLocal(Color color) { + this.r += color.r; + this.g += color.g; + this.b += color.b; + this.a += color.a; + return this; + } + + /** + * Adds the provided components to this color internally, and returns a handle to this color for + * easy chaining of calls. + * + * @param r the red component to add + * @param g the green component to add + * @param b the blue component to add + * @param a the alpha component to add + * @return this + */ + public Color addLocal(float r, float g, float b, float a) { + this.r += r; + this.g += g; + this.b += b; + this.a += a; + return this; + } + + /** + * Subtracts the components of a given color from those of this color creating a new color object. + * Each component is subtracted separately. If the provided color is null, an exception is thrown. + * + * @param color the color to subtract from this color + * @return the resultant color + */ + public Color subtract(Color color) { + return subtract(color, null); + } + + /** + * Subtracts the values of a given color from those of this color storing the result in the given + * color. Each component is subtracted separately. If the provided color c is null, an exception + * is thrown. If the provided result color is null, a new color is created. + * + * @param color the color to subtract from this color + * @param result the color to store the result in + * @return the resultant color + */ + public Color subtract(Color color, Color result) { + if (result == null) result = new Color(); + result.r = this.r - color.r; + result.g = this.g - color.g; + result.b = this.b - color.b; + result.a = this.a - color.a; + return result; + } + + /** + * * Subtracts the given r,g,b,a components from those of this color creating a new color object. + * Each component is subtracted separately. + * + * @param r the red component to subtract + * @param g the green component to subtract + * @param b the blue component to subtract + * @param a the alpha component to subtract + * @return the resultant color + */ + public Color subtract(float r, float g, float b, float a) { + return new Color(this.r - r, this.g - g, this.b - b, this.a - a); + } + + /** + * Subtracts the color c from this color internally, and returns a handle to this color for easy + * chaining of calls. Each component is subtracted separately. + * + * @param color the color to subtract from this color + * @return this + */ + public Color subtractLocal(Color color) { + this.r -= color.r; + this.g -= color.g; + this.b -= color.b; + this.a -= color.a; + return this; + } + + /** + * Subtracts the provided components from this color internally, and returns a handle to this + * color for easy chaining of calls. + * + * @param r the red component to subtract + * @param g the green component to subtract + * @param b the blue component to subtract + * @param a the alpha component to subtract + * @return this + */ + public Color subtractLocal(float r, float g, float b, float a) { + this.r -= r; + this.g -= g; + this.b -= b; + this.a -= a; + return this; + } + + /** + * Divides this color by a internally. Each component is scaled separately + * + * @return this + */ + public Color divideLocal(float a) { + this.r /= a; + this.g /= a; + this.b /= a; + this.a /= a; + return this; + } + + /** + * @param a + * @return + */ + public Color multLocal(float r, float g, float b, float a) { + this.r *= a; + this.g *= g; + this.b *= b; + this.a *= a; + return this; + } + + /** + * Clamps the components of this color between 0.0f and 1.0f internally, and returns a handle to + * this color for easy chaining of calls. + * + * @return this + */ + public Color clampLocal() { + r = Mathf.clamp01(r); + g = Mathf.clamp01(g); + b = Mathf.clamp01(b); + a = Mathf.clamp01(a); + return this; + } + + /** + * Sets all components of this color to 0.0f internally, and returns a handle to this color for + * easy chaining of calls. + * + * @return this + */ + public Color clearLocal() { + r = g = b = a = 0.0f; + return this; + } + + /** + * Returns the maximum color component value: Max(r,g,b). This method does not consider the alpha + * component. + * + * @return the maximum color component + */ + public float maxComponent() { + return Mathf.max(new float[] {r, g, b}); + } + + /** + * Returns a new float array containing the r,g,b,a components of this color in that order. + * + * @return the components of this color as array + */ + public float[] toArray() { + return new float[] {r, g, b, a}; + } + + /** + * Stores the r,g,b,a components in the given array. If the provided store array is null a new + * array is created to store the components in. + * + * @param store the array to store the components into + * @return store + * @throws IndexOutOfBoundsException if store.length < 4 + */ + public float[] toArray(float[] store) { + if (store == null) store = new float[4]; + store[0] = r; + store[1] = g; + store[2] = b; + store[3] = a; + return store; + } + + /** + * Sets the r,g,b,a components of this color to the specified new values. + * + * @param r the new red component for this color + * @param g the new green component for this color + * @param b the new blue component for this color + * @param a the new alpha component for this color + */ + public void set(float r, float g, float b, float a) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + /** + * Sets the r,g,b components of this color to the specified new values. The alpha component is set + * to 1.0f. + * + * @param r the new red component for this color + * @param g the new green component for this color + * @param b the new blue component for this color + */ + public void set(float r, float g, float b) { + this.r = r; + this.g = g; + this.b = b; + this.a = 1.0f; + } + + /** + * Returns the red component of this color as float value (0f to 1f). + * + * @return the red component of this color (0f to 1f) + */ + public float getRed() { + return r; + } + + /** + * Returns the green component of this color as float value (0f to 1f). + * + * @return the green component of this color (0f to 1f) + */ + public float getGreen() { + return g; + } + + /** + * Returns the blue component of this color as float value (0f to 1f). + * + * @return the blue component of this color (0f to 1f) + */ + public float getBlue() { + return b; + } + + /** + * Returns the alpha component of this color as float value (0f to 1f). + * + * @return the alpha component of this color (0f to 1f) + */ + public float getAlpha() { + return a; + } + + /** + * Returns the red component of this color as an integer value (0 to 255). + * + * @return the red component of this color (0 to 255) + */ + public int getRedInt() { + return (int) (r * 255 + 0.5); + } + + /** + * Returns the green component of this color as an integer value (0 to 255). + * + * @return the green component of this color (0 to 255) + */ + public int getGreenInt() { + return (int) (g * 255 + 0.5); + } + + /** + * Returns the blue component of this color as an integer value (0 to 255). + * + * @return the blue component of this color (0 to 255) + */ + public int getBlueInt() { + return (int) (b * 255 + 0.5); + } + + /** + * Returns the alpha component of this color as an integer value (0 to 255). + * + * @return the alpha component of this color (0 to 255) + */ + public int getAlphaInt() { + return (int) (a * 255 + 0.5); + } + + /** + * Returns the RGBA value representing the color. (Bits 24-31 are alpha, 16-23 are red, 8-15 are + * green, 0-7 are blue). + * + * @return the RGBA value of this color as integer + */ + public int getRGBA() { + int r = getRedInt(); + int g = getGreenInt(); + int b = getBlueInt(); + int a = getAlphaInt(); + return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | ((b & 0xFF) << 0); + } + + /** + * Returns a unique hash code for this color object based on it's values. If two colors are + * logically equivalent, they will return the same hash code value. + * + * @return the hash code value of this color + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Float.floatToIntBits(a); + result = prime * result + Float.floatToIntBits(b); + result = prime * result + Float.floatToIntBits(g); + result = prime * result + Float.floatToIntBits(r); + return result; + } + + /** + * Determines if this color is equals to the given object obj. + * + * @param obj the object to compare for equality + * @return true if they are equal + */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Color other = (Color) obj; + return Float.floatToIntBits(r) == Float.floatToIntBits(other.r) + && Float.floatToIntBits(g) == Float.floatToIntBits(other.g) + && Float.floatToIntBits(b) == Float.floatToIntBits(other.b) + && Float.floatToIntBits(a) == Float.floatToIntBits(other.a); + } + + /** + * Returns a string representation of this color. + * + * @return a string representation of this color + */ + @Override + public String toString() { + return "Color [r=" + r + ", g=" + g + ", b=" + b + ", a=" + a + "]"; + } } diff --git a/src/main/java/math/Matrix4f.java b/src/main/java/math/Matrix4f.java index 00a282fe..f148193e 100644 --- a/src/main/java/math/Matrix4f.java +++ b/src/main/java/math/Matrix4f.java @@ -4,389 +4,188 @@ public class Matrix4f { - public static final Matrix4f ZERO = new Matrix4f(0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0); - - public static final Matrix4f UNIT = new Matrix4f(1, 0, 0, 0, 0, 1, 0, 0, 0, - 0, 1, 0, 0, 0, 0, 1); - - public static final Matrix4f ONE = new Matrix4f(1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1); - - private static final int M00 = 0; - - private static final int M01 = 1; - - private static final int M02 = 2; - - private static final int M03 = 3; - - private static final int M10 = 4; - - private static final int M11 = 5; - - private static final int M12 = 6; - - private static final int M13 = 7; - - private static final int M20 = 8; - - private static final int M21 = 9; - - private static final int M22 = 10; - - private static final int M23 = 11; - - private static final int M30 = 12; - - private static final int M31 = 13; - - private static final int M32 = 14; - - private static final int M33 = 15; - - private float[] values; - - public Matrix4f() { - values = new float[16]; - } - - public Matrix4f(Matrix4f m) { - values = new float[16]; - for (int i = 0; i < m.values.length; i++) { - this.values[i] = m.values[i]; - } - } - - public Matrix4f(float m00, float m01, float m02, float m03, float m10, - float m11, float m12, float m13, float m20, float m21, float m22, - float m23, float m30, float m31, float m32, float m33) { - values = new float[16]; - - values[M00] = m00; - values[M01] = m01; - values[M02] = m02; - values[M03] = m03; - - values[M10] = m10; - values[M11] = m11; - values[M12] = m12; - values[M13] = m13; - - values[M20] = m20; - values[M21] = m21; - values[M22] = m22; - values[M23] = m23; - - values[M30] = m30; - values[M31] = m31; - values[M32] = m32; - values[M33] = m33; - } - - public Matrix4f transpose() { - Matrix4f tranpose = new Matrix4f(values[M00], values[M10], values[M20], - values[M30], - - values[M01], values[M11], values[M21], values[M31], - - values[M02], values[M12], values[M22], values[M32], - - values[M03], values[M13], values[M23], values[M33]); - - return tranpose; - } - - public Matrix4f add(Matrix4f m) { - Matrix4f result = new Matrix4f(); - float[] a = values; - float[] b = m.values; - float[] c = result.values; - - c[M00] = a[M00] + b[M00]; - c[M01] = a[M01] + b[M01]; - c[M02] = a[M02] + b[M02]; - c[M03] = a[M03] + b[M03]; - - c[M10] = a[M10] + b[M10]; - c[M11] = a[M11] + b[M11]; - c[M12] = a[M12] + b[M12]; - c[M13] = a[M13] + b[M13]; - - c[M20] = a[M20] + b[M20]; - c[M21] = a[M21] + b[M21]; - c[M22] = a[M22] + b[M22]; - c[M23] = a[M23] + b[M23]; - - c[M30] = a[M30] + b[M30]; - c[M31] = a[M31] + b[M31]; - c[M32] = a[M32] + b[M32]; - c[M33] = a[M33] + b[M33]; - - return result; - } - - public Matrix4f addLocal(Matrix4f m) { - values[M00] += m.values[M00]; - values[M01] += m.values[M01]; - values[M02] += m.values[M02]; - values[M03] += m.values[M03]; - - values[M10] += m.values[M10]; - values[M11] += m.values[M11]; - values[M12] += m.values[M12]; - values[M13] += m.values[M13]; - - values[M20] += m.values[M20]; - values[M21] += m.values[M21]; - values[M22] += m.values[M22]; - values[M23] += m.values[M23]; - - values[M30] += m.values[M30]; - values[M31] += m.values[M31]; - values[M32] += m.values[M32]; - values[M33] += m.values[M33]; - - return this; - } - - public Matrix4f multLocal(float scalar) { - values[M00] *= scalar; - values[M01] *= scalar; - values[M02] *= scalar; - values[M03] *= scalar; - - values[M10] *= scalar; - values[M11] *= scalar; - values[M12] *= scalar; - values[M13] *= scalar; - - values[M20] *= scalar; - values[M21] *= scalar; - values[M22] *= scalar; - values[M23] *= scalar; - - values[M30] *= scalar; - values[M31] *= scalar; - values[M32] *= scalar; - values[M33] *= scalar; - - return this; - } - - public Matrix4f mult(Matrix4f other) { - Matrix4f store = new Matrix4f(); - float[] m = new float[16]; - - m[0] = values[M00] * other.values[M00] + values[M01] * other.values[M10] - + values[M02] * other.values[M20] - + values[M03] * other.values[M30]; - m[1] = values[M00] * other.values[M01] + values[M01] * other.values[M11] - + values[M02] * other.values[M21] - + values[M03] * other.values[M31]; - m[2] = values[M00] * other.values[M02] + values[M01] * other.values[M12] - + values[M02] * other.values[M22] - + values[M03] * other.values[M32]; - m[3] = values[M00] * other.values[M03] + values[M01] * other.values[M13] - + values[M02] * other.values[M23] - + values[M03] * other.values[M33]; - - m[4] = values[M10] * other.values[M00] + values[M11] * other.values[M10] - + values[M12] * other.values[M20] - + values[M13] * other.values[M30]; - m[5] = values[M10] * other.values[M01] + values[M11] * other.values[M11] - + values[M12] * other.values[M21] - + values[M13] * other.values[M31]; - m[6] = values[M10] * other.values[M02] + values[M11] * other.values[M12] - + values[M12] * other.values[M22] - + values[M13] * other.values[M32]; - m[7] = values[M10] * other.values[M03] + values[M11] * other.values[M13] - + values[M12] * other.values[M23] - + values[M13] * other.values[M33]; - - m[8] = values[M20] * other.values[M00] + values[M21] * other.values[M10] - + values[M22] * other.values[M20] - + values[M23] * other.values[M30]; - m[9] = values[M20] * other.values[M01] + values[M21] * other.values[M11] - + values[M22] * other.values[M21] - + values[M23] * other.values[M31]; - m[10] = values[M20] * other.values[M02] - + values[M21] * other.values[M12] - + values[M22] * other.values[M22] - + values[M23] * other.values[M32]; - m[11] = values[M20] * other.values[M03] - + values[M21] * other.values[M13] - + values[M22] * other.values[M23] - + values[M23] * other.values[M33]; - - m[12] = values[M30] * other.values[M00] - + values[M31] * other.values[M10] - + values[M32] * other.values[M20] - + values[M33] * other.values[M30]; - m[13] = values[M30] * other.values[M01] - + values[M31] * other.values[M11] - + values[M32] * other.values[M21] - + values[M33] * other.values[M31]; - m[14] = values[M30] * other.values[M02] - + values[M31] * other.values[M12] - + values[M32] * other.values[M22] - + values[M33] * other.values[M32]; - m[15] = values[M30] * other.values[M03] - + values[M31] * other.values[M13] - + values[M32] * other.values[M23] - + values[M33] * other.values[M33]; - - store.values[M00] = m[0]; - store.values[M01] = m[1]; - store.values[M02] = m[2]; - store.values[M03] = m[3]; - store.values[M10] = m[4]; - store.values[M11] = m[5]; - store.values[M12] = m[6]; - store.values[M13] = m[7]; - store.values[M20] = m[8]; - store.values[M21] = m[9]; - store.values[M22] = m[10]; - store.values[M23] = m[11]; - store.values[M30] = m[12]; - store.values[M31] = m[13]; - store.values[M32] = m[14]; - store.values[M33] = m[15]; - - return store; - } - - public Vector3f mult(Vector3f v) { - Vector3f result = new Vector3f(); - float vx = v.x, vy = v.y, vz = v.z; - result.x = values[M00] * vx + values[M01] * vy + values[M02] * vz - + values[M03]; - result.y = values[M10] * vx + values[M11] * vy + values[M12] * vz - + values[M13]; - result.z = values[M20] * vx + values[M21] * vy + values[M22] * vz - + values[M23]; - return result; - } - - /** - * Look At, right-handed coordinate system. - * - * @param from - * @param to - * @param up - * @return the resulting view matrix - */ - public static Matrix4f lookAtRH(Vector3f eye, Vector3f target, - Vector3f up) { - // https://www.3dgep.com/understanding-the-view-matrix/ - Vector3f zaxis = eye.subtract(target).normalize(); // The "forward" - // vector. - Vector3f xaxis = up.cross(zaxis).normalize();// The "right" vector. - Vector3f yaxis = zaxis.cross(xaxis); // The "up" vector. - - Matrix4f viewMatrix = new Matrix4f(xaxis.x, yaxis.x, zaxis.x, 0, - xaxis.y, yaxis.y, zaxis.y, 0, xaxis.z, yaxis.z, zaxis.z, 0, - -xaxis.dot(eye), -yaxis.dot(eye), -zaxis.dot(eye), 1); - - return viewMatrix; - } - - /** - * FPS camera, right-handed coordinate system. - * - * @param eye - * @param pitch in radians - * @param yaw in radians - * @return the resulting view matrix - */ - public static Matrix4f fpsViewRH(Vector3f eye, float pitch, float yaw) { - // https://www.3dgep.com/understanding-the-view-matrix/ - float cosPitch = Mathf.cos(pitch); - float sinPitch = Mathf.sin(pitch); - float cosYaw = Mathf.cos(yaw); - float sinYaw = Mathf.sin(yaw); - - Vector3f xaxis = new Vector3f(cosYaw, 0, -sinYaw); - Vector3f yaxis = new Vector3f(sinYaw * sinPitch, cosPitch, - cosYaw * sinPitch); - Vector3f zaxis = new Vector3f(sinYaw * cosPitch, -sinPitch, - cosPitch * cosYaw); - - // Create a 4x4 view matrix from the right, up, forward and eye position - // vectors - Matrix4f viewMatrix = new Matrix4f(xaxis.x, yaxis.x, zaxis.x, 0, - xaxis.y, yaxis.y, zaxis.y, 0, xaxis.z, yaxis.z, zaxis.z, 0, - -xaxis.dot(eye), -yaxis.dot(eye), -zaxis.dot(eye), 1); - - return viewMatrix; - } - - public float[] getValues() { - return values; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(values); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Matrix4f other = (Matrix4f) obj; - if (!Arrays.equals(values, other.values)) - return false; - return true; - } - - @Override - public String toString() { - StringBuffer buffer = new StringBuffer(); - - buffer.append(values[M00]); - buffer.append("|"); - buffer.append(values[M01]); - buffer.append("|"); - buffer.append(values[M02]); - buffer.append("|"); - buffer.append(values[M03]); - buffer.append("\n"); - - buffer.append(values[M10]); - buffer.append("|"); - buffer.append(values[M11]); - buffer.append("|"); - buffer.append(values[M12]); - buffer.append("|"); - buffer.append(values[M13]); - buffer.append("\n"); - - buffer.append(values[M20]); - buffer.append("|"); - buffer.append(values[M21]); - buffer.append("|"); - buffer.append(values[M22]); - buffer.append("|"); - buffer.append(values[M23]); - buffer.append("\n"); - - buffer.append(values[M30]); - buffer.append("|"); - buffer.append(values[M31]); - buffer.append("|"); - buffer.append(values[M32]); - buffer.append("|"); - buffer.append(values[M33]); - - return buffer.toString(); - } - -} + public static final Matrix4f ZERO = new Matrix4f(); + + public static final Matrix4f IDENTITY = new Matrix4f().identity(); + + private final float[] values; + + public Matrix4f() { + this.values = new float[16]; + } + + public Matrix4f(float... elements) { + if (elements.length != 16) { + throw new IllegalArgumentException("Matrix4f requires 16 elements."); + } + this.values = Arrays.copyOf(elements, 16); + } + + public Matrix4f(Matrix4f other) { + this.values = Arrays.copyOf(other.values, 16); + } + + public Matrix4f identity() { + Arrays.fill(values, 0); + values[0] = values[5] = values[10] = values[15] = 1; + return this; + } + + public Matrix4f transpose() { + return new Matrix4f(values[0], values[4], values[8], values[12], values[1], + values[5], values[9], values[13], values[2], values[6], values[10], + values[14], values[3], values[7], values[11], values[15]); + } + + public Matrix4f add(Matrix4f other) { + float[] result = new float[16]; + for (int i = 0; i < 16; i++) { + result[i] = this.values[i] + other.values[i]; + } + return new Matrix4f(result); + } + + public Matrix4f addLocal(Matrix4f other) { + for (int i = 0; i < 16; i++) { + this.values[i] += other.values[i]; + } + return this; + } + + public Matrix4f multiply(Matrix4f other) { + float[] m = new float[16]; + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + m[row * 4 + col] = values[row * 4 + 0] * other.values[0 * 4 + col] + + values[row * 4 + 1] * other.values[1 * 4 + col] + + values[row * 4 + 2] * other.values[2 * 4 + col] + + values[row * 4 + 3] * other.values[3 * 4 + col]; + } + } + return new Matrix4f(m); + } + + public float[] getValues() { + return Arrays.copyOf(values, values.length); + } + + public float get(int row, int col) { + return values[row * 4 + col]; + } + + public void set(int row, int col, float value) { + values[row * 4 + col] = value; + } + + public static Matrix4f createTranslation(float x, float y, float z) { + return new Matrix4f(1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1); + } + + /** + * Sets the matrix to represent a perspective projection matrix. + * + *

+ * This method computes a standard perspective projection matrix based on the + * provided field of view, aspect ratio, near clipping plane, and far clipping + * plane. The resulting matrix transforms 3D points into normalized device + * coordinates for rendering with perspective projection. + *

+ * + * @param fov Field of view in radians (vertical field of view). Must be + * between 0 and π radians. + * @param aspect Aspect ratio of the viewport (width / height). Must be + * positive. + * @param nearPlane Distance to the near clipping plane. Must be less than + * `farPlane`. + * @param farPlane Distance to the far clipping plane. Must be greater than + * `nearPlane`. + * @return This matrix for chaining calls. + * @throws IllegalArgumentException if the input parameters are invalid. + */ + public Matrix4f setPerspective(float fov, float aspect, float nearPlane, + float farPlane) { + if (nearPlane > farPlane) { + throw new IllegalArgumentException(String.format( + "Near plane (%.2f) cannot be greater than far plane (%.2f).", + nearPlane, farPlane)); + } + if (aspect <= 0) { + throw new IllegalArgumentException( + "Aspect ratio must be a positive number."); + } + if (fov <= 0.0 || fov >= Math.PI) { + throw new IllegalArgumentException( + "Field of view must be between 0 and π radians."); + } + + float f = (float) (1.0 / Math.tan(fov / 2.0)); + Arrays.fill(values, 0); + values[0] = f / aspect; + values[5] = f; + values[10] = (farPlane + nearPlane) / (nearPlane - farPlane); + values[11] = -1; + values[14] = (2 * farPlane * nearPlane) / (nearPlane - farPlane); + + return this; + } + + /** + * Constructs a right-handed view matrix for an FPS (First-Person Shooter) + * style camera. + *

+ * The view matrix is computed based on the camera's position (`eye`), pitch, + * and yaw angles. It assumes a right-handed coordinate system where: + *

    + *
  • +X points to the right
  • + *
  • +Y points up
  • + *
  • +Z points backward (into the screen)
  • + *
+ * This method is particularly useful for creating a camera that can move and + * rotate freely in a 3D scene, such as in games or visualization + * applications. + *

+ * + * @param eye the position of the camera in world space, represented as a + * {@link Vector3f}. + * @param pitch the pitch angle (rotation around the X-axis), in radians. A + * positive value tilts the camera upward. + * @param yaw the yaw angle (rotation around the Y-axis), in radians. A + * positive value rotates the camera to the right. + * @return a {@link Matrix4f} representing the view matrix for the specified + * position and orientation. + * + * @see + * Understanding the View Matrix + */ + public static Matrix4f fpsViewRH(Vector3f eye, float pitch, float yaw) { + float cosPitch = Mathf.cos(pitch); + float sinPitch = Mathf.sin(pitch); + float cosYaw = Mathf.cos(yaw); + float sinYaw = Mathf.sin(yaw); + + Vector3f right = new Vector3f(cosYaw, 0, -sinYaw); + Vector3f up = new Vector3f(sinYaw * sinPitch, cosPitch, cosYaw * sinPitch); + Vector3f forward = new Vector3f(sinYaw * cosPitch, -sinPitch, + cosPitch * cosYaw); + + Matrix4f viewMatrix = new Matrix4f(right.x, up.x, forward.x, 0, right.y, + up.y, forward.y, 0, right.z, up.z, forward.z, 0, -right.dot(eye), + -up.dot(eye), -forward.dot(eye), 1); + + return viewMatrix; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Matrix4f:\n"); + for (int row = 0; row < 4; row++) { + sb.append("[ "); + for (int col = 0; col < 4; col++) { + sb.append(get(row, col)).append(" "); + } + sb.append("]\n"); + } + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/math/Plane.java b/src/main/java/math/Plane.java new file mode 100644 index 00000000..31000126 --- /dev/null +++ b/src/main/java/math/Plane.java @@ -0,0 +1,95 @@ +package math; + +/** + * Represents a geometric plane in 3D space defined by a normal vector and a + * distance from the origin. + *

+ * The equation of the plane is represented as:
+ * Ax + By + Cz + D = 0, where: + *

    + *
  • (A, B, C) is the normalized normal vector of the plane.
  • + *
  • D is the distance of the plane from the origin, along the + * normal direction.
  • + *
+ *

+ */ +public class Plane { + + /** The normal vector of the plane, representing its orientation. */ + private Vector3f normal; + + /** The distance of the plane from the origin along the normal vector. */ + private float distance; + + /** + * Constructs a plane with a default normal vector (0, 0, 0) and a distance of + * 0. + *

+ * The resulting plane is uninitialized and must be configured using the + * {@link #set(float, float, float, float)} method. + *

+ */ + public Plane() { + this.normal = new Vector3f(); + this.distance = 0; + } + + /** + * Sets the plane parameters using its coefficients. + *

+ * The coefficients (A, B, C, D) define the plane equation + * Ax + By + Cz + D = 0. The normal vector is automatically + * normalized during this operation. + *

+ * + * @param a the x-component of the normal vector. + * @param b the y-component of the normal vector. + * @param c the z-component of the normal vector. + * @param d the distance from the origin along the plane's normal vector. + */ + public void set(float a, float b, float c, float d) { + this.normal.set(a, b, c); + normal.normalizeLocal(); + this.distance = d; + } + + /** + * Calculates the signed distance from a given point to the plane. + *

+ * The signed distance is computed as:
+ * distance = dot(normal, point) + D, where: + *

    + *
  • normal is the plane's normal vector.
  • + *
  • point is the 3D point to test.
  • + *
  • D is the distance parameter of the plane.
  • + *
+ *

+ * + * @param point the point to calculate the distance from. + * @return the signed distance from the point to the plane. A positive value + * indicates the point is in the direction of the normal vector, and a + * negative value indicates it is on the opposite side. + */ + public float distanceToPoint(Vector3f point) { + return normal.dot(point) + distance; + } + + /** + * Gets the normal vector of the plane. + * + * @return the normal vector of the plane. + */ + public Vector3f getNormal() { + return normal; + } + + /** + * Gets the distance of the plane from the origin along its normal vector. + * + * @return the distance of the plane from the origin. + */ + public float getDistance() { + return distance; + } + +} \ No newline at end of file diff --git a/src/main/java/math/Ray3f.java b/src/main/java/math/Ray3f.java new file mode 100644 index 00000000..1dd351dc --- /dev/null +++ b/src/main/java/math/Ray3f.java @@ -0,0 +1,121 @@ +package math; + +/** + * Represents a ray in 3D space, defined by an origin point and a direction + * vector. + *

+ * A ray is a mathematical abstraction used in 3D graphics, physics simulations, + * and computational geometry. It extends infinitely from its origin in the + * specified direction. The direction vector is automatically normalized during + * initialization to ensure consistent calculations. + *

+ */ +public class Ray3f { + + /** + * The starting point of the ray. + */ + private final Vector3f origin; + + /** + * The normalized direction vector of the ray. + */ + private final Vector3f direction; + + /** + * The reciprocal of the direction vector, used for optimized ray-box + * intersection. + */ + private final Vector3f directionInv; + + /** + * Constructs a new {@code Ray3f} with the given origin and direction. + *

+ * The direction vector will be normalized internally to ensure correctness in + * calculations. Both {@code origin} and {@code direction} must be non-null. + *

+ * + * @param origin The starting point of the ray (non-null) + * @param direction The direction vector of the ray (non-null, normalized + * internally) + * @throws IllegalArgumentException if either {@code origin} or + * {@code direction} is null + */ + public Ray3f(Vector3f origin, Vector3f direction) { + if (origin == null) { + throw new IllegalArgumentException("Origin cannot be null."); + } + if (direction == null) { + throw new IllegalArgumentException("Direction cannot be null."); + } + this.origin = origin; + this.direction = direction; + this.direction.normalizeLocal(); + this.directionInv = direction.reciprocal(); + } + + /** + * Returns the origin of the ray. + *

+ * The origin is the starting point from which the ray emanates. + *

+ * + * @return The origin of the ray. + */ + public Vector3f getOrigin() { + return origin; + } + + /** + * Returns the normalized direction vector of the ray. + *

+ * The direction vector defines the direction in which the ray travels. The + * vector is normalized, ensuring consistent calculations for operations like + * intersections. + *

+ * + * @return The direction vector of the ray. + */ + public Vector3f getDirection() { + return direction; + } + + /** + * Returns the reciprocal of the direction vector of the ray. + *

+ * The reciprocal of the direction vector is precomputed to optimize ray-box + * intersection tests, where division by components of the direction vector is + * required. + *

+ * + * @return The reciprocal of the direction vector of the ray. + */ + public Vector3f getDirectionInv() { + return directionInv; + } + + /** + * Computes the point along the ray at a given parameter {@code t}. + *

+ * The formula for the point is: + * + *

+	 * {@code
+	 * point = origin + t * direction
+	 * }
+	 * 
+ * + * where {@code t} is a scalar representing the distance along the ray from + * the origin. Positive values of {@code t} will give points in the direction + * the ray is pointing, while negative values will give points in the opposite + * direction. + *

+ * + * @param t The parameter along the ray (can be negative, zero, or positive). + * @return The point at parameter {@code t}. + */ + public Vector3f getPointAt(float t) { + return origin.add(direction.mult(t)); + } + +} \ No newline at end of file diff --git a/src/main/java/math/Vector3f.java b/src/main/java/math/Vector3f.java index 7d756a56..935b0f5f 100644 --- a/src/main/java/math/Vector3f.java +++ b/src/main/java/math/Vector3f.java @@ -2,552 +2,569 @@ public class Vector3f { - public static final Vector3f BACK = new Vector3f(0, 0, -1); + public static final Vector3f BACK = new Vector3f(0, 0, -1); - public static final Vector3f DOWN = new Vector3f(0, -1, 0); + public static final Vector3f DOWN = new Vector3f(0, -1, 0); - public static final Vector3f FORWARD = new Vector3f(0, 0, 1); + public static final Vector3f FORWARD = new Vector3f(0, 0, 1); - public static final Vector3f LEFT = new Vector3f(-1, 0, 0); + public static final Vector3f LEFT = new Vector3f(-1, 0, 0); - public static final Vector3f MAX = new Vector3f(Float.MAX_VALUE, - Float.MAX_VALUE, Float.MAX_VALUE); + public static final Vector3f MAX = new Vector3f(Float.MAX_VALUE, + Float.MAX_VALUE, Float.MAX_VALUE); - public static final Vector3f MIN = new Vector3f(Float.MIN_VALUE, - Float.MIN_VALUE, Float.MIN_VALUE); + public static final Vector3f MIN = new Vector3f(Float.MIN_VALUE, + Float.MIN_VALUE, Float.MIN_VALUE); - public static final Vector3f NAN = new Vector3f(Float.NaN, Float.NaN, - Float.NaN); + public static final Vector3f NAN = new Vector3f(Float.NaN, Float.NaN, + Float.NaN); - public static final Vector3f NEGATIVE_INFINITY = new Vector3f( - Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, - Float.NEGATIVE_INFINITY); + public static final Vector3f NEGATIVE_INFINITY = new Vector3f( + Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, + Float.NEGATIVE_INFINITY); - public static final Vector3f ONE = new Vector3f(1, 1, 1); + public static final Vector3f ONE = new Vector3f(1, 1, 1); - public static final Vector3f POSITIVE_INFINITY = new Vector3f( - Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, - Float.POSITIVE_INFINITY); + public static final Vector3f POSITIVE_INFINITY = new Vector3f( + Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.POSITIVE_INFINITY); - public static final Vector3f RIGHT = new Vector3f(1, 0, 0); + public static final Vector3f RIGHT = new Vector3f(1, 0, 0); - public static final Vector3f UP = new Vector3f(0, 1, 0); + public static final Vector3f UP = new Vector3f(0, 1, 0); - public static final Vector3f ZERO = new Vector3f(0, 0, 0); + public static final Vector3f ZERO = new Vector3f(0, 0, 0); - public float x; + public float x; - public float y; + public float y; - public float z; + public float z; - public Vector3f() { - x = y = z = 0; - } + public Vector3f() { + x = y = z = 0; + } - public Vector3f(float value) { - x = y = z = value; - } + public Vector3f(float value) { + x = y = z = value; + } - public Vector3f(float x, float y) { - this(x, y, 0); - } + public Vector3f(float x, float y) { + this(x, y, 0); + } - public Vector3f(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - } - - public Vector3f(float[] values) { - x = values[0]; - y = values[1]; - z = values[2]; - } + public Vector3f(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3f(float[] values) { + x = values[0]; + y = values[1]; + z = values[2]; + } - public Vector3f(Vector3f v) { - set(v); - } - - /** - * Rounds the x, y, and z components of this vector to the specified number - * of decimal places. - * - * @param decimalPlaces The number of decimal places to round to. - */ - public void roundLocalDecimalPlaces(int decimalPlaces) { - float factor = Mathf.pow(10, decimalPlaces); - x = Mathf.round(x * factor) / factor; - y = Mathf.round(y * factor) / factor; - z = Mathf.round(z * factor) / factor; - } - - public boolean approximatelyEquals(Vector3f v, float threshold) { - if (threshold < 0.0f) - throw new IllegalArgumentException( - "Threshold must be greater or equal to 0.0f."); - - float diffX = Math.abs(x - v.x); - float diffY = Math.abs(y - v.y); - float diffZ = Math.abs(z - v.z); - - return (diffX <= threshold && diffY <= threshold && diffZ <= threshold); - } - - public float angle(Vector3f v) { - return (float) Math.acos(dot(v)); - } - - public float signedAngle(Vector3f v, Vector3f normal) { - float unsignedAngle = (float) Math.acos(dot(v)); - return unsignedAngle * Math.signum(normal.dot(cross(v))); - } - - public Vector3f project(Vector3f v) { - float scalar = dot(v) / v.lengthSquared(); - return v.mult(scalar); - } - - public Vector3f projectLocal(Vector3f v) { - float scalar = dot(v) / v.lengthSquared(); - set(v.mult(scalar)); - return this; - } - - public Vector3f projectOnPlane(Vector3f planeNormal) { - // FIXME Check if this implementation is correct. - float scalar = dot(planeNormal) / planeNormal.lengthSquared(); - return subtract(planeNormal.mult(scalar)); - } - - public Vector3f projectOnPlaneLocal(Vector3f planeNormal) { - // FIXME Check if this implementation is correct. - float scalar = dot(planeNormal) / planeNormal.lengthSquared(); - subtractLocal(planeNormal.mult(scalar)); - return this; - } - - public Vector3f clampLength(float maxLength) { - return normalize().mult(maxLength); - } - - public Vector3f clampLengthLocal(float maxLength) { - normalizeLocal(); - multLocal(maxLength); - return this; - } - - public float lengthSquared() { - return (x * x) + (y * y) + (z * z); - } - - public float length() { - return (float) Math.sqrt(lengthSquared()); - } - - public float distanceSquared(float x, float y, float z) { - float dx = this.x - x; - float dy = this.y - y; - float dz = this.z - z; - return (dx * dx) + (dy * dy) + (dz * dz); - } - - public float distance(float x, float y, float z) { - return (float) Math.sqrt(distanceSquared(x, y, z)); - } - - public float distanceSquared(Vector3f v) { - return distanceSquared(v.x, v.y, v.z); - } - - public float distance(Vector3f v) { - return (float) Math.sqrt(distanceSquared(v)); - } - - public Vector3f normalize() { - float length = length(); - if (length != 0) { - return divide(length); - } - return divide(1); - } - - public Vector3f normalizeLocal() { - float length = length(); - if (length != 0) { - return divideLocal(length); - } - return divideLocal(1); - } - - public float dot(Vector3f v) { - return (x * v.x) + (y * v.y) + z * (v.z); - } - - public Vector3f cross(float x, float y, float z) { - return new Vector3f((this.y * z) - (this.z * y), - (this.z * x) - (this.x * z), (this.x * y) - (this.y * x)); - } - - public Vector3f crossLocal(float x, float y, float z) { - this.x = (this.y * z) - (this.z * y); - this.y = (this.z * x) - (this.x * z); - this.z = (this.x * y) - (this.y * x); - return this; - } - - public Vector3f cross(Vector3f v) { - return new Vector3f((y * v.z) - (z * v.y), (z * v.x) - (x * v.z), - (x * v.y) - (y * v.x)); - } - - public Vector3f crossLocal(Vector3f v) { - x = (y * v.z) - (z * v.y); - y = (z * v.x) - (x * v.z); - z = (x * v.y) - (y * v.x); - return this; - } - - public Vector3f cross(Vector3f v, Vector3f result) { - if (result == null) - result = new Vector3f(); - return result.set(cross(v)); - } - - public Vector3f negate() { - return new Vector3f(-x, -y, -z); - } - - public Vector3f negateLocal() { - x = -x; - y = -y; - z = -z; - return this; - } - - public Vector3f add(float x, float y, float z) { - return new Vector3f(this.x + x, this.y + y, this.z + z); - } - - public Vector3f addLocal(float x, float y, float z) { - this.x += x; - this.y += y; - this.z += z; - return this; - } - - public Vector3f add(Vector3f v) { - return new Vector3f(x + v.x, y + v.y, z + v.z); - } - - public Vector3f addLocal(Vector3f v) { - x += v.x; - y += v.y; - z += v.z; - return this; - } - - public Vector3f add(Vector3f v, Vector3f result) { - if (result == null) - result = new Vector3f(); - return result.set(add(v)); - } - - public Vector3f subtract(float x, float y, float z) { - return new Vector3f(this.x - x, this.y - y, this.z - z); - } - - public Vector3f subtractLocal(float x, float y, float z) { - this.x -= x; - this.y -= y; - this.z -= z; - return this; - } - - public Vector3f subtract(Vector3f v) { - return new Vector3f(x - v.x, y - v.y, z - v.z); - } - - public Vector3f subtractLocal(Vector3f v) { - x -= v.x; - y -= v.y; - z -= v.z; - return this; - } - - public Vector3f subtract(Vector3f v, Vector3f result) { - if (result == null) - result = new Vector3f(); - return result.set(subtract(v)); - } - - public Vector3f mult(float x, float y, float z) { - return new Vector3f(this.x * x, this.y * y, this.z * z); - } - - public Vector3f multLocal(float x, float y, float z) { - this.x *= x; - this.y *= y; - this.z *= z; - return this; - } - - public Vector3f mult(Vector3f v) { - return new Vector3f(x * v.x, y * v.y, z * v.z); - } - - public Vector3f multLocal(Vector3f v) { - x *= v.x; - y *= v.y; - z *= v.z; - return this; - } - - public Vector3f mult(Vector3f v, Vector3f result) { - if (result == null) - result = new Vector3f(); - return result.set(mult(v)); - } - - public Vector3f divide(float x, float y, float z) { - return new Vector3f(this.x / x, this.y / y, this.z / z); - } - - public Vector3f divideLocal(float x, float y, float z) { - this.x /= x; - this.y /= y; - this.z /= z; - return this; - } - - public Vector3f divide(Vector3f v) { - return new Vector3f(x / v.x, y / v.y, z / v.z); - } - - public Vector3f divideLocal(Vector3f v) { - x /= v.x; - y /= v.y; - z /= v.z; - return this; - } - - public Vector3f mult(float scalar) { - return new Vector3f(x * scalar, y * scalar, z * scalar); - } - - public Vector3f multLocal(float scalar) { - x *= scalar; - y *= scalar; - z *= scalar; - return this; - } - - public Vector3f mult(Matrix3f m) { - float x0 = m.values[0] * x + m.values[1] * y + m.values[2] * z; - float y0 = m.values[3] * x + m.values[4] * y + m.values[5] * z; - float z0 = m.values[6] * x + m.values[7] * y + m.values[8] * z; - return new Vector3f(x0, y0, z0); - } - - public Vector3f multLocal(Matrix3f m) { - float x0 = m.values[0] * x + m.values[1] * y + m.values[2] * z; - float y0 = m.values[3] * x + m.values[4] * y + m.values[5] * z; - float z0 = m.values[6] * x + m.values[7] * y + m.values[8] * z; - set(x0, y0, z0); - return this; - } - - public Vector3f divide(float scalar) { - return new Vector3f(x / scalar, y / scalar, z / scalar); - } - - public Vector3f divideLocal(float scalar) { - x /= scalar; - y /= scalar; - z /= scalar; - return this; - } - - public Vector3f divide(Vector3f v, Vector3f result) { - if (result == null) - result = new Vector3f(); - return result.set(divide(v)); - } - - public Vector3f abs() { - return new Vector3f(Math.abs(x), Math.abs(y), Math.abs(z)); - } - - public Vector3f absLocal() { - x = Math.abs(x); - y = Math.abs(y); - z = Math.abs(z); - return this; - } - - public Vector3f min(Vector3f v) { - return new Vector3f(Math.min(x, v.x), Math.min(y, v.y), - Math.min(z, v.z)); - } - - public Vector3f minLocal(Vector3f v) { - x = Math.min(x, v.x); - y = Math.min(y, v.y); - z = Math.min(z, v.z); - return this; - } - - public Vector3f minLocal(Vector3f a, Vector3f b) { - x = Math.min(a.x, b.x); - y = Math.min(a.y, b.y); - z = Math.min(a.z, b.z); - return this; - } - - public Vector3f max(Vector3f v) { - return new Vector3f(Math.max(x, v.x), Math.max(y, v.y), - Math.max(z, v.z)); - } - - public Vector3f maxLocal(Vector3f v) { - x = Math.max(x, v.x); - y = Math.max(y, v.y); - z = Math.max(z, v.z); - return this; - } - - public Vector3f maxLocal(Vector3f a, Vector3f b) { - x = Math.max(a.x, b.x); - y = Math.max(a.y, b.y); - z = Math.max(a.z, b.z); - return this; - } - - public Vector3f lerpLocal(Vector3f finalVec, float changeAmnt) { - if (changeAmnt == 0) { - return this; - } - if (changeAmnt == 1) { - this.x = finalVec.x; - this.y = finalVec.y; - this.z = finalVec.z; - return this; - } - this.x = (1 - changeAmnt) * this.x + changeAmnt * finalVec.x; - this.y = (1 - changeAmnt) * this.y + changeAmnt * finalVec.y; - this.z = (1 - changeAmnt) * this.z + changeAmnt * finalVec.z; - return this; - } - - public Vector3f lerpLocal(Vector3f beginVec, Vector3f finalVec, float changeAmnt) { - if (changeAmnt == 0) { - this.x = beginVec.x; - this.y = beginVec.y; - this.z = beginVec.z; - return this; - } - if (changeAmnt == 1) { - this.x = finalVec.x; - this.y = finalVec.y; - this.z = finalVec.z; - return this; - } - this.x = (1 - changeAmnt) * beginVec.x + changeAmnt * finalVec.x; - this.y = (1 - changeAmnt) * beginVec.y + changeAmnt * finalVec.y; - this.z = (1 - changeAmnt) * beginVec.z + changeAmnt * finalVec.z; - return this; - } - - public static boolean isValid(Vector3f v) { - if (v == null) - return false; - if (Float.isNaN(v.x) || Float.isNaN(v.y) || Float.isNaN(v.z)) - return false; - if (Float.isInfinite(v.x) || Float.isInfinite(v.y) - || Float.isInfinite(v.z)) - return false; - return true; - } - - public Vector3f set(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - return this; - } - - public Vector3f set(Vector3f v) { - x = v.x; - y = v.y; - z = v.z; - return this; - } - - public Vector3f set(float[] values) { - x = values[0]; - y = values[1]; - z = values[2]; - return this; - } - - public float getX() { - return x; - } - - public Vector3f setX(float x) { - this.x = x; - return this; - } - - public float getY() { - return y; - } - - public Vector3f setY(float y) { - this.y = y; - return this; - } - - public float getZ() { - return z; - } - - public Vector3f setZ(float z) { - this.z = z; - return this; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Float.floatToIntBits(x); - result = prime * result + Float.floatToIntBits(y); - result = prime * result + Float.floatToIntBits(z); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Vector3f other = (Vector3f) obj; - if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x)) - return false; - if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y)) - return false; - if (Float.floatToIntBits(z) != Float.floatToIntBits(other.z)) - return false; - return true; - } - - @Override - public String toString() { - return "Vector3f [x=" + x + ", y=" + y + ", z=" + z + "]"; - } + public Vector3f(Vector3f v) { + set(v); + } + + /** + * Rounds the x, y, and z components of this vector to the specified number of + * decimal places. + * + * @param decimalPlaces The number of decimal places to round to. + */ + public void roundLocalDecimalPlaces(int decimalPlaces) { + float factor = Mathf.pow(10, decimalPlaces); + x = Mathf.round(x * factor) / factor; + y = Mathf.round(y * factor) / factor; + z = Mathf.round(z * factor) / factor; + } + + public boolean approximatelyEquals(Vector3f v, float threshold) { + if (threshold < 0.0f) + throw new IllegalArgumentException( + "Threshold must be greater or equal to 0.0f."); + + float diffX = Math.abs(x - v.x); + float diffY = Math.abs(y - v.y); + float diffZ = Math.abs(z - v.z); + + return (diffX <= threshold && diffY <= threshold && diffZ <= threshold); + } + + public float angle(Vector3f v) { + return (float) Math.acos(dot(v)); + } + + public float signedAngle(Vector3f v, Vector3f normal) { + float unsignedAngle = (float) Math.acos(dot(v)); + return unsignedAngle * Math.signum(normal.dot(cross(v))); + } + + public Vector3f project(Vector3f v) { + float scalar = dot(v) / v.lengthSquared(); + return v.mult(scalar); + } + + public Vector3f projectLocal(Vector3f v) { + float scalar = dot(v) / v.lengthSquared(); + set(v.mult(scalar)); + return this; + } + + public Vector3f projectOnPlane(Vector3f planeNormal) { + // FIXME Check if this implementation is correct. + float scalar = dot(planeNormal) / planeNormal.lengthSquared(); + return subtract(planeNormal.mult(scalar)); + } + + public Vector3f projectOnPlaneLocal(Vector3f planeNormal) { + // FIXME Check if this implementation is correct. + float scalar = dot(planeNormal) / planeNormal.lengthSquared(); + subtractLocal(planeNormal.mult(scalar)); + return this; + } + + public Vector3f clampLength(float maxLength) { + return normalize().mult(maxLength); + } + + public Vector3f clampLengthLocal(float maxLength) { + normalizeLocal(); + multLocal(maxLength); + return this; + } + + public float lengthSquared() { + return (x * x) + (y * y) + (z * z); + } + + public float length() { + return (float) Math.sqrt(lengthSquared()); + } + + public float distanceSquared(float x, float y, float z) { + float dx = this.x - x; + float dy = this.y - y; + float dz = this.z - z; + return (dx * dx) + (dy * dy) + (dz * dz); + } + + public float distance(float x, float y, float z) { + return (float) Math.sqrt(distanceSquared(x, y, z)); + } + + public float distanceSquared(Vector3f v) { + return distanceSquared(v.x, v.y, v.z); + } + + public float distance(Vector3f v) { + return (float) Math.sqrt(distanceSquared(v)); + } + + public Vector3f normalize() { + float length = length(); + if (length != 0) { + return divide(length); + } + return divide(1); + } + + public Vector3f normalizeLocal() { + float length = length(); + if (length != 0) { + return divideLocal(length); + } + return divideLocal(1); + } + + public float dot(Vector3f v) { + return (x * v.x) + (y * v.y) + z * (v.z); + } + + public Vector3f cross(float x, float y, float z) { + return new Vector3f((this.y * z) - (this.z * y), + (this.z * x) - (this.x * z), (this.x * y) - (this.y * x)); + } + + public Vector3f crossLocal(float x, float y, float z) { + this.x = (this.y * z) - (this.z * y); + this.y = (this.z * x) - (this.x * z); + this.z = (this.x * y) - (this.y * x); + return this; + } + + public Vector3f cross(Vector3f v) { + return new Vector3f((y * v.z) - (z * v.y), (z * v.x) - (x * v.z), + (x * v.y) - (y * v.x)); + } + + public Vector3f crossLocal(Vector3f v) { + x = (y * v.z) - (z * v.y); + y = (z * v.x) - (x * v.z); + z = (x * v.y) - (y * v.x); + return this; + } + + public Vector3f cross(Vector3f v, Vector3f result) { + if (result == null) + result = new Vector3f(); + return result.set(cross(v)); + } + + public Vector3f negate() { + return new Vector3f(-x, -y, -z); + } + + public Vector3f negateLocal() { + x = -x; + y = -y; + z = -z; + return this; + } + + public Vector3f add(float x, float y, float z) { + return new Vector3f(this.x + x, this.y + y, this.z + z); + } + + public Vector3f addLocal(float x, float y, float z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public Vector3f add(Vector3f v) { + return new Vector3f(x + v.x, y + v.y, z + v.z); + } + + public Vector3f addLocal(Vector3f v) { + x += v.x; + y += v.y; + z += v.z; + return this; + } + + public Vector3f add(Vector3f v, Vector3f result) { + if (result == null) + result = new Vector3f(); + return result.set(add(v)); + } + + public Vector3f subtract(float x, float y, float z) { + return new Vector3f(this.x - x, this.y - y, this.z - z); + } + + public Vector3f subtractLocal(float x, float y, float z) { + this.x -= x; + this.y -= y; + this.z -= z; + return this; + } + + public Vector3f subtract(Vector3f v) { + return new Vector3f(x - v.x, y - v.y, z - v.z); + } + + public Vector3f subtractLocal(Vector3f v) { + x -= v.x; + y -= v.y; + z -= v.z; + return this; + } + + public Vector3f subtract(Vector3f v, Vector3f result) { + if (result == null) + result = new Vector3f(); + return result.set(subtract(v)); + } + + public Vector3f mult(float x, float y, float z) { + return new Vector3f(this.x * x, this.y * y, this.z * z); + } + + public Vector3f multLocal(float x, float y, float z) { + this.x *= x; + this.y *= y; + this.z *= z; + return this; + } + + public Vector3f mult(Vector3f v) { + return new Vector3f(x * v.x, y * v.y, z * v.z); + } + + public Vector3f multLocal(Vector3f v) { + x *= v.x; + y *= v.y; + z *= v.z; + return this; + } + + public Vector3f mult(Vector3f v, Vector3f result) { + if (result == null) + result = new Vector3f(); + return result.set(mult(v)); + } + + public Vector3f divide(float x, float y, float z) { + return new Vector3f(this.x / x, this.y / y, this.z / z); + } + + public Vector3f divideLocal(float x, float y, float z) { + this.x /= x; + this.y /= y; + this.z /= z; + return this; + } + + public Vector3f divide(Vector3f v) { + return new Vector3f(x / v.x, y / v.y, z / v.z); + } + + public Vector3f divideLocal(Vector3f v) { + x /= v.x; + y /= v.y; + z /= v.z; + return this; + } + + public Vector3f mult(float scalar) { + return new Vector3f(x * scalar, y * scalar, z * scalar); + } + + public Vector3f multLocal(float scalar) { + x *= scalar; + y *= scalar; + z *= scalar; + return this; + } + + public Vector3f mult(Matrix3f m) { + float x0 = m.values[0] * x + m.values[1] * y + m.values[2] * z; + float y0 = m.values[3] * x + m.values[4] * y + m.values[5] * z; + float z0 = m.values[6] * x + m.values[7] * y + m.values[8] * z; + return new Vector3f(x0, y0, z0); + } + + public Vector3f multLocal(Matrix3f m) { + float x0 = m.values[0] * x + m.values[1] * y + m.values[2] * z; + float y0 = m.values[3] * x + m.values[4] * y + m.values[5] * z; + float z0 = m.values[6] * x + m.values[7] * y + m.values[8] * z; + set(x0, y0, z0); + return this; + } + + public Vector3f divide(float scalar) { + return new Vector3f(x / scalar, y / scalar, z / scalar); + } + + public Vector3f divideLocal(float scalar) { + x /= scalar; + y /= scalar; + z /= scalar; + return this; + } + + public Vector3f divide(Vector3f v, Vector3f result) { + if (result == null) + result = new Vector3f(); + return result.set(divide(v)); + } + + public Vector3f abs() { + return new Vector3f(Math.abs(x), Math.abs(y), Math.abs(z)); + } + + public Vector3f absLocal() { + x = Math.abs(x); + y = Math.abs(y); + z = Math.abs(z); + return this; + } + + public Vector3f min(Vector3f v) { + return new Vector3f(Math.min(x, v.x), Math.min(y, v.y), Math.min(z, v.z)); + } + + public Vector3f minLocal(Vector3f v) { + x = Math.min(x, v.x); + y = Math.min(y, v.y); + z = Math.min(z, v.z); + return this; + } + + public Vector3f minLocal(Vector3f a, Vector3f b) { + x = Math.min(a.x, b.x); + y = Math.min(a.y, b.y); + z = Math.min(a.z, b.z); + return this; + } + + public Vector3f max(Vector3f v) { + return new Vector3f(Math.max(x, v.x), Math.max(y, v.y), Math.max(z, v.z)); + } + + public Vector3f maxLocal(Vector3f v) { + x = Math.max(x, v.x); + y = Math.max(y, v.y); + z = Math.max(z, v.z); + return this; + } + + public Vector3f maxLocal(Vector3f a, Vector3f b) { + x = Math.max(a.x, b.x); + y = Math.max(a.y, b.y); + z = Math.max(a.z, b.z); + return this; + } + + public Vector3f reciprocal() { + return new Vector3f((x != 0 ? 1.0f / x : Float.POSITIVE_INFINITY), + (y != 0 ? 1.0f / y : Float.POSITIVE_INFINITY), + (z != 0 ? 1.0f / z : Float.POSITIVE_INFINITY)); + } + + public Vector3f lerpLocal(Vector3f finalVec, float changeAmnt) { + if (changeAmnt == 0) { + return this; + } + if (changeAmnt == 1) { + this.x = finalVec.x; + this.y = finalVec.y; + this.z = finalVec.z; + return this; + } + this.x = (1 - changeAmnt) * this.x + changeAmnt * finalVec.x; + this.y = (1 - changeAmnt) * this.y + changeAmnt * finalVec.y; + this.z = (1 - changeAmnt) * this.z + changeAmnt * finalVec.z; + return this; + } + + public Vector3f lerpLocal(Vector3f beginVec, Vector3f finalVec, + float changeAmnt) { + if (changeAmnt == 0) { + this.x = beginVec.x; + this.y = beginVec.y; + this.z = beginVec.z; + return this; + } + if (changeAmnt == 1) { + this.x = finalVec.x; + this.y = finalVec.y; + this.z = finalVec.z; + return this; + } + this.x = (1 - changeAmnt) * beginVec.x + changeAmnt * finalVec.x; + this.y = (1 - changeAmnt) * beginVec.y + changeAmnt * finalVec.y; + this.z = (1 - changeAmnt) * beginVec.z + changeAmnt * finalVec.z; + return this; + } + + public static boolean isValid(Vector3f v) { + if (v == null) + return false; + if (Float.isNaN(v.x) || Float.isNaN(v.y) || Float.isNaN(v.z)) + return false; + if (Float.isInfinite(v.x) || Float.isInfinite(v.y) || Float.isInfinite(v.z)) + return false; + return true; + } + + public Vector3f set(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public Vector3f set(Vector3f v) { + x = v.x; + y = v.y; + z = v.z; + return this; + } + + public Vector3f set(float[] values) { + x = values[0]; + y = values[1]; + z = values[2]; + return this; + } + + public float getX() { + return x; + } + + public Vector3f setX(float x) { + this.x = x; + return this; + } + + public float getY() { + return y; + } + + public Vector3f setY(float y) { + this.y = y; + return this; + } + + public float getZ() { + return z; + } + + public Vector3f setZ(float z) { + this.z = z; + return this; + } + + public float get(int i) { + switch (i) { + case 0: + return x; + case 1: + return y; + case 2: + return z; + default: + throw new IndexOutOfBoundsException("Index must be 0, 1, or 2."); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Float.floatToIntBits(x); + result = prime * result + Float.floatToIntBits(y); + result = prime * result + Float.floatToIntBits(z); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Vector3f other = (Vector3f) obj; + if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x)) + return false; + if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y)) + return false; + if (Float.floatToIntBits(z) != Float.floatToIntBits(other.z)) + return false; + return true; + } + + @Override + public String toString() { + return "Vector3f [x=" + x + ", y=" + y + ", z=" + z + "]"; + } } diff --git a/src/main/java/mesh/Edge3D.java b/src/main/java/mesh/Edge3D.java index 8f9387f8..ea4d3262 100644 --- a/src/main/java/mesh/Edge3D.java +++ b/src/main/java/mesh/Edge3D.java @@ -2,47 +2,41 @@ public class Edge3D { - public int fromIndex; - - public int toIndex; - - public Edge3D(int fromIndex, int toIndex) { - this.fromIndex = fromIndex; - this.toIndex = toIndex; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + fromIndex; - result = prime * result + toIndex; - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Edge3D other = (Edge3D) obj; - if (fromIndex != other.fromIndex) - return false; - if (toIndex != other.toIndex) - return false; - return true; - } - - public Edge3D createPair() { - return new Edge3D(toIndex, fromIndex); - } - - @Override - public String toString() { - return "Edge3D [fromIndex=" + fromIndex + ", toIndex=" + toIndex + "]"; - } - + public int fromIndex; + + public int toIndex; + + public Edge3D(int fromIndex, int toIndex) { + this.fromIndex = fromIndex; + this.toIndex = toIndex; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + fromIndex; + result = prime * result + toIndex; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Edge3D other = (Edge3D) obj; + if (fromIndex != other.fromIndex) return false; + if (toIndex != other.toIndex) return false; + return true; + } + + public Edge3D createPair() { + return new Edge3D(toIndex, fromIndex); + } + + @Override + public String toString() { + return "Edge3D [fromIndex=" + fromIndex + ", toIndex=" + toIndex + "]"; + } } diff --git a/src/main/java/mesh/Face3D.java b/src/main/java/mesh/Face3D.java index 5a84154c..46a00af0 100644 --- a/src/main/java/mesh/Face3D.java +++ b/src/main/java/mesh/Face3D.java @@ -7,51 +7,49 @@ public class Face3D { - public Color color; - - public int[] indices; - - public Vector3f normal; - - public String tag; - - public Face3D() { - this(new int[0]); - } - - public Face3D(int... indices) { - 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]; - } - - public boolean sharesSameIndices(Face3D face) { - int[] indices0 = Arrays.copyOf(face.indices, face.indices.length); - int[] indices1 = Arrays.copyOf(indices, indices.length); - Arrays.sort(indices0); - Arrays.sort(indices1); - return Arrays.equals(indices0, indices1); - } - - public int getIndexAt(int index) { - return indices[index % indices.length]; - } - - public int getVertexCount() { - return indices.length; - } - - public Face3D(Face3D f) { - this(f.indices); - this.tag = new String(f.tag); - } - - @Override - public String toString() { - return "Face3D [indices=" + Arrays.toString(indices) + "]"; - } - + public Color color; + + public int[] indices; + + public Vector3f normal; + + public String tag; + + public Face3D() { + this(new int[0]); + } + + public Face3D(int... indices) { + 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]; + } + + public boolean sharesSameIndices(Face3D face) { + int[] indices0 = Arrays.copyOf(face.indices, face.indices.length); + int[] indices1 = Arrays.copyOf(indices, indices.length); + Arrays.sort(indices0); + Arrays.sort(indices1); + return Arrays.equals(indices0, indices1); + } + + public int getIndexAt(int index) { + return indices[index % indices.length]; + } + + public int getVertexCount() { + return indices.length; + } + + public Face3D(Face3D f) { + this(f.indices); + this.tag = new String(f.tag); + } + + @Override + public String toString() { + return "Face3D [indices=" + Arrays.toString(indices) + "]"; + } } diff --git a/src/main/java/mesh/Mesh3D.java b/src/main/java/mesh/Mesh3D.java index adf62b36..914b0f4a 100644 --- a/src/main/java/mesh/Mesh3D.java +++ b/src/main/java/mesh/Mesh3D.java @@ -15,263 +15,252 @@ public class Mesh3D { - public ArrayList vertices; - - public ArrayList faces; - - public Mesh3D() { - vertices = new ArrayList(); - faces = new ArrayList(); - } - - /** - * Applies the provided {@link IMeshModifier} to this mesh. This is congruent - * to {@link IMeshModifier#modify(Mesh3D)}. - * - * @param modifier The modifier to apply to this mesh. - * @return this - */ - public Mesh3D apply(IMeshModifier modifier) { - return modifier.modify(this); - } - - /** - * Rotates the mesh around the Y-axis. - * - * @deprecated Use {@link RotateYModifier} instead. - */ - public Mesh3D rotateY(float angle) { - return new RotateYModifier(angle).modify(this); - } - - /** - * Rotates the mesh around the Z-axis. - * - * @deprecated Use {@link RotateZModifier} instead. - */ - public Mesh3D rotateZ(float angle) { - return new RotateZModifier(angle).modify(this); - } - - /** - * Translates the mesh along the X-axis. - * - * @deprecated Use {@link TranslateModifier} instead. - */ - @Deprecated - public Mesh3D translateX(float tx) { - return new TranslateModifier(tx, 0, 0).modify(this); - } - - /** - * Translates the mesh along the Y-axis. - * - * @deprecated Use {@link TranslateModifier} instead. - */ - public Mesh3D translateY(float ty) { - return new TranslateModifier(0, ty, 0).modify(this); - } - - /** - * Translates the mesh along the Z-axis. - * - * @deprecated Use {@link TranslateModifier} instead. - */ - public Mesh3D translateZ(float tz) { - return new TranslateModifier(0, 0, tz).modify(this); - } - - /** - * Calculates the axis-aligned bounding box (AABB) for the 3D mesh based on - * its vertices. - *

- * The bounding box is defined by the minimum and maximum extents of the - * vertices along the X, Y, and Z axes. If there are no vertices in the mesh, - * an empty `Bounds3` is returned. - *

- * - * @return A {@link Bounds3} object representing the calculated bounding box - * of the mesh. The bounding box extends from the minimum vertex - * coordinate to the maximum vertex coordinate. - */ - public Bounds3 calculateBounds() { - if (vertices.isEmpty()) - return new Bounds3(); - - Vector3f min = new Vector3f(getVertexAt(0)); - Vector3f max = new Vector3f(getVertexAt(0)); - Bounds3 bounds = new Bounds3(); - for (Vector3f v : vertices) { - float minX = v.getX() < min.getX() ? v.getX() : min.getX(); - float minY = v.getY() < min.getY() ? v.getY() : min.getY(); - float minZ = v.getZ() < min.getZ() ? v.getZ() : min.getZ(); - float maxX = v.getX() > max.getX() ? v.getX() : max.getX(); - float maxY = v.getY() > max.getY() ? v.getY() : max.getY(); - float maxZ = v.getZ() > max.getZ() ? v.getZ() : max.getZ(); - min.set(minX, minY, minZ); - max.set(maxX, maxY, maxZ); - } - bounds.setMinMax(min, max); - return bounds; - } - - public Vector3f calculateFaceNormal(Face3D face) { - Vector3f faceNormal = new Vector3f(); - for (int i = 0; i < face.indices.length; i++) { - Vector3f currentVertex = vertices.get(face.indices[i]); - Vector3f nextVertex = vertices - .get(face.indices[(i + 1) % face.indices.length]); - float x = (currentVertex.getY() - nextVertex.getY()) - * (currentVertex.getZ() + nextVertex.getZ()); - float y = (currentVertex.getZ() - nextVertex.getZ()) - * (currentVertex.getX() + nextVertex.getX()); - float z = (currentVertex.getX() - nextVertex.getX()) - * (currentVertex.getY() + nextVertex.getY()); - faceNormal.addLocal(x, y, z); - } - return faceNormal.normalize(); - } - - public void removeDoubles(int decimalPlaces) { - for (Vector3f v : vertices) - v.roundLocalDecimalPlaces(decimalPlaces); - removeDoubles(); - } - - /** - * Removes duplicated vertices. - * - * @deprecated Use {@link RemoveDoubleVerticesModifier} instead. - */ - public void removeDoubles() { - new RemoveDoubleVerticesModifier().modify(this); - } - - public Mesh3D copy() { - Mesh3D copy = new Mesh3D(); - List vertices = copy.vertices; - List faces = copy.faces; - - for (Vector3f v : this.vertices) - vertices.add(new Vector3f(v)); - - for (Face3D f : this.faces) - faces.add(new Face3D(f)); - - return copy; - } - - public Vector3f calculateFaceCenter(Face3D face) { - Vector3f center = new Vector3f(); - for (int i = 0; i < face.indices.length; i++) { - center.addLocal(vertices.get(face.indices[i])); - } - return center.divideLocal(face.indices.length); - } - - public Mesh3D append(Mesh3D... meshes) { - Mesh3D result = new Mesh3D(); - - result = appendUtil(meshes); - result = appendUtil(this, result); - - vertices.clear(); - vertices.addAll(result.vertices); - - faces.clear(); - faces.addAll(result.faces); - - return this; - } - - private Mesh3D appendUtil(Mesh3D... meshes) { - // FIXME copy vertices and faces - int n = 0; - Mesh3D mesh = new Mesh3D(); - List vertices = mesh.vertices; - List faces = mesh.faces; - - for (int i = 0; i < meshes.length; i++) { - Mesh3D m = meshes[i]; - vertices.addAll(m.vertices); - faces.addAll(meshes[i].faces); - for (Face3D f : meshes[i].faces) { - for (int j = 0; j < f.indices.length; j++) { - f.indices[j] += n; - } - } - n += m.getVertexCount(); - } - - return mesh; - } - - public void clearVertices() { - vertices.clear(); - } - - public void addVertex(float x, float y, float z) { - vertices.add(new Vector3f(x, y, z)); - } - - public void addFace(int... indices) { - faces.add(new Face3D(indices)); - } - - public void addVertices(Collection vertices) { - this.vertices.addAll(vertices); - } - - public void addFaces(Collection faces) { - this.faces.addAll(faces); - } - - public void removeFace(Face3D face) { - faces.remove(face); - } - - public void removeFaces(Collection faces) { - this.faces.removeAll(faces); - } - - public void add(Vector3f... vertices) { - this.vertices.addAll(Arrays.asList(vertices)); - } - - public void add(Face3D... faces) { - this.faces.addAll(Arrays.asList(faces)); - } - - public int getVertexCount() { - return vertices.size(); - } - - public int getFaceCount() { - return faces.size(); - } - - public List getFaces() { - return new ArrayList(faces); - } - - public List getFaces(int from, int to) { - return new ArrayList<>(faces.subList(from, to)); - } - - public List getVertices(int from, int to) { - return new ArrayList<>(vertices.subList(from, to)); - } - - public List getVertices() { - return new ArrayList<>(vertices); - } - - public Vector3f getVertexAt(int index) { - return vertices.get(index); - } - - public Face3D getFaceAt(int index) { - return faces.get(index); - } - + public ArrayList vertices; + public ArrayList faces; + + public Mesh3D() { + vertices = new ArrayList(); + faces = new ArrayList(); + } + + /** + * Applies the provided {@link IMeshModifier} to this mesh. This is congruent to {@link + * IMeshModifier#modify(Mesh3D)}. + * + * @param modifier The modifier to apply to this mesh. + * @return this + */ + public Mesh3D apply(IMeshModifier modifier) { + return modifier.modify(this); + } + + /** + * Rotates the mesh around the Y-axis. + * + * @deprecated Use {@link RotateYModifier} instead. + */ + public Mesh3D rotateY(float angle) { + return new RotateYModifier(angle).modify(this); + } + + /** + * Rotates the mesh around the Z-axis. + * + * @deprecated Use {@link RotateZModifier} instead. + */ + public Mesh3D rotateZ(float angle) { + return new RotateZModifier(angle).modify(this); + } + + /** + * Translates the mesh along the X-axis. + * + * @deprecated Use {@link TranslateModifier} instead. + */ + @Deprecated + public Mesh3D translateX(float tx) { + return new TranslateModifier(tx, 0, 0).modify(this); + } + + /** + * Translates the mesh along the Y-axis. + * + * @deprecated Use {@link TranslateModifier} instead. + */ + public Mesh3D translateY(float ty) { + return new TranslateModifier(0, ty, 0).modify(this); + } + + /** + * Translates the mesh along the Z-axis. + * + * @deprecated Use {@link TranslateModifier} instead. + */ + public Mesh3D translateZ(float tz) { + return new TranslateModifier(0, 0, tz).modify(this); + } + + /** + * Calculates the axis-aligned bounding box (AABB) for the 3D mesh based on its vertices. + * + *

The bounding box is defined by the minimum and maximum extents of the vertices along the X, + * Y, and Z axes. If there are no vertices in the mesh, an empty `Bounds3` is returned. + * + * @return A {@link Bounds3} object representing the calculated bounding box of the mesh. The + * bounding box extends from the minimum vertex coordinate to the maximum vertex coordinate. + */ + public Bounds3 calculateBounds() { + if (vertices.isEmpty()) return new Bounds3(); + + Vector3f min = new Vector3f(getVertexAt(0)); + Vector3f max = new Vector3f(getVertexAt(0)); + Bounds3 bounds = new Bounds3(); + for (Vector3f v : vertices) { + float minX = v.getX() < min.getX() ? v.getX() : min.getX(); + float minY = v.getY() < min.getY() ? v.getY() : min.getY(); + float minZ = v.getZ() < min.getZ() ? v.getZ() : min.getZ(); + float maxX = v.getX() > max.getX() ? v.getX() : max.getX(); + float maxY = v.getY() > max.getY() ? v.getY() : max.getY(); + float maxZ = v.getZ() > max.getZ() ? v.getZ() : max.getZ(); + min.set(minX, minY, minZ); + max.set(maxX, maxY, maxZ); + } + bounds.setMinMax(min, max); + return bounds; + } + + public Vector3f calculateFaceNormal(Face3D face) { + Vector3f faceNormal = new Vector3f(); + for (int i = 0; i < face.indices.length; i++) { + Vector3f currentVertex = vertices.get(face.indices[i]); + Vector3f nextVertex = vertices.get(face.indices[(i + 1) % face.indices.length]); + float x = + (currentVertex.getY() - nextVertex.getY()) * (currentVertex.getZ() + nextVertex.getZ()); + float y = + (currentVertex.getZ() - nextVertex.getZ()) * (currentVertex.getX() + nextVertex.getX()); + float z = + (currentVertex.getX() - nextVertex.getX()) * (currentVertex.getY() + nextVertex.getY()); + faceNormal.addLocal(x, y, z); + } + return faceNormal.normalize(); + } + + public void removeDoubles(int decimalPlaces) { + for (Vector3f v : vertices) v.roundLocalDecimalPlaces(decimalPlaces); + removeDoubles(); + } + + /** + * Removes duplicated vertices. + * + * @deprecated Use {@link RemoveDoubleVerticesModifier} instead. + */ + public void removeDoubles() { + new RemoveDoubleVerticesModifier().modify(this); + } + + public Mesh3D copy() { + Mesh3D copy = new Mesh3D(); + List vertices = copy.vertices; + List faces = copy.faces; + + for (Vector3f v : this.vertices) vertices.add(new Vector3f(v)); + + for (Face3D f : this.faces) faces.add(new Face3D(f)); + + return copy; + } + + public Vector3f calculateFaceCenter(Face3D face) { + Vector3f center = new Vector3f(); + for (int i = 0; i < face.indices.length; i++) { + center.addLocal(vertices.get(face.indices[i])); + } + return center.divideLocal(face.indices.length); + } + + public Mesh3D append(Mesh3D... meshes) { + Mesh3D result = new Mesh3D(); + + result = appendUtil(meshes); + result = appendUtil(this, result); + + vertices.clear(); + vertices.addAll(result.vertices); + + faces.clear(); + faces.addAll(result.faces); + + return this; + } + + private Mesh3D appendUtil(Mesh3D... meshes) { + // FIXME copy vertices and faces + int n = 0; + Mesh3D mesh = new Mesh3D(); + List vertices = mesh.vertices; + List faces = mesh.faces; + + for (int i = 0; i < meshes.length; i++) { + Mesh3D m = meshes[i]; + vertices.addAll(m.vertices); + faces.addAll(meshes[i].faces); + for (Face3D f : meshes[i].faces) { + for (int j = 0; j < f.indices.length; j++) { + f.indices[j] += n; + } + } + n += m.getVertexCount(); + } + + return mesh; + } + + public void clearVertices() { + vertices.clear(); + } + + public void addVertex(float x, float y, float z) { + vertices.add(new Vector3f(x, y, z)); + } + + public void addFace(int... indices) { + faces.add(new Face3D(indices)); + } + + public void addVertices(Collection vertices) { + this.vertices.addAll(vertices); + } + + public void addFaces(Collection faces) { + this.faces.addAll(faces); + } + + public void removeFace(Face3D face) { + faces.remove(face); + } + + public void removeFaces(Collection faces) { + this.faces.removeAll(faces); + } + + public void add(Vector3f... vertices) { + this.vertices.addAll(Arrays.asList(vertices)); + } + + public void add(Face3D... faces) { + this.faces.addAll(Arrays.asList(faces)); + } + + public int getVertexCount() { + return vertices.size(); + } + + public int getFaceCount() { + return faces.size(); + } + + public List getFaces() { + return new ArrayList(faces); + } + + public List getFaces(int from, int to) { + return new ArrayList<>(faces.subList(from, to)); + } + + public List getVertices(int from, int to) { + return new ArrayList<>(vertices.subList(from, to)); + } + + public List getVertices() { + return new ArrayList<>(vertices); + } + + public Vector3f getVertexAt(int index) { + return vertices.get(index); + } + + public Face3D getFaceAt(int index) { + return faces.get(index); + } } diff --git a/src/main/java/workspace/GraphicsPImpl.java b/src/main/java/workspace/GraphicsPImpl.java index 4d871a78..161358d5 100644 --- a/src/main/java/workspace/GraphicsPImpl.java +++ b/src/main/java/workspace/GraphicsPImpl.java @@ -18,6 +18,7 @@ public class GraphicsPImpl implements Graphics { public GraphicsPImpl(PApplet p) { this.g = p.g; renderer = new Mesh3DRenderer(p); + color = Color.BLACK; } @Override @@ -56,11 +57,21 @@ public void popMatrix() { g.popMatrix(); } + @Override + public void scale(float sx, float sy, float sz) { + g.scale(sx, sy, sz); + } + @Override public void translate(float x, float y) { g.translate(x, y); } + @Override + public void translate(float x, float y, float z) { + g.translate(x, y, z); + } + @Override public void strokeWeight(float weight) { g.strokeWeight(weight); @@ -100,6 +111,16 @@ public void drawLine(float x1, float y1, float x2, float y2) { g.popStyle(); } + @Override + public void drawLine(float x1, float y1, float z1, float x2, float y2, + float z2) { + g.pushStyle(); + g.noFill(); + stroke(); + g.line(x1, y1, z1, x2, y2, z2); + g.popStyle(); + } + @Override public void fillRect(float x, float y, float width, float height) { g.pushStyle(); diff --git a/src/main/java/workspace/Workspace.java b/src/main/java/workspace/Workspace.java index 075f248d..266a8333 100644 --- a/src/main/java/workspace/Workspace.java +++ b/src/main/java/workspace/Workspace.java @@ -12,6 +12,8 @@ import workspace.render.ObjectSelectionRender; import workspace.render.Shading; import workspace.ui.Color; +import workspace.ui.ui3d.Axis3D; +import workspace.ui.ui3d.Grid3D; public class Workspace extends Editor implements ModelListener { @@ -30,10 +32,16 @@ public class Workspace extends Editor implements ModelListener { private SceneObject selectedObject; private boolean select; + + private Grid3D grid; + + private Axis3D axis; private GraphicsPImpl gImpl; public Workspace(PApplet p) { + grid = new Grid3D(32, 32, 1); + axis = new Axis3D(1); this.p = p; registerMethods(); firstPersonView = new FirstPersonView(p); @@ -88,53 +96,17 @@ public void applyCamera() { firstPersonView.apply(); } - public void drawGrid(int rows, int cols, float size) { - if (!isGridVisible()) - return; - - p.stroke(UiValues.getColor(UiConstants.KEY_GRID_COLOR).getRGBA()); - p.noFill(); - - p.pushMatrix(); - p.rotateX(PApplet.radians(-90)); - p.translate(-cols / 2, -rows / 2); - - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - p.rect(j * size, i * size, size, size); - } - } - - p.popMatrix(); + public void drawGrid() { + grid.setVisible(model.isGridVisible()); + grid.render(gImpl); } protected void drawAxis(float size) { - p.pushStyle(); - p.pushMatrix(); - - p.noFill(); - p.strokeWeight(1.5f / getScale()); - - if (isxAxisVisible()) { - p.stroke(UiValues.getColor(UiConstants.KEY_AXIS_X_COLOR).getRGBA()); - p.line(size, 0, 0, 0, 0, 0); - p.line(-size, 0, 0, 0, 0, 0); - } - - if (isyAxisVisible()) { - p.stroke(UiValues.getColor(UiConstants.KEY_AXIS_Y_COLOR).getRGBA()); - p.line(0, size, 0, 0, 0, 0); - p.line(0, -size, 0, 0, 0, 0); - } - - if (iszAxisVisible()) { - p.stroke(UiValues.getColor(UiConstants.KEY_AXIS_Z_COLOR).getRGBA()); - p.line(0, 0, size, 0, 0, 0); - p.line(0, 0, -size, 0, 0, 0); - } - - p.popStyle(); - p.popMatrix(); + axis.setSize(size); + axis.setXAxisVisible(isxAxisVisible()); + axis.setYAxisVisible(isyAxisVisible()); + axis.setZAxisVisible(iszAxisVisible()); + axis.render(gImpl, getScale()); } public void pre() { @@ -145,7 +117,7 @@ public void pre() { p.lights(); applyTransformations(); p.strokeWeight(1 / getScale()); - drawGrid(32, 32, 1); + drawGrid(); drawAxis(2000); } diff --git a/src/main/java/workspace/ui/Graphics.java b/src/main/java/workspace/ui/Graphics.java index 1d7d95dc..d3d986c2 100644 --- a/src/main/java/workspace/ui/Graphics.java +++ b/src/main/java/workspace/ui/Graphics.java @@ -1,61 +1,5 @@ package workspace.ui; -import mesh.Mesh3D; - -public interface Graphics { - - int getWidth(); - - int getHeight(); - - void pushMatrix(); - - void popMatrix(); - - void translate(float x, float y); - - void strokeWeight(float weight); - - void setColor(Color color); - - void setColor(math.Color color); - - void setColor(int red, int green, int blue); - - void drawRect(float x, float y, float width, float height); - - void fillRect(float x, float y, float width, float height); - - void drawOval(float x, float y, float width, float height); - - void fillOval(float x, float y, float width, float height); - - void drawLine(float x1, float y1, float x2, float y2); - - void fillFaces(Mesh3D mesh); - - void textSize(float size); - - float getTextSize(); - - float textWidth(String text); - - float textAscent(); - - float textDescent(); - - void text(String text, float x, float y); - - void enableDepthTest(); - - void disableDepthTest(); - - void rotate(float angle); - - void rotateX(float angle); - - void rotateY(float angle); - - void rotateZ(float angle); +public interface Graphics extends Graphics3D { } diff --git a/src/main/java/workspace/ui/Graphics2D.java b/src/main/java/workspace/ui/Graphics2D.java new file mode 100644 index 00000000..f957c87a --- /dev/null +++ b/src/main/java/workspace/ui/Graphics2D.java @@ -0,0 +1,247 @@ +package workspace.ui; + +/** + * Defines the 2D rendering context and operations for a 2D rendering system. + *

+ * This interface provides methods for basic 2D graphics rendering operations + * such as drawing geometric shapes (rectangles, ovals, and lines), text + * rendering, color setting, transformations (translate, scale, rotate), and + * text metrics calculations. It serves as the foundation for implementing 2D + * rendering capabilities in a graphics pipeline. + *

+ * + *

+ * Implementations of this interface are responsible for providing concrete + * rendering logic with support for transformations and state management (e.g., + * push and pop matrix operations). + *

+ */ +public interface Graphics2D { + + /** + * Retrieves the current width of the rendering context's viewport. + * + * @return The width in pixels. + */ + int getWidth(); + + /** + * Retrieves the current height of the rendering context's viewport. + * + * @return The height in pixels. + */ + int getHeight(); + + /** + * Sets the current drawing color using a {@link Color}. + * + * @param color The color to set for rendering operations. + */ + void setColor(Color color); + + /** + * Sets the current drawing color using a math-defined {@link math.Color}. + * + * @param color The math-defined color to use for rendering operations. + */ + void setColor(math.Color color); + + /** + * Sets the current drawing color using RGB integer values. + * + * @param red The red channel value (0-255). + * @param green The green channel value (0-255). + * @param blue The blue channel value (0-255). + */ + void setColor(int red, int green, int blue); + + /** + * Sets the thickness of strokes (lines) used in subsequent drawing commands. + * + * @param weight The stroke weight to apply (e.g., 1.0 for standard line + * thickness). + */ + void strokeWeight(float weight); + + /** + * Saves the current transformation matrix onto a stack for future + * restoration. + * + * This allows temporary transformations without permanently altering the + * rendering state. + */ + void pushMatrix(); + + /** + * Restores the last saved transformation matrix from the stack. + * + * This undoes any temporary transformations applied since the last + * pushMatrix(). + */ + void popMatrix(); + + /** + * Translates the rendering context by a specified distance in 2D space. + * + * @param x The amount to translate along the x-axis. + * @param y The amount to translate along the y-axis. + */ + void translate(float x, float y); + + /** + * Scales the rendering context by specified scaling factors along the x and y + * axes. + * + * @param sx The scaling factor along the x-axis. + * @param sy The scaling factor along the y-axis. + */ + void scale(float sx, float sy); + + /** + * Rotates the rendering context by the given angle in radians. + * + * @param angle The angle to rotate by, in radians. + */ + void rotate(float angle); + + /** + * Draws an unfilled rectangle at the specified coordinates with the given + * dimensions. + * + * @param x The x-coordinate of the top-left corner of the rectangle. + * @param y The y-coordinate of the top-left corner of the rectangle. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + */ + void drawRect(float x, float y, float width, float height); + + /** + * Draws a filled rectangle at the specified coordinates with the given + * dimensions. + * + * @param x The x-coordinate of the top-left corner of the rectangle. + * @param y The y-coordinate of the top-left corner of the rectangle. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + */ + void fillRect(float x, float y, float width, float height); + + /** + * Draws the outline of a rounded rectangle with specified position, + * dimensions, and corner radius. + * + *

+ * The rectangle is defined by its top-left corner (x, y), its width, and its + * height. The "radii" parameter specifies the corner radius, which determines + * how rounded the corners appear. If the corner radius is 0, this method + * behaves like {@link #drawRect(float, float, float, float)}. + * + * @param x The x-coordinate of the top-left corner of the rectangle. + * @param y The y-coordinate of the top-left corner of the rectangle. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @param radii The corner radius for rounding the corners of the rectangle. + * A larger value results in more rounded corners. + */ + void drawRoundRect(float x, float y, float width, float height, float radii); + + /** + * Draws a filled rounded rectangle with specified position, dimensions, and + * corner radius. + * + *

+ * The rectangle is defined by its top-left corner (x, y), its width, and its + * height. The "radii" parameter specifies the corner radius, which determines + * how rounded the corners appear. If the corner radius is 0, this method + * behaves like {@link #fillRect(float, float, float, float)}. + * + * @param x The x-coordinate of the top-left corner of the rectangle. + * @param y The y-coordinate of the top-left corner of the rectangle. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @param radii The corner radius for rounding the corners of the rectangle. + * A larger value results in more rounded corners. + */ + void fillRoundRect(float x, float y, float width, float height, float radii); + + /** + * Draws an unfilled oval at the specified coordinates with the given + * dimensions. + * + * @param x The x-coordinate of the top-left corner of the bounding box + * of the oval. + * @param y The y-coordinate of the top-left corner of the bounding box + * of the oval. + * @param width The width of the bounding box. + * @param height The height of the bounding box. + */ + void drawOval(float x, float y, float width, float height); + + /** + * Draws a filled oval at the specified coordinates with the given dimensions. + * + * @param x The x-coordinate of the top-left corner of the bounding box + * of the oval. + * @param y The y-coordinate of the top-left corner of the bounding box + * of the oval. + * @param width The width of the bounding box. + * @param height The height of the bounding box. + */ + void fillOval(float x, float y, float width, float height); + + /** + * Draws a line from (x1, y1) to (x2, y2). + * + * @param x1 Starting x-coordinate. + * @param y1 Starting y-coordinate. + * @param x2 Ending x-coordinate. + * @param y2 Ending y-coordinate. + */ + void drawLine(float x1, float y1, float x2, float y2); + + /** + * Sets the size of text to render for subsequent text rendering operations. + * + * @param size The desired text size. + */ + void textSize(float size); + + /** + * Retrieves the current text size used by text rendering operations. + * + * @return The current text size. + */ + float getTextSize(); + + /** + * Computes the width of the given text string at the current text size. + * + * @param text The text to compute the width for. + * @return The width of the rendered text. + */ + float textWidth(String text); + + /** + * Retrieves the ascent of text (the portion of text above the baseline). + * + * @return The ascent value of the text. + */ + float textAscent(); + + /** + * Retrieves the descent of text (the portion of text below the baseline). + * + * @return The descent value of the text. + */ + float textDescent(); + + /** + * Renders text at the given screen coordinates. + * + * @param text The text to render. + * @param x The x-coordinate to start rendering the text. + * @param y The y-coordinate to start rendering the text. + */ + void text(String text, float x, float y); + +} \ No newline at end of file diff --git a/src/main/java/workspace/ui/Graphics3D.java b/src/main/java/workspace/ui/Graphics3D.java new file mode 100644 index 00000000..138e6a18 --- /dev/null +++ b/src/main/java/workspace/ui/Graphics3D.java @@ -0,0 +1,60 @@ +package workspace.ui; + +import java.util.List; + +import engine.render.Material; +import engine.scene.light.Light; +import math.Matrix4f; +import mesh.Mesh3D; + +public interface Graphics3D extends Graphics2D { + + void translate(float x, float y, float z); + + void scale(float sx, float sy, float sz); + + void rotateX(float angle); + + void rotateY(float angle); + + void rotateZ(float angle); + + void render(Light light); + + void fillFaces(Mesh3D mesh); + + void renderInstances(Mesh3D mesh, List instanceTransforms); + + void setShader(String vertexShaderName, String fragmentShaderName); + + void enableDepthTest(); + + void disableDepthTest(); + + void setMaterial(Material material); + + void drawLine(float x1, float y1, float z1, float x2, float y2, float z2); + + void camera(); + + void lightsOff(); + + /** + * Sets the current view matrix for rendering. The view matrix transforms + * coordinates from world space to camera (view) space. + * + * @param viewMatrix The 4x4 view matrix to be applied for rendering. + */ + void setViewMatrix(Matrix4f viewMatrix); + + /** + * Sets the current projection matrix for rendering. The projection matrix + * defines how 3D coordinates are projected into the 2D viewport for rendering + * purposes. + * + * @param projectionMatrix The 4x4 projection matrix to be applied for + * rendering. + */ + void setProjectionMatrix(Matrix4f projectionMatrix); + +} diff --git a/src/main/java/workspace/ui/UiComponent.java b/src/main/java/workspace/ui/UiComponent.java index 39c72eca..62971e00 100644 --- a/src/main/java/workspace/ui/UiComponent.java +++ b/src/main/java/workspace/ui/UiComponent.java @@ -6,6 +6,7 @@ import workspace.ui.border.Border; import workspace.ui.border.Insets; import workspace.ui.elements.UiElement; +import workspace.ui.layout.Anchor; import workspace.ui.layout.Layout; import workspace.ui.renderer.Renderer; @@ -27,12 +28,14 @@ public class UiComponent implements UiElement { protected Border border; - protected List components; - protected Layout layout; - + + protected Anchor anchor; + private Renderer renderer; + protected List components; + public UiComponent() { this(0, 0, 0, 0, true, Color.BLACK, Color.GRAY); } @@ -48,29 +51,6 @@ public UiComponent(int x, int y, int width, int height, boolean visible, this.background = background; this.components = new ArrayList(); } - - @Override - public void setRenderer(Renderer renderer) { - this.renderer = renderer; - } - - @Override - public Renderer getRenderer() { - return renderer; - } - - @Override - public Insets getInsets() { - if (border == null) - return new Insets(); - return border.getInsets(); - } - - - @Override - public void setBorder(Border border) { - this.border = border; - } @Override public void render(Graphics g) { @@ -101,12 +81,40 @@ protected void renderBorder(Graphics g) { border.renderBorder(g, 0, 0, getWidth(), getHeight()); } + protected void renderSelf(Graphics g) { + if (renderer == null) + return; + renderer.render(g, this); + } + protected void renderChildren(Graphics g) { for (UiComponent component : components) { component.render(g); } } + @Override + public void setRenderer(Renderer renderer) { + this.renderer = renderer; + } + + @Override + public Renderer getRenderer() { + return renderer; + } + + @Override + public Insets getInsets() { + if (border == null) + return new Insets(); + return border.getInsets(); + } + + @Override + public void setBorder(Border border) { + this.border = border; + } + @Override public void setLayout(Layout layout) { this.layout = layout; @@ -128,12 +136,6 @@ public boolean isVisible() { return visible; } - public void renderSelf(Graphics g) { - if (renderer == null) - return; - renderer.render(g, this); - } - protected void layout() { if (layout == null) return; @@ -187,34 +189,23 @@ public void add(UiComponent component) { components.add(component); layout(); } -// -// public void remove(UiComponent component) { -// if (component == null) -// return; -// components.remove(component); -// layout(); -// } -// -// public int getComponentCount() { -// return components.size(); -// } -// -// public UiComponent getComponentAt(int index) { -// return components.get(index); -// } + @Override public int getX() { return x; } + @Override public void setX(int x) { this.x = x; } + @Override public int getY() { return y; } + @Override public void setY(int y) { this.y = y; } @@ -224,6 +215,7 @@ public int getWidth() { return width; } + @Override public void setWidth(int width) { this.width = width; } @@ -233,6 +225,7 @@ public int getHeight() { return height; } + @Override public void setHeight(int height) { this.height = height; } @@ -253,5 +246,14 @@ public void setBackground(Color background) { this.background = background; } + @Override + public Anchor getAnchor() { + return anchor; + } + + @Override + public void setAnchor(Anchor anchor) { + this.anchor = anchor; + } } diff --git a/src/main/java/workspace/ui/elements/UiElement.java b/src/main/java/workspace/ui/elements/UiElement.java index 4f44f24e..ba2abd57 100644 --- a/src/main/java/workspace/ui/elements/UiElement.java +++ b/src/main/java/workspace/ui/elements/UiElement.java @@ -4,6 +4,7 @@ import workspace.ui.Graphics; import workspace.ui.border.Border; import workspace.ui.border.Insets; +import workspace.ui.layout.Anchor; import workspace.ui.layout.Layout; import workspace.ui.renderer.Renderer; @@ -18,6 +19,74 @@ */ public interface UiElement { + /** + * Gets the X-coordinate of this element. + * + * @return The X-coordinate in pixels. + */ + int getX(); + + /** + * Sets the X-coordinate of this element. + * + * @param x The new X-coordinate in pixels. + */ + void setX(int x); + + /** + * Gets the Y-coordinate of this element. + * + * @return The Y-coordinate in pixels. + */ + int getY(); + + /** + * Sets the Y-coordinate of this element. + * + * @param y The new Y-coordinate in pixels. + */ + void setY(int y); + + /** + * Retrieves the width of this {@code UiElement}. + *

+ * The width represents the horizontal size of the UI element. It is used + * during rendering and layout calculations to determine how much horizontal + * space the element occupies. + *

+ * + * @return The width of this UI element in pixels. + */ + int getWidth(); + + /** + * Sets the width of this {@link UiElement} to the specified new width. + * + * @see #getWidth() + * @param width The new width in pixels. + */ + void setWidth(int width); + + /** + * Retrieves the height of this {@code UiElement}. + *

+ * The height represents the vertical size of the UI element. It is used + * during rendering and layout calculations to determine how much vertical + * space the element occupies. + *

+ * + * @return The height of this UI element in pixels. + */ + int getHeight(); + + /** + * Sets the height of this {@link UiElement} to the specified new height. + * + * @see #getHeight() + * @param height The new height in pixels. + */ + void setHeight(int height); + /** * Renders this {@code UiElement} using its specific rendering logic. *

@@ -60,6 +129,30 @@ public interface UiElement { */ void setLayout(Layout layout); + /** + * Retrieves the current anchor type of this UI element. + *

+ * Anchors define how this element is aligned relative to its parent or + * specific layout logic. Examples include {@code TOP_LEFT}, {@code CENTER}, + * and {@code BOTTOM_RIGHT}. + *

+ * + * @return The current {@link Anchor} setting for this element. + */ + Anchor getAnchor(); + + /** + * Sets the anchor type for this UI element. + *

+ * The anchor determines how this element aligns in its container based on the + * defined anchor type, such as centering, top-left, or bottom-right + * alignment. + *

+ * + * @param anchor The {@link Anchor} to set for this element. + */ + void setAnchor(Anchor anchor); + /** * Determines if the specified coordinates are within the bounds of this * {@code UiElement}. @@ -130,30 +223,6 @@ public interface UiElement { */ Insets getInsets(); - /** - * Retrieves the width of this {@code UiElement}. - *

- * The width represents the horizontal size of the UI element. It is used - * during rendering and layout calculations to determine how much horizontal - * space the element occupies. - *

- * - * @return The width of this UI element in pixels. - */ - int getWidth(); - - /** - * Retrieves the height of this {@code UiElement}. - *

- * The height represents the vertical size of the UI element. It is used - * during rendering and layout calculations to determine how much vertical - * space the element occupies. - *

- * - * @return The height of this UI element in pixels. - */ - int getHeight(); - /** * Retrieves the background color of this {@code UiElement}. *

diff --git a/src/main/java/workspace/ui/ui3d/Axis3D.java b/src/main/java/workspace/ui/ui3d/Axis3D.java new file mode 100644 index 00000000..375ccc27 --- /dev/null +++ b/src/main/java/workspace/ui/ui3d/Axis3D.java @@ -0,0 +1,196 @@ +package workspace.ui.ui3d; + +import workspace.laf.UiConstants; +import workspace.laf.UiValues; +import workspace.ui.Graphics; + +/** + * Represents a 3D axis visualization that can be rendered with customizable + * visibility for each axis. Each axis is drawn with its corresponding color, as + * defined by the UI constants, and can be toggled on or off individually. + *

+ * The axis lines are rendered in the X, Y, and Z directions, with their lengths + * determined by the specified size parameter. + *

+ */ +public class Axis3D { + + /** + * Visibility flag for the X-axis. + */ + private boolean xAxisVisible; + + /** + * Visibility flag for the Y-axis. + */ + private boolean yAxisVisible; + + /** + * Visibility flag for the Z-axis. + */ + private boolean zAxisVisible; + + /** + * The length of each axis, extending equally in both positive and negative + * directions. + */ + private float size; + + /** + * Constructs an {@code Axis3D} with the specified size. All axes are visible + * by default. + * + * @param size the length of each axis; must be positive + * @throws IllegalArgumentException if the size is not positive + */ + public Axis3D(float size) { + if (size <= 0) { + throw new IllegalArgumentException("Size must be positive."); + } + this.size = size; + this.xAxisVisible = true; + this.yAxisVisible = true; + this.zAxisVisible = true; + } + + /** + * Renders the 3D axis using the provided {@link Graphics} context. The axis + * lines are drawn based on their visibility flags and scaled according to the + * specified stroke weight factor. + * + * @param g the {@link Graphics} context to draw on + * @param scale the scale factor for adjusting the stroke weight; must be + * positive + */ + public void render(Graphics g, float scale) { + if (scale <= 0) { + throw new IllegalArgumentException("Scale must be positive."); + } + g.pushMatrix(); + g.strokeWeight(1.5f / scale); + renderXAxis(g); + renderYAxis(g); + renderZAxis(g); + g.popMatrix(); + } + + /** + * Renders the X-axis if it is visible. The X-axis is drawn in the positive + * and negative X directions with the color defined by + * {@link UiConstants#KEY_AXIS_X_COLOR}. + * + * @param g the {@link Graphics} context to draw on + */ + private void renderXAxis(Graphics g) { + if (!xAxisVisible) { + return; + } + g.setColor(UiValues.getColor(UiConstants.KEY_AXIS_X_COLOR)); + g.drawLine(size, 0, 0, 0, 0, 0); + g.drawLine(-size, 0, 0, 0, 0, 0); + } + + /** + * Renders the Y-axis if it is visible. The Y-axis is drawn in the positive + * and negative Y directions with the color defined by + * {@link UiConstants#KEY_AXIS_Y_COLOR}. + * + * @param g the {@link Graphics} context to draw on + */ + private void renderYAxis(Graphics g) { + if (!yAxisVisible) { + return; + } + g.setColor(UiValues.getColor(UiConstants.KEY_AXIS_Y_COLOR)); + g.drawLine(0, size, 0, 0, 0, 0); + g.drawLine(0, -size, 0, 0, 0, 0); + } + + /** + * Renders the Z-axis if it is visible. The Z-axis is drawn in the positive + * and negative Z directions with the color defined by + * {@link UiConstants#KEY_AXIS_Z_COLOR}. + * + * @param g the {@link Graphics} context to draw on + */ + private void renderZAxis(Graphics g) { + if (!zAxisVisible) { + return; + } + g.setColor(UiValues.getColor(UiConstants.KEY_AXIS_Z_COLOR)); + g.drawLine(0, 0, size, 0, 0, 0); + g.drawLine(0, 0, -size, 0, 0, 0); + } + + /** + * Returns whether the X-axis is visible. + * + * @return {@code true} if the X-axis is visible; {@code false} otherwise + */ + public boolean isXAxisVisible() { + return xAxisVisible; + } + + /** + * Sets the visibility of the X-axis. + * + * @param xAxisVisible {@code true} to make the X-axis visible; {@code false} + * to hide it + */ + public void setXAxisVisible(boolean xAxisVisible) { + this.xAxisVisible = xAxisVisible; + } + + /** + * Returns whether the Y-axis is visible. + * + * @return {@code true} if the Y-axis is visible; {@code false} otherwise + */ + public boolean isYAxisVisible() { + return yAxisVisible; + } + + /** + * Sets the visibility of the Y-axis. + * + * @param yAxisVisible {@code true} to make the Y-axis visible; {@code false} + * to hide it + */ + public void setYAxisVisible(boolean yAxisVisible) { + this.yAxisVisible = yAxisVisible; + } + + /** + * Returns whether the Z-axis is visible. + * + * @return {@code true} if the Z-axis is visible; {@code false} otherwise + */ + public boolean isZAxisVisible() { + return zAxisVisible; + } + + /** + * Sets the visibility of the Z-axis. + * + * @param zAxisVisible {@code true} to make the Z-axis visible; {@code false} + * to hide it + */ + public void setZAxisVisible(boolean zAxisVisible) { + this.zAxisVisible = zAxisVisible; + } + + /** + * Sets the length of each axis, extending equally in both positive and + * negative directions. + * + * @param newSize the new size of the axes; must be positive + * @throws IllegalArgumentException if the new size is not positive + */ + public void setSize(float newSize) { + if (newSize <= 0) { + throw new IllegalArgumentException("Size must be positive."); + } + this.size = newSize; + } + +} \ No newline at end of file diff --git a/src/main/java/workspace/ui/ui3d/Grid3D.java b/src/main/java/workspace/ui/ui3d/Grid3D.java new file mode 100644 index 00000000..a21f8f52 --- /dev/null +++ b/src/main/java/workspace/ui/ui3d/Grid3D.java @@ -0,0 +1,107 @@ +package workspace.ui.ui3d; + +import math.Mathf; +import workspace.laf.UiConstants; +import workspace.laf.UiValues; +import workspace.ui.Graphics; + +/** + * Represents a 3D grid visualization that can be rendered in a 3D UI space. + *

+ * This class provides the logic for creating and rendering a 3D spatial grid + * with a given number of rows, columns, and cell size. It is designed to work + * with a custom 3D graphics rendering context, allowing visualization of UI + * layouts or spatial relationships. + *

+ */ +public class Grid3D { + + /** The number of rows in the 3D grid. */ + private int rows; + + /** The number of columns in the 3D grid. */ + private int cols; + + /** The size of each individual cell in the 3D grid. */ + private float size; + + /** + * Indicates whether the 3D grid should currently be visible when rendered. + */ + private boolean visible; + + /** + * Constructs a new {@code Grid3D} instance with the specified number of rows, + * columns, and cell size. + * + * @param rows The number of rows to create in the 3D grid. + * @param cols The number of columns to create in the 3D grid. + * @param size The size (width/height) of each individual cell in the 3D grid. + */ + public Grid3D(int rows, int cols, float size) { + this.rows = rows; + this.cols = cols; + this.size = size; + this.visible = true; + } + + /** + * Renders the 3D grid visualization using the provided graphics context. + *

+ * This method only performs rendering if the grid is currently visible. The + * rendering logic draws a series of rectangles in a 3D space to represent the + * cells of the grid. It applies necessary transformations for proper + * alignment and orientation in a 3D context. + *

+ * + * @param g The graphics context used for rendering the 3D grid. + */ + public void render(Graphics g) { + if (!isVisible()) { + return; + } + g.setColor(UiValues.getColor(UiConstants.KEY_GRID_COLOR)); + g.pushMatrix(); + g.rotateX(-Mathf.HALF_PI); + g.translate(-cols / 2.0f, -rows / 2.0f); + renderGridLines(g); + g.popMatrix(); + } + + /** + * Renders the grid lines. + * + * @param g The graphics context used for rendering. + */ + private void renderGridLines(Graphics g) { + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + g.drawRect(j * size, i * size, size, size); + } + } + } + + /** + * Checks whether the 3D grid visualization is currently visible. + * + * @return {@code true} if the 3D grid is visible; {@code false} otherwise. + */ + public boolean isVisible() { + return visible; + } + + /** + * Sets the visibility state of the 3D grid visualization. + *

+ * When set to {@code true}, the grid will be rendered during the next + * invocation of the {@code render} method. If set to {@code false}, rendering + * will be skipped. + *

+ * + * @param visible The new visibility state of the 3D grid. + */ + public void setVisible(boolean visible) { + this.visible = visible; + } + +} \ No newline at end of file