From 9ee35d5f1e579af0e18bbad0f4a2a310e501cfba Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Thu, 9 Jan 2025 10:01:53 +0100 Subject: [PATCH 1/4] Removed clutter. Added material color support. --- .../java/engine/processing/VBOProcessing.java | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/main/java/engine/processing/VBOProcessing.java b/src/main/java/engine/processing/VBOProcessing.java index 12e3b9fc..f2dcd86b 100644 --- a/src/main/java/engine/processing/VBOProcessing.java +++ b/src/main/java/engine/processing/VBOProcessing.java @@ -1,7 +1,6 @@ package engine.processing; import engine.render.Material; -import engine.resources.FilterMode; import engine.resources.Texture; import engine.vbo.VBO; import math.Vector2f; @@ -23,15 +22,16 @@ public VBOProcessing(PGraphics graphics) { } @Override - public void create(Mesh3D mesh, Material material) { // TODO Auto-generated method stub + public void create(Mesh3D mesh, Material material) { faceCount = mesh.getFaceCount(); vertexCount = mesh.getVertexCount(); shape = graphics.createShape(); shape.beginShape(); - shape.noStroke(); + // TODO Full material support + shape.fill(material.getColor().getRGBA()); for (Face3D f : mesh.getFaces()) { if (f.indices.length == 3) { @@ -41,9 +41,6 @@ public void create(Mesh3D mesh, Material material) { // TODO Auto-generated meth } else { shape.beginShape(PApplet.POLYGON); } - - // applyTexture(); - int[] indices = f.indices; for (int i = 0; i < indices.length; i++) { Vector3f v = mesh.vertices.get(f.indices[i]); @@ -55,9 +52,7 @@ public void create(Mesh3D mesh, Material material) { // TODO Auto-generated meth shape.vertex(v.getX(), v.getY(), v.getZ()); } } - // shape.endShape(); } - shape.endShape(); Texture texture = material.getDiffuseTexture(); @@ -68,35 +63,17 @@ public void create(Mesh3D mesh, Material material) { // TODO Auto-generated meth } } - private int getSamplingMode(FilterMode filterMode) { - switch (filterMode) { - case POINT: - return 2; - case LINEAR: - return 3; - case BILINEAR: - return 4; - case TRILINEAR: - return 5; - default: - System.err.println("Warning: Unexpected filter mode value: " + filterMode); - return 4; // Default to BILINEAR - } - } - @Override public void create(float[] vertices, int[] indices) { + // TODO Full implementation // Create a PShape object for the mesh - // shape = new PShape(PShape.POINTS, vertices.length / 3); shape = new PShape(); - // Load vertices into the PShape shape.beginShape(); for (int i = 0; i < vertices.length; i += 3) { shape.vertex(vertices[i], vertices[i + 1], vertices[i + 2]); } shape.endShape(); - // Optional: store indices if using indexed rendering vertexCount = vertices.length / 3; // Assuming vertices are in 3D (x, y, z) } From ff0ccf34e6171230679c62d426f721736270998c Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Thu, 9 Jan 2025 18:47:25 +0100 Subject: [PATCH 2/4] Added active feature to nodes and components. Added sound system. Updated landmass demo. --- .../engine/components/AbstractComponent.java | 89 +++++++++- .../CinematicBlackBarsRenderer.java | 2 +- .../CircularAnimationComponent.java | 2 +- .../java/engine/components/Component.java | 95 +++++++++-- .../java/engine/components/ControlWASD.java | 2 +- .../engine/components/FlyByCameraControl.java | 2 +- src/main/java/engine/components/Geometry.java | 32 ++-- .../engine/components/RotationComponent.java | 2 +- .../java/engine/components/RoundReticle.java | 2 +- .../components/SmoothFlyByCameraControl.java | 2 +- .../engine/components/StaticGeometry.java | 2 +- .../java/engine/components/Transform.java | 24 ++- .../engine/demos/landmass/MapGenerator.java | 14 +- .../demos/landmass/NoiseMapDisplay.java | 2 +- .../landmass/ProceduralLandmassDemo.java | 13 +- .../render/effects/ParticleComponent.java | 2 +- src/main/java/engine/scene/Scene.java | 27 ++- src/main/java/engine/scene/SceneNode.java | 91 +++++++++- .../engine/scene/audio/AudioListener.java | 28 +++ .../java/engine/scene/audio/AudioSource.java | 160 ++++++++++++++++++ .../java/engine/scene/audio/AudioSystem.java | 22 +++ .../scene/audio/BackgroundMusicManager.java | 15 ++ src/main/java/engine/scene/audio/Sound.java | 71 ++++++++ .../java/engine/scene/audio/SoundLoader.java | 8 + .../java/engine/scene/audio/SoundManager.java | 51 ++++++ 25 files changed, 694 insertions(+), 66 deletions(-) create mode 100644 src/main/java/engine/scene/audio/AudioListener.java create mode 100644 src/main/java/engine/scene/audio/AudioSource.java create mode 100644 src/main/java/engine/scene/audio/AudioSystem.java create mode 100644 src/main/java/engine/scene/audio/BackgroundMusicManager.java create mode 100644 src/main/java/engine/scene/audio/Sound.java create mode 100644 src/main/java/engine/scene/audio/SoundLoader.java create mode 100644 src/main/java/engine/scene/audio/SoundManager.java diff --git a/src/main/java/engine/components/AbstractComponent.java b/src/main/java/engine/components/AbstractComponent.java index cdf4231b..f600f725 100644 --- a/src/main/java/engine/components/AbstractComponent.java +++ b/src/main/java/engine/components/AbstractComponent.java @@ -6,20 +6,29 @@ * 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. + * reducing boilerplate code and centralizing shared logic for ease of maintenance. + * + *

Components extending this class inherit basic lifecycle management, including activation state + * handling and owner node reference management. The {@link #update(float)} method includes a check + * for the active state, ensuring that inactive components are automatically skipped during the + * update cycle. */ public abstract class AbstractComponent implements Component { - /** Reference to the owning SceneNode */ + /** Indicates whether the component is currently active. */ + protected boolean active; + + /** Reference to the owning {@link 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. + *

This method is called when the component is added to a {@link SceneNode}. It stores a + * reference to the owning node, which can be used for interactions with other components or scene + * graph operations. * - * @param owner The SceneNode that owns this component. + * @param owner The {@link SceneNode} that owns this component; must not be {@code null}. */ @Override public void setOwner(SceneNode owner) { @@ -27,11 +36,77 @@ public void setOwner(SceneNode owner) { } /** - * Retrieves the owning node for convenience. + * Retrieves the owning {@link SceneNode} of this component. * - * @return The owning SceneNode instance. + *

This method provides convenient access to the parent node, allowing components to interact + * with the scene graph or access shared properties such as transformations or child nodes. + * + * @return The {@link SceneNode} instance that owns this component, or {@code null} if not set. */ + @Override public SceneNode getOwner() { return owner; } + + /** + * Sets the active state of this component. + * + *

An active component participates in updates and other operations within the scene's + * lifecycle. Inactive components are effectively disabled and will not be processed during + * updates or rendering. + * + * @param active A boolean value where {@code true} sets the component as active, and {@code + * false} sets it as inactive. + */ + @Override + public void setActive(boolean active) { + this.active = active; + } + + /** + * Returns the active state of this component. + * + *

If the component is active, it will be included in updates and other scene lifecycle + * processes. Inactive components are skipped to optimize performance. + * + * @return {@code true} if the component is active, {@code false} otherwise. + */ + @Override + public boolean isActive() { + return active; + } + + /** + * Updates this component during the scene's update cycle. + * + *

This method checks if the component is active before invoking the {@link #onUpdate(float)} + * method. If the component is inactive, the update logic is skipped. + * + * @param tpf The time per frame in seconds (time delta) since the last update. + */ + @Override + public void update(float tpf) { + if (active) { + onUpdate(tpf); + } + } + + /** + * Abstract method to be implemented by subclasses to define component-specific update logic. + * + *

This method is only called if the component is active. Subclasses should implement their + * frame-specific logic here, such as animations, physics calculations, or interactions with other + * components. + * + *

Example use cases include: + * + *

+ * + * @param tpf The time per frame in seconds (time delta) since the last update. + */ + public abstract void onUpdate(float tpf); } diff --git a/src/main/java/engine/components/CinematicBlackBarsRenderer.java b/src/main/java/engine/components/CinematicBlackBarsRenderer.java index 76bab0da..0c642881 100644 --- a/src/main/java/engine/components/CinematicBlackBarsRenderer.java +++ b/src/main/java/engine/components/CinematicBlackBarsRenderer.java @@ -66,7 +66,7 @@ public CinematicBlackBarsRenderer(float fadeSpeed, float targetBarHeight) { * @param tpf Time per frame (time elapsed since the last frame in seconds). */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { if (!isFading()) { return; } diff --git a/src/main/java/engine/components/CircularAnimationComponent.java b/src/main/java/engine/components/CircularAnimationComponent.java index 2c3117b1..89875933 100644 --- a/src/main/java/engine/components/CircularAnimationComponent.java +++ b/src/main/java/engine/components/CircularAnimationComponent.java @@ -68,7 +68,7 @@ public CircularAnimationComponent(float radius, float angularSpeed) { * @param tpf Time per frame (time in seconds since the last frame). */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { // Update the elapsed time based on the time per frame timeElapsed += tpf; diff --git a/src/main/java/engine/components/Component.java b/src/main/java/engine/components/Component.java index e9a27e8e..aafb08b7 100644 --- a/src/main/java/engine/components/Component.java +++ b/src/main/java/engine/components/Component.java @@ -9,45 +9,112 @@ * 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. + *

Each component should manage its lifecycle with {@code onAttach()}, {@code update(float)}, and + * {@code onDetach()} methods, allowing nodes to manage component behavior efficiently. */ public interface Component { + /** + * Sets the active state of this component. + * + *

An active component will participate in updates, rendering, or other operations during the + * scene's lifecycle. Inactive components are skipped in these processes, allowing for optimized + * performance and dynamic behavior control. + * + * @param active A boolean value where {@code true} sets the component as active, and {@code + * false} sets it as inactive. + */ + void setActive(boolean active); + + /** + * Returns the active state of this component. + * + *

An active component participates in updates and other operations. If a component is + * inactive, it is effectively disabled and will not be updated or rendered by the owning {@link + * SceneNode}. + * + * @return {@code true} if the component is active, {@code false} otherwise. + */ + boolean isActive(); + + /** + * Returns the owning {@link SceneNode} of this component. + * + *

This method provides access to the {@link SceneNode} that owns this component, allowing the + * component to interact with its parent node and other components within the scene graph. + * + *

If the component has not been attached to a {@link SceneNode}, this method may return {@code + * null}. + * + * @return The {@link SceneNode} that owns this component, or {@code null} if not attached. + */ + SceneNode getOwner(); + /** * 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. + *

This method is called when the component is added to a {@link SceneNode}. The owning node + * provides context and access to the scene graph, allowing the component to interact with other + * components, transformations, and scene properties. * - * @param owner The {@link SceneNode} that owns this component; cannot be null. + * @param owner The {@link SceneNode} that owns this component; must not be {@code null}. */ void setOwner(SceneNode owner); /** - * Updates the component's logic every frame. + * Updates the component during the scene's update cycle. + * + *

This method is responsible for invoking the component's update logic if it is active. It + * checks the component's active state and then calls {@link #onUpdate(float)} to perform any + * custom behavior defined by the component. * - *

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. + *

The {@link AbstractComponent} base class typically implements this method to handle active + * state checks, ensuring that {@link #onUpdate(float)} is only called when the component is + * enabled. * - * @param tpf The time per frame in seconds (time delta) since the last update. + *

Subclasses should override {@link #onUpdate(float)} to define their specific update logic, + * rather than overriding this method directly. + * + * @param tpf The time per frame in seconds since the last update. */ void update(float tpf); + /** + * Called during the scene's update cycle to perform component-specific logic. + * + *

This method is intended to be overridden by components to define their unique behavior. + * Unlike {@code update(float)}, which is provided by the {@link AbstractComponent} base class, + * this method does not need to handle active state checks. It is only called if the component is + * active. + * + *

Implementations should ensure that the logic is efficient and avoids unnecessary + * computations to maintain performance. + * + * @param tpf The time per frame in seconds since the last update. + */ + void onUpdate(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. + *

This method provides an opportunity to perform any initialization or setup required when the + * component becomes part of a node. It can be used to register listeners, allocate resources, or + * initialize state. + * + *

Components should avoid performing heavy computations in this method to minimize performance + * impact during scene setup. */ 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. + *

This method allows the component to clean up any resources, references, or listeners that + * were established during its lifecycle. It is essential to release resources here to avoid + * memory leaks or lingering state that could impact performance or stability. + * + *

Common tasks include unregistering listeners, stopping threads, or nullifying references to + * other objects in the scene. */ void onDetach(); } diff --git a/src/main/java/engine/components/ControlWASD.java b/src/main/java/engine/components/ControlWASD.java index e8a2c67f..7986ec45 100644 --- a/src/main/java/engine/components/ControlWASD.java +++ b/src/main/java/engine/components/ControlWASD.java @@ -82,7 +82,7 @@ public ControlWASD(Input input, float speed) { * @param tpf Time per frame, used to ensure frame-rate-independent movement. */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { SceneNode node = getOwner(); Vector3f velocity = handleInput(); diff --git a/src/main/java/engine/components/FlyByCameraControl.java b/src/main/java/engine/components/FlyByCameraControl.java index e4ce9aa5..3013bb91 100644 --- a/src/main/java/engine/components/FlyByCameraControl.java +++ b/src/main/java/engine/components/FlyByCameraControl.java @@ -73,7 +73,7 @@ public FlyByCameraControl(Input input, PerspectiveCamera camera) { * @param tpf The time per frame (delta time) used for movement scaling. */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { float mouseX = input.getMouseDeltaX() * mouseSensitivity * tpf; float mouseY = input.getMouseDeltaY() * mouseSensitivity * tpf; diff --git a/src/main/java/engine/components/Geometry.java b/src/main/java/engine/components/Geometry.java index 823e599a..a500061e 100644 --- a/src/main/java/engine/components/Geometry.java +++ b/src/main/java/engine/components/Geometry.java @@ -114,21 +114,21 @@ public void debugRenderBounds(Graphics g) { 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); +// 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); } /** @@ -138,7 +138,7 @@ public void debugRenderBounds(Graphics g) { * @param tpf The time per frame used for the update (in seconds). */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { // Placeholder for potential mesh state updates } diff --git a/src/main/java/engine/components/RotationComponent.java b/src/main/java/engine/components/RotationComponent.java index f5b889f3..c73a0053 100644 --- a/src/main/java/engine/components/RotationComponent.java +++ b/src/main/java/engine/components/RotationComponent.java @@ -55,7 +55,7 @@ public RotationComponent(Vector3f axis, float angularSpeed) { * @param tpf Time per frame (in seconds since the last frame). */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { SceneNode node = getOwner(); if (node == null) return; diff --git a/src/main/java/engine/components/RoundReticle.java b/src/main/java/engine/components/RoundReticle.java index 0972da06..af7773cd 100644 --- a/src/main/java/engine/components/RoundReticle.java +++ b/src/main/java/engine/components/RoundReticle.java @@ -82,7 +82,7 @@ private void renderCenteredOval( * @param tpf time per frame, used for animations or updates. */ @Override - public void update(float tpf) {} + public void onUpdate(float tpf) {} /** Called when the component is attached to a {@link engine.SceneNode}. */ @Override diff --git a/src/main/java/engine/components/SmoothFlyByCameraControl.java b/src/main/java/engine/components/SmoothFlyByCameraControl.java index 5ff2d70e..4eb90438 100644 --- a/src/main/java/engine/components/SmoothFlyByCameraControl.java +++ b/src/main/java/engine/components/SmoothFlyByCameraControl.java @@ -72,7 +72,7 @@ public SmoothFlyByCameraControl(Input input, PerspectiveCamera camera) { * @param tpf Time per frame, used to adjust movement and smoothing. */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { float rawMouseX = input.getMouseDeltaX() * mouseSensitivity * tpf; float rawMouseY = input.getMouseDeltaY() * mouseSensitivity * tpf; diff --git a/src/main/java/engine/components/StaticGeometry.java b/src/main/java/engine/components/StaticGeometry.java index 96eb8d1a..c08a549c 100644 --- a/src/main/java/engine/components/StaticGeometry.java +++ b/src/main/java/engine/components/StaticGeometry.java @@ -75,7 +75,7 @@ public void render(Graphics g) { } @Override - public void update(float tpf) {} + public void onUpdate(float tpf) {} @Override public void onAttach() {} diff --git a/src/main/java/engine/components/Transform.java b/src/main/java/engine/components/Transform.java index 99287b3d..9dd56a98 100644 --- a/src/main/java/engine/components/Transform.java +++ b/src/main/java/engine/components/Transform.java @@ -251,8 +251,30 @@ public Vector3f getRight() { return new Vector3f(-sinY, 0, cosY).normalizeLocal(); } + /** + * Sets the forward direction of this transform. + * + *

This method calculates the rotation angles (pitch and yaw) needed to make the object face + * the given forward direction and updates the rotation vector accordingly. + * + * @param forward The desired forward direction as a normalized vector. + */ + public void setForward(Vector3f forward) { + if (forward == null || forward.length() == 0) { + throw new IllegalArgumentException("Forward vector cannot be null or zero-length."); + } + + // Calculate yaw (rotation around the Y-axis) and pitch (rotation around the X-axis) + float yaw = (float) Math.atan2(forward.z, forward.x); + float pitch = + (float) Math.atan2(forward.y, Math.sqrt(forward.x * forward.x + forward.z * forward.z)); + + // Update the rotation vector + this.rotation.set(pitch, yaw, 0); + } + @Override - public void update(float tpf) {} + public void onUpdate(float tpf) {} @Override public void onAttach() {} diff --git a/src/main/java/engine/demos/landmass/MapGenerator.java b/src/main/java/engine/demos/landmass/MapGenerator.java index c04ea4c5..59566a66 100644 --- a/src/main/java/engine/demos/landmass/MapGenerator.java +++ b/src/main/java/engine/demos/landmass/MapGenerator.java @@ -13,10 +13,11 @@ */ public class MapGenerator { -// private int chunkSize = 481; - private int chunkSize = 961; - private int mapWidth = chunkSize; - private int mapHeight = chunkSize; + private int chunkSize; + // private int chunkSize = 481; + // private int chunkSize = 961; + private int mapWidth; + private int mapHeight; private int seed = 221; private int octaves = 4; private float scale = 50; @@ -26,7 +27,10 @@ public class MapGenerator { private TerrainType[] regions; /** Constructs a new {@code MapGenerator} and initializes the height map and terrain regions. */ - public MapGenerator() { + public MapGenerator(int chunkSize) { + this.chunkSize = chunkSize + 1; + this.mapWidth = this.chunkSize; + this.mapHeight = this.chunkSize; initializeRegions(); heightMap = Noise.createHeightMap(mapWidth, mapHeight, seed, scale, octaves, persistance, lacunarity); diff --git a/src/main/java/engine/demos/landmass/NoiseMapDisplay.java b/src/main/java/engine/demos/landmass/NoiseMapDisplay.java index 6d5ab554..bf0b2a23 100644 --- a/src/main/java/engine/demos/landmass/NoiseMapDisplay.java +++ b/src/main/java/engine/demos/landmass/NoiseMapDisplay.java @@ -107,7 +107,7 @@ public void render(Graphics g) { * @param tpf time per frame, in seconds */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { // No updates are required for this component } diff --git a/src/main/java/engine/demos/landmass/ProceduralLandmassDemo.java b/src/main/java/engine/demos/landmass/ProceduralLandmassDemo.java index 751850c7..ee8fd3ac 100644 --- a/src/main/java/engine/demos/landmass/ProceduralLandmassDemo.java +++ b/src/main/java/engine/demos/landmass/ProceduralLandmassDemo.java @@ -2,9 +2,9 @@ import engine.application.ApplicationSettings; import engine.application.BasicApplication; -import engine.components.StaticGeometry; import engine.components.RoundReticle; import engine.components.SmoothFlyByCameraControl; +import engine.components.StaticGeometry; import engine.render.Material; import engine.resources.Texture2D; import engine.scene.Scene; @@ -42,6 +42,8 @@ public enum DrawMode { // Configuration fields private int levelOfDetail = 0; // Level of detail for the terrain mesh (0 - 6) + private int chunkSize = 240; // TODO Note that the size has to fit LOD + private int chunkScale = 3; private DrawMode drawMode = DrawMode.COLOR_MAP; private Scene scene; @@ -80,7 +82,7 @@ private void setupUI() { /** Creates the terrain based on the selected draw mode and level of detail. */ private void createTerrain() { - MapGenerator generator = new MapGenerator(); + MapGenerator generator = new MapGenerator(chunkSize); float[][] noiseMap = generator.getHeightMap(); // Create and display the noise map or color map based on the draw mode @@ -100,13 +102,18 @@ private void createTerrain() { // Generate the terrain mesh, apply transformations, and create a geometry node Mesh3D terrainMesh = new TerrainMeshLOD(noiseMap, levelOfDetail).getMesh(); - terrainMesh.apply(new ScaleModifier(3)); + terrainMesh.apply(new ScaleModifier(chunkScale)); terrainMesh.apply(new CenterAtModifier()); StaticGeometry terrainGeometry = new StaticGeometry(terrainMesh, mapMaterial); SceneNode terrainNode = new SceneNode(); terrainNode.addComponent(terrainGeometry); scene.addNode(terrainNode); + + // Visualize chunk + SceneNode chunkDisplayNode = new SceneNode(); + chunkDisplayNode.addComponent(new ChunkBoxDisplay(chunkSize * chunkScale)); + scene.addNode(chunkDisplayNode); } /** Sets up the camera with smooth fly-by controls. */ diff --git a/src/main/java/engine/render/effects/ParticleComponent.java b/src/main/java/engine/render/effects/ParticleComponent.java index e5012955..1cf15f25 100644 --- a/src/main/java/engine/render/effects/ParticleComponent.java +++ b/src/main/java/engine/render/effects/ParticleComponent.java @@ -47,7 +47,7 @@ public void onAttach() { * time. */ @Override - public void update(float tpf) { + public void onUpdate(float tpf) { emitter.update(tpf); } diff --git a/src/main/java/engine/scene/Scene.java b/src/main/java/engine/scene/Scene.java index 9348a9b7..b2877a44 100644 --- a/src/main/java/engine/scene/Scene.java +++ b/src/main/java/engine/scene/Scene.java @@ -6,9 +6,12 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import engine.scene.audio.AudioListener; +import engine.scene.audio.AudioSystem; import engine.scene.camera.Camera; import engine.scene.light.Light; import math.Color; +import math.Vector3f; import workspace.GraphicsPImpl; import workspace.ui.Graphics; @@ -44,9 +47,12 @@ public class Scene { /** The currently active camera that determines the scene's view transformation. */ private Camera activeCamera; + private AudioSystem audioSystem; + /** Constructs a {@code Scene} with a default name. */ public Scene() { this(DEFAULT_NAME); + audioSystem = new AudioSystem(); } /** @@ -112,7 +118,22 @@ public void removeNode(SceneNode node) { */ public void update(float deltaTime) { for (SceneNode node : rootNodes) { - updateExecutor.submit(() -> node.update(deltaTime)); + // updateExecutor.submit(() -> node.update(deltaTime)); + node.update(deltaTime); + } + updateAudio(); + } + + private void updateAudio() { + if (activeCamera == null) return; + + AudioListener listener = new AudioListener(); + listener.setPosition(activeCamera.getTransform().getPosition()); + listener.setForward(activeCamera.getTransform().getForward()); + + audioSystem.setListener(listener); + for (SceneNode node : rootNodes) { + node.updateAudio(audioSystem); } } @@ -121,12 +142,12 @@ public void update(float deltaTime) { * compatibility with most rendering APIs. */ public void render(Graphics g) { - g.clear(background); - if (activeCamera != null) { g.applyCamera(activeCamera); } + g.clear(background); + g.setWireframeMode(wireframeMode); renderLights(g); diff --git a/src/main/java/engine/scene/SceneNode.java b/src/main/java/engine/scene/SceneNode.java index 64cc3808..0590aa2f 100644 --- a/src/main/java/engine/scene/SceneNode.java +++ b/src/main/java/engine/scene/SceneNode.java @@ -6,6 +6,8 @@ import engine.components.Component; import engine.components.RenderableComponent; import engine.components.Transform; +import engine.scene.audio.AudioSource; +import engine.scene.audio.AudioSystem; import workspace.ui.Graphics; /** @@ -37,12 +39,16 @@ public class SceneNode { /** The default name assigned to a scene node if no name is provided. */ private static final String DEFAULT_NAME = "Untitled-Node"; + private boolean active; + /** 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; + private Transform transform; + /** List of child nodes attached to this node. */ private List children; @@ -59,11 +65,11 @@ public SceneNode(String name) { if (name == null) { throw new IllegalArgumentException("Name cannot be null."); } + this.active = true; this.name = name; - this.children = new ArrayList(); + this.transform = new Transform(); this.components = new ArrayList(); - // Add a default Transform component - this.components.add(new Transform()); + this.components.add(transform); } /** @@ -84,6 +90,8 @@ public SceneNode() { * @param g The graphics context used for rendering this node and its children. */ public void render(Graphics g) { + if (!active) return; + g.pushMatrix(); applyLocalTransform(g); @@ -110,6 +118,7 @@ private void applyLocalTransform(Graphics g) { * @param g The graphics context used for rendering. */ protected void renderComponents(Graphics g) { + if (!active) return; for (RenderableComponent renderer : getRenderComponents()) { renderer.render(g); } @@ -121,16 +130,26 @@ protected void renderComponents(Graphics g) { * @param tpf The time per frame in seconds (delta time). */ public void update(float tpf) { + if (!active) return; updateComponents(tpf); updateChildren(tpf); } + public void updateAudio(AudioSystem audioSystem) { + if (!active) return; + audioSystem.update(getComponents(AudioSource.class)); + for (SceneNode child : children) { + child.updateAudio(audioSystem); + } + } + /** * Updates all components attached to this node. * * @param tpf The time per frame in seconds. */ protected void updateComponents(float tpf) { + if (!active) return; for (Component component : components) { component.update(tpf); } @@ -142,6 +161,7 @@ protected void updateComponents(float tpf) { * @param tpf The time per frame in seconds. */ protected void updateChildren(float tpf) { + if (!active) return; for (SceneNode child : children) { child.update(tpf); } @@ -182,6 +202,9 @@ public void addChild(SceneNode child) { if (child == null) { throw new IllegalArgumentException("Child node cannot be null."); } + if (children == null) { + children = new ArrayList(); + } if (children.contains(child)) { return; } @@ -322,10 +345,7 @@ public boolean isLeaf() { /** Retrieves the Transform component associated with this node. */ public Transform getTransform() { - return getComponents(Transform.class) - .stream() - .findFirst() - .orElseThrow(() -> new IllegalStateException("Transform component is missing.")); + return transform; } /** @@ -349,4 +369,61 @@ public void setName(String name) { } this.name = name; } + + /** + * Retrieves the active state of this node. + * + *

This method returns whether the node is active or not. An active node is typically involved + * in updates, rendering, and other game logic, whereas an inactive node may be ignored during + * these processes. + * + * @return {@code true} if the node is active, {@code false} if it is inactive. + */ + public boolean isActive() { + return active; + } + + /** + * Sets the active state of this node. + * + *

This method changes the active state of the node. If the node's active state is already in + * the desired state, it does nothing to avoid unnecessary changes. When the active state is + * updated, it also propagates the change to the node's components and child nodes. + * + * @param active The desired active state for this node. {@code true} sets the node as active, + * {@code false} deactivates it. + */ + public void setActive(boolean active) { + // Skip if already in the desired state + if (active == this.active) return; + this.active = active; + setComponentsActive(); + setChildrenActive(); + } + + /** + * Sets the active state for all child nodes of this node. + * + *

This method recursively sets the active state of each child node in the hierarchy. If the + * parent node is set to active, all of its child nodes will also be activated, and vice versa. + * This ensures that all child nodes are consistently updated with the parent node's active state. + */ + protected void setChildrenActive() { + for (SceneNode child : children) { + child.setActive(active); + } + } + + /** + * Sets the active state for all components attached to this node. + * + *

This method updates the active state of each component attached to the node. This ensures + * that all components are either activated or deactivated in sync with the node itself, depending + * on the current active state of the node. + */ + protected void setComponentsActive() { + for (Component component : components) { + component.setActive(active); + } + } } diff --git a/src/main/java/engine/scene/audio/AudioListener.java b/src/main/java/engine/scene/audio/AudioListener.java new file mode 100644 index 00000000..b7f13989 --- /dev/null +++ b/src/main/java/engine/scene/audio/AudioListener.java @@ -0,0 +1,28 @@ +package engine.scene.audio; + +import math.Vector3f; + +public class AudioListener { + private Vector3f position; + private Vector3f forward; + + public AudioListener() { + this.position = new Vector3f(0, 0, 0); + } + + public Vector3f getPosition() { + return position; + } + + public void setPosition(Vector3f position) { + this.position = position; + } + + public Vector3f getForward() { + return forward; + } + + public void setForward(Vector3f forward) { + this.forward = forward; + } +} diff --git a/src/main/java/engine/scene/audio/AudioSource.java b/src/main/java/engine/scene/audio/AudioSource.java new file mode 100644 index 00000000..6728ebbb --- /dev/null +++ b/src/main/java/engine/scene/audio/AudioSource.java @@ -0,0 +1,160 @@ +package engine.scene.audio; + +import engine.components.AbstractComponent; +import math.Mathf; +import math.Vector3f; + +/** + * Represents an audio source attached to a scene node. The position of the audio source is + * determined by the position of the node, and the volume is adjusted based on the listener's + * distance from the source. + */ +public class AudioSource extends AbstractComponent { + + private Sound sound; + private float maxDistance; // Max distance for sound attenuation + private float volume; // Volume of the sound + private boolean loop; // Whether the sound should loop + + /** + * Constructs an {@code AudioSource} with a sound and a maximum distance for attenuation. + * + * @param sound the sound to be played by this audio source. + * @param maxDistance the maximum distance at which the sound will be audible. + */ + public AudioSource(Sound sound, float maxDistance) { + this.sound = sound; + this.maxDistance = maxDistance; + this.volume = 1.0f; + } + + /** Plays the sound, either looping or once depending on the loop setting. */ + public void play() { + if (sound == null) { + return; + } + if (loop) { + sound.loop(); + } else { + sound.play(); + } + } + + /** Stops the currently playing sound. */ + public void stop() { + if (sound != null) { + sound.stop(); + } + } + + /** + * Updates the volume of the sound based on the distance to the listener. + * + * @param audioListener The current listener (player, camera) in 3D space. + */ + public void update(AudioListener audioListener) { + if (sound == null) return; + + // Get the position of the audio source from the owner node's transform + Vector3f sourcePosition = getOwner().getTransform().getPosition(); + + // Calculate the distance between the audio source and the listener + float distance = audioListener.getPosition().distance(sourcePosition); + + // // Update the volume based on the distance + float newVolume = calculateVolume(distance); + // Set the new volume + sound.setVolume(newVolume); + + // Apply a basic distance attenuation (volume decreases as distance increases) + // float distanceFactor = Math.max(0, 1 - distance / 100.0f); // Example attenuation factor + // sound.setVolume(volume * distanceFactor); + + // Apply directional effects based on the listener's orientation + Vector3f directionToSound = sourcePosition.subtract(audioListener.getPosition()).normalize(); + float dotProduct = directionToSound.dot(audioListener.getForward()); // Directional panning + applyPanning(dotProduct); + } + + private void applyPanning(float dotProduct) { + // Adjust panning based on the listener's orientation (dot product of listener's forward + // direction) + // Positive values (dot > 0) = sound in front of listener + // Negative values (dot < 0) = sound behind the listener + + // Calculate the pan value: between -1 (left) and 1 (right) + // A simple approach is to map the dot product (-1 to 1) directly to pan + float pan = Mathf.clamp(dotProduct, -1.0f, 1.0f); + + // Apply the pan to the sound object +// sound.setPan(pan); + } + + // TODO Other methods for controlling playback, volume, pitch, etc. + + /** + * Calculates the volume of the sound based on the distance to the listener. The volume decreases + * with distance according to a linear attenuation model. + * + * @param distance the distance from the listener to the sound source. + * @return the new volume for the sound. + */ + private float calculateVolume(float distance) { + if (distance > maxDistance) return 0.0f; + return 1.0f - (distance / maxDistance); + } + + /** + * Sets whether the audio should loop when played. + * + * @param loop {@code true} if the sound should loop, {@code false} otherwise. + */ + public void setLoop(boolean loop) { + this.loop = loop; + } + + /** + * Sets the volume of the audio source. + * + * @param volume the new volume level (between 0.0f and 1.0f). + */ + public void setVolume(float volume) { + this.volume = volume; + if (sound != null) { + sound.setVolume(volume); + } + } + + /** + * Gets the current volume of the audio source. + * + * @return the volume level of the sound. + */ + public float getVolume() { + return volume; + } + + /** + * Gets the maximum distance for sound attenuation. + * + * @return the maximum distance for the sound. + */ + public float getMaxDistance() { + return maxDistance; + } + + @Override + public void onUpdate(float tpf) { + // Can be used for additional updates in the future. + } + + @Override + public void onAttach() { + // Logic to handle attachment if needed. + } + + @Override + public void onDetach() { + // Logic to handle detachment if needed. + } +} diff --git a/src/main/java/engine/scene/audio/AudioSystem.java b/src/main/java/engine/scene/audio/AudioSystem.java new file mode 100644 index 00000000..dc25fb71 --- /dev/null +++ b/src/main/java/engine/scene/audio/AudioSystem.java @@ -0,0 +1,22 @@ +package engine.scene.audio; + +import java.util.List; + +public class AudioSystem { + + private AudioListener listener; + + public void update(List audioSources) { + for (AudioSource audioSource : audioSources) { + audioSource.update(listener); + } + } + + public AudioListener getListener() { + return listener; + } + + public void setListener(AudioListener listener) { + this.listener = listener; + } +} diff --git a/src/main/java/engine/scene/audio/BackgroundMusicManager.java b/src/main/java/engine/scene/audio/BackgroundMusicManager.java new file mode 100644 index 00000000..86405193 --- /dev/null +++ b/src/main/java/engine/scene/audio/BackgroundMusicManager.java @@ -0,0 +1,15 @@ +package engine.scene.audio; + +public class BackgroundMusicManager { + private static Sound currentMusic; + + public static void playMusic(String name) { + if (currentMusic != null) { + currentMusic.stop(); + } + currentMusic = SoundManager.getSound(name); + if (currentMusic != null) { + currentMusic.loop(); + } + } +} diff --git a/src/main/java/engine/scene/audio/Sound.java b/src/main/java/engine/scene/audio/Sound.java new file mode 100644 index 00000000..1d2de2a3 --- /dev/null +++ b/src/main/java/engine/scene/audio/Sound.java @@ -0,0 +1,71 @@ +package engine.scene.audio; + +import java.io.File; +import java.io.IOException; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.sound.sampled.FloatControl; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.UnsupportedAudioFileException; + +import math.Mathf; + +public class Sound { + private Clip clip; + private FloatControl panControl; + + public Sound(String filePath) { + try { + AudioInputStream audioStream = AudioSystem.getAudioInputStream(new File(filePath)); + clip = AudioSystem.getClip(); + clip.open(audioStream); + + // Check if the clip supports panning + if (clip.isControlSupported(FloatControl.Type.PAN)) { + panControl = (FloatControl) clip.getControl(FloatControl.Type.PAN); + } + } catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) { + e.printStackTrace(); + } + } + + public void play() { + if (clip != null) { + clip.setFramePosition(0); // Rewind to the start + clip.start(); + } + } + + public void loop() { + if (clip != null) { + clip.loop(Clip.LOOP_CONTINUOUSLY); + } + } + + public void stop() { + if (clip != null) { + clip.stop(); + } + } + + public void setVolume(float volume) { + if (clip != null) { + FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); + float value = 20f * (float) Mathf.log10(volume); + gainControl.setValue(Mathf.clamp(value, gainControl.getMinimum(), gainControl.getMaximum())); + } + } + + /** + * Set the pan (left-right balance) for the sound. + * + * @param pan The pan value between -1.0 (left) and 1.0 (right). + */ + public void setPan(float pan) { + if (panControl != null) { + panControl.setValue(Mathf.clamp(pan, panControl.getMinimum(), panControl.getMaximum())); + } + } +} diff --git a/src/main/java/engine/scene/audio/SoundLoader.java b/src/main/java/engine/scene/audio/SoundLoader.java new file mode 100644 index 00000000..93aca20e --- /dev/null +++ b/src/main/java/engine/scene/audio/SoundLoader.java @@ -0,0 +1,8 @@ +package engine.scene.audio; + +public class SoundLoader { + public static Sound load(String filePath) { + return new Sound( + SoundLoader.class.getClassLoader().getResource("audio/" + filePath).getPath()); + } +} diff --git a/src/main/java/engine/scene/audio/SoundManager.java b/src/main/java/engine/scene/audio/SoundManager.java new file mode 100644 index 00000000..b90eb75d --- /dev/null +++ b/src/main/java/engine/scene/audio/SoundManager.java @@ -0,0 +1,51 @@ +package engine.scene.audio; + +import java.util.HashMap; +import java.util.Map; + +public class SoundManager { + private static final Map sounds = new HashMap<>(); + + public static void addSound(String name, String filePath) { + sounds.put(name, SoundLoader.load(filePath)); + } + + public static void playSound(String name) { + Sound sound = sounds.get(name); + if (sound != null) { + sound.play(); + } else { + sendErrorMessage(name); + } + } + + public static void loopSound(String name) { + Sound sound = sounds.get(name); + if (sound != null) { + sound.loop(); + } else { + sendErrorMessage(name); + } + } + + public static void stopSound(String name) { + Sound sound = sounds.get(name); + if (sound != null) { + sound.stop(); + } else { + sendErrorMessage(name); + } + } + + public static Sound getSound(String name) { + Sound sound = sounds.get(name); + if (sound == null) { + sendErrorMessage(name); + } + return sound; + } + + private static void sendErrorMessage(String name) { + System.err.println("Sound '" + name + "' not found!"); + } +} From 63f9668d97d7746a52b62f4ff3391f5f53424928 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Thu, 9 Jan 2025 18:56:45 +0100 Subject: [PATCH 3/4] Removed lazy init --- src/main/java/engine/scene/SceneNode.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/engine/scene/SceneNode.java b/src/main/java/engine/scene/SceneNode.java index 0590aa2f..182e65c2 100644 --- a/src/main/java/engine/scene/SceneNode.java +++ b/src/main/java/engine/scene/SceneNode.java @@ -68,6 +68,7 @@ public SceneNode(String name) { this.active = true; this.name = name; this.transform = new Transform(); + this.children = new ArrayList(); this.components = new ArrayList(); this.components.add(transform); } @@ -202,9 +203,6 @@ public void addChild(SceneNode child) { if (child == null) { throw new IllegalArgumentException("Child node cannot be null."); } - if (children == null) { - children = new ArrayList(); - } if (children.contains(child)) { return; } From 21e97678c5cdda7f85010f1edab5c89ecb551072 Mon Sep 17 00:00:00 2001 From: Simon Dietz Date: Thu, 9 Jan 2025 18:58:30 +0100 Subject: [PATCH 4/4] Components are active by default. --- src/main/java/engine/components/AbstractComponent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/engine/components/AbstractComponent.java b/src/main/java/engine/components/AbstractComponent.java index f600f725..a24afe86 100644 --- a/src/main/java/engine/components/AbstractComponent.java +++ b/src/main/java/engine/components/AbstractComponent.java @@ -16,7 +16,7 @@ public abstract class AbstractComponent implements Component { /** Indicates whether the component is currently active. */ - protected boolean active; + protected boolean active = true; /** Reference to the owning {@link SceneNode}. */ protected SceneNode owner;