diff --git a/include/polyscope/context.h b/include/polyscope/context.h index b5902af6..24d76323 100644 --- a/include/polyscope/context.h +++ b/include/polyscope/context.h @@ -31,9 +31,9 @@ class Widget; class TransformationGizmo; class FloatingQuantityStructure; namespace view { -extern const double defaultNearClipRatio; -extern const double defaultFarClipRatio; -extern const double defaultFov; +extern const float defaultNearClipRatio; +extern const float defaultFarClipRatio; +extern const float defaultFov; } // namespace view // A context object wrapping all global state used by Polyscope. @@ -85,12 +85,13 @@ struct Context { NavigateStyle navigateStyle = NavigateStyle::Turntable; UpDir upDir = UpDir::YUp; FrontDir frontDir = FrontDir::ZFront; - double moveScale = 1.0; - double nearClipRatio = view::defaultNearClipRatio; - double farClipRatio = view::defaultFarClipRatio; + float moveScale = 1.0; + ViewRelativeMode viewRelativeMode = ViewRelativeMode::CenterRelative; + float nearClip = view::defaultNearClipRatio; + float farClip = view::defaultFarClipRatio; std::array bgColor{{1.0, 1.0, 1.0, 0.0}}; glm::mat4x4 viewMat{std::numeric_limits::quiet_NaN()}; - double fov = view::defaultFov; + float fov = view::defaultFov; ProjectionMode projectionMode = ProjectionMode::Perspective; glm::vec3 viewCenter; bool midflight = false; diff --git a/include/polyscope/types.h b/include/polyscope/types.h index fac79714..53c4cebe 100644 --- a/include/polyscope/types.h +++ b/include/polyscope/types.h @@ -2,36 +2,192 @@ #pragma once +#include "polyscope/utilities.h" + // Various types / enums / forward declarations which are broadly useful +// The string templates allow to/from string like: +// to_string(ProjectionMode::Perspective) +// from_string("Perspective") +// bool success = try_from_string("Perspective", val_out); +// see utilities.h for implementation + +// clang-format off + namespace polyscope { +// Navigate Style enum class NavigateStyle { Turntable = 0, Free, Planar, Arcball, None, FirstPerson }; +POLYSCOPE_DEFINE_ENUM_NAMES(NavigateStyle, + {NavigateStyle::Turntable, "Turntable"}, + {NavigateStyle::Free, "Free"}, + {NavigateStyle::Planar, "Planar"}, + {NavigateStyle::Arcball, "Arcball"}, + {NavigateStyle::None, "None"}, + {NavigateStyle::FirstPerson, "First Person"} +); + +// Up Direction enum class UpDir { XUp = 0, YUp, ZUp, NegXUp, NegYUp, NegZUp }; +POLYSCOPE_DEFINE_ENUM_NAMES(UpDir, + {UpDir::XUp, "X Up"}, + {UpDir::YUp, "Y Up"}, + {UpDir::ZUp, "Z Up"}, + {UpDir::NegXUp, "NegX Up"}, + {UpDir::NegYUp, "NegY Up"}, + {UpDir::NegZUp, "NegZ Up"}, +); + enum class FrontDir { XFront = 0, YFront, ZFront, NegXFront, NegYFront, NegZFront }; +POLYSCOPE_DEFINE_ENUM_NAMES(FrontDir, + {FrontDir::XFront, "X Forward"}, + {FrontDir::YFront, "Y Forward"}, + {FrontDir::ZFront, "Z Forward"}, + {FrontDir::NegXFront, "NegX Forward"}, + {FrontDir::NegYFront, "NegY Forward"}, + {FrontDir::NegZFront, "NegZ Forward"}, +); + enum class BackgroundView { None = 0 }; +POLYSCOPE_DEFINE_ENUM_NAMES(BackgroundView, + {BackgroundView::None, "None"} +); + + +// Projection Mode enum class ProjectionMode { Perspective = 0, Orthographic }; +POLYSCOPE_DEFINE_ENUM_NAMES(ProjectionMode, + {ProjectionMode::Perspective, "Perspective"}, + {ProjectionMode::Orthographic, "Orthographic"} +); + +enum class ViewRelativeMode { CenterRelative = 0, LengthRelative }; +POLYSCOPE_DEFINE_ENUM_NAMES(ViewRelativeMode, + {ViewRelativeMode::CenterRelative, "Center Relative"}, + {ViewRelativeMode::LengthRelative, "Length Relative"} +); + enum class TransparencyMode { None = 0, Simple, Pretty }; +POLYSCOPE_DEFINE_ENUM_NAMES(TransparencyMode, + {TransparencyMode::None, "None"}, + {TransparencyMode::Simple, "Simple"}, + {TransparencyMode::Pretty, "Pretty"} +); + enum class GroundPlaneMode { None, Tile, TileReflection, ShadowOnly }; +POLYSCOPE_DEFINE_ENUM_NAMES(GroundPlaneMode, + {GroundPlaneMode::None, "None"}, + {GroundPlaneMode::Tile, "Tile"}, + {GroundPlaneMode::TileReflection, "Tile Reflection"}, + {GroundPlaneMode::ShadowOnly, "Shadow Only"} +); + enum class GroundPlaneHeightMode { Automatic = 0, Manual }; +POLYSCOPE_DEFINE_ENUM_NAMES(GroundPlaneHeightMode, + {GroundPlaneHeightMode::Automatic, "Automatic"}, + {GroundPlaneHeightMode::Manual, "Manual"} +); + enum class BackFacePolicy { Identical, Different, Custom, Cull }; +POLYSCOPE_DEFINE_ENUM_NAMES(BackFacePolicy, + {BackFacePolicy::Identical, "Identical"}, + {BackFacePolicy::Different, "Different"}, + {BackFacePolicy::Custom, "Custom"}, + {BackFacePolicy::Cull, "Cull"} +); + enum class LimitFPSMode { IgnoreLimits = 0, BlockToHitTarget, SkipFramesToHitTarget }; +POLYSCOPE_DEFINE_ENUM_NAMES(LimitFPSMode, + {LimitFPSMode::IgnoreLimits, "Ignore Limits"}, + {LimitFPSMode::BlockToHitTarget, "Block To Hit Target"}, + {LimitFPSMode::SkipFramesToHitTarget, "Skip Frames To Hit Target"} +); enum class PointRenderMode { Sphere = 0, Quad }; +POLYSCOPE_DEFINE_ENUM_NAMES(PointRenderMode, + {PointRenderMode::Sphere, "Sphere"}, + {PointRenderMode::Quad, "Quad"} +); + enum class MeshElement { VERTEX = 0, FACE, EDGE, HALFEDGE, CORNER }; +POLYSCOPE_DEFINE_ENUM_NAMES(MeshElement, + {MeshElement::VERTEX, "Vertex"}, + {MeshElement::FACE, "Face"}, + {MeshElement::EDGE, "Edge"}, + {MeshElement::HALFEDGE, "Half-Edge"}, + {MeshElement::CORNER, "Corner"} +); + enum class MeshShadeStyle { Smooth = 0, Flat, TriFlat }; +POLYSCOPE_DEFINE_ENUM_NAMES(MeshShadeStyle, + {MeshShadeStyle::Smooth, "Smooth"}, + {MeshShadeStyle::Flat, "Flat"}, + {MeshShadeStyle::TriFlat, "Tri-Flat"} +); + enum class MeshSelectionMode { Auto = 0, VerticesOnly, FacesOnly }; +POLYSCOPE_DEFINE_ENUM_NAMES(MeshSelectionMode, + {MeshSelectionMode::Auto, "Auto"}, + {MeshSelectionMode::VerticesOnly, "Vertices Only"}, + {MeshSelectionMode::FacesOnly, "Faces Only"} +); + enum class CurveNetworkElement { NODE = 0, EDGE }; +POLYSCOPE_DEFINE_ENUM_NAMES(CurveNetworkElement, + {CurveNetworkElement::NODE, "Node"}, + {CurveNetworkElement::EDGE, "Edge"} +); + enum class VolumeMeshElement { VERTEX = 0, EDGE, FACE, CELL }; +POLYSCOPE_DEFINE_ENUM_NAMES(VolumeMeshElement, + {VolumeMeshElement::VERTEX, "Vertex"}, + {VolumeMeshElement::EDGE, "Edge"}, + {VolumeMeshElement::FACE, "Face"}, + {VolumeMeshElement::CELL, "Cell"} +); + enum class VolumeCellType { TET = 0, HEX }; +POLYSCOPE_DEFINE_ENUM_NAMES(VolumeCellType, + {VolumeCellType::TET, "Tet"}, + {VolumeCellType::HEX, "Hex"} +); + enum class VolumeGridElement { NODE = 0, CELL }; +POLYSCOPE_DEFINE_ENUM_NAMES(VolumeGridElement, + {VolumeGridElement::NODE, "Node"}, + {VolumeGridElement::CELL, "Cell"} +); + enum class IsolineStyle { Stripe = 0, Contour }; +POLYSCOPE_DEFINE_ENUM_NAMES(IsolineStyle, + {IsolineStyle::Stripe, "Stripe"}, + {IsolineStyle::Contour, "Contour"} +); enum class ImplicitRenderMode { SphereMarch, FixedStep }; +POLYSCOPE_DEFINE_ENUM_NAMES(ImplicitRenderMode, + {ImplicitRenderMode::SphereMarch, "Sphere March"}, + {ImplicitRenderMode::FixedStep, "Fixed Step"} +); + enum class ImageOrigin { LowerLeft, UpperLeft }; +POLYSCOPE_DEFINE_ENUM_NAMES(ImageOrigin, + {ImageOrigin::LowerLeft, "Lower Left"}, + {ImageOrigin::UpperLeft, "Upper Left"} +); + enum class FilterMode { Nearest = 0, Linear }; +POLYSCOPE_DEFINE_ENUM_NAMES(FilterMode, + {FilterMode::Nearest, "Nearest"}, + {FilterMode::Linear, "Linear"} +); enum class ParamCoordsType { UNIT = 0, WORLD }; // UNIT -> [0,1], WORLD -> length-valued +POLYSCOPE_DEFINE_ENUM_NAMES(ParamCoordsType, + {ParamCoordsType::UNIT, "Unit"}, + {ParamCoordsType::WORLD, "World"} +); + enum class ParamVizStyle { CHECKER = 0, GRID, @@ -39,6 +195,13 @@ enum class ParamVizStyle { LOCAL_RAD, CHECKER_ISLANDS }; // TODO add "UV" with test UV map +POLYSCOPE_DEFINE_ENUM_NAMES(ParamVizStyle, + {ParamVizStyle::CHECKER, "Checker"}, + {ParamVizStyle::GRID, "Grid"}, + {ParamVizStyle::LOCAL_CHECK, "Local Check"}, + {ParamVizStyle::LOCAL_RAD, "Local Rad"}, + {ParamVizStyle::CHECKER_ISLANDS, "Checker Islands"} +); enum class ManagedBufferType { Float, @@ -55,6 +218,21 @@ enum class ManagedBufferType { UVec3, UVec4 }; +POLYSCOPE_DEFINE_ENUM_NAMES(ManagedBufferType, + {ManagedBufferType::Float, "Float"}, + {ManagedBufferType::Double, "Double"}, + {ManagedBufferType::Vec2, "Vec2"}, + {ManagedBufferType::Vec3, "Vec3"}, + {ManagedBufferType::Vec4, "Vec4"}, + {ManagedBufferType::Arr2Vec3, "Arr2Vec3"}, + {ManagedBufferType::Arr3Vec3, "Arr3Vec3"}, + {ManagedBufferType::Arr4Vec3, "Arr4Vec3"}, + {ManagedBufferType::UInt32, "UInt32"}, + {ManagedBufferType::Int32, "Int32"}, + {ManagedBufferType::UVec2, "UVec2"}, + {ManagedBufferType::UVec3, "UVec3"}, + {ManagedBufferType::UVec4, "UVec4"} +); // What is the meaningful range of these values? @@ -64,6 +242,13 @@ enum class ManagedBufferType { // MAGNITUDE: [0, inf], zero is special (ie, length of a vector) // CATEGORICAL: data is integers corresponding to labels, etc enum class DataType { STANDARD = 0, SYMMETRIC, MAGNITUDE, CATEGORICAL }; +POLYSCOPE_DEFINE_ENUM_NAMES(DataType, + {DataType::STANDARD, "Standard"}, + {DataType::SYMMETRIC, "Symmetric"}, + {DataType::MAGNITUDE, "Magnitude"}, + {DataType::CATEGORICAL, "Categorical"} +); +// clang-format on }; // namespace polyscope diff --git a/include/polyscope/utilities.h b/include/polyscope/utilities.h index 2d518439..1e0442d0 100644 --- a/include/polyscope/utilities.h +++ b/include/polyscope/utilities.h @@ -14,7 +14,6 @@ #include #include - #include @@ -166,6 +165,84 @@ inline double randomNormal(double mean = 0.0, double stddev = 1.0) { return dist(util_mersenne_twister); } +// === Helpers for enum to/from string +template +struct EnumMapping { + E value; + const char* name; +}; + +// Forward declaration +template +struct EnumTraits; + +// Convert enum to string +template +std::string enum_to_string(E value) { + const EnumMapping* mappings = EnumTraits::get_mappings(); + size_t count = EnumTraits::get_count(); + + for (size_t i = 0; i < count; i++) { + if (mappings[i].value == value) { + return mappings[i].name; + } + } + + throw std::invalid_argument("Unknown enum value"); +} + +// Convert string to enum +template +E from_string(const std::string& str) { + const EnumMapping* mappings = EnumTraits::get_mappings(); + size_t count = EnumTraits::get_count(); + + for (size_t i = 0; i < count; i++) { + if (mappings[i].name == str) { + return mappings[i].value; + } + } + + std::stringstream ss; + ss << "Unknown enum string: " << str << ". Valid options are: "; + for (size_t i = 0; i < count; i++) { + ss << mappings[i].name; + if (i < count - 1) { + ss << ", "; + } + } + throw std::invalid_argument(ss.str()); +} + +// Safe conversion - returns true if successful +template +bool try_enum_from_string(const std::string& str, E& out) { + const EnumMapping* mappings = EnumTraits::get_mappings(); + size_t count = EnumTraits::get_count(); + for (size_t i = 0; i < count; i++) { + if (mappings[i].name == str) { + out = mappings[i].value; + return true; + } + } + return false; +} + +// Macro to define enum traits - use inside namespace +#define POLYSCOPE_DEFINE_ENUM_NAMES(EnumType, ...) \ + template <> \ + struct EnumTraits { \ + static const EnumMapping* get_mappings() { \ + static const EnumMapping mappings[] = {__VA_ARGS__}; \ + return mappings; \ + } \ + static size_t get_count() { \ + static const EnumMapping mappings[] = {__VA_ARGS__}; \ + return sizeof(mappings) / sizeof(mappings[0]); \ + } \ + }; + + // === ImGui utilities // Displays a little helper icon which shows the text on hover diff --git a/include/polyscope/view.h b/include/polyscope/view.h index d24bb426..a529b84c 100644 --- a/include/polyscope/view.h +++ b/include/polyscope/view.h @@ -45,16 +45,17 @@ extern bool& windowResizable; extern NavigateStyle& style; extern UpDir& upDir; extern FrontDir& frontDir; -extern double& moveScale; -extern double& nearClipRatio; -extern double& farClipRatio; +extern float& moveScale; +extern ViewRelativeMode& viewRelativeMode; +extern float& nearClip; +extern float& farClip; extern std::array& bgColor; // Current view camera parameters // TODO deprecate these one day, and just use a CameraParameters member instead. But this would break existing code, so // for now we leave these as-is and wrap inputs/outputs to a CameraParameters extern glm::mat4x4& viewMat; -extern double& fov; // in the y direction +extern float& fov; // in the y direction extern ProjectionMode& projectionMode; extern glm::vec3& viewCenter; // center about which view transformations are performed @@ -72,9 +73,9 @@ extern float& flightInitialFov; // Default values extern const int defaultWindowWidth; extern const int defaultWindowHeight; -extern const double defaultNearClipRatio; -extern const double defaultFarClipRatio; -extern const double defaultFov; +extern const float defaultNearClipRatio; +extern const float defaultFarClipRatio; +extern const float defaultFov; // === View methods @@ -90,10 +91,17 @@ void setProjectionMode(ProjectionMode newMode); glm::mat4 getCameraPerspectiveMatrix(); glm::vec3 getCameraWorldPosition(); void getCameraFrame(glm::vec3& lookDir, glm::vec3& upDir, glm::vec3& rightDir); +glm::vec3 getLookVec(); // vector giving the "look" direction of the camera frame glm::vec3 getUpVec(); // vector giving the "up" direction for the scene (unrelated to current camera view) glm::vec3 getFrontVec(); // vector giving the "front" direction for the scene (unrelated to current camera view) float getVerticalFieldOfViewDegrees(); float getAspectRatioWidthOverHeight(); +ViewRelativeMode getViewRelativeMode(); +void setViewRelativeMode(ViewRelativeMode newMode); + +// Clip planes +void setClipPlanes(float newNearClip, float newFarClip); +std::tuple getClipPlanes(); // Set the camera extrinsics to look at a particular location void setViewToCamera(const CameraParameters& p); @@ -187,14 +195,18 @@ bool viewIsValid(); void invalidateView(); void ensureViewValid(); +float computeRelativeMotionScale(); + // Process user inputs which affect the view void processTranslate(glm::vec2 delta); void processRotate(glm::vec2 startP, glm::vec2 endP); -void processClipPlaneShift(double amount); -void processZoom(double amount, bool relativeToCenter = false); +void processClipPlaneShift(float amount); +void processZoom(float amount, bool relativeToCenter = false); void processKeyboardNavigation(ImGuiIO& io); void processSetCenter(glm::vec2 screenCoords); +std::tuple computeClipPlanes(); + // deprecated, bad names, see variants above glm::vec3 bufferCoordsToWorldRay(glm::vec2 bufferCoords); diff --git a/src/polyscope.cpp b/src/polyscope.cpp index f3355e86..142953e0 100644 --- a/src/polyscope.cpp +++ b/src/polyscope.cpp @@ -461,29 +461,27 @@ void processInputEvents() { if (!io.WantCaptureMouse && !widgetCapturedMouse) { { // Process scroll via "mouse wheel" (which might be a touchpad) - double xoffset = io.MouseWheelH; - double yoffset = io.MouseWheel; + float xoffset = io.MouseWheelH; + float yoffset = io.MouseWheel; + float scrollOffset = yoffset; - if (xoffset != 0 || yoffset != 0) { - requestRedraw(); + // NOTE: here we used to scroll according the to the larger of the two offsets (x or y) + // (there was a comment about 'shift swaps scroll on some platforms'). However, on many + // common machines (e.g. macs with touchpads), two finger scrolling produces both x and y + // offsets simultaneously, leading to jumpy zooms when an x was greater than a y. + // So now we just use the y offset always. - // On some setups, shift flips the scroll direction, so take the max - // scrolling in any direction - double maxScroll = xoffset; - if (std::abs(yoffset) > std::abs(xoffset)) { - maxScroll = yoffset; - } + if (scrollOffset != 0.0f) { + requestRedraw(); // Pass camera commands to the camera - if (maxScroll != 0.0) { - bool scrollClipPlane = io.KeyShift && !io.KeyCtrl; - bool relativeZoom = io.KeyShift && io.KeyCtrl; - - if (scrollClipPlane) { - view::processClipPlaneShift(maxScroll); - } else { - view::processZoom(maxScroll, relativeZoom); - } + bool scrollClipPlane = io.KeyShift && !io.KeyCtrl; + + if (scrollClipPlane) { + view::processClipPlaneShift(scrollOffset); + } else { + // always relative + view::processZoom(scrollOffset, true); } } } @@ -776,7 +774,6 @@ void buildPolyscopeGui() { ImGui::TextUnformatted(" Zoom: [scroll] OR [ctrl/cmd] + [shift] + [left click drag]"); ImGui::TextUnformatted(" To set the view orbit center, double-click OR hold"); ImGui::TextUnformatted(" [ctrl/cmd] + [shift] and [left click] in the scene."); - ImGui::TextUnformatted(" To zoom towards the center, hold [ctrl/cmd] + [shift] and scroll."); ImGui::TextUnformatted(" Save and restore camera poses via the system clipboard with"); ImGui::TextUnformatted(" [ctrl/cmd-c] and [ctrl/cmd-v]."); ImGui::TextUnformatted("\nMenu Navigation:"); diff --git a/src/view.cpp b/src/view.cpp index 074eb82b..b15fd295 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -25,12 +25,13 @@ bool& windowResizable = state::globalContext.windowResizable; NavigateStyle& style = state::globalContext.navigateStyle; UpDir& upDir = state::globalContext.upDir; FrontDir& frontDir = state::globalContext.frontDir; -double& moveScale = state::globalContext.moveScale; -double& nearClipRatio = state::globalContext.nearClipRatio; -double& farClipRatio = state::globalContext.farClipRatio; +float& moveScale = state::globalContext.moveScale; +ViewRelativeMode& viewRelativeMode = state::globalContext.viewRelativeMode; +float& nearClip = state::globalContext.nearClip; +float& farClip = state::globalContext.farClip; std::array& bgColor = state::globalContext.bgColor; glm::mat4x4& viewMat = state::globalContext.viewMat; -double& fov = state::globalContext.fov; +float& fov = state::globalContext.fov; ProjectionMode& projectionMode = state::globalContext.projectionMode; glm::vec3& viewCenter = state::globalContext.viewCenter; bool& midflight = state::globalContext.midflight; @@ -47,52 +48,16 @@ float& flightInitialFov = state::globalContext.flightInitialFov; // Default values const int defaultWindowWidth = 1280; const int defaultWindowHeight = 720; -const double defaultNearClipRatio = 0.005; -const double defaultFarClipRatio = 20.0; -const double defaultFov = 45.; -const double minFov = 5.; // for UI -const double maxFov = 160.; // for UI +const float defaultNearClipRatio = 1e-2; +const float defaultFarClipRatio = 1e2; +const float defaultFov = 45.; +const float minFov = 5.; // for UI +const float maxFov = 160.; // for UI // Small helpers -std::string to_string(ProjectionMode mode) { - switch (mode) { - case ProjectionMode::Perspective: - return "Perspective"; - case ProjectionMode::Orthographic: - return "Orthographic"; - } - return ""; // unreachable -} - -std::string to_string(NavigateStyle style) { - - switch (style) { - case NavigateStyle::Turntable: - return "Turntable"; - break; - case NavigateStyle::Free: - return "Free"; - break; - case NavigateStyle::Planar: - return "Planar"; - break; - case NavigateStyle::Arcball: - return "Arcball"; - break; - case NavigateStyle::None: - return "None"; - break; - case NavigateStyle::FirstPerson: - return "First Person"; - break; - } - - return ""; // unreachable -} namespace { // anonymous helpers - // A default pairing of directions to fall back on when something goes wrong. const std::vector> defaultUpFrontPairs{ {UpDir::NegXUp, FrontDir::NegYFront}, {UpDir::XUp, FrontDir::YFront}, {UpDir::NegYUp, FrontDir::NegZFront}, @@ -232,9 +197,9 @@ void processRotate(glm::vec2 startP, glm::vec2 endP) { case NavigateStyle::Arcball: { // Map inputs to unit sphere auto toSphere = [](glm::vec2 v) { - double x = glm::clamp(v.x, -1.0f, 1.0f); - double y = glm::clamp(v.y, -1.0f, 1.0f); - double mag = x * x + y * y; + float x = glm::clamp(v.x, -1.0f, 1.0f); + float y = glm::clamp(v.y, -1.0f, 1.0f); + float mag = x * x + y * y; if (mag <= 1.0) { return glm::vec3{x, y, -std::sqrt(1.0 - mag)}; } else { @@ -245,7 +210,7 @@ void processRotate(glm::vec2 startP, glm::vec2 endP) { glm::vec3 sphereEnd = toSphere(endP); glm::vec3 rotAxis = -cross(sphereStart, sphereEnd); - double rotMag = std::acos(glm::clamp(dot(sphereStart, sphereEnd), -1.0f, 1.0f) * moveScale); + float rotMag = std::acos(glm::clamp(dot(sphereStart, sphereEnd), -1.0f, 1.0f) * moveScale); glm::mat4 cameraRotate = glm::rotate(glm::mat4x4(1.0), (float)rotMag, glm::vec3(rotAxis.x, rotAxis.y, rotAxis.z)); @@ -298,7 +263,8 @@ void processTranslate(glm::vec2 delta) { } // Process a translation - float movementScale = state::lengthScale * 0.6 * moveScale; + float s = computeRelativeMotionScale(); + float movementScale = 0.6f * s * moveScale; glm::mat4x4 camSpaceT = glm::translate(glm::mat4x4(1.0), movementScale * glm::vec3(delta.x, delta.y, 0.0)); viewMat = camSpaceT * viewMat; @@ -315,14 +281,14 @@ void processTranslate(glm::vec2 delta) { immediatelyEndFlight(); } -void processClipPlaneShift(double amount) { +void processClipPlaneShift(float amount) { if (amount == 0.0) return; // Adjust the near clipping plane - nearClipRatio += .03 * amount * nearClipRatio; + nearClip += .03 * amount * nearClip; requestRedraw(); } -void processZoom(double amount, bool relativeToCenter) { +void processZoom(float amount, bool relativeToCenter) { if (amount == 0.0) return; if (getNavigateStyle() == NavigateStyle::None || getNavigateStyle() == NavigateStyle::FirstPerson) { return; @@ -332,18 +298,24 @@ void processZoom(double amount, bool relativeToCenter) { switch (projectionMode) { case ProjectionMode::Perspective: { - float movementScale; - if (relativeToCenter) { - movementScale = glm::length(view::viewCenter - view::getCameraWorldPosition()) * 0.3 * moveScale; - } else { - movementScale = state::lengthScale * 0.1 * moveScale; + float s = computeRelativeMotionScale(); + float totalZoom = 0.15f * s * amount; + + // Disallow zooming that would cross the center point + if (getNavigateStyle() == NavigateStyle::Turntable) { + float currSignedDistToCenter = glm::dot(getLookVec(), view::viewCenter - view::getCameraWorldPosition()); + float minDistToCenter = computeRelativeMotionScale() * 1e-5; + float maxAllowedZoom = currSignedDistToCenter - minDistToCenter; + totalZoom = glm::min(totalZoom, maxAllowedZoom); } - glm::mat4x4 camSpaceT = glm::translate(glm::mat4x4(1.0), glm::vec3(0., 0., movementScale * amount)); + + glm::mat4x4 camSpaceT = glm::translate(glm::mat4x4(1.0), glm::vec3(0., 0., totalZoom)); viewMat = camSpaceT * viewMat; + break; } case ProjectionMode::Orthographic: { - double fovScale = std::min(fov - minFov, maxFov - fov) / (maxFov - minFov); + float fovScale = std::min(fov - minFov, maxFov - fov) / (maxFov - minFov); fov += -fovScale * amount; fov = glm::clamp(fov, minFov, maxFov); break; @@ -393,8 +365,9 @@ void processKeyboardNavigation(ImGuiIO& io) { hasMovement = true; } - float movementScale = state::lengthScale * ImGui::GetIO().DeltaTime * moveScale; - glm::mat4x4 camSpaceT = glm::translate(glm::mat4x4(1.0), movementScale * delta); + float s = computeRelativeMotionScale(); + float movementMult = s * ImGui::GetIO().DeltaTime * moveScale; + glm::mat4x4 camSpaceT = glm::translate(glm::mat4x4(1.0), movementMult * delta); viewMat = camSpaceT * viewMat; } @@ -439,6 +412,18 @@ void ensureViewValid() { } } +float computeRelativeMotionScale() { + switch (viewRelativeMode) { + case ViewRelativeMode::CenterRelative: { + float distToCenter = glm::length(view::viewCenter - view::getCameraWorldPosition()); + return distToCenter; + } + case ViewRelativeMode::LengthRelative: { + return state::lengthScale; + } + } +} + glm::mat4 computeHomeView() { glm::vec3 target = view::viewCenter; @@ -467,8 +452,8 @@ void resetCameraToHomeView() { viewMat = computeHomeView(); fov = defaultFov; - nearClipRatio = defaultNearClipRatio; - farClipRatio = defaultFarClipRatio; + nearClip = defaultNearClipRatio; + farClip = defaultFarClipRatio; requestRedraw(); } @@ -481,8 +466,8 @@ void flyToHomeView() { glm::mat4x4 T = computeHomeView(); float Tfov = defaultFov; - nearClipRatio = defaultNearClipRatio; - farClipRatio = defaultFarClipRatio; + nearClip = defaultNearClipRatio; + farClip = defaultFarClipRatio; startFlightTo(T, Tfov); } @@ -583,14 +568,23 @@ void updateViewAndChangeCenter(glm::vec3 newCenter, bool flyTo) { // to the center. switch (style) { case NavigateStyle::Turntable: - case NavigateStyle::Planar: case NavigateStyle::Arcball: + case NavigateStyle::Free: + case NavigateStyle::FirstPerson: // this is a decent baseliny policy that always does _something_ sane // might want nicer policies for certain cameras lookAt(getCameraWorldPosition(), view::viewCenter, flyTo); break; - case NavigateStyle::Free: - case NavigateStyle::FirstPerson: + case NavigateStyle::Planar: { + // move the camera within the planar constraint + glm::vec3 lookDir = getCameraParametersForCurrentView().getLookDir(); + glm::vec3 camPos = getCameraWorldPosition(); + glm::vec3 targetVec = newCenter - camPos; + glm::vec3 planarDir = getFrontVec(); + glm::vec3 newCamPos = newCenter - planarDir * glm::dot(planarDir, targetVec); + lookAt(newCamPos, view::viewCenter, flyTo); + break; + } case NavigateStyle::None: // no change needed break; @@ -657,7 +651,7 @@ void setViewToCamera(const CameraParameters& p) { CameraParameters getCameraParametersForCurrentView() { ensureViewValid(); - double aspectRatio = (float)bufferWidth / bufferHeight; + float aspectRatio = (float)bufferWidth / bufferHeight; return CameraParameters(CameraIntrinsics::fromFoVDegVerticalAndAspect(fov, aspectRatio), CameraExtrinsics::fromMatrix(viewMat)); } @@ -680,24 +674,40 @@ void setProjectionMode(ProjectionMode newMode) { requestRedraw(); } + +ViewRelativeMode getViewRelativeMode() { return viewRelativeMode; } +void setViewRelativeMode(ViewRelativeMode newMode) { + viewRelativeMode = newMode; + requestRedraw(); +} +void setClipPlanes(float newNearClip, float newFarClip) { + nearClip = newNearClip; + farClip = newFarClip; + requestRedraw(); +} +std::tuple getClipPlanes() { return std::tuple(nearClip, farClip); } + float getVerticalFieldOfViewDegrees() { return view::fov; } float getAspectRatioWidthOverHeight() { return (float)bufferWidth / bufferHeight; } glm::mat4 getCameraPerspectiveMatrix() { - double farClip = farClipRatio * state::lengthScale; - double nearClip = nearClipRatio * state::lengthScale; - double fovRad = glm::radians(fov); - double aspectRatio = (float)bufferWidth / bufferHeight; + + // Set the clip plane + float absNearClip, absFarClip; + std::tie(absNearClip, absFarClip) = computeClipPlanes(); + + float fovRad = glm::radians(fov); + float aspectRatio = (float)bufferWidth / bufferHeight; switch (projectionMode) { case ProjectionMode::Perspective: { - return glm::perspective(fovRad, aspectRatio, nearClip, farClip); + return glm::perspective(fovRad, aspectRatio, absNearClip, absFarClip); break; } case ProjectionMode::Orthographic: { - double vert = tan(fovRad / 2.) * state::lengthScale * 2.; - double horiz = vert * aspectRatio; - return glm::ortho(-horiz, horiz, -vert, vert, nearClip, farClip); + float vert = tan(fovRad / 2.) * state::lengthScale * 2.; + float horiz = vert * aspectRatio; + return glm::ortho(-horiz, horiz, -vert, vert, absNearClip, absFarClip); break; } } @@ -725,6 +735,18 @@ void getCameraFrame(glm::vec3& lookDir, glm::vec3& upDir, glm::vec3& rightDir) { rightDir = Rt * glm::vec3(1.0, 0.0, 0.0); } +glm::vec3 getLookVec() { + glm::mat3x3 R; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + R[i][j] = viewMat[i][j]; + } + } + glm::mat3x3 Rt = glm::transpose(R); + + return Rt * glm::vec3(0.0, 0.0, -1.0); +} + glm::vec3 screenCoordsToWorldRay(glm::vec2 screenCoords) { glm::mat4 view = getCameraViewMatrix(); @@ -753,6 +775,12 @@ glm::vec3 bufferCoordsToWorldRay(glm::vec2 bufferCoords) { return worldRayDir; } +std::tuple computeClipPlanes() { + float s = computeRelativeMotionScale(); + float absFarClip = farClip * s; + float absNearClip = nearClip * s; + return std::make_tuple(absNearClip, absFarClip); +} glm::vec3 screenCoordsAndDepthToWorldPosition(glm::vec2 screenCoords, float clipDepth) { @@ -791,10 +819,14 @@ void startFlightTo(const glm::mat4& T, float targetFov, float flightLengthInSeco flightStartTime = ImGui::GetTime(); flightEndTime = ImGui::GetTime() + flightLengthInSeconds; + // NOTE: we interpolate the _inverse_ view matrix (then invert back), because it looks better + // when far from the origin + // Initial parameters glm::mat3x4 Rstart; glm::vec3 Tstart; - splitTransform(getCameraViewMatrix(), Rstart, Tstart); + glm::mat4 viewInv = glm::inverse(getCameraViewMatrix()); + splitTransform(viewInv, Rstart, Tstart); flightInitialViewR = glm::dualquat_cast(Rstart); flightInitialViewT = Tstart; flightInitialFov = fov; @@ -802,7 +834,8 @@ void startFlightTo(const glm::mat4& T, float targetFov, float flightLengthInSeco // Final parameters glm::mat3x4 Rend; glm::vec3 Tend; - splitTransform(T, Rend, Tend); + glm::mat4 Tinv = glm::inverse(T); + splitTransform(Tinv, Rend, Tend); flightTargetViewR = glm::dualquat_cast(Rend); flightTargetViewT = Tend; flightTargetFov = targetFov; @@ -813,11 +846,15 @@ void startFlightTo(const glm::mat4& T, float targetFov, float flightLengthInSeco void immediatelyEndFlight() { midflight = false; } void updateFlight() { + + // NOTE: we interpolate the _inverse_ view matrix (then invert back), because it looks better + // when far from the origin + if (midflight) { if (ImGui::GetTime() > flightEndTime) { // Flight is over, ensure we end exactly at target location midflight = false; - viewMat = buildTransform(glm::mat3x4_cast(flightTargetViewR), flightTargetViewT); + viewMat = glm::inverse(buildTransform(glm::mat3x4_cast(flightTargetViewR), flightTargetViewT)); fov = flightTargetFov; } else { // normalized time for spline on [0,1] @@ -827,10 +864,9 @@ void updateFlight() { // linear spline glm::dualquat interpR = glm::lerp(flightInitialViewR, flightTargetViewR, tSmooth); - glm::vec3 interpT = glm::mix(flightInitialViewT, flightTargetViewT, tSmooth); - viewMat = buildTransform(glm::mat3x4_cast(interpR), interpT); + viewMat = glm::inverse(buildTransform(glm::mat3x4_cast(interpR), interpT)); // linear spline fov = (1.0f - t) * flightInitialFov + t * flightTargetFov; @@ -843,7 +879,7 @@ std::string getViewAsJson() { // Get the view matrix (note weird glm indexing, glm is [col][row]) glm::mat4 viewMat = getCameraViewMatrix(); - std::array viewMatFlat; + std::array viewMatFlat; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { viewMatFlat[4 * i + j] = viewMat[j][i]; @@ -854,11 +890,16 @@ std::string getViewAsJson() { json j = { {"fov", fov}, {"viewMat", viewMatFlat}, - {"nearClipRatio", nearClipRatio}, - {"farClipRatio", farClipRatio}, + {"nearClip", nearClip}, + {"farClip", farClip}, {"windowWidth", view::windowWidth}, {"windowHeight", view::windowHeight}, - {"projectionMode", to_string(view::projectionMode)}, + {"projectionMode", enum_to_string(view::projectionMode)}, + {"navigateStyle", enum_to_string(view::style)}, + {"upDir", enum_to_string(view::upDir)}, + {"frontDir", enum_to_string(view::frontDir)}, + {"viewRelativeMode", enum_to_string(view::viewRelativeMode)}, + {"viewCenter", {view::viewCenter.x, view::viewCenter.y, view::viewCenter.z}}, }; std::string outString = j.dump(); @@ -868,10 +909,10 @@ std::string getCameraJson() { return getViewAsJson(); } void setViewFromJson(std::string jsonData, bool flyTo) { // Values will go here - glm::mat4 newViewMat; - double newFov = -777; - double newNearClipRatio = -777; - double newFarClipRatio = -777; + glm::mat4 newViewMat = viewMat; + float newFov = fov; + float newNearClipRatio = nearClip; + float newFarClipRatio = farClip; int windowWidth = view::windowWidth; int windowHeight = view::windowHeight; @@ -884,62 +925,120 @@ void setViewFromJson(std::string jsonData, bool flyTo) { s >> j; // Read out the data - auto matData = j["viewMat"]; - if (matData.size() != 16) return; // check size - auto it = matData.begin(); - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - newViewMat[j][i] = *it; - it++; - } - } - newFov = j["fov"]; // Get the clip ratios, but only if present - if (j.find("nearClipRatio") != j.end()) { - newNearClipRatio = j["nearClipRatio"]; + bool clipChanged = false; + if (j.find("nearClip") != j.end()) { + newNearClipRatio = j["nearClip"]; + clipChanged = true; + } + if (j.find("farClip") != j.end()) { + newFarClipRatio = j["farClip"]; + clipChanged = true; } - if (j.find("farClipRatio") != j.end()) { - newFarClipRatio = j["farClipRatio"]; + if (clipChanged) { + setClipPlanes(newNearClipRatio, newFarClipRatio); } // Get the window sizes, if present + bool windowSizeChanged = false; if (j.find("windowWidth") != j.end()) { windowWidth = j["windowWidth"]; + windowSizeChanged = true; } if (j.find("windowHeight") != j.end()) { windowHeight = j["windowHeight"]; + windowSizeChanged = true; + } + if (windowSizeChanged) { + view::setWindowSize(windowWidth, windowHeight); } if (j.find("projectionMode") != j.end()) { std::string projectionModeStr = j["projectionMode"]; - if (projectionModeStr == to_string(ProjectionMode::Perspective)) { - view::projectionMode = ProjectionMode::Perspective; - } else if (projectionModeStr == to_string(ProjectionMode::Orthographic)) { - view::projectionMode = ProjectionMode::Orthographic; + ProjectionMode newProjectionMode; + try_enum_from_string(projectionModeStr, newProjectionMode); // fail silently if unrecognized + setProjectionMode(newProjectionMode); + } + + if (j.find("navigateStyle") != j.end()) { + std::string navigateStyleStr = j["navigateStyle"]; + NavigateStyle newStyle; + if (try_enum_from_string(navigateStyleStr, newStyle)) { + setNavigateStyle(newStyle, flyTo); } } - } catch (...) { - // If anything goes wrong parsing, just give up - return; - } + if (j.find("upDir") != j.end()) { + std::string upDirStr = j["upDir"]; + UpDir newUpDir; + if (try_enum_from_string(upDirStr, newUpDir)) { + updateViewAndChangeUpDir(newUpDir, flyTo); + } + } - // === Assign the new values + if (j.find("frontDir") != j.end()) { + std::string frontDirStr = j["frontDir"]; + FrontDir newFrontDir; + if (try_enum_from_string(frontDirStr, newFrontDir)) { + updateViewAndChangeFrontDir(newFrontDir, flyTo); + } + } - view::setWindowSize(windowWidth, windowHeight); + if (j.find("viewRelativeMode") != j.end()) { + std::string viewRelativeModeStr = j["viewRelativeMode"]; + ViewRelativeMode newViewRelativeMode; + if (try_enum_from_string(viewRelativeModeStr, newViewRelativeMode)) { + setViewRelativeMode(newViewRelativeMode); + } + } - if (newNearClipRatio > 0) nearClipRatio = newNearClipRatio; - if (newFarClipRatio > 0) farClipRatio = newFarClipRatio; + if (j.find("viewCenter") != j.end()) { + auto centerData = j["viewCenter"]; + if (centerData.size() == 3) { + glm::vec3 newCenter; + newCenter.x = centerData[0]; + newCenter.y = centerData[1]; + newCenter.z = centerData[2]; + setViewCenter(newCenter, flyTo); + } + } + + // NOTE: it's important that we do this after the mode/dir settings, as this + // lets us do our flight + bool viewChanged = false; + if (j.find("fov") == j.end()) { + newFov = j["fov"]; + viewChanged = true; + } + if (j.find("viewMat") != j.end()) { + auto matData = j["viewMat"]; + if (matData.size() != 16) return; // check size + auto it = matData.begin(); + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + newViewMat[j][i] = *it; + it++; + } + } + viewChanged = true; + } + if (viewChanged) { + if (flyTo) { + startFlightTo(newViewMat, fov); + } else { + viewMat = newViewMat; + fov = newFov; + requestRedraw(); + } + } - if (flyTo) { - startFlightTo(newViewMat, fov); - } else { - viewMat = newViewMat; - fov = newFov; - requestRedraw(); + } catch (...) { + // If anything goes wrong parsing, just give up + return; } } + void setCameraFromJson(std::string jsonData, bool flyTo) { setViewFromJson(jsonData, flyTo); } void buildViewGui() { @@ -953,7 +1052,7 @@ void buildViewGui() { // == Camera style - std::string viewStyleName = to_string(view::style); + std::string viewStyleName = enum_to_string(view::style); ImGui::PushItemWidth(120 * options::uiScale); std::array styles{NavigateStyle::Turntable, NavigateStyle::FirstPerson, NavigateStyle::Free, @@ -961,7 +1060,7 @@ void buildViewGui() { if (ImGui::BeginCombo("##View Style", viewStyleName.c_str())) { for (NavigateStyle s : styles) { - if (ImGui::Selectable(to_string(s).c_str(), view::style == s)) { + if (ImGui::Selectable(enum_to_string(s).c_str(), view::style == s)) { setNavigateStyle(s, true); ImGui::SetItemDefaultFocus(); } @@ -1098,6 +1197,20 @@ void buildViewGui() { ImGuiSliderFlags_Logarithmic | ImGuiSliderFlags_NoRoundToFormat); view::moveScale = moveScaleF; + // Relative movement + int viewRelativeModeInt = static_cast(viewRelativeMode); + if (ImGui::RadioButton("center relative##relMode", &viewRelativeModeInt, + static_cast(ViewRelativeMode::CenterRelative))) { + setViewRelativeMode(ViewRelativeMode::CenterRelative); + } + ImGui::SameLine(); + if (ImGui::RadioButton("length relative##relMode", &viewRelativeModeInt, + static_cast(ViewRelativeMode::LengthRelative))) { + setViewRelativeMode(ViewRelativeMode::LengthRelative); + } + + // Show the center + ImGui::Text("Center: <%.3g, %.3g, %.3g>", view::viewCenter.x, view::viewCenter.y, view::viewCenter.z); if (ImGui::TreeNode("Scene Extents")) { @@ -1105,35 +1218,35 @@ void buildViewGui() { updateStructureExtents(); } - if (!options::automaticallyComputeSceneExtents) { + ImGui::BeginDisabled(options::automaticallyComputeSceneExtents); - static float lengthScaleUpper = -777; - if (lengthScaleUpper == -777) lengthScaleUpper = 2. * state::lengthScale; - if (ImGui::SliderFloat("Length Scale", &state::lengthScale, 0, lengthScaleUpper, "%.5f")) { - requestRedraw(); - } - if (ImGui::IsItemDeactivatedAfterEdit()) { - // the upper bound for the slider is dynamically adjust to be a bit bigger than the lower bound, but only - // does so on release of the widget (so it doesn't scaleo off to infinity), and only ever gets larger (so - // you don't get stuck at 0) - lengthScaleUpper = std::fmax(2. * state::lengthScale, lengthScaleUpper); - } - - ImGui::TextUnformatted("Bounding Box:"); - ImGui::PushItemWidth(200 * options::uiScale); - glm::vec3& bboxMin = std::get<0>(state::boundingBox); - glm::vec3& bboxMax = std::get<1>(state::boundingBox); - if (ImGui::InputFloat3("min", &bboxMin[0])) updateStructureExtents(); - if (ImGui::InputFloat3("max", &bboxMax[0])) updateStructureExtents(); - ImGui::PopItemWidth(); + static float lengthScaleUpper = -777; + if (lengthScaleUpper == -777) lengthScaleUpper = 2. * state::lengthScale; + if (ImGui::SliderFloat("Length Scale", &state::lengthScale, 0, lengthScaleUpper, "%.5f")) { + requestRedraw(); + } + if (ImGui::IsItemDeactivatedAfterEdit()) { + // the upper bound for the slider is dynamically adjust to be a bit bigger than the lower bound, but only + // does so on release of the widget (so it doesn't scaleo off to infinity), and only ever gets larger (so + // you don't get stuck at 0) + lengthScaleUpper = std::fmax(2. * state::lengthScale, lengthScaleUpper); } + ImGui::TextUnformatted("Bounding Box:"); + ImGui::PushItemWidth(200 * options::uiScale); + glm::vec3& bboxMin = std::get<0>(state::boundingBox); + glm::vec3& bboxMax = std::get<1>(state::boundingBox); + if (ImGui::InputFloat3("min", &bboxMin[0])) updateStructureExtents(); + if (ImGui::InputFloat3("max", &bboxMax[0])) updateStructureExtents(); + ImGui::PopItemWidth(); + + ImGui::EndDisabled(); + ImGui::TreePop(); } - ImGui::SetNextItemOpen(false, ImGuiCond_FirstUseEver); if (ImGui::TreeNode("Camera Parameters")) { @@ -1144,22 +1257,7 @@ void buildViewGui() { requestRedraw(); }; - // Clip planes - float nearClipRatioF = nearClipRatio; - float farClipRatioF = farClipRatio; - if (ImGui::SliderFloat(" Clip Near", &nearClipRatioF, 0., 10., "%.5f", - ImGuiSliderFlags_Logarithmic | ImGuiSliderFlags_NoRoundToFormat)) { - nearClipRatio = nearClipRatioF; - requestRedraw(); - } - if (ImGui::SliderFloat(" Clip Far", &farClipRatioF, 1., 1000., "%.2f", - ImGuiSliderFlags_Logarithmic | ImGuiSliderFlags_NoRoundToFormat)) { - farClipRatio = farClipRatioF; - requestRedraw(); - } - - - std::string projectionModeStr = to_string(view::projectionMode); + std::string projectionModeStr = enum_to_string(view::projectionMode); if (ImGui::BeginCombo("##ProjectionMode", projectionModeStr.c_str())) { if (ImGui::Selectable("Perspective", view::projectionMode == ProjectionMode::Perspective)) { setProjectionMode(ProjectionMode::Perspective); @@ -1174,6 +1272,23 @@ void buildViewGui() { ImGui::SameLine(); ImGui::Text("Projection"); + if (ImGui::TreeNode("Clip Planes")) { + if (ImGui::SliderFloat("Near", &nearClip, 0., 10., "%.5f", + ImGuiSliderFlags_Logarithmic | ImGuiSliderFlags_NoRoundToFormat)) { + requestRedraw(); + } + if (ImGui::SliderFloat("Far", &farClip, 1., 10000., "%.2f", + ImGuiSliderFlags_Logarithmic | ImGuiSliderFlags_NoRoundToFormat)) { + requestRedraw(); + } + float absNearClip, absFarClip; + std::tie(absNearClip, absFarClip) = computeClipPlanes(); + ImGui::TextUnformatted("Computed:"); + ImGui::Text(" near: %g", absNearClip); + ImGui::Text(" far: %g", absFarClip); + + ImGui::TreePop(); + } ImGui::TreePop(); }