diff --git a/src/main/java/engine/application/BasicApplication.java b/src/main/java/engine/application/BasicApplication.java index fc85406f..3c3dab78 100644 --- a/src/main/java/engine/application/BasicApplication.java +++ b/src/main/java/engine/application/BasicApplication.java @@ -1,6 +1,7 @@ package engine.application; import engine.Timer; +import engine.components.FlyByCameraControl; import engine.debug.DebugInfoUpdater; import engine.debug.DebugOverlay; import engine.debug.FpsGraph; @@ -10,13 +11,14 @@ import engine.processing.ProcessingApplication; import engine.scene.Scene; import engine.scene.SceneNode; +import engine.scene.camera.PerspectiveCamera; import workspace.ui.Graphics; public abstract class BasicApplication implements Application { private boolean launched; - private boolean displayInfoText = true; + private boolean displayInfo = true; private boolean isPaused = false; @@ -74,6 +76,18 @@ public void initialize() { initializeDebugOverlay(); fpsGraph = new FpsGraph(new FpsHistory()); onInitialize(); + setupDefaultCamera(); + } + + private void setupDefaultCamera() { + if (activeScene == null) return; + if (activeScene.getActiveCamera() != null) return; + + PerspectiveCamera defaultCamera = new PerspectiveCamera(); + activeScene.setActiveCamera(defaultCamera); + SceneNode cameraNode = new SceneNode("DefaultCamera"); + cameraNode.addComponent(new FlyByCameraControl(input, defaultCamera)); + activeScene.addNode(cameraNode); } private void initializeDebugOverlay() { @@ -110,7 +124,7 @@ public void update() { } @Override - public void render(Graphics g) { + public void render(Graphics g) { if (activeScene != null) { activeScene.render(g); } @@ -124,7 +138,7 @@ public void render(Graphics g) { g.strokeWeight(1); renderUi(g); renderDebugUi(g); - fpsGraph.render(g); + g.enableDepthTest(); } @@ -134,8 +148,9 @@ private void renderUi(Graphics g) { } private void renderDebugUi(Graphics g) { - if (!displayInfoText) return; + if (!displayInfo) return; debugOverlay.render(g); + fpsGraph.render(g); } @Override @@ -170,8 +185,8 @@ public Scene getActiveScene() { public void setActiveScene(Scene activeScene) { this.activeScene = activeScene; } - - public void setDisplayInfoText(boolean displayInfoText) { - this.displayInfoText = displayInfoText; + + public void setDisplayInfo(boolean displayInfo) { + this.displayInfo = displayInfo; } } diff --git a/src/main/java/engine/processing/ProcessingTexture.java b/src/main/java/engine/processing/ProcessingTexture.java index b350bc2a..585e6b31 100644 --- a/src/main/java/engine/processing/ProcessingTexture.java +++ b/src/main/java/engine/processing/ProcessingTexture.java @@ -24,7 +24,6 @@ public int getHeight() { @Override public void bind(int unit) { // Processing doesn't use texture units in the same way, just bind globally - image.loadPixels(); } @Override diff --git a/src/main/java/engine/render/Material.java b/src/main/java/engine/render/Material.java index 40debe56..17a1e5d5 100644 --- a/src/main/java/engine/render/Material.java +++ b/src/main/java/engine/render/Material.java @@ -68,8 +68,7 @@ public class Material { /** Shininess factor for specular highlights. */ private final float shininess; - private Texture normalTexture; - + /** The diffuse texture map (map_Kd) of the material. */ private Texture diffuseTexture; /** @@ -88,7 +87,6 @@ private Material(Builder builder) { this.diffuse = builder.diffuse; this.specular = builder.specular; this.shininess = builder.shininess; - this.normalTexture = builder.normalTexture; this.diffuseTexture = builder.diffuseTexture; } @@ -112,8 +110,6 @@ public static class Builder { private Texture diffuseTexture = null; - private Texture normalTexture = null; - /** * Sets the base color of the material. * @@ -174,17 +170,6 @@ public Builder setUseLighting(boolean useLighting) { return this; } - /** - * Sets the normal texture of the material. - * - * @param normalTexture The normal texture, can be null - * @return The builder instance for chaining - */ - public Builder setNormalTexture(Texture normalTexture) { - this.normalTexture = normalTexture; - return this; - } - /** * Sets the diffuse texture of the material. * @@ -217,9 +202,6 @@ public void apply(Graphics g) { if (diffuseTexture != null) { g.bindTexture(diffuseTexture, 0); // Bind to texture unit 0 } - if (normalTexture != null) { - g.bindTexture(normalTexture, 1); // Bind to texture unit 1 - } } /** @@ -281,11 +263,19 @@ public float getShininess() { return shininess; } + /** + * Returns the diffuse texture map (map_Kd) of the material. + * + *

The diffuse texture is a 2D image used to define the base color and pattern of the surface, + * simulating the appearance of the material under diffuse lighting. This texture is typically + * applied using UV mapping to wrap the image onto the geometry of a 3D model. + * + *

In the context of material definition files (e.g., MTL for OBJ models), this corresponds to + * the `map_Kd` property, which specifies the file path to the texture image. + * + * @return The diffuse texture map as {@link Texture}. + */ public Texture getDiffuseTexture() { return diffuseTexture; } - - public Texture getNormalTexture() { - return normalTexture; - } } diff --git a/src/main/java/engine/scene/Scene.java b/src/main/java/engine/scene/Scene.java index d8ccdd64..9348a9b7 100644 --- a/src/main/java/engine/scene/Scene.java +++ b/src/main/java/engine/scene/Scene.java @@ -8,6 +8,7 @@ import engine.scene.camera.Camera; import engine.scene.light.Light; +import math.Color; import workspace.GraphicsPImpl; import workspace.ui.Graphics; @@ -37,6 +38,9 @@ public class Scene { /** Name of the scene. Used for identification or debugging purposes. */ private final String name; + /** The background color of the scene. Defaults to black if not explicitly set. */ + private Color background; + /** The currently active camera that determines the scene's view transformation. */ private Camera activeCamera; @@ -56,6 +60,7 @@ public Scene(String name) { throw new IllegalArgumentException("Name cannot be null."); } this.name = name; + this.background = new Color(0, 0, 0, 1); } /** @@ -116,6 +121,8 @@ public void update(float deltaTime) { * compatibility with most rendering APIs. */ public void render(Graphics g) { + g.clear(background); + if (activeCamera != null) { g.applyCamera(activeCamera); } @@ -295,4 +302,30 @@ public boolean isWireframeMode() { public void setWireframeMode(boolean wireframeMode) { this.wireframeMode = wireframeMode; } + + /** + * Retrieves the background color of the scene. + * + *

The background color is used to clear the rendering surface before drawing the scene. + * + * @return The current background color of the scene. + */ + public Color getBackground() { + return background; + } + + /** + * Sets the background color of the scene. + * + *

The background color is used to clear the rendering surface before drawing the scene. + * + * @param background The new background color for the scene. Must not be {@code null}. + * @throws IllegalArgumentException if the provided background color is {@code null}. + */ + public void setBackground(Color background) { + if (background == null) { + throw new IllegalArgumentException("Background color cannot be null."); + } + this.background = background; + } } diff --git a/src/main/java/engine/scene/light/DirectionalLight.java b/src/main/java/engine/scene/light/DirectionalLight.java index b1399d92..9be2fd29 100644 --- a/src/main/java/engine/scene/light/DirectionalLight.java +++ b/src/main/java/engine/scene/light/DirectionalLight.java @@ -45,6 +45,25 @@ public DirectionalLight() { this(Color.WHITE, new Vector3f(0, 1, 0), 1.0f); } + /** + * Creates a new DirectionalLight instance with the specified color and direction. + * + *

This constructor initializes the light with the given color and direction, and a default + * intensity of 1.0. The provided direction vector is normalized during initialization to ensure + * consistent light behavior. + * + * @param color The color of the light emitted by the directional light source. Represents the RGB + * values of the light's color. This parameter cannot be null. + * @param direction The direction of the light source. This vector determines the direction in + * which the light rays travel. The provided vector is automatically normalized during + * construction, ensuring the direction's magnitude is always 1. This parameter cannot be + * null. + * @throws IllegalArgumentException if the direction or color is null. + */ + public DirectionalLight(Color color, Vector3f direction) { + this(color, direction, 1.0f); + } + /** * Creates a new DirectionalLight instance. * diff --git a/src/main/java/math/Plane.java b/src/main/java/math/Plane.java index 3dba9b2a..ae8d5b28 100644 --- a/src/main/java/math/Plane.java +++ b/src/main/java/math/Plane.java @@ -75,7 +75,7 @@ public float distanceToPoint(Vector3f point) { * @return the normal vector of the plane. */ public Vector3f getNormal() { - return normal; + return new Vector3f(normal); } /** diff --git a/src/main/java/math/Ray3f.java b/src/main/java/math/Ray3f.java index d3eaf9b5..e417af79 100644 --- a/src/main/java/math/Ray3f.java +++ b/src/main/java/math/Ray3f.java @@ -36,12 +36,69 @@ public Ray3f(Vector3f origin, Vector3f direction) { if (direction == null) { throw new IllegalArgumentException("Direction cannot be null."); } - this.origin = origin; - this.direction = direction; + this.origin = new Vector3f(origin); + this.direction = new Vector3f(direction); this.direction.normalizeLocal(); this.directionInv = direction.reciprocal(); } + /** + * Computes the shortest distance from the ray to a point in 3D space. + * + *

This method calculates the perpendicular distance from the given point to the ray. If the + * point lies behind the origin of the ray, the distance from the point to the ray's origin is + * returned. If the point lies along the ray's path (in the direction of the ray), the + * perpendicular distance is calculated. The ray is considered to extend infinitely in both + * directions from the origin. + * + * @param point The point in 3D space to compute the distance to (non-null). + * @return The shortest distance from the ray to the point. If the point is behind the ray's + * origin, the distance from the point to the origin is returned. + * @throws IllegalArgumentException if the point is null. + */ + public float distanceToPoint(Vector3f point) { + if (point == null) { + throw new IllegalArgumentException("Point cannot be null."); + } + + // Calculate vector from ray origin to the point + Vector3f toPoint = point.subtract(origin); + + // Project the vector to the ray's direction (dot product normalized) + float projection = toPoint.dot(direction); + + // If the projection is negative, the point is behind the ray's origin + if (projection < 0) { + // Return the distance from the origin to the point + return toPoint.length(); + } + + // Return the perpendicular distance (calculate as the distance from the point to the closest + // point on the ray) + Vector3f closestPoint = getPointAt(projection); + return closestPoint.subtract(point).length(); + } + + /** + * 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)); + } + /** * Returns the origin of the ray. * @@ -50,7 +107,7 @@ public Ray3f(Vector3f origin, Vector3f direction) { * @return The origin of the ray. */ public Vector3f getOrigin() { - return origin; + return new Vector3f(origin); } /** @@ -62,7 +119,7 @@ public Vector3f getOrigin() { * @return The direction vector of the ray. */ public Vector3f getDirection() { - return direction; + return new Vector3f(direction); } /** @@ -74,26 +131,6 @@ public Vector3f getDirection() { * @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)); + return new Vector3f(directionInv); } } diff --git a/src/main/java/mesh/creator/special/HoneyCombCreator.java b/src/main/java/mesh/creator/special/HoneyCombCreator.java index 0b3f8b07..d11b10ae 100644 --- a/src/main/java/mesh/creator/special/HoneyCombCreator.java +++ b/src/main/java/mesh/creator/special/HoneyCombCreator.java @@ -1,145 +1,143 @@ package mesh.creator.special; +import math.Bounds; import math.Mathf; import mesh.Mesh3D; import mesh.creator.FillType; import mesh.creator.IMeshCreator; import mesh.creator.primitives.CircleCreator; +import mesh.modifier.CenterAtModifier; import mesh.modifier.ExtrudeModifier; +import mesh.modifier.RotateYModifier; import mesh.modifier.SolidifyModifier; import mesh.selection.FaceSelection; import mesh.util.Bounds3; +import mesh.util.MeshBoundsCalculator; public class HoneyCombCreator implements IMeshCreator { - private int rowCount; - - private int colCount; - - private float cellRadius; - - private float height; - - private float innerScale; - - private Mesh3D mesh; - - public HoneyCombCreator() { - rowCount = 2; - colCount = 2; - cellRadius = 0.5f; - height = 0.2f; - innerScale = 0.9f; - } - - @Override - public Mesh3D create() { - initializeMesh(); - createSegments(); - removeDoubleVertices(); - createInsets(); - solidify(); - center(); - return mesh; - } - - private void solidify() { - if (height == 0) - return; - new SolidifyModifier(height).modify(mesh); - } - - private void center() { - Bounds3 bounds = mesh.calculateBounds(); - mesh.translateX(-bounds.getCenterX()); - mesh.translateZ(-bounds.getCenterZ()); - mesh.translateY(-height / 2f); - } - - private void createInsets() { - ExtrudeModifier modifier = new ExtrudeModifier(innerScale, 0); - FaceSelection selection = new FaceSelection(mesh); - selection.selectAll(); - modifier.modify(mesh, selection.getFaces()); - mesh.removeFaces(selection.getFaces()); - } - - private void removeDoubleVertices() { - mesh.removeDoubles(4); - } - - private void createSegments() { - for (int i = 0; i < colCount; i++) { - for (int j = 0; j < rowCount; j++) { - Mesh3D segment = createHexSegment(); - Bounds3 bounds = segment.calculateBounds(); - float width = bounds.getWidth(); - float depth = bounds.getDepth(); - segment.translateX(i * width); - segment.translateZ(j * (depth - cellRadius / 2f)); - if (j % 2 == 1) - segment.translateX(width / 2.0f); - mesh.append(segment); - } - } - } - - private CircleCreator createCircleCreator() { - CircleCreator creator = new CircleCreator(); - creator.setFillType(FillType.N_GON); - creator.setRadius(cellRadius); - creator.setVertices(6); - return creator; - } - - private Mesh3D createHexSegment() { - Mesh3D segment = createCircleCreator().create(); - segment.rotateY(Mathf.HALF_PI); - return segment; - } - - private void initializeMesh() { - mesh = new Mesh3D(); - } - - public int getRowCount() { - return rowCount; - } - - public void setRowCount(int rowCount) { - this.rowCount = rowCount; - } - - public int getColCount() { - return colCount; - } - - public void setColCount(int colCount) { - this.colCount = colCount; - } - - public float getCellRadius() { - return cellRadius; - } - - public void setCellRadius(float cellRadius) { - this.cellRadius = cellRadius; - } - - public float getHeight() { - return height; - } - - public void setHeight(float height) { - this.height = height; - } - - public float getInnerScale() { - return innerScale; - } - - public void setInnerScale(float innerScale) { - this.innerScale = innerScale; - } - + private int rowCount; + + private int colCount; + + private float cellRadius; + + private float height; + + private float innerScale; + + private Mesh3D mesh; + + public HoneyCombCreator() { + rowCount = 2; + colCount = 2; + cellRadius = 0.5f; + height = 0.2f; + innerScale = 0.9f; + } + + @Override + public Mesh3D create() { + initializeMesh(); + createSegments(); + removeDoubleVertices(); + createInsets(); + solidify(); + centerAtOrigin(); + return mesh; + } + + private void solidify() { + if (height == 0) return; + new SolidifyModifier(height).modify(mesh); + } + + private void centerAtOrigin() { + mesh.apply(new CenterAtModifier()); + } + + private void createInsets() { + ExtrudeModifier modifier = new ExtrudeModifier(innerScale, 0); + FaceSelection selection = new FaceSelection(mesh); + selection.selectAll(); + modifier.modify(mesh, selection.getFaces()); + mesh.removeFaces(selection.getFaces()); + } + + private void removeDoubleVertices() { + mesh.removeDoubles(4); + } + + private void createSegments() { + for (int i = 0; i < colCount; i++) { + for (int j = 0; j < rowCount; j++) { + Mesh3D segment = createHexSegment(); + Bounds3 bounds = segment.calculateBounds(); + float width = bounds.getWidth(); + float depth = bounds.getDepth(); + segment.translateX(i * width); + segment.translateZ(j * (depth - cellRadius / 2f)); + if (j % 2 == 1) segment.translateX(width / 2.0f); + mesh.append(segment); + } + } + } + + private CircleCreator createCircleCreator() { + CircleCreator creator = new CircleCreator(); + creator.setFillType(FillType.N_GON); + creator.setRadius(cellRadius); + creator.setVertices(6); + return creator; + } + + private Mesh3D createHexSegment() { + Mesh3D segment = createCircleCreator().create(); + segment.apply(new RotateYModifier(Mathf.HALF_PI)); + return segment; + } + + private void initializeMesh() { + mesh = new Mesh3D(); + } + + public int getRowCount() { + return rowCount; + } + + public void setRowCount(int rowCount) { + this.rowCount = rowCount; + } + + public int getColCount() { + return colCount; + } + + public void setColCount(int colCount) { + this.colCount = colCount; + } + + public float getCellRadius() { + return cellRadius; + } + + public void setCellRadius(float cellRadius) { + this.cellRadius = cellRadius; + } + + public float getHeight() { + return height; + } + + public void setHeight(float height) { + this.height = height; + } + + public float getInnerScale() { + return innerScale; + } + + public void setInnerScale(float innerScale) { + this.innerScale = innerScale; + } } diff --git a/src/main/java/workspace/GraphicsPImpl.java b/src/main/java/workspace/GraphicsPImpl.java index e0ec545f..35a234bf 100644 --- a/src/main/java/workspace/GraphicsPImpl.java +++ b/src/main/java/workspace/GraphicsPImpl.java @@ -550,4 +550,9 @@ public void drawImage(Image image, float x, float y, float width, float height) throw new IllegalArgumentException("Unsupported image backend."); } } + + @Override + public void clear(math.Color color) { + g.background(color.getRedInt(), color.getGreenInt(), color.getBlueInt(), color.getAlphaInt()); + } } diff --git a/src/main/java/workspace/ui/Graphics2D.java b/src/main/java/workspace/ui/Graphics2D.java index 1b155c54..a1cbf700 100644 --- a/src/main/java/workspace/ui/Graphics2D.java +++ b/src/main/java/workspace/ui/Graphics2D.java @@ -226,6 +226,17 @@ public interface Graphics2D { */ void text(String text, float x, float y); + /** + * Clears the rendering context with the specified color. + * + *

This method fills the entire viewport with the provided color, effectively resetting the + * drawing surface to a blank state. Any previously drawn content is overwritten. + * + * @param color The {@link math.Color} to use for clearing the viewport. The color is applied + * uniformly across the entire rendering surface. + */ + void clear(math.Color color); + void drawImage(Image image, float x, float y); void drawImage(Image image, float x, float y, float width, float height);