From 9536db0e9ce089cf2dd0b94c8b6a1a0a074f7036 Mon Sep 17 00:00:00 2001 From: ShadelessFox Date: Mon, 20 May 2024 22:54:54 +0200 Subject: [PATCH] Model Viewer: Improve mouse capture; code cleanup and add perf metrics --- .../main/java/com/shade/gl/DebugGroup.java | 27 ++ .../data/viewer/model/ModelViewerPanel.java | 159 ++++++++++- .../com/shade/decima/model/viewer/Camera.java | 215 ++++++++++++++- .../{InputHandler.java => InputState.java} | 9 +- .../decima/model/viewer/ModelViewport.java | 124 ++++----- .../shade/decima/model/viewer/RenderLoop.java | 160 ----------- .../shade/decima/model/viewer/Renderer.java | 2 +- .../viewer/camera/FirstPersonCamera.java | 252 ------------------ .../model/viewer/renderer/ModelRenderer.java | 7 +- .../viewer/renderer/OutlineRenderer.java | 5 +- .../model/viewer/renderer/QuadRenderer.java | 3 +- .../viewer/renderer/ViewportRenderer.java | 5 +- .../ui/controls/graph/GraphComponent.java | 14 +- .../shade/platform/model/util/MathUtils.java | 12 +- .../shade/platform/util/MathUtilsTest.java | 16 ++ 15 files changed, 496 insertions(+), 514 deletions(-) create mode 100644 modules/bundle-lwjgl/src/main/java/com/shade/gl/DebugGroup.java rename modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/{InputHandler.java => InputState.java} (57%) delete mode 100644 modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/RenderLoop.java delete mode 100644 modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/camera/FirstPersonCamera.java create mode 100644 modules/platform-model/src/test/java/com/shade/platform/util/MathUtilsTest.java diff --git a/modules/bundle-lwjgl/src/main/java/com/shade/gl/DebugGroup.java b/modules/bundle-lwjgl/src/main/java/com/shade/gl/DebugGroup.java new file mode 100644 index 000000000..e5965325d --- /dev/null +++ b/modules/bundle-lwjgl/src/main/java/com/shade/gl/DebugGroup.java @@ -0,0 +1,27 @@ +package com.shade.gl; + +import com.shade.util.NotNull; +import org.lwjgl.opengl.GL43; + +/** + * Creates a named debug group for the current OpenGL context. + *

+ * Note: you can't reuse the same instance of this class. It's meant to be used in a try-with-resources block. + *

+ * try (var group = new DebugGroup("My Group")) {
+ *     // OpenGL calls
+ * }
+ *
+ * + * @param name the name of the debug group + */ +public record DebugGroup(@NotNull String name) implements AutoCloseable { + public DebugGroup { + GL43.glPushDebugGroup(GL43.GL_DEBUG_SOURCE_APPLICATION, 0, name); + } + + @Override + public void close() { + GL43.glPopDebugGroup(); + } +} diff --git a/modules/decima-ext-model-exporter/src/main/java/com/shade/decima/ui/data/viewer/model/ModelViewerPanel.java b/modules/decima-ext-model-exporter/src/main/java/com/shade/decima/ui/data/viewer/model/ModelViewerPanel.java index 253d55fd0..f06337d87 100644 --- a/modules/decima-ext-model-exporter/src/main/java/com/shade/decima/ui/data/viewer/model/ModelViewerPanel.java +++ b/modules/decima-ext-model-exporter/src/main/java/com/shade/decima/ui/data/viewer/model/ModelViewerPanel.java @@ -2,9 +2,8 @@ import com.formdev.flatlaf.FlatClientProperties; import com.shade.decima.model.rtti.objects.RTTIObject; +import com.shade.decima.model.viewer.Camera; import com.shade.decima.model.viewer.ModelViewport; -import com.shade.decima.model.viewer.RenderLoop; -import com.shade.decima.model.viewer.camera.FirstPersonCamera; import com.shade.decima.model.viewer.isr.Node; import com.shade.decima.model.viewer.isr.impl.NodeModel; import com.shade.decima.ui.data.ValueController; @@ -15,14 +14,22 @@ import com.shade.platform.ui.UIColor; import com.shade.platform.ui.dialogs.ProgressDialog; import com.shade.platform.ui.menus.MenuManager; +import com.shade.util.NotNull; import com.shade.util.Nullable; +import org.lwjgl.opengl.awt.AWTGLCanvas; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; +import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; public class ModelViewerPanel extends JComponent implements Disposable, PropertyChangeListener { public static final DataKey PANEL_KEY = new DataKey<>("panel", ModelViewerPanel.class); @@ -46,7 +53,7 @@ public ModelViewerPanel() { add(bottomToolbar, BorderLayout.SOUTH); try { - viewport = new ModelViewport(new FirstPersonCamera()); + viewport = new ModelViewport(new Camera()); viewport.setPreferredSize(new Dimension(400, 400)); viewport.setMinimumSize(new Dimension(100, 100)); viewport.addPropertyChangeListener(this); @@ -163,4 +170,150 @@ private void updatePreview() { public ValueController getController() { return controller; } + + private static class RenderLoop extends Thread implements Disposable { + private final Window window; + private final AWTGLCanvas canvas; + + private final Handler handler; + + private final AtomicBoolean isRunning = new AtomicBoolean(true); + private final AtomicBoolean isThrottling = new AtomicBoolean(false); + + private final Lock renderLock = new ReentrantLock(); + private final Condition canRender = renderLock.newCondition(); + + public RenderLoop(@NotNull Window window, @NotNull AWTGLCanvas canvas) { + super("Render Loop"); + + this.window = window; + this.canvas = canvas; + this.handler = new Handler(); + + canvas.addHierarchyListener(handler); + canvas.addComponentListener(handler); + window.addWindowListener(handler); + } + + @Override + public void run() { + while (isRunning.get()) { + try { + renderLock.lock(); + + while (isThrottling.get()) { + canRender.awaitUninterruptibly(); + } + } finally { + renderLock.unlock(); + } + + try { + SwingUtilities.invokeAndWait(() -> { + beforeRender(); + + if (canvas.isValid()) { + canvas.render(); + } + + afterRender(); + }); + } catch (InterruptedException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + canvas.removeHierarchyListener(handler); + window.removeWindowListener(handler); + } + + @Override + public void dispose() { + isRunning.set(false); + } + + protected void beforeRender() { + // do nothing by default + } + + protected void afterRender() { + // do nothing by default + } + + private class Handler extends WindowAdapter implements HierarchyListener, ComponentListener { + @Override + public void hierarchyChanged(HierarchyEvent e) { + if (e.getID() == HierarchyEvent.HIERARCHY_CHANGED && (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) { + handle(); + } + } + + @Override + public void componentResized(ComponentEvent e) { + handle(); + } + + @Override + public void componentMoved(ComponentEvent e) { + handle(); + } + + @Override + public void componentShown(ComponentEvent e) { + handle(); + } + + @Override + public void componentHidden(ComponentEvent e) { + handle(); + } + + @Override + public void windowActivated(WindowEvent e) { + handleAsync(); + } + + @Override + public void windowDeactivated(WindowEvent e) { + handleAsync(); + } + + private void handleAsync() { + SwingUtilities.invokeLater(this::handle); + } + + private void handle() { + renderLock.lock(); + + try { + isThrottling.set(isThrottling()); + canRender.signal(); + } finally { + renderLock.unlock(); + } + } + + private boolean isThrottling() { + return !canvas.isShowing() || canvas.getWidth() <= 0 || canvas.getHeight() <= 0 || !isActive(window); + } + + private static boolean isActive(@NotNull Window window) { + if (window instanceof Dialog dialog && dialog.getModalityType() != Dialog.ModalityType.MODELESS) { + return false; + } + + if (window.isActive()) { + return true; + } + + for (Window ownedWindow : window.getOwnedWindows()) { + if (isActive(ownedWindow)) { + return true; + } + } + + return false; + } + } + } } diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/Camera.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/Camera.java index 00a47b145..623c964bf 100644 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/Camera.java +++ b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/Camera.java @@ -1,22 +1,219 @@ package com.shade.decima.model.viewer; +import com.shade.platform.model.util.MathUtils; import com.shade.util.NotNull; -import org.joml.Matrix4fc; -import org.joml.Vector3fc; +import org.joml.*; -public interface Camera { - void update(float dt, @NotNull InputHandler input); +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.lang.Math; - void resize(int width, int height); +public class Camera { + private static final Vector3fc UP = new Vector3f(0.0f, 1.0f, 0.0f); + private static final Vector3fc FORWARD = new Vector3f(0.0f, 0.0f, 1.0f); + + private static final float FOV = 45.0f; + private static final float SENSITIVITY = 0.2f; + private static final float CLIP_NEAR = 0.01f; + private static final float CLIP_FAR = 1000.0f; + private static final float SPEED_FACTOR = 5.0f; + + private final Vector3f target = new Vector3f(0.0f, 0.0f, 0.0f); + private final Vector3f position = new Vector3f(0.0f, 0.0f, 0.0f); + private final Quaternionf rotation = new Quaternionf(); + + private final Matrix4f projectionMatrix = new Matrix4f(); + private final Matrix4f viewMatrix = new Matrix4f(); + + private float yaw = -90.0f; + private float pitch = -15.0f; + private float speed = 1.0f; + private float distance = 2.0f; + + public Camera() { + updateRotation(); + updatePositionFromTarget(); + } + + public void update(float dt, @NotNull InputState input) { + final CameraMode mode; + + if (input.isMouseDown(MouseEvent.BUTTON1)) { + mode = CameraMode.FPS; + } else if (input.isMouseDown(MouseEvent.BUTTON2)) { + mode = CameraMode.PAN; + } else if (input.isMouseDown(MouseEvent.BUTTON3)) { + mode = CameraMode.ARCBALL; + } else { + mode = null; + } + + if (mode != null) { + final var mouseDelta = input.getMousePositionDelta().mul(SENSITIVITY); + final var wheelDelta = input.getMouseWheelRotationDelta() * SENSITIVITY; + + switch (mode) { + case FPS -> updateFps(input, mouseDelta, wheelDelta, dt); + case PAN -> updatePan(input, mouseDelta, wheelDelta, dt); + case ARCBALL -> updateArcball(input, mouseDelta, wheelDelta, dt); + } + } + + viewMatrix.setLookAt(position, target, UP); + } + + public void resize(int width, int height) { + projectionMatrix.setPerspective((float) Math.toRadians(FOV), (float) width / height, CLIP_NEAR, CLIP_FAR); + } @NotNull - Vector3fc getPosition(); + public Vector3fc getPosition() { + return position; + } - void setPosition(@NotNull Vector3fc position); + public void setPosition(@NotNull Vector3fc position) { + this.position.set(position); + updateTargetFromPosition(); + } @NotNull - Matrix4fc getViewMatrix(); + public Matrix4fc getViewMatrix() { + return viewMatrix; + } @NotNull - Matrix4fc getProjectionMatrix(); + public Matrix4fc getProjectionMatrix() { + return projectionMatrix; + } + + private void updateFps(@NotNull InputState input, @NotNull Vector2fc mouseDelta, float wheelDelta, float dt) { + boolean updateTarget = false; + + if (mouseDelta.x() != 0.0f || mouseDelta.y() != 0.0f) { + yaw = (yaw + mouseDelta.x()) % 360; + pitch = MathUtils.clamp(pitch + mouseDelta.y(), -89.0f, 89.0f); + + updateRotation(); + updateTarget = true; + } + + if (wheelDelta != 0.0f) { + speed = MathUtils.clamp((float) Math.exp(Math.log(speed) + wheelDelta), 0.01f, 10.0f); + } + + float speed = this.speed * dt; + + if (input.isKeyDown(KeyEvent.VK_SHIFT)) { + speed *= SPEED_FACTOR; + } + + if (input.isKeyDown(KeyEvent.VK_CONTROL)) { + speed /= SPEED_FACTOR; + } + + if (input.isKeyDown(KeyEvent.VK_Q) || input.isKeyDown(KeyEvent.VK_E)) { + final Vector3f up = new Vector3f(UP).mul(speed); + + if (input.isKeyDown(KeyEvent.VK_E)) { + position.add(up); + } else { + position.sub(up); + } + + updateTarget = true; + } + + if (input.isKeyDown(KeyEvent.VK_W) || input.isKeyDown(KeyEvent.VK_S)) { + final Vector3f forward = getForwardVector().mul(speed); + + if (input.isKeyDown(KeyEvent.VK_W)) { + position.add(forward); + } else { + position.sub(forward); + } + + updateTarget = true; + } + + if (input.isKeyDown(KeyEvent.VK_A) || input.isKeyDown(KeyEvent.VK_D)) { + final Vector3f right = getForwardVector().cross(UP).normalize().mul(speed); + + if (input.isKeyDown(KeyEvent.VK_D)) { + position.add(right); + } else { + position.sub(right); + } + + updateTarget = true; + } + + if (updateTarget) { + distance = target.distance(position); + target.set(position).add(getForwardVector().mul(distance)); + } + } + + private void updatePan(@NotNull InputState input, @NotNull Vector2fc mouseDelta, float wheelDelta, float dt) { + if (mouseDelta.x() != 0.0f || mouseDelta.y() != 0.0f) { + final Vector3f forward = getForwardVector(); + + if (input.isKeyDown(KeyEvent.VK_SHIFT)) { + position.add(forward.mul(mouseDelta.y(), new Vector3f())); + } else { + final Vector3f right = forward.cross(UP, new Vector3f()).normalize(); + final Vector3f up = forward.cross(right, new Vector3f()).normalize(); + + position.sub(right.mul(mouseDelta.x() / SPEED_FACTOR)); + position.add(up.mul(mouseDelta.y() / SPEED_FACTOR)); + } + + updateTargetFromPosition(); + } + } + + private void updateArcball(@NotNull InputState input, @NotNull Vector2fc mouseDelta, float wheelDelta, float dt) { + boolean updatePosition = false; + + if (mouseDelta.x() != 0.0f || mouseDelta.y() != 0.0f) { + yaw = (yaw + mouseDelta.x()) % 360; + pitch = MathUtils.clamp(pitch + mouseDelta.y(), -89.0f, 89.0f); + + updateRotation(); + updatePosition = true; + } + + if (wheelDelta != 0.0f) { + distance = Math.max(0.0f, (float) Math.exp(Math.log(distance) - wheelDelta)); + updatePosition = true; + } + + if (updatePosition) { + updatePositionFromTarget(); + } + } + + private void updateTargetFromPosition() { + target.set(position).add(getForwardVector().mul(distance)); + } + + private void updatePositionFromTarget() { + position.set(target).sub(getForwardVector().mul(distance)); + } + + private void updateRotation() { + rotation.identity(); + rotation.rotateY((float) -Math.toRadians(yaw)); + rotation.rotateX((float) -Math.toRadians(pitch)); + } + + @NotNull + private Vector3f getForwardVector() { + return FORWARD.rotate(rotation, new Vector3f()); + } + + private enum CameraMode { + FPS, + PAN, + ARCBALL + } } diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/InputHandler.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/InputState.java similarity index 57% rename from modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/InputHandler.java rename to modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/InputState.java index 5c5d3305d..aae2926f7 100644 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/InputHandler.java +++ b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/InputState.java @@ -3,16 +3,13 @@ import com.shade.util.NotNull; import org.joml.Vector2f; -public interface InputHandler { +public interface InputState { boolean isKeyDown(int keyCode); boolean isMouseDown(int mouseButton); @NotNull - Vector2f getMouseOrigin(); + Vector2f getMousePositionDelta(); - @NotNull - Vector2f getMousePosition(); - - float getMouseWheelRotation(); + float getMouseWheelRotationDelta(); } diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/ModelViewport.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/ModelViewport.java index 5ed3cb319..6b45b9879 100644 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/ModelViewport.java +++ b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/ModelViewport.java @@ -7,8 +7,10 @@ import com.shade.decima.model.viewer.renderer.ModelRenderer; import com.shade.decima.model.viewer.renderer.OutlineRenderer; import com.shade.decima.model.viewer.renderer.ViewportRenderer; +import com.shade.gl.DebugGroup; import com.shade.platform.model.Disposable; import com.shade.platform.model.data.DataKey; +import com.shade.platform.model.util.MathUtils; import com.shade.util.NotNull; import com.shade.util.Nullable; import org.joml.Vector2f; @@ -22,12 +24,11 @@ import javax.swing.*; import java.awt.*; import java.awt.event.*; -import java.awt.image.BufferedImage; import java.io.IOException; import java.io.UncheckedIOException; -import java.util.HashMap; -import java.util.Map; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import static org.lwjgl.opengl.GL15.*; import static org.lwjgl.opengl.GL43.*; @@ -92,6 +93,8 @@ public void initGL() { glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, true); + glDebugMessageControl(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_PUSH_GROUP, GL_DONT_CARE, 0, false); + glDebugMessageControl(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_POP_GROUP, GL_DONT_CARE, 0, false); glDebugMessageCallback(new DebugCallback(), 0); try { @@ -117,21 +120,24 @@ public void paintGL() { camera.resize(width, height); camera.update(delta, handler); + handler.clear(); - viewportRenderer.update(delta, handler, this); + try (var ignored = new DebugGroup("viewport")) { + viewportRenderer.update(delta, this); + } outlineRenderer.bind(width, height); - { + try (var ignored = new DebugGroup("model")) { modelRenderer.setSelectionOnly(true); - modelRenderer.update(delta, handler, this); + modelRenderer.update(delta, this); modelRenderer.setSelectionOnly(false); - modelRenderer.update(delta, handler, this); + modelRenderer.update(delta, this); } outlineRenderer.unbind(); - outlineRenderer.update(delta, handler, this); + outlineRenderer.update(delta, this); lastFrameTime = currentFrameTime; swapBuffers(); @@ -235,19 +241,15 @@ public void setSoftShading(boolean softShading) { this.softShading = softShading; } - private class Handler extends MouseAdapter implements KeyListener, InputHandler, FocusListener { - private static final Cursor EMPTY_CURSOR = Toolkit.getDefaultToolkit().createCustomCursor( - new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB), - new Point(0, 0), - "empty cursor" - ); - + private class Handler extends MouseAdapter implements KeyListener, FocusListener, InputState { private final Robot robot; - private final Map mouseState = new HashMap<>(); - private final Map keyState = new HashMap<>(); - private final Vector2f origin = new Vector2f(); - private final Vector2f position = new Vector2f(); - private float wheelRotation = 0.0f; + private final Set mouseState = new HashSet<>(); + private final Set keyState = new HashSet<>(3); + + private final Point mouseStart = new Point(); + private final Point mouseRecent = new Point(); + private final Point mouseDelta = new Point(); + private float mouseWheelDelta; public Handler(@Nullable Robot robot) { this.robot = robot; @@ -255,40 +257,43 @@ public Handler(@Nullable Robot robot) { @Override public void mousePressed(MouseEvent e) { - mouseState.put(e.getButton(), true); - origin.set(e.getX(), e.getY()); - position.set(e.getX(), e.getY()); - - if (robot != null) { - setCursor(EMPTY_CURSOR); - } + mouseState.add(e.getButton()); + mouseStart.setLocation(e.getPoint()); + mouseRecent.setLocation(mouseStart); + mouseDelta.setLocation(0, 0); + SwingUtilities.convertPointToScreen(mouseRecent, ModelViewport.this); } @Override public void mouseReleased(MouseEvent e) { - mouseState.put(e.getButton(), false); - - if (robot != null) { - setCursor(null); - } + mouseState.remove(e.getButton()); } @Override public void mouseDragged(MouseEvent e) { - if (robot != null) { - final Point point = new Point((int) origin.x, (int) origin.y); - SwingUtilities.convertPointToScreen(point, ModelViewport.this); + final Point mouse = e.getLocationOnScreen(); + final Rectangle bounds = new Rectangle(getLocationOnScreen(), getSize()); + + // Shrink the bounds in case the window is maximized so the mouse can move out of bounds there + bounds.width -= 1; + bounds.height -= 1; - robot.mouseMove(point.x, point.y); - position.add(e.getX(), e.getY()).sub(origin); + if (robot != null && !bounds.contains(mouse)) { + mouse.x = MathUtils.wrapAround(mouse.x, bounds.x, bounds.x + bounds.width); + mouse.y = MathUtils.wrapAround(mouse.y, bounds.y, bounds.y + bounds.height); + + robot.mouseMove(mouse.x, mouse.y); + mouseRecent.setLocation(mouse.x, mouse.y); } else { - position.set(e.getX(), e.getY()); + mouseDelta.x += mouse.x - mouseRecent.x; + mouseDelta.y -= mouse.y - mouseRecent.y; + mouseRecent.setLocation(mouse); } } @Override public void mouseWheelMoved(MouseWheelEvent e) { - wheelRotation -= (float) e.getPreciseWheelRotation(); + mouseWheelDelta -= (float) e.getPreciseWheelRotation(); } @Override @@ -298,51 +303,50 @@ public void keyTyped(KeyEvent e) { @Override public void keyPressed(KeyEvent e) { - keyState.put(e.getKeyCode(), true); + keyState.add(e.getKeyCode()); } @Override public void keyReleased(KeyEvent e) { - keyState.put(e.getKeyCode(), false); + keyState.remove(e.getKeyCode()); } @Override - public boolean isKeyDown(int keyCode) { - return keyState.getOrDefault(keyCode, false); + public void focusGained(FocusEvent e) { + // do nothing } @Override - public boolean isMouseDown(int mouseButton) { - return mouseState.getOrDefault(mouseButton, false); + public void focusLost(FocusEvent e) { + keyState.clear(); + mouseState.clear(); + setCursor(null); } - @NotNull @Override - public Vector2f getMouseOrigin() { - return new Vector2f(origin); + public boolean isKeyDown(int keyCode) { + return keyState.contains(keyCode); } - @NotNull @Override - public Vector2f getMousePosition() { - return new Vector2f(position); + public boolean isMouseDown(int mouseButton) { + return mouseState.contains(mouseButton); } + @NotNull @Override - public float getMouseWheelRotation() { - return wheelRotation; + public Vector2f getMousePositionDelta() { + return new Vector2f(mouseDelta.x, mouseDelta.y); } @Override - public void focusGained(FocusEvent e) { - // do nothing + public float getMouseWheelRotationDelta() { + return mouseWheelDelta; } - @Override - public void focusLost(FocusEvent e) { - keyState.clear(); - mouseState.clear(); - setCursor(null); + private void clear() { + mouseDelta.setLocation(0, 0); + mouseWheelDelta = 0.0f; } } diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/RenderLoop.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/RenderLoop.java deleted file mode 100644 index 95803b6c3..000000000 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/RenderLoop.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.shade.decima.model.viewer; - -import com.shade.platform.model.Disposable; -import com.shade.util.NotNull; -import org.lwjgl.opengl.awt.AWTGLCanvas; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.*; -import java.lang.reflect.InvocationTargetException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -public class RenderLoop extends Thread implements Disposable { - private final Window window; - private final AWTGLCanvas canvas; - - private final Handler handler; - - private final AtomicBoolean isRunning = new AtomicBoolean(true); - private final AtomicBoolean isThrottling = new AtomicBoolean(false); - - private final Lock renderLock = new ReentrantLock(); - private final Condition canRender = renderLock.newCondition(); - - public RenderLoop(@NotNull Window window, @NotNull AWTGLCanvas canvas) { - super("Render Loop"); - - this.window = window; - this.canvas = canvas; - this.handler = new Handler(); - - canvas.addHierarchyListener(handler); - canvas.addComponentListener(handler); - window.addWindowListener(handler); - } - - @Override - public void run() { - while (isRunning.get()) { - try { - renderLock.lock(); - - while (isThrottling.get()) { - canRender.awaitUninterruptibly(); - } - } finally { - renderLock.unlock(); - } - - try { - SwingUtilities.invokeAndWait(() -> { - beforeRender(); - - if (canvas.isValid()) { - canvas.render(); - } - - afterRender(); - }); - } catch (InterruptedException | InvocationTargetException e) { - throw new IllegalStateException(e); - } - } - - canvas.removeHierarchyListener(handler); - window.removeWindowListener(handler); - } - - @Override - public void dispose() { - isRunning.set(false); - } - - protected void beforeRender() { - // do nothing by default - } - - protected void afterRender() { - // do nothing by default - } - - private class Handler extends WindowAdapter implements HierarchyListener, ComponentListener { - @Override - public void hierarchyChanged(HierarchyEvent e) { - if (e.getID() == HierarchyEvent.HIERARCHY_CHANGED && (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) { - handle(); - } - } - - @Override - public void componentResized(ComponentEvent e) { - handle(); - } - - @Override - public void componentMoved(ComponentEvent e) { - handle(); - } - - @Override - public void componentShown(ComponentEvent e) { - handle(); - } - - @Override - public void componentHidden(ComponentEvent e) { - handle(); - } - - @Override - public void windowActivated(WindowEvent e) { - handleAsync(); - } - - @Override - public void windowDeactivated(WindowEvent e) { - handleAsync(); - } - - private void handleAsync() { - SwingUtilities.invokeLater(this::handle); - } - - private void handle() { - renderLock.lock(); - - try { - isThrottling.set(isThrottling()); - canRender.signal(); - } finally { - renderLock.unlock(); - } - } - - private boolean isThrottling() { - return !canvas.isShowing() || canvas.getWidth() <= 0 || canvas.getHeight() <= 0 || !isActive(window); - } - - private static boolean isActive(@NotNull Window window) { - if (window instanceof Dialog dialog && dialog.getModalityType() != Dialog.ModalityType.MODELESS) { - return false; - } - - if (window.isActive()) { - return true; - } - - for (Window ownedWindow : window.getOwnedWindows()) { - if (isActive(ownedWindow)) { - return true; - } - } - - return false; - } - } -} diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/Renderer.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/Renderer.java index 6d00bf99f..e19d1b6ce 100644 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/Renderer.java +++ b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/Renderer.java @@ -8,5 +8,5 @@ public interface Renderer extends Disposable { void setup() throws IOException; - void update(float dt, @NotNull InputHandler handler, @NotNull ModelViewport viewport); + void update(float dt, @NotNull ModelViewport viewport); } diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/camera/FirstPersonCamera.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/camera/FirstPersonCamera.java deleted file mode 100644 index 3e1fedee2..000000000 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/camera/FirstPersonCamera.java +++ /dev/null @@ -1,252 +0,0 @@ -package com.shade.decima.model.viewer.camera; - -import com.shade.decima.model.viewer.Camera; -import com.shade.decima.model.viewer.InputHandler; -import com.shade.platform.model.util.MathUtils; -import com.shade.util.NotNull; -import org.joml.*; - -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.lang.Math; - -public class FirstPersonCamera implements Camera { - private static final Vector3fc UP = new Vector3f(0.0f, 1.0f, 0.0f); - private static final Vector3fc FORWARD = new Vector3f(0.0f, 0.0f, 1.0f); - - private static final float FOV = 45.0f; - private static final float SENSITIVITY = 0.2f; - private static final float CLIP_NEAR = 0.01f; - private static final float CLIP_FAR = 1000.0f; - private static final float SPEED_FACTOR = 5.0f; - - private final Vector3f target = new Vector3f(0.0f, 0.0f, 0.0f); - private final Vector3f position = new Vector3f(0.0f, 0.0f, 0.0f); - private final Quaternionf rotation = new Quaternionf(); - - private final Vector2f lastMouseOrigin = new Vector2f(); - private final Vector2f lastMousePosition = new Vector2f(); - private float lastWheelRotation = 0.0f; - - private final Matrix4f projectionMatrix = new Matrix4f(); - private final Matrix4f viewMatrix = new Matrix4f(); - - private float yaw = 0.0f; - private float pitch = 0.0f; - private float speed = 1.0f; - private float distance = 1.0f; - - public FirstPersonCamera() { - yaw = -90.0f; - pitch = -15.0f; - distance = 2.0f; - - updateRotation(); - updatePositionFromTarget(); - } - - @Override - public void update(float dt, @NotNull InputHandler input) { - final CameraMode mode; - - if (input.isMouseDown(MouseEvent.BUTTON1)) { - mode = CameraMode.FPS; - } else if (input.isMouseDown(MouseEvent.BUTTON2)) { - mode = CameraMode.PAN; - } else if (input.isMouseDown(MouseEvent.BUTTON3)) { - mode = CameraMode.ARCBALL; - } else { - mode = null; - } - - final float oldWheelRotation = lastWheelRotation; - final float newWheelRotation = input.getMouseWheelRotation() * 0.2f; - final float wheelDelta = newWheelRotation - oldWheelRotation; - lastWheelRotation = newWheelRotation; - - if (mode != null) { - final Vector2f mouse = input.getMousePosition(); - final Vector2f origin = input.getMouseOrigin(); - final Vector2f mouseDelta = new Vector2f(); - - if (lastMouseOrigin.equals(origin)) { - mouseDelta.x = mouse.x - lastMousePosition.x; - mouseDelta.y = lastMousePosition.y - mouse.y; - mouseDelta.mul(SENSITIVITY); - } - - switch (mode) { - case FPS -> updateFps(input, mouseDelta, wheelDelta, dt); - case PAN -> updatePan(input, mouseDelta, wheelDelta, dt); - case ARCBALL -> updateArcball(input, mouseDelta, wheelDelta, dt); - } - - lastMousePosition.set(mouse); - lastMouseOrigin.set(origin); - } else { - lastMouseOrigin.set(input.getMousePosition()); - } - - viewMatrix.setLookAt(position, target, UP); - } - - @Override - public void resize(int width, int height) { - projectionMatrix.setPerspective((float) Math.toRadians(FOV), (float) width / height, CLIP_NEAR, CLIP_FAR); - } - - @NotNull - @Override - public Vector3fc getPosition() { - return position; - } - - @Override - public void setPosition(@NotNull Vector3fc position) { - this.position.set(position); - updateTargetFromPosition(); - } - - @NotNull - @Override - public Matrix4fc getViewMatrix() { - return viewMatrix; - } - - @NotNull - @Override - public Matrix4fc getProjectionMatrix() { - return projectionMatrix; - } - - private void updateFps(@NotNull InputHandler input, @NotNull Vector2fc mouseDelta, float wheelDelta, float dt) { - boolean updateTarget = false; - - if (mouseDelta.x() != 0.0f || mouseDelta.y() != 0.0f) { - yaw = (yaw + mouseDelta.x()) % 360; - pitch = MathUtils.clamp(pitch + mouseDelta.y(), -89.0f, 89.0f); - - updateRotation(); - updateTarget = true; - } - - if (wheelDelta != 0.0f) { - speed = MathUtils.clamp((float) Math.exp(Math.log(speed) + wheelDelta), 0.01f, 10.0f); - } - - float speed = this.speed * dt; - - if (input.isKeyDown(KeyEvent.VK_SHIFT)) { - speed *= SPEED_FACTOR; - } - - if (input.isKeyDown(KeyEvent.VK_CONTROL)) { - speed /= SPEED_FACTOR; - } - - if (input.isKeyDown(KeyEvent.VK_Q) || input.isKeyDown(KeyEvent.VK_E)) { - final Vector3f up = new Vector3f(UP).mul(speed); - - if (input.isKeyDown(KeyEvent.VK_E)) { - position.add(up); - } else { - position.sub(up); - } - - updateTarget = true; - } - - if (input.isKeyDown(KeyEvent.VK_W) || input.isKeyDown(KeyEvent.VK_S)) { - final Vector3f forward = getForwardVector().mul(speed); - - if (input.isKeyDown(KeyEvent.VK_W)) { - position.add(forward); - } else { - position.sub(forward); - } - - updateTarget = true; - } - - if (input.isKeyDown(KeyEvent.VK_A) || input.isKeyDown(KeyEvent.VK_D)) { - final Vector3f right = getForwardVector().cross(UP).normalize().mul(speed); - - if (input.isKeyDown(KeyEvent.VK_D)) { - position.add(right); - } else { - position.sub(right); - } - - updateTarget = true; - } - - if (updateTarget) { - distance = target.distance(position); - target.set(position).add(getForwardVector().mul(distance)); - } - } - - private void updatePan(@NotNull InputHandler input, @NotNull Vector2fc mouseDelta, float wheelDelta, float dt) { - if (mouseDelta.x() != 0.0f || mouseDelta.y() != 0.0f) { - final Vector3f forward = getForwardVector(); - - if (input.isKeyDown(KeyEvent.VK_SHIFT)) { - position.add(forward.mul(mouseDelta.y(), new Vector3f())); - } else { - final Vector3f right = forward.cross(UP, new Vector3f()).normalize(); - final Vector3f up = forward.cross(right, new Vector3f()).normalize(); - - position.sub(right.mul(mouseDelta.x() / SPEED_FACTOR)); - position.add(up.mul(mouseDelta.y() / SPEED_FACTOR)); - } - - updateTargetFromPosition(); - } - } - - private void updateArcball(@NotNull InputHandler input, @NotNull Vector2fc mouseDelta, float wheelDelta, float dt) { - boolean updatePosition = false; - - if (mouseDelta.x() != 0.0f || mouseDelta.y() != 0.0f) { - yaw = (yaw + mouseDelta.x()) % 360; - pitch = MathUtils.clamp(pitch + mouseDelta.y(), -89.0f, 89.0f); - - updateRotation(); - updatePosition = true; - } - - if (wheelDelta != 0.0f) { - distance = Math.max(0.0f, (float) Math.exp(Math.log(distance) - wheelDelta)); - updatePosition = true; - } - - if (updatePosition) { - updatePositionFromTarget(); - } - } - - private void updateTargetFromPosition() { - target.set(position).add(getForwardVector().mul(distance)); - } - - private void updatePositionFromTarget() { - position.set(target).sub(getForwardVector().mul(distance)); - } - - private void updateRotation() { - rotation.identity(); - rotation.rotateY((float) -Math.toRadians(yaw)); - rotation.rotateX((float) -Math.toRadians(pitch)); - } - - @NotNull - private Vector3f getForwardVector() { - return FORWARD.rotate(rotation, new Vector3f()); - } - - private enum CameraMode { - FPS, - PAN, - ARCBALL - } -} diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/ModelRenderer.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/ModelRenderer.java index c8302c34d..4838ae11c 100644 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/ModelRenderer.java +++ b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/ModelRenderer.java @@ -1,6 +1,9 @@ package com.shade.decima.model.viewer.renderer; -import com.shade.decima.model.viewer.*; +import com.shade.decima.model.viewer.Camera; +import com.shade.decima.model.viewer.Model; +import com.shade.decima.model.viewer.ModelViewport; +import com.shade.decima.model.viewer.Renderer; import com.shade.decima.model.viewer.isr.impl.NodeModel; import com.shade.decima.model.viewer.shader.NormalShaderProgram; import com.shade.decima.model.viewer.shader.RegularShaderProgram; @@ -28,7 +31,7 @@ public void setup() throws IOException { } @Override - public void update(float dt, @NotNull InputHandler handler, @NotNull ModelViewport viewport) { + public void update(float dt, @NotNull ModelViewport viewport) { if (model == null) { return; } diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/OutlineRenderer.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/OutlineRenderer.java index 6a00d0956..fb36c21f5 100644 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/OutlineRenderer.java +++ b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/OutlineRenderer.java @@ -1,6 +1,5 @@ package com.shade.decima.model.viewer.renderer; -import com.shade.decima.model.viewer.InputHandler; import com.shade.decima.model.viewer.ModelViewport; import com.shade.decima.model.viewer.shader.OutlineShaderProgram; import com.shade.platform.model.Disposable; @@ -84,7 +83,7 @@ public void unbind() { } @Override - public void update(float dt, @NotNull InputHandler handler, @NotNull ModelViewport viewport) { + public void update(float dt, @NotNull ModelViewport viewport) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, colorTextureId); @@ -95,7 +94,7 @@ public void update(float dt, @NotNull InputHandler handler, @NotNull ModelViewpo program.colorSampler.set(0); program.maskSampler.set(1); - super.update(dt, handler, viewport); + super.update(dt, viewport); } } diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/QuadRenderer.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/QuadRenderer.java index c5639ddc8..0f30b7d58 100644 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/QuadRenderer.java +++ b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/QuadRenderer.java @@ -1,6 +1,5 @@ package com.shade.decima.model.viewer.renderer; -import com.shade.decima.model.viewer.InputHandler; import com.shade.decima.model.viewer.ModelViewport; import com.shade.decima.model.viewer.Renderer; import com.shade.gl.Attribute; @@ -34,7 +33,7 @@ public void setup() throws IOException { } @Override - public void update(float dt, @NotNull InputHandler handler, @NotNull ModelViewport viewport) { + public void update(float dt, @NotNull ModelViewport viewport) { try (VAO ignored = vao.bind()) { GL11.glDrawArrays(GL11.GL_TRIANGLE_STRIP, 0, 4); } diff --git a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/ViewportRenderer.java b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/ViewportRenderer.java index 05a7ea360..536fb8ce4 100644 --- a/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/ViewportRenderer.java +++ b/modules/decima-ext-model-viewer/src/main/java/com/shade/decima/model/viewer/renderer/ViewportRenderer.java @@ -1,7 +1,6 @@ package com.shade.decima.model.viewer.renderer; import com.shade.decima.model.viewer.Camera; -import com.shade.decima.model.viewer.InputHandler; import com.shade.decima.model.viewer.ModelViewport; import com.shade.decima.model.viewer.shader.ViewportShaderProgram; import com.shade.platform.model.Disposable; @@ -25,7 +24,7 @@ public void setup() throws IOException { } @Override - public void update(float dt, @NotNull InputHandler handler, @NotNull ModelViewport viewport) { + public void update(float dt, @NotNull ModelViewport viewport) { final Camera camera = viewport.getCamera(); glEnable(GL_BLEND); @@ -39,7 +38,7 @@ public void update(float dt, @NotNull InputHandler handler, @NotNull ModelViewpo program.getOddColor().set(new Vector3f(ColorIcon.getColor(viewport.getBackground(), true).getColorComponents(null))); program.getEvenColor().set(new Vector3f(ColorIcon.getColor(viewport.getBackground(), false).getColorComponents(null))); - super.update(dt, handler, viewport); + super.update(dt, viewport); } glEnable(GL_DEPTH_TEST); diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/controls/graph/GraphComponent.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/controls/graph/GraphComponent.java index 1d9da4e11..9aa0e97cb 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/controls/graph/GraphComponent.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/controls/graph/GraphComponent.java @@ -5,6 +5,7 @@ import com.shade.decima.model.util.graph.GraphLayout; import com.shade.decima.model.util.graph.GraphLayoutConfig; import com.shade.decima.model.util.graph.impl.HorizontalGraphVisualizer; +import com.shade.platform.model.util.MathUtils; import com.shade.util.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -366,17 +367,8 @@ public void mouseDragged(MouseEvent e) { final Rectangle bounds = new Rectangle(viewport.getLocationOnScreen(), viewport.getSize()); if (robot != null && !bounds.contains(mouse)) { - if (mouse.x >= bounds.x + bounds.width) { - mouse.x = bounds.x + 1; - } else if (mouse.x < bounds.x) { - mouse.x = bounds.x + bounds.width - 1; - } - - if (mouse.y >= bounds.y + bounds.height) { - mouse.y = bounds.y + 1; - } else if (mouse.y < bounds.y) { - mouse.y = bounds.y + bounds.height - 1; - } + mouse.x = MathUtils.wrapAround(mouse.x, bounds.x, bounds.x + bounds.width); + mouse.y = MathUtils.wrapAround(mouse.y, bounds.y, bounds.y + bounds.height); robot.mouseMove(mouse.x, mouse.y); origin.x = mouse.x; diff --git a/modules/platform-model/src/main/java/com/shade/platform/model/util/MathUtils.java b/modules/platform-model/src/main/java/com/shade/platform/model/util/MathUtils.java index 8805a6c78..47cb88f71 100644 --- a/modules/platform-model/src/main/java/com/shade/platform/model/util/MathUtils.java +++ b/modules/platform-model/src/main/java/com/shade/platform/model/util/MathUtils.java @@ -9,8 +9,16 @@ public static int alignUp(int value, int alignment) { return ceilDiv(value, alignment) * alignment; } - public static int wrapAround(int index, int max) { - return (index % max + max) % max; + public static int wrapAround(int value, int max) { + return (value % max + max) % max; + } + + public static int wrapAround(int value, int min, int max) { + if (value < min) { + return max - (min - value) % (max - min); + } else { + return min + (value - min) % (max - min); + } } // TODO: Replace with Math#ceilDiv once requires Java 18 diff --git a/modules/platform-model/src/test/java/com/shade/platform/util/MathUtilsTest.java b/modules/platform-model/src/test/java/com/shade/platform/util/MathUtilsTest.java new file mode 100644 index 000000000..38b3f78e7 --- /dev/null +++ b/modules/platform-model/src/test/java/com/shade/platform/util/MathUtilsTest.java @@ -0,0 +1,16 @@ +package com.shade.platform.util; + +import com.shade.platform.model.util.MathUtils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MathUtilsTest { + @Test + public void wrapAroundTest() { + assertEquals(1, MathUtils.wrapAround(1, 1, 3)); + assertEquals(2, MathUtils.wrapAround(2, 1, 3)); + assertEquals(1, MathUtils.wrapAround(3, 1, 3)); + assertEquals(9, MathUtils.wrapAround(4, 5, 10)); + } +}