From 2e0e19d9ce7cf91b07def081e2c04b2be91ec690 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 22 Apr 2020 13:04:19 -0700 Subject: [PATCH] Add a new free flight camera manipulator (#2436) --- .../src/main/cpp/Manipulator.cpp | 49 ++++- .../android/filament/utils/GestureDetector.kt | 2 +- .../android/filament/utils/Manipulator.java | 147 +++++++++++-- libs/camutils/CMakeLists.txt | 1 + libs/camutils/include/camutils/Bookmark.h | 10 +- libs/camutils/include/camutils/Manipulator.h | 80 ++++++- libs/camutils/src/FreeFlightManipulator.h | 201 ++++++++++++++++++ libs/camutils/src/Manipulator.cpp | 58 ++++- libs/camutils/src/MapManipulator.h | 2 +- libs/camutils/src/OrbitManipulator.h | 2 +- .../include/filamentapp/FilamentApp.h | 15 +- libs/filamentapp/src/FilamentApp.cpp | 115 ++++++++-- 12 files changed, 633 insertions(+), 49 deletions(-) create mode 100644 libs/camutils/src/FreeFlightManipulator.h diff --git a/android/filament-utils-android/src/main/cpp/Manipulator.cpp b/android/filament-utils-android/src/main/cpp/Manipulator.cpp index fd4def1310..22d471def2 100644 --- a/android/filament-utils-android/src/main/cpp/Manipulator.cpp +++ b/android/filament-utils-android/src/main/cpp/Manipulator.cpp @@ -90,6 +90,36 @@ extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBu builder->mapMinDistance(arg); } +extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBuilderFlightStartPosition(JNIEnv*, jclass, jlong nativeBuilder, jfloat x, jfloat y, jfloat z) { + Builder* builder = (Builder*) nativeBuilder; + builder->flightStartPosition(x, y, z); +} + +extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBuilderFlightStartOrientation(JNIEnv*, jclass, jlong nativeBuilder, jfloat pitch, jfloat yaw) { + Builder* builder = (Builder*) nativeBuilder; + builder->flightStartOrientation(pitch, yaw); +} + +extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBuilderFlightMaxMoveSpeed(JNIEnv*, jclass, jlong nativeBuilder, jfloat maxSpeed) { + Builder* builder = (Builder*) nativeBuilder; + builder->flightMaxMoveSpeed(maxSpeed); +} + +extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBuilderFlightSpeedSteps(JNIEnv*, jclass, jlong nativeBuilder, jint steps) { + Builder* builder = (Builder*) nativeBuilder; + builder->flightSpeedSteps(steps); +} + +extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBuilderFlightPanSpeed(JNIEnv*, jclass, jlong nativeBuilder, jfloat x, jfloat y) { + Builder* builder = (Builder*) nativeBuilder; + builder->flightPanSpeed(x, y); +} + +extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBuilderFlightMoveDamping(JNIEnv*, jclass, jlong nativeBuilder, jfloat damping) { + Builder* builder = (Builder*) nativeBuilder; + builder->flightMoveDamping(damping); +} + extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBuilderGroundPlane(JNIEnv*, jclass, jlong nativeBuilder, jfloat a, jfloat b, jfloat c, jfloat d) { Builder* builder = (Builder*) nativeBuilder; builder->groundPlane(a, b, c, d); @@ -170,9 +200,24 @@ extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_utils_Manipul manip->grabEnd(); } -extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_utils_Manipulator_nZoom(JNIEnv*, jclass, jlong nativeManip, jint x, jint y, jfloat scrolldelta) { +extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_utils_Manipulator_nKeyDown(JNIEnv*, jclass, jlong nativeManip, jint key) { auto manip = (Manipulator*) nativeManip; - manip->zoom(x, y, scrolldelta); + manip->keyDown((Manipulator::Key) key); +} + +extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_utils_Manipulator_nKeyUp(JNIEnv*, jclass, jlong nativeManip, jint key) { + auto manip = (Manipulator*) nativeManip; + manip->keyUp((Manipulator::Key) key); +} + +extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_utils_Manipulator_nScroll(JNIEnv*, jclass, jlong nativeManip, jint x, jint y, jfloat scrolldelta) { + auto manip = (Manipulator*) nativeManip; + manip->scroll(x, y, scrolldelta); +} + +extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_utils_Manipulator_nUpdate(JNIEnv*, jclass, jlong nativeManip, jfloat deltaTime) { + auto manip = (Manipulator*) nativeManip; + manip->update(deltaTime); } extern "C" JNIEXPORT jlong JNICALL Java_com_google_android_filament_utils_Manipulator_nGetCurrentBookmark(JNIEnv*, jclass, jlong nativeManip) { diff --git a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/GestureDetector.kt b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/GestureDetector.kt index 015d021822..3a4f0d692c 100644 --- a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/GestureDetector.kt +++ b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/GestureDetector.kt @@ -77,7 +77,7 @@ class GestureDetector(private val view: View, private val manipulator: Manipulat if (currentGesture == Gesture.ZOOM) { val d0 = previousTouch.separation val d1 = touch.separation - manipulator.zoom(touch.x, touch.y, (d0 - d1) * kZoomSpeed) + manipulator.scroll(touch.x, touch.y, (d0 - d1) * kZoomSpeed) previousTouch = touch return } diff --git a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/Manipulator.java b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/Manipulator.java index fdb955cae9..cc024f999d 100644 --- a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/Manipulator.java +++ b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/Manipulator.java @@ -25,8 +25,9 @@ import androidx.annotation.Size; * Helper that enables camera interaction similar to sketchfab or Google Maps. * * Clients notify the camera manipulator of various mouse or touch events, then periodically call - * its getLookAt() method so that they can adjust their camera(s). Two modes are supported: ORBIT - * and MAP. To construct a manipulator instance, the desired mode is passed into the create method. + * its getLookAt() method so that they can adjust their camera(s). Three modes are supported: ORBIT, + * MAP, and FREE_FLIGHT. To construct a manipulator instance, the desired mode is passed into the + * create method. * * @see Bookmark */ @@ -37,10 +38,22 @@ public class Manipulator { mNativeObject = nativeIndexBuffer; } - public enum Mode { ORBIT, MAP }; + public enum Mode { ORBIT, MAP, FREE_FLIGHT }; public enum Fov { VERTICAL, HORIZONTAL }; + /** + * Keys used to translate the camera in FREE_FLIGHT mode. + * UP and DOWN dolly the camera forwards and backwards. + * LEFT and RIGHT strafe the camera left and right. + */ + public enum Key { + UP, + LEFT, + DOWN, + RIGHT + } + public static class Builder { @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) // Keep to finalize native resources @@ -178,6 +191,72 @@ public class Manipulator { return this; } + /** + * Sets the initial eye position in world space for FREE_FLIGHT mode. Defaults to (0,0,0). + * + * @return this Builder object for chaining calls + */ + public Builder flightStartPosition(float x, float y, float z) { + nFlightStartPosition(mNativeBuilder, x, y, z); + return this; + } + + /** + * Sets the initial orientation in pitch and yaw for FREE_FLIGHT mode. Defaults to (0,0). + * + * @return this Builder object for chaining calls + */ + public Builder flightStartOrientation(float pitch, float yaw) { + nFlightStartOrientation(mNativeBuilder, pitch, yaw); + return this; + } + + /** + * Sets the maximum camera translation speed in world units per second for FREE_FLIGHT mode. + * Defaults to 10. + * + * @return this Builder object for chaining calls + */ + public Builder flightMaxMoveSpeed(float maxSpeed) { + nFlightMaxMoveSpeed(mNativeBuilder, maxSpeed); + return this; + } + + /** + * Sets the number of speed steps adjustable with scroll wheel for FREE_FLIGHT mode. + * Defaults to 80. + * + * @return this Builder object for chaining calls + */ + public Builder flightSpeedSteps(int steps) { + nFlightSpeedSteps(mNativeBuilder, steps); + return this; + } + + /** + * Sets the multiplier with viewport delta for FREE_FLIGHT mode. + * This defaults to 0.01. + * + * @return this Builder object for chaining calls + */ + public Builder flightPanSpeed(float x, float y) { + nFlightPanSpeed(mNativeBuilder, x, y); + return this; + } + + /** + * Applies a deceleration to camera movement in FREE_FLIGHT mode. Defaults to 0 (no damping). + * + * Lower values give slower damping times. A good default is 15.0. Too high a value may lead + * to instability. + * + * @return this Builder object for chaining calls + */ + public Builder flightMoveDamping(float damping) { + nFlightMoveDamping(mNativeBuilder, damping); + return this; + } + /** * Sets the ground plane equation used for ray casts. * This is a plane equation as in Ax + By + Cz + D = 0. @@ -281,11 +360,13 @@ public class Manipulator { /** * Starts a grabbing session (i.e. the user is dragging around in the viewport). * - * This starts a panning session in MAP mode, and start either rotating or strafing in ORBIT. + * In MAP mode, this starts a panning session. + * In ORBIT mode, this starts either rotating or strafing. + * In FREE_FLIGHT mode, this starts a nodal panning session. * * @param x X-coordinate for point of interest in viewport space * @param y Y-coordinate for point of interest in viewport space - * @param strafe ORBIT mode only; if true, starts a translation rather than a rotation. + * @param strafe ORBIT mode only; if true, starts a translation rather than a rotation */ public void grabBegin(int x, int y, boolean strafe) { nGrabBegin(mNativeObject, x, y, strafe); @@ -308,14 +389,45 @@ public class Manipulator { } /** - * Dollys the camera along the viewing direction. - * - * @param x X-coordinate for point of interest in viewport space - * @param y Y-coordinate for point of interest in viewport space - * @param scrolldelta Positive means "zoom in", negative means "zoom out" + * Keys used to translate the camera in FREE_FLIGHT mode. + * UP and DOWN dolly the camera forwards and backwards. + * LEFT and RIGHT strafe the camera left and right. */ - public void zoom(int x, int y, float scrolldelta) { - nZoom(mNativeObject, x, y, scrolldelta); + public void keyDown(Key key) { + nKeyDown(mNativeObject, key.ordinal()); + } + + /** + * Signals that a key is now in the up state. + * + * @see keyDown + */ + public void keyUp(Key key) { + nKeyUp(mNativeObject, key.ordinal()); + } + + /** + * In MAP and ORBIT modes, dollys the camera along the viewing direction. + * In FREE_FLIGHT mode, adjusts the move speed of the camera. + * + * @param x X-coordinate for point of interest in viewport space, ignored in FREE_FLIGHT mode + * @param y Y-coordinate for point of interest in viewport space, ignored in FREE_FLIGHT mode + * @param scrolldelta In MAP and ORBIT modes, negative means "zoom in", positive means "zoom out" + * In FREE_FLIGHT mode, negative means "slower", positive means "faster" + */ + public void scroll(int x, int y, float scrolldelta) { + nScroll(mNativeObject, x, y, scrolldelta); + } + + /** + * Processes input and updates internal state. + * + * This must be called once every frame before getLookAt is valid. + * + * @param deltaTime The amount of time, in seconds, passed since the previous call to update. + */ + public void update(float deltaTime) { + nUpdate(mNativeObject, deltaTime); } /** @@ -359,6 +471,12 @@ public class Manipulator { private static native void nBuilderFarPlane(long nativeBuilder, float distance); private static native void nBuilderMapExtent(long nativeBuilder, float width, float height); private static native void nBuilderMapMinDistance(long nativeBuilder, float arg); + private static native void nFlightStartPosition(long nativeBuilder, float x, float y, float z); + private static native void nFlightStartOrientation(long nativeBuilder, float pitch, float yaw); + private static native void nFlightMaxMoveSpeed(long nativeBuilder, float maxSpeed); + private static native void nFlightSpeedSteps(long nativeBuilder, int steps); + private static native void nFlightPanSpeed(long nativeBuilder, float x, float y); + private static native void nFlightMoveDamping(long nativeBuilder, float damping); private static native void nBuilderGroundPlane(long nativeBuilder, float a, float b, float c, float d); private static native long nBuilderBuild(long nativeBuilder, int mode); @@ -371,7 +489,10 @@ public class Manipulator { private static native void nGrabBegin(long nativeManip, int x, int y, boolean strafe); private static native void nGrabUpdate(long nativeManip, int x, int y); private static native void nGrabEnd(long nativeManip); - private static native void nZoom(long nativeManip, int x, int y, float scrolldelta); + private static native void nKeyDown(long nativeManip, int key); + private static native void nKeyUp(long nativeManip, int key); + private static native void nScroll(long nativeManip, int x, int y, float scrolldelta); + private static native void nUpdate(long nativeManip, float deltaTime); private static native long nGetCurrentBookmark(long nativeManip); private static native long nGetHomeBookmark(long nativeManip); private static native void nJumpToBookmark(long nativeManip, long nativeBookmark); diff --git a/libs/camutils/CMakeLists.txt b/libs/camutils/CMakeLists.txt index 3604d24f44..b3107c3f6d 100644 --- a/libs/camutils/CMakeLists.txt +++ b/libs/camutils/CMakeLists.txt @@ -14,6 +14,7 @@ set(PUBLIC_HDRS set(SRCS src/Bookmark.cpp + src/FreeFlightManipulator.h src/Manipulator.cpp src/MapManipulator.h src/OrbitManipulator.h diff --git a/libs/camutils/include/camutils/Bookmark.h b/libs/camutils/include/camutils/Bookmark.h index 1241c0a154..6661ebfe1c 100644 --- a/libs/camutils/include/camutils/Bookmark.h +++ b/libs/camutils/include/camutils/Bookmark.h @@ -23,11 +23,12 @@ namespace filament { namespace camutils { +template class FreeFlightManipulator; template class OrbitManipulator; template class MapManipulator; template class Manipulator; -enum class Mode { ORBIT, MAP }; +enum class Mode { ORBIT, MAP, FREE_FLIGHT }; /** * Opaque memento to a viewing position and orientation (e.g. the "home" camera position). @@ -62,9 +63,16 @@ private: FLOAT distance; filament::math::vec3 pivot; }; + struct FlightParams { + FLOAT pitch; + FLOAT yaw; + filament::math::vec3 position; + }; Mode mode; MapParams map; OrbitParams orbit; + FlightParams flight; + friend class FreeFlightManipulator; friend class OrbitManipulator; friend class MapManipulator; }; diff --git a/libs/camutils/include/camutils/Manipulator.h b/libs/camutils/include/camutils/Manipulator.h index be0f240254..874f10714d 100644 --- a/libs/camutils/include/camutils/Manipulator.h +++ b/libs/camutils/include/camutils/Manipulator.h @@ -34,8 +34,9 @@ enum class Fov { VERTICAL, HORIZONTAL }; * Helper that enables camera interaction similar to sketchfab or Google Maps. * * Clients notify the camera manipulator of various mouse or touch events, then periodically call - * its getLookAt() method so that they can adjust their camera(s). Two modes are supported: ORBIT - * and MAP. To construct a manipulator instance, the desired mode is passed into the create method. + * its getLookAt() method so that they can adjust their camera(s). Three modes are supported: ORBIT, + * MAP, and FREE_FLIGHT. To construct a manipulator instance, the desired mode is passed into the + * create method. * * Usage example: * @@ -97,6 +98,13 @@ public: FLOAT farPlane; vec2 mapExtent; FLOAT mapMinDistance; + vec3 flightStartPosition; + FLOAT flightStartPitch; + FLOAT flightStartYaw; + FLOAT flightMaxSpeed; + FLOAT flightSpeedSteps; + vec2 flightPanSpeed; + FLOAT flightMoveDamping; vec4 groundPlane; RayCallback raycastCallback; void* raycastUserdata; @@ -120,12 +128,22 @@ public: Builder& mapExtent(FLOAT worldWidth, FLOAT worldHeight); //! The ground size for computing home position Builder& mapMinDistance(FLOAT mindist); //! Constrains the zoom-in level + // Free flight properties + Builder& flightStartPosition(FLOAT x, FLOAT y, FLOAT z); //! Initial eye position in world space, defaults to (0,0,0) + Builder& flightStartOrientation(FLOAT pitch, FLOAT yaw); //! Initial orientation in pitch and yaw, defaults to (0,0) + Builder& flightMaxMoveSpeed(FLOAT maxSpeed); //! The maximum camera speed in world units per second, defaults to 10 + Builder& flightSpeedSteps(int steps); //! The number of speed steps adjustable with scroll wheel, defaults to 80 + Builder& flightPanSpeed(FLOAT x, FLOAT y); //! Multiplied with viewport delta, defaults to 0.01,0.01 + Builder& flightMoveDamping(FLOAT damping); //! Applies a deceleration to camera movement, defaults to 0 (no damping) + //! Lower values give slower damping times, a good default is 15 + //! Too high a value may lead to instability + // Raycast properties Builder& groundPlane(FLOAT a, FLOAT b, FLOAT c, FLOAT d); //! Plane equation used as a raycast fallback Builder& raycastCallback(RayCallback cb, void* userdata); //! Raycast function for accurate grab-and-pan /** - * Creates a new camera manipulator, either ORBIT or MAP. + * Creates a new camera manipulator, either ORBIT, MAP, or FREE_FLIGHT. * * Clients can simply use "delete" to destroy the manipulator. */ @@ -165,7 +183,9 @@ public: /** * Starts a grabbing session (i.e. the user is dragging around in the viewport). * - * This starts a panning session in MAP mode, and starts either rotating or strafing in ORBIT. + * In MAP mode, this starts a panning session. + * In ORBIT mode, this starts either rotating or strafing. + * In FREE_FLIGHT mode, this starts a nodal panning session. * * @param x X-coordinate for point of interest in viewport space * @param y Y-coordinate for point of interest in viewport space @@ -186,13 +206,53 @@ public: virtual void grabEnd() = 0; /** - * Dollys the camera along the viewing direction. - * - * @param x X-coordinate for point of interest in viewport space - * @param y Y-coordinate for point of interest in viewport space - * @param scrolldelta Negative means "zoom in", positive means "zoom out" + * Keys used to translate the camera in FREE_FLIGHT mode. + * UP and DOWN dolly the camera forwards and backwards. + * LEFT and RIGHT strafe the camera left and right. */ - virtual void zoom(int x, int y, FLOAT scrolldelta) = 0; + enum class Key { + UP, + LEFT, + DOWN, + RIGHT, + + COUNT + }; + + /** + * Signals that a key is now in the down state. + * + * In FREE_FLIGHT mode, the camera is translated forward and backward and strafed left and right + * depending on the depressed keys. This allows WASD-style movement. + */ + virtual void keyDown(Key key); + + /** + * Signals that a key is now in the up state. + * + * @see keyDown + */ + virtual void keyUp(Key key); + + /** + * In MAP and ORBIT modes, dollys the camera along the viewing direction. + * In FREE_FLIGHT mode, adjusts the move speed of the camera. + * + * @param x X-coordinate for point of interest in viewport space, ignored in FREE_FLIGHT mode + * @param y Y-coordinate for point of interest in viewport space, ignored in FREE_FLIGHT mode + * @param scrolldelta In MAP and ORBIT modes, negative means "zoom in", positive means "zoom out" + * In FREE_FLIGHT mode, negative means "slower", positive means "faster" + */ + virtual void scroll(int x, int y, FLOAT scrolldelta) = 0; + + /** + * Processes input and updates internal state. + * + * This must be called once every frame before getLookAt is valid. + * + * @param deltaTime The amount of time, in seconds, passed since the previous call to update. + */ + virtual void update(FLOAT deltaTime); /** * Gets a handle that can be used to reset the manipulator back to its current position. diff --git a/libs/camutils/src/FreeFlightManipulator.h b/libs/camutils/src/FreeFlightManipulator.h new file mode 100644 index 0000000000..bc41eb46db --- /dev/null +++ b/libs/camutils/src/FreeFlightManipulator.h @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CAMUTILS_FREEFLIGHT_MANIPULATOR_H +#define CAMUTILS_FREEFLIGHT_MANIPULATOR_H + +#include + +#include +#include +#include +#include + +#include + +#include + +namespace filament { +namespace camutils { + +using namespace filament::math; + +template +class FreeFlightManipulator : public Manipulator { +public: + using vec2 = filament::math::vec2; + using vec3 = filament::math::vec3; + using vec4 = filament::math::vec4; + using Bookmark = filament::camutils::Bookmark; + using Base = Manipulator; + using Config = typename Base::Config; + + FreeFlightManipulator(Mode mode, const Config& props) : Base(mode, props) { + setProperties(props); + Base::mEye = Base::mProps.flightStartPosition; + const auto pitch = Base::mProps.flightStartPitch; + const auto yaw = Base::mProps.flightStartYaw; + mTargetEuler = {pitch, yaw}; + updateTarget(pitch, yaw); + } + + void setProperties(const Config& props) override { + Config resolved = props; + + if (resolved.flightPanSpeed == vec2(0, 0)) { + resolved.flightPanSpeed = vec2(0.01, 0.01); + } + if (resolved.flightMaxSpeed == 0.0) { + resolved.flightMaxSpeed = 10.0; + } + if (resolved.flightSpeedSteps == 0) { + resolved.flightSpeedSteps = 80; + } + + Base::setProperties(resolved); + } + + void updateTarget(FLOAT pitch, FLOAT yaw) { + Base::mTarget = Base::mEye + (mat3::eulerZYX(0, yaw, pitch) * vec3(0.0, 0.0, -1.0)); + } + + void grabBegin(int x, int y, bool strafe) override { + mGrabWin = {x, y}; + mGrabbing = true; + mGrabEuler = mTargetEuler; + } + + void grabUpdate(int x, int y) override { + if (!mGrabbing) { + return; + } + + const vec2 del = mGrabWin - vec2{x, y}; + + const auto& grabPitch = mGrabEuler.x; + const auto& grabYaw = mGrabEuler.y; + auto& pitch = mTargetEuler.x; + auto& yaw = mTargetEuler.y; + + constexpr double EPSILON = 0.001; + + auto panSpeed = Base::mProps.flightPanSpeed; + constexpr FLOAT minPitch = (-F_PI_2 + EPSILON); + constexpr FLOAT maxPitch = ( F_PI_2 - EPSILON); + pitch = clamp(grabPitch + del.y * -panSpeed.y, minPitch, maxPitch); + yaw = fmod(grabYaw + del.x * panSpeed.x, 2.0 * F_PI); + + updateTarget(pitch, yaw); + } + + void grabEnd() override { + mGrabbing = false; + } + + void keyDown(typename Base::Key key) override { + mKeyDown[(int) key] = true; + } + + void keyUp(typename Base::Key key) override { + mKeyDown[(int) key] = false; + } + + void scroll(int x, int y, FLOAT scrolldelta) override { + const FLOAT halfSpeedSteps = Base::mProps.flightSpeedSteps / 2; + mScrollWheel = clamp(mScrollWheel + scrolldelta, -halfSpeedSteps, halfSpeedSteps); + // Normalize the scroll position from -1 to 1 and calculate the move speed, in world + // units per second. + mScrollPositionNormalized = (mScrollWheel + halfSpeedSteps) / halfSpeedSteps - 1.0; + mMoveSpeed = pow(Base::mProps.flightMaxSpeed, mScrollPositionNormalized); + } + + void update(FLOAT deltaTime) override { + vec3 forceLocal { 0.0, 0.0, 0.0 }; + + if (mKeyDown[(int) Base::Key::UP]) { + forceLocal += vec3{ 0.0, 0.0, -1.0 }; + } + if (mKeyDown[(int) Base::Key::LEFT]) { + forceLocal += vec3{ -1.0, 0.0, 0.0 }; + } + if (mKeyDown[(int) Base::Key::DOWN]) { + forceLocal += vec3{ 0.0, 0.0, 1.0 }; + } + if (mKeyDown[(int) Base::Key::RIGHT]) { + forceLocal += vec3{ 1.0, 0.0, 0.0 }; + } + + forceLocal *= mMoveSpeed; + + const mat4 orientation = mat4::lookAt(Base::mEye, Base::mTarget, Base::mProps.upVector); + const vec3 forceWorld = (orientation * vec4{ forceLocal, 0.0f }).xyz; + + const auto dampingFactor = Base::mProps.flightMoveDamping; + if (dampingFactor == 0.0) { + // Without damping, we simply treat the force as our velocity. + mEyeVelocity = forceWorld; + } else { + // The dampingFactor acts as "friction", which acts upon the camera in the direction + // opposite its velocity. + // Force is also multiplied by the dampingFactor, to "make up" for the friction. + // This ensures that the max velocity still approaches mMoveSpeed; + vec3 velocityDelta = (forceWorld - mEyeVelocity) * dampingFactor; + mEyeVelocity += velocityDelta * deltaTime; + } + + const vec3 positionDelta = mEyeVelocity * deltaTime; + + Base::mEye += positionDelta; + Base::mTarget += positionDelta; + } + + Bookmark getCurrentBookmark() const override { + Bookmark bookmark; + bookmark.flight.position = Base::mEye; + bookmark.flight.pitch = mTargetEuler.x; + bookmark.flight.yaw = mTargetEuler.y; + return bookmark; + } + + Bookmark getHomeBookmark() const override { + Bookmark bookmark; + bookmark.flight.position = Base::mProps.flightStartPosition;; + bookmark.flight.pitch = Base::mProps.flightStartPitch; + bookmark.flight.yaw = Base::mProps.flightStartYaw; + return bookmark; + } + + void jumpToBookmark(const Bookmark& bookmark) override { + Base::mEye = bookmark.flight.position; + updateTarget(bookmark.flight.pitch, bookmark.flight.yaw); + } + +private: + vec2 mGrabWin; + vec2 mTargetEuler; // (pitch, yaw) + vec2 mGrabEuler; // (pitch, yaw) + bool mKeyDown[(int) Base::Key::COUNT] = {false}; + bool mGrabbing = false; + FLOAT mScrollWheel = 0.0f; + FLOAT mScrollPositionNormalized = 0.0f; + FLOAT mMoveSpeed = 1.0f; + vec3 mEyeVelocity; +}; + +} // namespace camutils +} // namespace filament + +#endif /* CAMUTILS_FREEFLIGHT_MANIPULATOR_H */ diff --git a/libs/camutils/src/Manipulator.cpp b/libs/camutils/src/Manipulator.cpp index dcf488d850..d44528cd50 100644 --- a/libs/camutils/src/Manipulator.cpp +++ b/libs/camutils/src/Manipulator.cpp @@ -18,6 +18,7 @@ #include +#include "FreeFlightManipulator.h" #include "MapManipulator.h" #include "OrbitManipulator.h" @@ -93,6 +94,43 @@ Manipulator::Builder& Manipulator::Builder::mapMinDistance(FLOAT m return *this; } +template typename +Manipulator::Builder& Manipulator::Builder::flightStartPosition(FLOAT x, FLOAT y, FLOAT z) { + details.flightStartPosition = {x, y, z}; + return *this; +} + +template typename +Manipulator::Builder& Manipulator::Builder::flightStartOrientation(FLOAT pitch, FLOAT yaw) { + details.flightStartPitch = pitch; + details.flightStartYaw = yaw; + return *this; +} + +template typename +Manipulator::Builder& Manipulator::Builder::flightMaxMoveSpeed(FLOAT maxSpeed) { + details.flightMaxSpeed = maxSpeed; + return *this; +} + +template typename +Manipulator::Builder& Manipulator::Builder::flightSpeedSteps(int steps) { + details.flightSpeedSteps = steps; + return *this; +} + +template typename +Manipulator::Builder& Manipulator::Builder::flightPanSpeed(FLOAT x, FLOAT y) { + details.flightPanSpeed = {x, y}; + return *this; +} + +template typename +Manipulator::Builder& Manipulator::Builder::flightMoveDamping(FLOAT damping) { + details.flightMoveDamping = damping; + return *this; +} + template typename Manipulator::Builder& Manipulator::Builder::groundPlane(FLOAT a, FLOAT b, FLOAT c, FLOAT d) { details.groundPlane = {a, b, c, d}; @@ -106,13 +144,16 @@ Manipulator::Builder& Manipulator::Builder::raycastCallback(RayCal return *this; } - template Manipulator* Manipulator::Builder::build(Mode mode) { - if (mode == Mode::MAP) { - return new MapManipulator(mode, details); + switch (mode) { + case Mode::FREE_FLIGHT: + return new FreeFlightManipulator(mode, details); + case Mode::MAP: + return new MapManipulator(mode, details); + case Mode::ORBIT: + return new OrbitManipulator(mode, details); } - return new OrbitManipulator(mode, details); } template @@ -267,6 +308,15 @@ filament::math::vec3 Manipulator::raycastFarPlane(int x, int y) co return mEye + dir * mProps.farPlane; } +template +void Manipulator::keyDown(Manipulator::Key key) { } + +template +void Manipulator::keyUp(Manipulator::Key key) { } + +template +void Manipulator::update(FLOAT deltaTime) { } + template class Manipulator; } // namespace camutils diff --git a/libs/camutils/src/MapManipulator.h b/libs/camutils/src/MapManipulator.h index 9ca6f47a50..6df5b4c5e9 100644 --- a/libs/camutils/src/MapManipulator.h +++ b/libs/camutils/src/MapManipulator.h @@ -71,7 +71,7 @@ public: mGrabbing = false; } - void zoom(int x, int y, FLOAT scrolldelta) override { + void scroll(int x, int y, FLOAT scrolldelta) override { vec3 grabScene; if (!Base::raycast(x, y, &grabScene)) { return; diff --git a/libs/camutils/src/OrbitManipulator.h b/libs/camutils/src/OrbitManipulator.h index 8e855c3a7e..54e325cc3b 100644 --- a/libs/camutils/src/OrbitManipulator.h +++ b/libs/camutils/src/OrbitManipulator.h @@ -111,7 +111,7 @@ public: mGrabState = INACTIVE; } - void zoom(int x, int y, FLOAT scrolldelta) override { + void scroll(int x, int y, FLOAT scrolldelta) override { const vec3 gaze = normalize(Base::mTarget - Base::mEye); const vec3 movement = gaze * Base::mProps.zoomSpeed * -scrolldelta; const vec3 v0 = mPivot - Base::mEye; diff --git a/libs/filamentapp/include/filamentapp/FilamentApp.h b/libs/filamentapp/include/filamentapp/FilamentApp.h index 1537030a23..36a306ec23 100644 --- a/libs/filamentapp/include/filamentapp/FilamentApp.h +++ b/libs/filamentapp/include/filamentapp/FilamentApp.h @@ -19,8 +19,8 @@ #include #include -#include #include +#include #include #include @@ -109,6 +109,8 @@ private: using CameraManipulator = filament::camutils::Manipulator; + static bool manipulatorKeyFromKeycode(SDL_Scancode scancode, CameraManipulator::Key& key); + class CView { public: CView(filament::Renderer& renderer, std::string name); @@ -123,9 +125,12 @@ private: virtual void mouseUp(ssize_t x, ssize_t y); virtual void mouseMoved(ssize_t x, ssize_t y); virtual void mouseWheel(ssize_t x); + virtual void keyDown(SDL_Scancode scancode); + virtual void keyUp(SDL_Scancode scancode); filament::View const* getView() const { return view; } filament::View* getView() { return view; } + CameraManipulator* getCameraManipulator() { return mCameraManipulator; } private: enum class Mode : uint8_t { @@ -156,6 +161,8 @@ private: void mouseUp(ssize_t x, ssize_t y); void mouseMoved(ssize_t x, ssize_t y); void mouseWheel(ssize_t x); + void keyDown(SDL_Scancode scancode); + void keyUp(SDL_Scancode scancode); void resize(); filament::Renderer* getRenderer() { return mRenderer; } @@ -195,7 +202,11 @@ private: size_t mHeight = 0; ssize_t mLastX = 0; ssize_t mLastY = 0; - CView* mEventTarget = nullptr; + + CView* mMouseEventTarget = nullptr; + + // Keep track of which view should receive a key's keyUp event. + std::unordered_map mKeyEventTarget; }; friend class Window; diff --git a/libs/filamentapp/src/FilamentApp.cpp b/libs/filamentapp/src/FilamentApp.cpp index 40653659f7..a0118069a6 100644 --- a/libs/filamentapp/src/FilamentApp.cpp +++ b/libs/filamentapp/src/FilamentApp.cpp @@ -269,6 +269,10 @@ void FilamentApp::run(const Config& config, SetupCallback setupCallback, if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { mClosed = true; } + window->keyDown(event.key.keysym.scancode); + break; + case SDL_KEYUP: + window->keyUp(event.key.keysym.scancode); break; case SDL_MOUSEWHEEL: if (!io || !io->WantCaptureMouse) @@ -306,6 +310,13 @@ void FilamentApp::run(const Config& config, SetupCallback setupCallback, } } + // Calculate the time step. + static Uint64 frequency = SDL_GetPerformanceFrequency(); + Uint64 now = SDL_GetPerformanceCounter(); + const float timeStep = mTime > 0 ? (float)((double)(now - mTime) / frequency) : + (float)(1.0f / 60.0f); + mTime = now; + // Populate the UI scene, regardless of whether Filament wants to a skip frame. We should // always let ImGui generate a command list; if it skips a frame it'll destroy its widgets. if (mImGuiHelper) { @@ -337,14 +348,17 @@ void FilamentApp::run(const Config& config, SetupCallback setupCallback, } // Populate the UI Scene. - static Uint64 frequency = SDL_GetPerformanceFrequency(); - Uint64 now = SDL_GetPerformanceCounter(); - float timeStep = mTime > 0 ? (float)((double)(now - mTime) / frequency) : - (float)(1.0f / 60.0f); - mTime = now; mImGuiHelper->render(timeStep, imguiCallback); } + // Update the camera manipulators for each view. + for (auto const& view : window->mViews) { + auto* cm = view->getCameraManipulator(); + if (cm) { + cm->update(timeStep); + } + } + // Update the position and orientation of the two cameras. filament::math::float3 eye, center, up; window->mMainCameraMan->getLookAt(&eye, ¢er, &up); @@ -589,7 +603,7 @@ void FilamentApp::Window::mouseDown(int button, ssize_t x, ssize_t y) { y = mHeight - y; for (auto const& view : mViews) { if (view->intersects(x, y)) { - mEventTarget = view.get(); + mMouseEventTarget = view.get(); view->mouseDown(button, x, y); break; } @@ -597,8 +611,8 @@ void FilamentApp::Window::mouseDown(int button, ssize_t x, ssize_t y) { } void FilamentApp::Window::mouseWheel(ssize_t x) { - if (mEventTarget) { - mEventTarget->mouseWheel(x); + if (mMouseEventTarget) { + mMouseEventTarget->mouseWheel(x); } else { for (auto const& view : mViews) { if (view->intersects(mLastX, mLastY)) { @@ -611,23 +625,60 @@ void FilamentApp::Window::mouseWheel(ssize_t x) { void FilamentApp::Window::mouseUp(ssize_t x, ssize_t y) { fixupMouseCoordinatesForHdpi(x, y); - if (mEventTarget) { + if (mMouseEventTarget) { y = mHeight - y; - mEventTarget->mouseUp(x, y); - mEventTarget = nullptr; + mMouseEventTarget->mouseUp(x, y); + mMouseEventTarget = nullptr; } } void FilamentApp::Window::mouseMoved(ssize_t x, ssize_t y) { fixupMouseCoordinatesForHdpi(x, y); y = mHeight - y; - if (mEventTarget) { - mEventTarget->mouseMoved(x, y); + if (mMouseEventTarget) { + mMouseEventTarget->mouseMoved(x, y); } mLastX = x; mLastY = y; } +void FilamentApp::Window::keyDown(SDL_Scancode key) { + auto& eventTarget = mKeyEventTarget[key]; + + // keyDown events can be sent multiple times per key (for key repeat) + // If this key is already down, do nothing. + if (eventTarget) { + return; + } + + // Decide which view will get this key's corresponding keyUp event. + // If we're currently in a mouse grap session, it should be the mouse grap's target view. + // Otherwise, it should be whichever view we're currently hovering over. + CView* targetView = nullptr; + if (mMouseEventTarget) { + targetView = mMouseEventTarget; + } else { + for (auto const& view : mViews) { + if (view->intersects(mLastX, mLastY)) { + targetView = view.get(); + break; + } + } + } + + targetView->keyDown(key); + eventTarget = targetView; +} + +void FilamentApp::Window::keyUp(SDL_Scancode key) { + auto& eventTarget = mKeyEventTarget[key]; + if (!eventTarget) { + return; + } + eventTarget->keyUp(key); + eventTarget = nullptr; +} + void FilamentApp::Window::fixupMouseCoordinatesForHdpi(ssize_t& x, ssize_t& y) const { int dw, dh, ww, wh; SDL_GL_GetDrawableSize(mWindow, &dw, &dh); @@ -749,7 +800,43 @@ void FilamentApp::CView::mouseMoved(ssize_t x, ssize_t y) { void FilamentApp::CView::mouseWheel(ssize_t x) { if (mCameraManipulator) { - mCameraManipulator->zoom(0, 0, x); + mCameraManipulator->scroll(0, 0, x); + } +} + +bool FilamentApp::manipulatorKeyFromKeycode(SDL_Scancode scancode, CameraManipulator::Key& key) { + switch (scancode) { + case SDL_SCANCODE_W: + key = CameraManipulator::Key::UP; + return true; + case SDL_SCANCODE_A: + key = CameraManipulator::Key::LEFT; + return true; + case SDL_SCANCODE_S: + key = CameraManipulator::Key::DOWN; + return true; + case SDL_SCANCODE_D: + key = CameraManipulator::Key::RIGHT; + return true; + } + return false; +} + +void FilamentApp::CView::keyUp(SDL_Scancode scancode) { + if (mCameraManipulator) { + CameraManipulator::Key key; + if (manipulatorKeyFromKeycode(scancode, key)) { + mCameraManipulator->keyUp(key); + } + } +} + +void FilamentApp::CView::keyDown(SDL_Scancode scancode) { + if (mCameraManipulator) { + CameraManipulator::Key key; + if (manipulatorKeyFromKeycode(scancode, key)) { + mCameraManipulator->keyDown(key); + } } }