diff --git a/src/main/java/engine/components/ControlWASD.java b/src/main/java/engine/components/ControlWASD.java index 7b1c49c4..e8a2c67f 100644 --- a/src/main/java/engine/components/ControlWASD.java +++ b/src/main/java/engine/components/ControlWASD.java @@ -7,16 +7,16 @@ /** * 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. + * input (W/A/S/D) or custom key mappings. 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. + *

Example usage: Attach this component to a {@link SceneNode} to allow movement using 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 { @@ -26,6 +26,18 @@ public class ControlWASD extends AbstractComponent { /** The Input instance to monitor keyboard events (injected dependency). */ private Input input; + /** Key used for moving left. Default is 'A'. */ + private Key leftKey = Key.A; + + /** Key used for moving right. Default is 'D'. */ + private Key rightKey = Key.D; + + /** Key used for moving forward. Default is 'W'. */ + private Key forwardKey = Key.W; + + /** Key used for moving backward. Default is 'S'. */ + private Key backwardKey = Key.S; + /** * Constructs a new ControlWASD component, injecting the Input logic needed for detecting key * presses. @@ -63,7 +75,7 @@ public ControlWASD(Input input, float 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 + *

This method calculates the velocity vector by processing the input keys 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). * @@ -81,8 +93,9 @@ public void update(float 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. + * Processes keyboard input to determine velocity. Handles the configured 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. */ @@ -90,10 +103,10 @@ 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); + if (input.isKeyPressed(forwardKey)) velocity.addLocal(0, 0, -1); + if (input.isKeyPressed(leftKey)) velocity.addLocal(-1, 0, 0); + if (input.isKeyPressed(backwardKey)) velocity.addLocal(0, 0, 1); + if (input.isKeyPressed(rightKey)) velocity.addLocal(1, 0, 0); // Normalize diagonal movement to prevent unintended speed boosts if (velocity.length() > 0) { @@ -128,9 +141,49 @@ public void setSpeed(float speed) { this.speed = speed; } + /** + * Sets the key used to move left. + * + * @param leftKey The new key to use for left movement. + */ + public void setLeftKey(Key leftKey) { + this.leftKey = leftKey; + } + + /** + * Sets the key used to move right. + * + * @param rightKey The new key to use for right movement. + */ + public void setRightKey(Key rightKey) { + this.rightKey = rightKey; + } + + /** + * Sets the key used to move forward. + * + * @param forwardKey The new key to use for forward movement. + */ + public void setForwardKey(Key forwardKey) { + this.forwardKey = forwardKey; + } + + /** + * Sets the key used to move backward. + * + * @param backwardKey The new key to use for backward movement. + */ + public void setBackwardKey(Key backwardKey) { + this.backwardKey = backwardKey; + } + + /** Called when the component is attached to a {@link SceneNode}. Override for custom behavior. */ @Override public void onAttach() {} + /** + * Called when the component is detached from a {@link SceneNode}. Override for custom behavior. + */ @Override public void onDetach() {} } diff --git a/src/main/java/engine/components/RoundReticle.java b/src/main/java/engine/components/RoundReticle.java new file mode 100644 index 00000000..0972da06 --- /dev/null +++ b/src/main/java/engine/components/RoundReticle.java @@ -0,0 +1,160 @@ +package engine.components; + +import math.Color; +import workspace.ui.Graphics; + +/** + * A component that renders a round reticle in the center of the screen. The reticle consists of an + * outer circle and a smaller, filled inner circle, both customizable in terms of radius and color. + */ +public class RoundReticle extends AbstractComponent implements RenderableComponent { + + private float outerRadius; + + private float innerRadius; + + private Color color; + + /** + * Creates a default RoundReticle with preset values. Default configuration: - Outer radius: 30 - + * Inner radius: 6 - Color: White + */ + public RoundReticle() { + this(30, 6, Color.WHITE); + } + + /** + * Creates a RoundReticle with specified outer radius, inner radius, and color. + * + * @param outerRadius the radius of the outer circle. + * @param innerRadius the radius of the inner filled circle. + * @param color the color of the reticle. + * @throws IllegalArgumentException if any radius is non-positive or color is null. + */ + public RoundReticle(float outerRadius, float innerRadius, Color color) { + setOuterRadius(outerRadius); + setInnerRadius(innerRadius); + setColor(color); + } + + /** + * Renders the reticle on the screen, centered in the viewport. The reticle includes an outer + * circle and a filled inner circle. + * + * @param g the {@link Graphics} context used for rendering. + */ + @Override + public void render(Graphics g) { + int centerX = g.getWidth() / 2; + int centerY = g.getHeight() / 2; + + // Render reticle circles + g.setColor(color); + renderCenteredOval(g, centerX, centerY, outerRadius, false); + renderCenteredOval(g, centerX, centerY, innerRadius, true); + } + + /** + * Draws a centered oval at the specified position. + * + * @param g the {@link Graphics} context used for rendering. + * @param centerX the x-coordinate of the oval's center. + * @param centerY the y-coordinate of the oval's center. + * @param radius the radius of the oval. + * @param filled whether the oval should be filled. + */ + private void renderCenteredOval( + Graphics g, int centerX, int centerY, float radius, boolean filled) { + int diameter = (int) radius; + int topLeftX = centerX - diameter / 2; + int topLeftY = centerY - diameter / 2; + + if (filled) { + g.fillOval(topLeftX, topLeftY, diameter, diameter); + } else { + g.drawOval(topLeftX, topLeftY, diameter, diameter); + } + } + + /** + * Updates the component. Currently, this method does nothing. + * + * @param tpf time per frame, used for animations or updates. + */ + @Override + public void update(float tpf) {} + + /** Called when the component is attached to a {@link engine.SceneNode}. */ + @Override + public void onAttach() {} + + /** Called when the component is detached from a {@link engine.SceneNode}. */ + @Override + public void onDetach() {} + + /** + * Gets the outer radius of the reticle. + * + * @return the outer radius. + */ + public float getOuterRadius() { + return outerRadius; + } + + /** + * Sets the outer radius of the reticle. + * + * @param outerRadius the new outer radius. + * @throws IllegalArgumentException if the radius is non-positive. + */ + public void setOuterRadius(float outerRadius) { + if (outerRadius <= 0) { + throw new IllegalArgumentException("Outer radius must be greater than 0."); + } + this.outerRadius = outerRadius; + } + + /** + * Gets the inner radius of the reticle. + * + * @return the inner radius. + */ + public float getInnerRadius() { + return innerRadius; + } + + /** + * Sets the inner radius of the reticle. + * + * @param innerRadius the new inner radius. + * @throws IllegalArgumentException if the radius is non-positive. + */ + public void setInnerRadius(float innerRadius) { + if (innerRadius <= 0) { + throw new IllegalArgumentException("Inner radius must be greater than 0."); + } + this.innerRadius = innerRadius; + } + + /** + * Gets the color of the reticle. + * + * @return the color. + */ + public Color getColor() { + return color; + } + + /** + * Sets the color of the reticle. + * + * @param color the new color. + * @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; + } +} diff --git a/src/main/java/engine/debug/DebugInfoUpdater.java b/src/main/java/engine/debug/DebugInfoUpdater.java index abdcd2c1..a9177341 100644 --- a/src/main/java/engine/debug/DebugInfoUpdater.java +++ b/src/main/java/engine/debug/DebugInfoUpdater.java @@ -104,6 +104,9 @@ private void updateCameraInfo(Camera camera) { setInfo(CATEGORY_CAMERA, "FOV", Mathf.toDegrees(camera.getFieldOfView())); setInfo(CATEGORY_CAMERA, "Near", camera.getNearPlane()); setInfo(CATEGORY_CAMERA, "Far", camera.getFarPlane()); + setInfo(CATEGORY_CAMERA, "PositionX", camera.getTransform().getPosition().getX()); + setInfo(CATEGORY_CAMERA, "PositionY", camera.getTransform().getPosition().getY()); + setInfo(CATEGORY_CAMERA, "PositionZ", camera.getTransform().getPosition().getZ()); } private void updateOsMetrics() { diff --git a/src/main/java/engine/processing/LightRendererImpl.java b/src/main/java/engine/processing/LightRendererImpl.java index c7f7f636..2d1828a3 100644 --- a/src/main/java/engine/processing/LightRendererImpl.java +++ b/src/main/java/engine/processing/LightRendererImpl.java @@ -66,10 +66,24 @@ public void render(SpotLight light) { public void render(PointLight light) { renderCommon(light.getColor(), light.getIntensity()); + + float intensity = light.getIntensity(); + + // Retrieve light color + float red = light.getColor().getRed(); // Gives a value from 0 to 1 + float green = light.getColor().getGreen(); // Same for green + float blue = light.getColor().getBlue(); // Same for blue + + // Apply intensity to each color component + red *= intensity; + green *= intensity; + blue *= intensity; + + // Call pointLight() with adjusted color p.pointLight( - light.getColor().getRedInt(), - light.getColor().getGreenInt(), - light.getColor().getBlueInt(), + red * 255, // Scale back to 0-255 range for Processing's pointLight + green * 255, // Same for green + blue * 255, // Same for blue light.getPosition().getX(), light.getPosition().getY(), light.getPosition().getZ()); diff --git a/src/main/java/engine/processing/ProcessingTexture.java b/src/main/java/engine/processing/ProcessingTexture.java index 3712dcba..829ce539 100644 --- a/src/main/java/engine/processing/ProcessingTexture.java +++ b/src/main/java/engine/processing/ProcessingTexture.java @@ -2,6 +2,7 @@ import engine.resources.FilterMode; import engine.resources.Texture; +import engine.resources.TextureWrapMode; import processing.core.PImage; public class ProcessingTexture implements Texture { @@ -10,9 +11,12 @@ public class ProcessingTexture implements Texture { private FilterMode filterMode; + private TextureWrapMode textureWrapMode; + public ProcessingTexture(PImage image) { this.image = image; this.filterMode = FilterMode.BILINEAR; + this.textureWrapMode = TextureWrapMode.CLAMP; } @Override @@ -61,6 +65,16 @@ public void setFilterMode(FilterMode filterMode) { this.filterMode = filterMode; } + @Override + public TextureWrapMode getTextureWrapMode() { + return textureWrapMode; + } + + @Override + public void setTextureWrapMode(TextureWrapMode textureWrapMode) { + this.textureWrapMode = textureWrapMode; + } + @Override public Texture getBackendTexture() { return this; diff --git a/src/main/java/engine/resources/Texture.java b/src/main/java/engine/resources/Texture.java index af14417e..33571f5e 100644 --- a/src/main/java/engine/resources/Texture.java +++ b/src/main/java/engine/resources/Texture.java @@ -55,6 +55,29 @@ public interface Texture { */ void setFilterMode(FilterMode filterMode); + /** + * Gets the current texture wrapping mode. + * + *

The texture wrap mode determines how the texture is applied when texture coordinates exceed + * the [0, 1] range. For example, in {@link TextureWrapMode#REPEAT}, the texture will repeat, + * whereas in {@link TextureWrapMode#CLAMP}, the texture will extend the edge pixels. + * + * @return the {@link TextureWrapMode} currently applied to the texture. + */ + TextureWrapMode getTextureWrapMode(); + + /** + * Sets the texture wrapping mode. + * + *

This method controls how the texture is mapped outside the standard [0, 1] texture + * coordinate range. Use {@link TextureWrapMode#REPEAT} to tile the texture across a surface, or + * {@link TextureWrapMode#CLAMP} to stretch the texture's edge pixels when coordinates exceed the + * valid range. + * + * @param textureWrapMode the new {@link TextureWrapMode} to apply to the texture. + */ + void setTextureWrapMode(TextureWrapMode textureWrapMode); + /** * Gets the underlying backend texture implementation. This is useful for accessing * engine-specific or platform-specific features. diff --git a/src/main/java/engine/resources/Texture2D.java b/src/main/java/engine/resources/Texture2D.java index 2a3bcb99..c6279dea 100644 --- a/src/main/java/engine/resources/Texture2D.java +++ b/src/main/java/engine/resources/Texture2D.java @@ -55,20 +55,33 @@ public int getHeight() { /** * Binds the texture to the specified texture unit for rendering. * + *

Binding a texture makes it available for use in subsequent rendering operations. The texture + * unit represents a specific slot in the graphics pipeline to which the texture is assigned. + * * @param unit the texture unit to bind this texture to. + * @see #unbind() */ @Override public void bind(int unit) { texture.bind(unit); } - /** Unbinds the texture from its currently bound texture unit. */ + /** + * Unbinds the texture from its currently bound texture unit. + * + *

Once a texture is unbound, it is no longer available for rendering until it is bound again. + */ @Override public void unbind() { texture.unbind(); } - /** Deletes the texture and releases any associated resources. */ + /** + * Deletes the texture and releases any associated resources. + * + *

This method should be called when the texture is no longer needed to free up GPU memory. + * After a texture is deleted, it cannot be used for rendering unless it is recreated. + */ @Override public void delete() { texture.delete(); @@ -77,7 +90,11 @@ public void delete() { /** * Updates the pixel data of the texture. * + *

This method replaces the existing texture data with new pixel values. The pixel data must + * match the dimensions of the texture. + * * @param pixels an array of pixel data to set for this texture. + * @throws IllegalArgumentException if the pixel array size does not match the texture dimensions. */ @Override public void setPixels(int[] pixels) { @@ -87,6 +104,9 @@ public void setPixels(int[] pixels) { /** * Gets the filter mode currently applied to the texture. * + *

The filter mode determines how the texture is sampled when it is magnified or minified + * during rendering. Common filter modes include nearest-neighbor and linear filtering. + * * @return the filter mode of the texture. * @see FilterMode */ @@ -98,6 +118,10 @@ public FilterMode getFilterMode() { /** * Sets the filter mode for the texture. * + *

The filter mode controls how the texture is sampled when rendered at different sizes. For + * example, linear filtering smooths the texture when scaled, while nearest-neighbor filtering + * preserves sharp edges. + * * @param filterMode the desired filter mode to apply to the texture. * @see FilterMode */ @@ -106,9 +130,42 @@ public void setFilterMode(FilterMode filterMode) { texture.setFilterMode(filterMode); } + /** + * Gets the texture wrapping mode currently applied to this texture. + * + *

The texture wrap mode controls how the texture is applied when texture coordinates exceed + * the [0, 1] range. For example, in {@link TextureWrapMode#REPEAT}, the texture will tile + * infinitely, whereas in {@link TextureWrapMode#CLAMP}, the texture's edge pixels are stretched. + * + * @return the {@link TextureWrapMode} applied to the texture. + */ + @Override + public TextureWrapMode getTextureWrapMode() { + return texture.getTextureWrapMode(); + } + + /** + * Sets the texture wrapping mode for this texture. + * + *

This method controls how the texture is mapped outside the standard [0, 1] texture + * coordinate range. Use {@link TextureWrapMode#REPEAT} to tile the texture across a surface, or + * {@link TextureWrapMode#CLAMP} to stretch the texture's edge pixels when coordinates exceed the + * valid range. + * + * @param textureWrapMode the new {@link TextureWrapMode} to apply to the texture. + */ + @Override + public void setTextureWrapMode(TextureWrapMode textureWrapMode) { + this.texture.setTextureWrapMode(textureWrapMode); + } + /** * Retrieves the backend texture instance used by this facade. * + *

This method provides access to the underlying texture object managed by the {@link + * TextureManager}. It is useful for accessing low-level or engine-specific features not exposed + * by the {@link Texture2D} class. + * * @return the underlying {@link Texture} instance. */ @Override diff --git a/src/main/java/engine/resources/TextureWrapMode.java b/src/main/java/engine/resources/TextureWrapMode.java new file mode 100644 index 00000000..fce2ed44 --- /dev/null +++ b/src/main/java/engine/resources/TextureWrapMode.java @@ -0,0 +1,55 @@ +package engine.resources; + +/** + * Represents the texture wrapping mode used to determine how textures are applied when texture + * coordinates exceed the standard [0, 1] range. + * + *

This enum provides two wrapping modes: + * + *

+ * + *

Depending on the rendering backend, more wrapping modes may be supported. This abstraction + * allows for easy integration with different graphics libraries while keeping the codebase flexible + * and maintainable. + * + * @see engine.texture.Texture + */ +public enum TextureWrapMode { + + /** + * Clamps texture coordinates to the edges of the texture. + * + *

This mode ensures that any texture coordinate outside the [0, 1] range will be mapped to the + * nearest edge of the texture, effectively stretching the border pixels. It is commonly used to + * avoid visible seams on models when the texture should not repeat. + * + *

Example Use Case: + * + *

+ */ + CLAMP, + + /** + * Repeats the texture in both directions when the texture coordinates exceed the [0, 1] range. + * This mode is useful for creating seamless, tiled patterns such as terrain, walls, or floors in + * games and 3D environments. + * + *

Example Use Case: + * + *

+ */ + REPEAT +} diff --git a/src/main/java/workspace/GraphicsPImpl.java b/src/main/java/workspace/GraphicsPImpl.java index 12ceadf2..c0c923f3 100644 --- a/src/main/java/workspace/GraphicsPImpl.java +++ b/src/main/java/workspace/GraphicsPImpl.java @@ -9,6 +9,7 @@ import engine.resources.FilterMode; import engine.resources.Image; import engine.resources.Texture; +import engine.resources.TextureWrapMode; import engine.scene.camera.Camera; import engine.scene.light.Light; import engine.scene.light.LightRenderer; @@ -96,6 +97,13 @@ public void fillFaces(Mesh3D mesh) { } } + @Override + public void drawFaces(Mesh3D mesh) { + g.noFill(); + stroke(); + drawMeshFaces(mesh); + } + @Override public void renderInstances(Mesh3D mesh, List instanceTransforms) { if (mesh.getFaces().isEmpty() || mesh.getVertices().isEmpty()) { @@ -135,13 +143,56 @@ private void applyTransform(Matrix4f transform) { } /** - * Applies the specified texture to the current rendering context, setting the appropriate texture - * sampling mode based on the filter mode of the texture. + * Applies the specified texture to the current rendering context with the appropriate sampling + * mode. + * + *

The filter mode of the texture is used to determine the sampling mode, which is then applied + * to the rendering context. If the texture is not initialized, this method does nothing. If an + * unexpected filter mode is encountered, a warning is logged, and BILINEAR mode is used as a + * fallback. + * + * @see #getSamplingMode(FilterMode) + * @see processing.opengl.Texture + */ + private void applyTexture() { + if (texture == null || texture.getImage() == null) { + return; // Ensure texture is properly initialized before applying it. + } + ((PGraphicsOpenGL) g).textureSampling(getSamplingMode(texture.getFilterMode())); + g.textureWrap(getTextureWrapMode()); + g.textureMode(PApplet.NORMAL); + g.texture(texture.getImage()); + } + + /** + * Converts the internal {@link TextureWrapMode} to a corresponding Processing constant. * - *

This method checks the texture's filter mode and applies the corresponding texture sampling - * mode. It also ensures that the texture is properly initialized before usage. + *

This method maps the engine's {@link TextureWrapMode} values to the equivalent constants + * defined by the {@link PApplet} class for use in Processing-based rendering. The supported + * mappings are: * - *

The available filter modes are mapped to the following sampling modes: + *

+ * + *

If an unsupported or unexpected wrap mode is encountered, a warning is printed to {@code + * System.err}, and {@link PApplet#CLAMP} is returned as a fallback. + * + * @return the corresponding Processing constant for the texture wrap mode. + */ + private int getTextureWrapMode() { + TextureWrapMode textureWrapMode = texture.getTextureWrapMode(); + if (textureWrapMode == TextureWrapMode.CLAMP) return PApplet.CLAMP; + if (textureWrapMode == TextureWrapMode.REPEAT) return PApplet.REPEAT; + System.err.println("Warning: Unexpected texture wrap mode value: " + textureWrapMode); + return PApplet.CLAMP; + } + + /** + * Maps the given filter mode to the corresponding texture sampling mode. + * + *

Supported mappings: * *

* - * If an unexpected filter mode is encountered, a warning is logged, and the default BILINEAR mode - * (sampling mode 4) is applied. + *

If an unexpected filter mode is provided, a warning is logged, and BILINEAR mode (4) is + * returned. * - * @throws IllegalArgumentException if the filter mode is unexpected and no fallback is defined. + * @param filterMode the filter mode to map + * @return the corresponding sampling mode * @see processing.opengl.Texture */ - private void applyTexture() { - if (texture == null || texture.getImage() == null) { - return; // Ensure texture is properly initialized before applying it. - } - - FilterMode filterMode = texture.getFilterMode(); - int textureSampling; - + private int getSamplingMode(FilterMode filterMode) { switch (filterMode) { case POINT: - textureSampling = 2; - break; + return 2; case LINEAR: - textureSampling = 3; - break; + return 3; case BILINEAR: - textureSampling = 4; - break; + return 4; case TRILINEAR: - textureSampling = 5; - break; + return 5; default: - // Log a warning for unexpected filter modes (in case of future extensions). System.err.println("Warning: Unexpected filter mode value: " + filterMode); - textureSampling = 4; // Default to BILINEAR if undefined - break; + return 4; // Default to BILINEAR } - - // Apply the texture with the corresponding sampling mode - ((PGraphicsOpenGL) g).textureSampling(textureSampling); - g.texture(texture.getImage()); - g.textureMode(PApplet.NORMAL); } private void drawMeshFaces(Mesh3D mesh) { diff --git a/src/main/java/workspace/ui/Graphics3D.java b/src/main/java/workspace/ui/Graphics3D.java index c0c0621e..531f7d58 100644 --- a/src/main/java/workspace/ui/Graphics3D.java +++ b/src/main/java/workspace/ui/Graphics3D.java @@ -25,6 +25,8 @@ public interface Graphics3D extends Graphics2D { void render(Light light); + void drawFaces(Mesh3D mesh); + void fillFaces(Mesh3D mesh); void renderInstances(Mesh3D mesh, List instanceTransforms);