diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6268e1a..02d82d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,19 @@ env: PROGNAME: managlyph jobs: + version-sync: + name: Version sync check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Verify application version consistency + run: ./scripts/check-version-sync.sh + linux: name: Linux (Ubuntu) + needs: version-sync runs-on: ubuntu-latest steps: @@ -45,6 +56,7 @@ jobs: # -------------------------------------------------- windows-mingw: name: Windows (MSYS2 MinGW64) + needs: version-sync runs-on: windows-latest defaults: diff --git a/CMakeLists.txt b/CMakeLists.txt index e2e4df6..374a497 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,7 @@ add_executable(managlyph src/data/orbitals/wavefunction.cpp src/gui/anaglyph_widget.cpp src/gui/interface_window.cpp - src/gui/lighting_settings_dialog.cpp + src/gui/visualisation_settings_dialog.cpp src/gui/logwindow.cpp src/gui/mainwindow.cpp src/gui/orbital_widget.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0af7957 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,92 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Ensure any install or build dependencies are removed before the end of the layer when doing a + build. +2. Update the README.md with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +3. Increase the version numbers in any examples files and the README.md to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/README.md b/README.md index c1f246a..3975559 100644 --- a/README.md +++ b/README.md @@ -5,47 +5,51 @@ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) ## Overview -Managlyph is a powerful educational tool for visualizing atomic and molecular -structures in 3D. It enables students and educators to explore atomic orbitals, -reaction mechanisms, and molecular properties interactively, enhancing learning -through dynamic 3D visualizations using red/cyan glasses or 3D monitors. Its -versatility and ease of use make it an invaluable resource for teaching complex -chemical concepts. - -> [!TIP] -> * Download the Windows 64 bit installer [via this link](https://github.com/ifilot/managlyph/releases/latest/download/managlyph-installer-win64.exe) -> * Detailed information on how to use Managlyph can be found in the [online manual](https://ifilot.github.io/managlyph/). - -## Features - -- **Anaglyph Projection**: Immerse yourself in stunning 3D visualizations with - just a simple pair of red/cyan anaglyph glasses, bringing depth and realism to - your viewing experience. For those with specialized 3D equipment, the system - also supports interlacing, enabling seamless compatibility with a wide range - of 3D monitors and projectors. Whether you're exploring 3D content on basic - setups or advanced displays, this feature ensures accessibility and enhanced - visual performance for everyone. - - ![red/cyan stereographic projection](img/screenshot-anaglyph.png) -- **Molecular Orbitals**: Managlyph leverages the versatile 'Atom Bond Object' - (ABO) file format, a powerful tool for embedding and managing multiple - atomistic structures. These files can encapsulate complex details, such as - molecular geometries, chemical bonding, and even molecular orbitals - represented as isosurfaces. This flexibility allows for the seamless - integration and visualization of intricate structural and electronic - properties, making it an invaluable resource for advanced scientific modeling - and analysis. - - ![molecular orbitals](img/screenshot-molecular-orbitals.png) -- **Atomic Orbitals**: Create, project, and rotate atomic orbitals up to the 4f - subshell, providing an unparalleled opportunity to explore the intricate - details of atomic structures. This capability allows users to visualize and - analyze the shapes, orientations, and complexities of orbitals in 3D, offering - a deeper understanding of quantum mechanics and atomic interactions. - - ![molecular orbitals](img/screenshot-atomic-orbitals.png) - -## Boost - -**Managlyph** was developed with the generous financial support of the -[TU/e boost program](https://boost.tue.nl/projects/3d-visualization-with-artificial-intelligence-and-projection/). \ No newline at end of file +Managlyph is a desktop application for 3D visualization of atomic and molecular +structures. + +The application supports stereoscopic rendering via red/cyan anaglyph projection +and interlaced output for compatible 3D displays. Typical use cases include +classroom demonstrations, self-study, and interactive exploration of molecular +models. + +## Documentation and Downloads +- Windows 64-bit installer: + https://github.com/ifilot/managlyph/releases/latest/download/managlyph-installer-win64.exe +- User manual: + https://ifilot.github.io/managlyph/ + +## Capabilities + +### Stereoscopic Rendering +Managlyph provides red/cyan anaglyph rendering and interlaced stereo output. +These modes are intended for use with either anaglyph glasses or supported 3D +monitors/projectors. + +![red/cyan stereographic projection](img/screenshot-anaglyph.png) + +### ABO File Support and Molecular Orbitals +Managlyph uses the Atom Bond Object (ABO) format to store atomistic structures +and related metadata. ABO datasets can include molecular geometries, bonding +information, and molecular-orbital isosurfaces for visualization. + +![molecular orbitals](img/screenshot-molecular-orbitals.png) + +### Atomic Orbitals +The application can generate and render atomic orbitals up to the 4f subshell, +including rotation and inspection in 3D. + +![atomic orbitals](img/screenshot-atomic-orbitals.png) + +### Reaction Pathway Visualization +Managlyph can visualize elementary reaction pathways by interpolating between +structures (for example: reactants, intermediates, transition states, and +products). This allows stepwise inspection of geometry changes during a +reaction. + +![reaction pathway](img/screenshot-reaction.png) + +## Project Support +Development of Managlyph received financial support from the TU/e Boost +program: +https://boost.tue.nl/projects/3d-visualization-with-artificial-intelligence-and-projection/ diff --git a/assets/containers/catalysis/co1121_ch_hydrogenation.abo b/assets/containers/catalysis/co1121_ch_hydrogenation.abo new file mode 100644 index 0000000..66110c7 Binary files /dev/null and b/assets/containers/catalysis/co1121_ch_hydrogenation.abo differ diff --git a/assets/containers/catalysis/co1121_co_dissociation.abo b/assets/containers/catalysis/co1121_co_dissociation.abo new file mode 100644 index 0000000..56158e4 Binary files /dev/null and b/assets/containers/catalysis/co1121_co_dissociation.abo differ diff --git a/assets/shaders/phong.fs b/assets/shaders/phong.fs index 4a0b2a4..66b0780 100644 --- a/assets/shaders/phong.fs +++ b/assets/shaders/phong.fs @@ -12,6 +12,7 @@ uniform float specular_strength; uniform float shininess; // specular exponent (e.g. 16–128) uniform float edge_strength; uniform float edge_power; +uniform int camera_mode; out vec4 fragColor; @@ -21,6 +22,9 @@ void main() vec3 N = normalize(normal_eyespace); vec3 L = normalize(lightdirection_eyespace); vec3 V = normalize(vertex_direction_eyespace); + if (camera_mode == 1) { + V = vec3(0.0, 0.0, 1.0); + } // --- Ambient --- vec3 ambient = ambient_strength * light_color; diff --git a/assets/shaders/simplecanvas.fs b/assets/shaders/simplecanvas.fs index c02aaa1..df8979e 100644 --- a/assets/shaders/simplecanvas.fs +++ b/assets/shaders/simplecanvas.fs @@ -9,10 +9,5 @@ layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord; void main() { - vec3 col = texture(regular_texture, TexCoords).rgb; - if(length(col) > .1) { - FragColor = vec4(col, 1.0f); - } else { - FragColor = vec4(col, 0.0f); - } + FragColor = texture(regular_texture, TexCoords); } diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index da5c0c9..e28d7e5 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -30,4 +30,15 @@ background: #24292e; padding: 6px 10px; border-radius: 4px; +} + +/* Disable horizontal scrolling for tables */ +.wy-table-responsive { + overflow-x: visible !important; +} + +/* Allow table cells to wrap normally */ +.wy-table-responsive table td, +.wy-table-responsive table th { + white-space: normal !important; } \ No newline at end of file diff --git a/docs/source/_static/img/screenshot_4dz2.png b/docs/source/_static/img/screenshot_4dz2.png new file mode 100644 index 0000000..f64955c Binary files /dev/null and b/docs/source/_static/img/screenshot_4dz2.png differ diff --git a/docs/source/_static/img/screenshot_anaglyph.png b/docs/source/_static/img/screenshot_anaglyph.png new file mode 100644 index 0000000..a4b1632 Binary files /dev/null and b/docs/source/_static/img/screenshot_anaglyph.png differ diff --git a/docs/source/_static/img/screenshot_checkerboard.png b/docs/source/_static/img/screenshot_checkerboard.png new file mode 100644 index 0000000..3e4cf24 Binary files /dev/null and b/docs/source/_static/img/screenshot_checkerboard.png differ diff --git a/docs/source/_static/img/screenshot_select_anaglyph.png b/docs/source/_static/img/screenshot_select_anaglyph.png new file mode 100644 index 0000000..3cb4131 Binary files /dev/null and b/docs/source/_static/img/screenshot_select_anaglyph.png differ diff --git a/docs/source/_static/img/screenshot_visualisation_settings.png b/docs/source/_static/img/screenshot_visualisation_settings.png new file mode 100644 index 0000000..12b23bd Binary files /dev/null and b/docs/source/_static/img/screenshot_visualisation_settings.png differ diff --git a/docs/source/building_models.rst b/docs/source/building_models.rst new file mode 100644 index 0000000..6d79658 --- /dev/null +++ b/docs/source/building_models.rst @@ -0,0 +1,40 @@ +Building Models +=============== + +Managlyph renders 3D geometry stored inside ABO containers. These models can +represent molecular orbitals, isosurfaces, or general 3D meshes. Models are +typically prepared using **AboBuilder**, a companion tool for constructing ABO +files compatible with Managlyph. + +Using AboBuilder +---------------- + +AboBuilder provides tools for constructing atomic structures and generating +renderable geometry for use in Managlyph. It supports: + +- construction of molecular and atomic structures +- generation of orbital and isosurface meshes +- export of frame data and geometry to the ABO container format + +AboBuilder is available at: `https://ifilot.github.io/abobuilder/ `_ + +Generated ABO files can be opened directly in Managlyph. + +Integration with PyQInt +----------------------- + +AboBuilder can integrate with **PyQInt**, a quantum chemistry library used to +compute electronic structure properties such as molecular orbitals and electron +density distributions. + +PyQInt project page: `https://github.com/ifilot/pyqint `_ + +This integration enables the generation of orbital surfaces and related data +for visualization. + +Typical workflow: + +1. Compute molecular orbitals or electron density using PyQInt. +2. Use AboBuilder (in conjunction with PyTessel) to convert computed data into isosurfaces or meshes. +3. Export the result to an ABO container. +4. Open the ABO file in Managlyph for stereographic visualization. \ No newline at end of file diff --git a/docs/source/community_guidelines.rst b/docs/source/community_guidelines.rst new file mode 100644 index 0000000..7cc5947 --- /dev/null +++ b/docs/source/community_guidelines.rst @@ -0,0 +1,13 @@ +.. index:: Community Guidelines + +Community guidelines +==================== + +* Contributions to :program:`Managlyph` are always welcome and appreciated. Before doing + so, please first read the `CONTRIBUTING `_ + guide. +* For reporting issues or problems with the software, you are kindly invited to + `to open a new issue on Github with the bug tag `_. +* If you seek support in using :program:`Managlyph`, please + `open an issue with the question tag `_. +* If you wish to contact the developers, please send an e-mail to ivo@ivofilot.nl. diff --git a/docs/source/file_format.rst b/docs/source/file_format.rst new file mode 100644 index 0000000..de5215c --- /dev/null +++ b/docs/source/file_format.rst @@ -0,0 +1,304 @@ +File format +=========== + +File Variants and Detection +--------------------------- + +ABO files are stored as **little-endian binary** data. Managlyph supports a +legacy variant and an extended variant (ABOF). The loader distinguishes these +variants using a sentinel value stored in the first two bytes of the file. + +.. list-table:: + :header-rows: 1 + + * - First field (offset 0) + - Interpretation + * - ``uint16 nr_frames`` is non-zero + - **Legacy ABO**. + The value is the frame count and parsing continues + with the first frame record. + * - ``uint16 nr_frames`` is zero + - **ABOF**. + A header follows immediately and the actual frame + count is read from the payload stream after the + header has been processed. + +ABOF Header +----------- + +When the initial ``nr_frames`` value is zero, the loader reads an ABOF header: + +.. list-table:: + :widths: 20 15 65 + :header-rows: 1 + + * - Offset + - Size + - Description + * - 2 + - 4 bytes + - Magic identifier ``ABOF`` (ASCII) + * - 6 + - 1 byte + - Format version (supported: 1 or 2) + * - 7 + - 1 byte + - Flags + +The magic identifier must match ``ABOF``. Unsupported versions are rejected. + +ABOF Flags +---------- + +The flags byte controls global properties of the payload: + +.. list-table:: + :widths: 20 80 + :header-rows: 1 + + * - Flag + - Meaning + * - ``0x01`` + - Payload is compressed using **Zstandard (zstd)**. + * - ``0x02`` + - Container represents a reaction pathway (NEB); interpolation may be + performed after loading. + +If the compression flag is set, the loader reads the remaining file content +as a zstd frame, decompresses it, and continues parsing from the decompressed +byte stream. If compression is not set, parsing continues directly from the +file stream. + +After header processing (and optional decompression), the loader reads the +actual frame count (``uint16 nr_frames``) from the selected input stream. + +Legacy vs. ABOF v1 vs. ABOF v2 +------------------------------ + +The following differences are relevant to readers and writers. + +.. list-table:: + :widths: 25 25 50 + :header-rows: 1 + + * - Variant + - Detection + - Behavioral differences in the loader + * - Legacy ABO + - First ``uint16 nr_frames`` is non-zero. + - No header, no global flags. Vertex normals are read as + 3 × ``float32`` per vertex. + * - ABOF v1 + - First ``uint16`` is zero, followed by ``ABOF`` and version ``1``. + - Supports global flags, including optional zstd compression and + reaction-pathway marking. Vertex normals are read using octahedral + encoding (2 × ``int16`` per vertex). + * - ABOF v2 + - First ``uint16`` is zero, followed by ``ABOF`` and version ``2``. + - Same as v1, plus **per-frame flags**. Frames may optionally include a + unit cell matrix (9 × ``float32``) when the unit-cell flag is set. + +Notes: + +- The reaction-pathway flag (``0x02``) does not change the on-disk layout of + frames. It signals post-processing: after loading, Managlyph may generate + interpolated frames if all frames have compatible atom ordering and identity. +- Normal encoding is selected implicitly by the variant: legacy uses float32 + normals; ABOF uses octahedral-encoded normals. + +Frame Record +------------ + +Each frame is stored sequentially and contains structural data and optional +renderable models. + +.. list-table:: + :widths: 30 20 50 + :header-rows: 1 + + * - Field + - Type + - Description + * - Frame index + - ``uint16`` + - Sequential frame identifier. + * - Description length + - ``uint16`` + - Length of descriptor string. + * - Description + - bytes + - Optional UTF-8 descriptor. + * - Frame flags *(v2 only)* + - ``uint8`` + - Indicates presence of optional frame data. + * - Unit cell *(optional)* + - 9 × ``float32`` + - 3×3 unit cell matrix when enabled. + * - Atom count + - ``uint16`` + - Number of atoms in the frame. + * - Atom records + - variable + - Atomic structure data. + * - Model count + - ``uint16`` + - Number of 3D models. + * - Model records + - variable + - Renderable mesh data. + +Frame Flags (Version 2) +----------------------- + +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Flag + - Meaning + * - ``0x01`` + - Unit cell matrix is present. + +If the flag is set, a 3×3 matrix (9 float32 values) immediately follows. + +Atomic Structure +---------------- + +Atomic data defines molecular or crystalline structures. + +.. list-table:: + :widths: 30 20 50 + :header-rows: 1 + + * - Field + - Type + - Description + * - Atomic number + - ``uint8`` + - Element identifier. + * - Position + - 3 × ``float32`` + - Cartesian coordinates. + +Atoms are stored in sequence and must remain consistently ordered across +frames to enable interpolation. + +3D Model Records +---------------- + +Frames may include one or more renderable mesh models. + +.. list-table:: + :widths: 30 20 50 + :header-rows: 1 + + * - Field + - Type + - Description + * - Model index + - ``uint16`` + - Identifier for the model. + * - Color + - 4 × ``float32`` + - RGBA color. + * - Vertex count + - ``uint32`` + - Number of vertices. + * - Vertex positions + - 3 × ``float32`` each + - Vertex coordinates. + * - Vertex normals + - encoded + - Surface normals (encoding depends on format). + * - Face count + - ``uint32`` + - Number of triangular faces. + * - Indices + - 3 × ``uint32`` per face + - Triangle vertex indices. + +Empty models (zero vertices or faces) may be present and should be ignored. + +Normal Encoding +--------------- + +Two normal encoding schemes are supported: + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - Encoding + - Description + * - Float32 (legacy) + - Three 32-bit floats per normal vector. + * - Octahedral 16-bit (ABOF) + - Two 16-bit integers decoded into a normalized vector. + +Octahedral encoding reduces storage size while maintaining accuracy. + +Reaction Pathways & Interpolation +--------------------------------- + +If the reaction pathway flag is set in the file header, the container is +treated as a reaction sequence. + +Interpolation is performed when: + +- frames contain the same number of atoms +- atomic ordering is identical +- atomic identities match + +If these conditions are met, intermediate frames are generated using +Catmull–Rom cubic interpolation. + +Interpolation properties: + +.. list-table:: + :widths: 40 60 + :header-rows: 1 + + * - Property + - Value + * - Steps per segment + - 10 + * - Method + - Catmull–Rom spline interpolation + * - Interpolated data + - atomic positions + * - Result + - smooth reaction trajectory + +If compatibility checks fail, interpolation is skipped. + +Scene Composition +----------------- + +A frame may combine multiple data types: + +- atomic structures +- orbital or isosurface meshes +- general 3D geometry +- periodic unit cell definitions + +This design allows chemical and geometric data to be rendered together. + +Compression +----------- + +When enabled in the header, the payload is compressed using **Zstandard +(zstd)**. The entire payload is decompressed before parsing. + +Capabilities +------------ + +The ABO container format supports: + +- multi-frame scenes and animations +- reaction pathway visualization with interpolation +- atomic and periodic structures +- orbital and surface meshes +- hybrid scenes combining scientific and geometric data +- compact normal encoding +- optional payload compression +- per-frame metadata and descriptors diff --git a/docs/source/index.rst b/docs/source/index.rst index f5cb651..e62ff1d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,20 +1,50 @@ -.. Managlyph documentation master file, created by - sphinx-quickstart on Sun Feb 1 18:16:20 2026. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - Managlyph documentation ======================= -Managlyph is a powerful educational tool for visualizing atomic and molecular -structures in 3D. It enables students and educators to explore atomic orbitals, -reaction mechanisms, and molecular properties interactively, enhancing learning -through dynamic 3D visualizations using red/cyan glasses or 3D monitors. Its -versatility and ease of use make it an invaluable resource for teaching complex -chemical concepts. +Managlyph is a visualization tool designed for stereographic rendering of +scientific and 3D data. It supports anaglyph and interlaced output for 3D +displays, and can visualize atomic structures, molecular and atomic orbitals, +reaction sequences, and general 3D objects using its native ABO container +format. + +Features +======== + +* **Stereographic rendering** + Native support for stereographic visualization as the primary rendering mode. + +* **3D display compatibility** + Output for red/cyan anaglyph glasses and interlaced formats for polarized + 3D monitors and televisions. + +* **Atomic structure visualization** + Rendering of molecular and crystalline structures. + +* **Atomic and molecular orbitals** + Visualization of molecular orbitals and a dedicated mode for atomic orbitals. + +* **Reaction sequence rendering** + Smooth interpolation of reaction pathways using cubic interpolation between frames. + +* **ABO container format** + Native ``.abo`` storage format for bundling atomic data, orbitals, reaction + sequences, and general 3D objects. + +* **Hybrid scene composition** + Combine chemical systems and generic 3D geometry within a single scene. + +* **Frame interpolation** + Automatic generation of smooth transitions for tagged reaction sequences. +* **Scientific visualization focus** + Designed for chemical and structural visualization workflows. .. toctree:: :maxdepth: 2 :caption: Contents: + installation.rst + file_format.rst + building_models.rst + usage.rst + community_guidelines.rst \ No newline at end of file diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 0000000..02db2ce --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,7 @@ +Installation +============ + +Managlyph can be installed using the provided installer. Released versions are +available on the `GitHub Releases page `_. + +The most recent Windows installer can be downloaded directly: `Download latest installer `_ \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 0000000..2a6b485 --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,163 @@ +Usage +===== + +Opening Files +------------- + +Managlyph loads scene data from ABO containers. Files can be opened in several +ways: + +- Select **File → Open** and choose a ``.abo`` file from the file system. +- Select **File → Open from Library** to browse the built-in library. The + library is organized into categories and provides a search function for + quickly locating example datasets. +- Drag and drop a ``.abo`` file directly into the anaglyph rendering widget. + +Once loaded, the scene is rendered immediately. + +3D Rendering +------------ + +Managlyph supports stereographic output for 3D visualization. The projection +mode can be selected via **View → Projection**. + +.. figure:: _static/img/screenshot_select_anaglyph.png + :alt: File menu showing open options + :align: center + :width: 70% + + File menu showing the available rendering options. + +Available modes include: + +- **Anaglyph (red/cyan)** for use with red/cyan 3D glasses. +- **Interlaced** for compatible 3D displays and televisions. + +.. figure:: _static/img/screenshot_anaglyph.png + :alt: Red cyan anaglyph rendering example + :align: center + :width: 70% + + Example of red/cyan anaglyph rendering. When viewed with red/cyan glasses, + the scene appears in stereoscopic 3D. + +When using interlaced output, additional layout options are available: + +- line interlaced +- column interlaced +- checkerboard pattern + +.. figure:: _static/img/screenshot_checkerboard.png + :alt: Checkerboard interlaced rendering pattern + :align: center + :width: 70% + + Checkerboard interlaced rendering. The image may appear visually unusual + because adjacent pixels are alternately assigned to the left and right eye. + When viewed on a compatible display, the image resolves into a stereoscopic + 3D scene. + +The display order of the stereo pair can also be configured by selecting which +eye is rendered first. + +Changes to projection settings are applied immediately. + +Interface Controls +------------------ + +The main toolbar provides quick access to visualization and playback controls. + +**Axes** + Toggles the display of the 3D coordinate axes. When enabled, the axes + provide spatial orientation within the scene. + +**Rotate** + Enables continuous rotation of the scene about the z-axis. This can help + reveal three-dimensional structure and depth. + +**Ping-pong** + Controls playback behavior when multiple frames are present. When enabled, + playback reverses direction at the final frame, producing a back-and-forth + motion. When disabled, playback restarts from the first frame after reaching + the end. + +**FPS** + Sets the playback speed in frames per second. Higher values increase motion + smoothness, while lower values slow the animation for closer inspection. + +Atomic Orbitals +--------------- + +Managlyph can visualize atomic orbitals using the built-in orbital generator. + +To construct an orbital, open **Tools → Orbital**. The menu lists available +orbitals from **1s** through **4f**. Selecting the **“+”** button next to an +orbital generates its surface representation and displays it in the scene. + +Multiple orbitals may be added to the same frame for comparison or +visualization purposes. + +.. figure:: _static/img/screenshot_4dz2.png + :alt: 4dz2 atomic orbital visualization + :align: center + :width: 60% + + Visualization of the 4d\ :sub:`z²` atomic orbital. The orbital surface + represents regions of equal probability density for the electron. + +Visualization Settings +---------------------- + +Visualization settings control lighting, shading, and rendering quality. +These settings can be accessed via **Tools → Visualisation settings**. + +.. figure:: _static/img/screenshot_visualisation_settings.png + :alt: Visualisation settings dialog + :align: center + :width: 80% + + Visualisation settings panel. + +Lighting Parameters +------------------- + +Lighting controls are provided separately for **Atoms and bonds** and +**Objects (including orbitals)**. This allows independent tuning of molecular +structures and surface models. + +**Ambient strength** + Controls the baseline illumination applied uniformly to the object. + Higher values brighten shadowed regions. + +**Diffuse strength** + Controls how strongly surfaces respond to directional light. + Increasing this value enhances shape perception. + +**Specular strength** + Controls the intensity of reflective highlights. + Higher values produce stronger glossy reflections. + +**Shininess** + Controls the size and sharpness of specular highlights. + Higher values produce smaller, sharper highlights. + +**Edge darkening** + Darkens object edges to enhance depth perception and separation between + overlapping surfaces. + +**Edge power** + Controls the strength and falloff of the edge-darkening effect. + +Rendering +--------- + +**MSAA samples** + Sets the number of multisample anti-aliasing samples used to smooth jagged + edges. Higher values improve visual quality at increased GPU cost. + +**Sphere tessellation** + Controls the geometric detail used when rendering atoms as spheres. + Higher values produce smoother spheres but require more processing. + +**Reset lighting defaults** + Restarts all lighting parameters to their default values. \ No newline at end of file diff --git a/img/screenshot-anaglyph.png b/img/screenshot-anaglyph.png index 692330c..1aa1fd1 100644 Binary files a/img/screenshot-anaglyph.png and b/img/screenshot-anaglyph.png differ diff --git a/img/screenshot-atomic-orbitals.png b/img/screenshot-atomic-orbitals.png index e2fddc3..7f082d0 100644 Binary files a/img/screenshot-atomic-orbitals.png and b/img/screenshot-atomic-orbitals.png differ diff --git a/img/screenshot-molecular-orbitals.png b/img/screenshot-molecular-orbitals.png index e19fada..45effc9 100644 Binary files a/img/screenshot-molecular-orbitals.png and b/img/screenshot-molecular-orbitals.png differ diff --git a/img/screenshot-reaction.png b/img/screenshot-reaction.png new file mode 100644 index 0000000..2e2f28b Binary files /dev/null and b/img/screenshot-reaction.png differ diff --git a/installer.nsi b/installer.nsi index 269253a..fb0c25f 100644 --- a/installer.nsi +++ b/installer.nsi @@ -3,7 +3,7 @@ ; ============================ !define APP_NAME "managlyph" -!define APP_VERSION "1.1.1" +!define APP_VERSION "1.2.0" !define APP_PUBLISHER "Ivo Filot" !define APP_EXE "managlyph.exe" diff --git a/scripts/check-version-sync.sh b/scripts/check-version-sync.sh new file mode 100755 index 0000000..1feb427 --- /dev/null +++ b/scripts/check-version-sync.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +config_version=$(sed -n 's/^#define PROGRAM_VERSION "\([^"]*\)"$/\1/p' src/config.h) +installer_version=$(sed -n 's/^!define APP_VERSION "\([^"]*\)"$/\1/p' installer.nsi) + +if [[ -z "${config_version}" ]]; then + echo "ERROR: Could not read PROGRAM_VERSION from src/config.h" + exit 1 +fi + +if [[ -z "${installer_version}" ]]; then + echo "ERROR: Could not read APP_VERSION from installer.nsi" + exit 1 +fi + +if [[ "${config_version}" != "${installer_version}" ]]; then + echo "ERROR: Version mismatch detected" + echo " src/config.h PROGRAM_VERSION=${config_version}" + echo " installer.nsi APP_VERSION=${installer_version}" + exit 1 +fi + +echo "Versions are in sync: ${config_version}" diff --git a/src/config.h b/src/config.h index e72797c..0267c43 100644 --- a/src/config.h +++ b/src/config.h @@ -22,4 +22,4 @@ #pragma once #define PROGRAM_NAME "Managlyph" -#define PROGRAM_VERSION "1.1.1" +#define PROGRAM_VERSION "1.2.0" diff --git a/src/data/container.h b/src/data/container.h index 2696eea..9a258e8 100644 --- a/src/data/container.h +++ b/src/data/container.h @@ -32,6 +32,7 @@ class Container { private: std::vector> frames; + bool flag_is_neb_pathway = false; public: Container(); @@ -52,6 +53,14 @@ class Container return this->frames.size(); } + inline void set_is_neb_pathway(bool is_neb_pathway) { + this->flag_is_neb_pathway = is_neb_pathway; + } + + inline bool is_neb_pathway() const { + return this->flag_is_neb_pathway; + } + /** * @brief Get maximum dimension of all objects * @return Maximal dimension diff --git a/src/data/container_loader.cpp b/src/data/container_loader.cpp index 1a1503f..fcfad7d 100644 --- a/src/data/container_loader.cpp +++ b/src/data/container_loader.cpp @@ -23,6 +23,8 @@ #include #include +#include +#include #include #include #include @@ -32,6 +34,29 @@ */ ContainerLoader::ContainerLoader() {} +namespace { + +constexpr unsigned int NEB_INTERPOLATION_STEPS_PER_SEGMENT = 10; +constexpr uint8_t FRAME_UNIT_CELL_FLAG_BIT = 0x01; + +using UnitCellMatrix = std::array; + +glm::vec3 catmull_rom(const glm::vec3& p0, + const glm::vec3& p1, + const glm::vec3& p2, + const glm::vec3& p3, + float t) { + const float t2 = t * t; + const float t3 = t2 * t; + return 0.5f * ((2.0f * p1) + + (-p0 + p2) * t + + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 + + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3); +} + + +} // namespace + /** * @brief Loads an abo file from hard drive stored as little endian binary * @@ -82,6 +107,8 @@ std::shared_ptr ContainerLoader::load_data_abo(const std::string& pat NormalEncoding normal_encoding = NormalEncoding::Float32; std::unique_ptr payload_stream; std::string payload_data; + bool is_neb_pathway = false; + uint8_t abof_version = 0; if (nr_frames == 0) { std::array magic{}; read_or_throw(file, magic.data(), magic.size()); @@ -92,10 +119,12 @@ std::shared_ptr ContainerLoader::load_data_abo(const std::string& pat uint8_t flags = 0; read_or_throw(file, reinterpret_cast(&version), sizeof(version)); read_or_throw(file, reinterpret_cast(&flags), sizeof(flags)); - if (version != 1) + if (version != 1 && version != 2) throw std::runtime_error("Unsupported ABO format version: " + std::to_string(version)); + abof_version = version; const bool is_compressed = (flags & 0x01) != 0; + is_neb_pathway = (flags & 0x02) != 0; if (is_compressed) { std::vector compressed((std::istreambuf_iterator(file)), std::istreambuf_iterator()); @@ -159,14 +188,17 @@ std::shared_ptr ContainerLoader::load_data_abo(const std::string& pat qDebug() << "ABOF header detected. Version:" << version << "flags:" << flags; } - qDebug() << "Number of frames:" << nr_frames; + qDebug() << " Number of frames:" << nr_frames; + + std::vector> loaded_frames; + loaded_frames.reserve(nr_frames); std::istream& input = payload_stream ? static_cast(*payload_stream) : static_cast(file); for (uint16_t f = 0; f < nr_frames; ++f) { uint16_t frame_idx = 0; read_or_throw(input, reinterpret_cast(&frame_idx), sizeof(frame_idx)); - qDebug() << "Frame idx:" << frame_idx; + qDebug() << " Frame idx:" << frame_idx; // ---- Description ---- uint16_t descriptor_length = 0; @@ -178,10 +210,23 @@ std::shared_ptr ContainerLoader::load_data_abo(const std::string& pat read_or_throw(input, description.data(), descriptor_length); } + std::optional frame_unit_cell; + if (abof_version == 2) { + uint8_t frame_flags = 0; + read_or_throw(input, reinterpret_cast(&frame_flags), sizeof(frame_flags)); + if ((frame_flags & FRAME_UNIT_CELL_FLAG_BIT) != 0) { + UnitCellMatrix unit_cell{}; + read_or_throw(input, + reinterpret_cast(unit_cell.data()), + unit_cell.size() * sizeof(float)); + frame_unit_cell = unit_cell; + } + } + // ---- Atoms ---- uint16_t nr_atoms = 0; read_or_throw(input, reinterpret_cast(&nr_atoms), sizeof(nr_atoms)); - qDebug() << "Number of atoms:" << nr_atoms; + qDebug() << " Number of atoms:" << nr_atoms; std::vector elements(nr_atoms); std::vector positions(nr_atoms); @@ -198,11 +243,12 @@ std::shared_ptr ContainerLoader::load_data_abo(const std::string& pat structure->update(); auto frame = std::make_shared(structure, description); + frame->set_unit_cell(frame_unit_cell); // ---- Models ---- uint16_t nr_models = 0; read_or_throw(input, reinterpret_cast(&nr_models), sizeof(nr_models)); - qDebug() << "Number of models:" << nr_models; + qDebug() << " Number of models:" << nr_models; for (uint16_t m = 0; m < nr_models; ++m) { uint16_t model_idx = 0; @@ -237,7 +283,7 @@ std::shared_ptr ContainerLoader::load_data_abo(const std::string& pat if (!indices.empty()) read_or_throw(input, reinterpret_cast(indices.data()), indices.size() * sizeof(uint32_t)); - qDebug() << "Model idx:" << model_idx << "faces:" << nr_faces; + qDebug() << " Model idx:" << model_idx << "faces:" << nr_faces; if(nr_vertices == 0 || nr_faces == 0) { qDebug() << "Skipping empty model:" << model_idx; @@ -248,6 +294,80 @@ std::shared_ptr ContainerLoader::load_data_abo(const std::string& pat } } + loaded_frames.push_back(frame); + } + + container->set_is_neb_pathway(is_neb_pathway); + + if (is_neb_pathway && loaded_frames.size() >= 2) { + const auto& reference_atoms = loaded_frames.front()->get_structure()->get_atoms(); + const size_t nr_atoms = reference_atoms.size(); + + bool can_interpolate = nr_atoms > 0; + for (size_t frame_idx = 1; frame_idx < loaded_frames.size() && can_interpolate; ++frame_idx) { + const auto& atoms = loaded_frames[frame_idx]->get_structure()->get_atoms(); + if (atoms.size() != nr_atoms) { + can_interpolate = false; + break; + } + for (size_t atom_idx = 0; atom_idx < nr_atoms; ++atom_idx) { + if (atoms[atom_idx].atnr != reference_atoms[atom_idx].atnr) { + can_interpolate = false; + break; + } + } + } + + if (!can_interpolate) { + qWarning() << "NEB interpolation skipped due to incompatible atom ordering between frames."; + } else { + std::vector> interpolated_frames; + interpolated_frames.reserve((loaded_frames.size() - 1) * NEB_INTERPOLATION_STEPS_PER_SEGMENT + 1); + + const auto make_interpolated_frame = [&](size_t seg_idx, float t) { + const size_t i0 = (seg_idx == 0) ? seg_idx : seg_idx - 1; + const size_t i1 = seg_idx; + const size_t i2 = seg_idx + 1; + const size_t i3 = (seg_idx + 2 < loaded_frames.size()) ? seg_idx + 2 : loaded_frames.size() - 1; + + const auto& atoms0 = loaded_frames[i0]->get_structure()->get_atoms(); + const auto& atoms1 = loaded_frames[i1]->get_structure()->get_atoms(); + const auto& atoms2 = loaded_frames[i2]->get_structure()->get_atoms(); + const auto& atoms3 = loaded_frames[i3]->get_structure()->get_atoms(); + + auto structure = std::make_shared(); + for (size_t atom_idx = 0; atom_idx < nr_atoms; ++atom_idx) { + const glm::vec3 p0(atoms0[atom_idx].x, atoms0[atom_idx].y, atoms0[atom_idx].z); + const glm::vec3 p1(atoms1[atom_idx].x, atoms1[atom_idx].y, atoms1[atom_idx].z); + const glm::vec3 p2(atoms2[atom_idx].x, atoms2[atom_idx].y, atoms2[atom_idx].z); + const glm::vec3 p3(atoms3[atom_idx].x, atoms3[atom_idx].y, atoms3[atom_idx].z); + + const glm::vec3 ipos = catmull_rom(p0, p1, p2, p3, t); + structure->add_atom(atoms1[atom_idx].atnr, ipos.x, ipos.y, ipos.z); + } + structure->update(); + + std::ostringstream descriptor; + descriptor.precision(std::numeric_limits::max_digits10); + descriptor << "NEB interpolated frame " << seg_idx << " t=" << t; + return std::make_shared(structure, descriptor.str()); + }; + + for (size_t seg_idx = 0; seg_idx + 1 < loaded_frames.size(); ++seg_idx) { + for (unsigned int step = 0; step < NEB_INTERPOLATION_STEPS_PER_SEGMENT; ++step) { + const float t = static_cast(step) / + static_cast(NEB_INTERPOLATION_STEPS_PER_SEGMENT); + interpolated_frames.push_back(make_interpolated_frame(seg_idx, t)); + } + } + + interpolated_frames.push_back(loaded_frames.back()); + loaded_frames = std::move(interpolated_frames); + qDebug() << "Generated interpolated NEB frames:" << loaded_frames.size(); + } + } + + for (const auto& frame : loaded_frames) { container->add_frame(frame); } diff --git a/src/data/frame.h b/src/data/frame.h index 08b111c..5be5c76 100644 --- a/src/data/frame.h +++ b/src/data/frame.h @@ -22,7 +22,9 @@ #ifndef FRAME_H #define FRAME_H +#include #include +#include #include #include @@ -38,6 +40,7 @@ class Frame std::shared_ptr structure; std::vector> models; std::string description; + std::optional> unit_cell; public: /** @@ -79,6 +82,22 @@ class Frame inline const auto& get_models() const { return this->models; } + + /** + * @brief Set optional unit-cell matrix for this frame + * @param frame_unit_cell row-major 3x3 matrix + */ + inline void set_unit_cell(const std::optional>& frame_unit_cell) { + this->unit_cell = frame_unit_cell; + } + + /** + * @brief Get optional unit-cell matrix for this frame + * @return optional row-major 3x3 matrix + */ + inline const auto& get_unit_cell() const { + return this->unit_cell; + } }; #endif // FRAME_H diff --git a/src/gui/anaglyph_widget.cpp b/src/gui/anaglyph_widget.cpp index 9c8161b..9f7e975 100644 --- a/src/gui/anaglyph_widget.cpp +++ b/src/gui/anaglyph_widget.cpp @@ -22,6 +22,25 @@ #include "anaglyph_widget.h" #include +#include + +namespace { +int normalize_msaa_samples(int samples) { + switch(samples) { + case 1: + case 2: + case 4: + case 8: + return samples; + default: + return 4; + } +} + +int normalize_sphere_tesselation_level(int level) { + return std::clamp(level, 0, 6); +} +} /** * @brief Constructs a new instance. @@ -58,6 +77,11 @@ void AnaglyphWidget::rotate_scene(float angle) { this->update(); } +void AnaglyphWidget::reset_panning() { + this->pan_offset = QVector3D(0.0f, 0.0f, 0.0f); + this->update(); +} + /** * @brief Set lighting settings for atoms and bonds. * @@ -174,6 +198,10 @@ void AnaglyphWidget::load_lighting_settings() { settings.value("lighting/objects/edge_strength", this->scene->object_lighting.edge_strength).toFloat(); this->scene->object_lighting.edge_power = settings.value("lighting/objects/edge_power", this->scene->object_lighting.edge_power).toFloat(); + + this->msaa_samples = normalize_msaa_samples(settings.value("rendering/msaa_samples", this->msaa_samples).toInt()); + this->sphere_tesselation_level = normalize_sphere_tesselation_level( + settings.value("rendering/sphere_tesselation_level", this->sphere_tesselation_level).toInt()); } /** @@ -270,6 +298,74 @@ void AnaglyphWidget::set_camera_mode(int mode) { this->update(); } + +void AnaglyphWidget::set_msaa_samples(int samples) { + const int normalized_samples = normalize_msaa_samples(samples); + + if (this->msaa_samples == normalized_samples) { + return; + } + + this->msaa_samples = normalized_samples; + + QSettings settings; + settings.setValue("rendering/msaa_samples", this->msaa_samples); + + if (!this->context()) { + return; + } + + makeCurrent(); + this->destroy_framebuffers(); + this->build_framebuffers(); + doneCurrent(); + + this->update(); +} +void AnaglyphWidget::set_sphere_tesselation_level(int tesselation_level) { + const int normalized_level = normalize_sphere_tesselation_level(tesselation_level); + + if (this->sphere_tesselation_level == normalized_level) { + return; + } + + this->sphere_tesselation_level = normalized_level; + + QSettings settings; + settings.setValue("rendering/sphere_tesselation_level", this->sphere_tesselation_level); + + if (this->structure_renderer) { + makeCurrent(); + this->structure_renderer->set_sphere_tesselation_level(this->sphere_tesselation_level); + doneCurrent(); + } + + this->update(); +} + +void AnaglyphWidget::reset_lighting_settings_to_defaults() { + const LightingSettings defaults; + + this->set_atom_lighting_settings(defaults.ambient_strength, + defaults.diffuse_strength, + defaults.specular_strength, + defaults.shininess, + defaults.edge_strength, + defaults.edge_power); + + this->set_object_lighting_settings(defaults.ambient_strength, + defaults.diffuse_strength, + defaults.specular_strength, + defaults.shininess, + defaults.edge_strength, + defaults.edge_power); + + this->set_msaa_samples(4); + this->set_sphere_tesselation_level(4); +} + + + /** * @brief Destroys the object. */ @@ -300,6 +396,7 @@ QSize AnaglyphWidget::sizeHint() const { */ void AnaglyphWidget::cleanup() { makeCurrent(); + this->destroy_framebuffers(); doneCurrent(); } @@ -321,6 +418,7 @@ void AnaglyphWidget::initializeGL() { qDebug() << "Create structure renderer object"; this->structure_renderer = std::make_unique(this->scene, this->shader_manager); + this->structure_renderer->set_sphere_tesselation_level(this->sphere_tesselation_level); qDebug() << "Build Framebuffers"; this->build_framebuffers(); @@ -352,12 +450,13 @@ void AnaglyphWidget::paintGL() { // paint coordinate axes to its own framebuffer if(this->flag_axis_enabled) { - glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers[FrameBuffer::COORDINATE_AXES]); + this->bind_render_framebuffer(FrameBuffer::COORDINATE_AXES); this->set_render_viewport(); glEnable(GL_DEPTH_TEST); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); this->structure_renderer->draw_coordinate_axes(); + this->resolve_framebuffer(FrameBuffer::COORDINATE_AXES); glBindFramebuffer(GL_FRAMEBUFFER, 0); } @@ -371,7 +470,8 @@ void AnaglyphWidget::paintGL() { // then combine everything if(this->flag_axis_enabled) { glDisable(GL_DEPTH_TEST); - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE); + glEnable(GL_BLEND); + glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendEquation(GL_FUNC_ADD); ShaderProgram *shader = this->shader_manager->get_shader_program("simple_canvas_shader"); @@ -440,10 +540,18 @@ void AnaglyphWidget::resizeGL(int w, int h) { // resize textures and render buffers for (unsigned int i = 0; i < FrameBuffer::NR_FRAMEBUFFERS; ++i) { glBindTexture(GL_TEXTURE_2D, this->texture_color_buffers[i]); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, target.width(), target.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, target.width(), target.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glBindRenderbuffer(GL_RENDERBUFFER, this->rbo[i]); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, target.width(), target.height()); + + if (this->msaa_enabled) { + glBindRenderbuffer(GL_RENDERBUFFER, this->rbo_msaa_color[i]); + QOpenGLContext::currentContext()->extraFunctions()->glRenderbufferStorageMultisample(GL_RENDERBUFFER, this->msaa_samples, GL_RGBA8, target.width(), target.height()); + + glBindRenderbuffer(GL_RENDERBUFFER, this->rbo_msaa_depth_stencil[i]); + QOpenGLContext::currentContext()->extraFunctions()->glRenderbufferStorageMultisample(GL_RENDERBUFFER, this->msaa_samples, GL_DEPTH24_STENCIL8, target.width(), target.height()); + } } } @@ -461,6 +569,11 @@ void AnaglyphWidget::mousePressEvent(QMouseEvent *event) { // store positions of mouse this->m_lastPos = event->pos(); } + + if (event->buttons() & Qt::RightButton) { + this->pan_flag = true; + this->m_lastPos = event->pos(); + } } /** @@ -479,6 +592,10 @@ void AnaglyphWidget::mouseReleaseEvent(QMouseEvent *event) { // unset arcball rotation mode this->arcball_rotation_flag = false; } + + if (this->pan_flag && !(event->buttons() & Qt::RightButton)) { + this->pan_flag = false; + } } /** @@ -525,8 +642,23 @@ void AnaglyphWidget::mouseMoveEvent(QMouseEvent *event) { // set the rotation this->set_arcball_rotation(qRadiansToDegrees(angle), axis_model_space); } - } else if(event->buttons() & Qt::RightButton) { - // Nothing for the time being. + } else if(this->pan_flag) { + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + double ex = event->position().x(); + double ey = event->position().y(); + #else + double ex = event->pos().x(); + double ey = event->pos().y(); + #endif + + const float dx = static_cast(ex - this->m_lastPos.x()); + const float dy = static_cast(ey - this->m_lastPos.y()); + const float zoom = std::max(1.0f, -this->scene->camera_position[1]); + const float pan_scale = zoom * 0.0012f; + + this->pan_offset += QVector3D(-dx * pan_scale, 0.0f, dy * pan_scale); + this->m_lastPos = QPoint(static_cast(ex), static_cast(ey)); + this->update(); } } @@ -646,15 +778,23 @@ void AnaglyphWidget::build_framebuffers() { const int render_width = std::max(1, this->geometry().width() * supersample_scale); const int render_height = std::max(1, this->geometry().height() * supersample_scale); + this->msaa_enabled = this->msaa_samples > 1; + // initialize frame buffers glGenFramebuffers(FrameBuffer::NR_FRAMEBUFFERS, this->framebuffers); glGenTextures(FrameBuffer::NR_FRAMEBUFFERS, this->texture_color_buffers); glGenRenderbuffers(FrameBuffer::NR_FRAMEBUFFERS, this->rbo); + if (this->msaa_enabled) { + glGenFramebuffers(FrameBuffer::NR_FRAMEBUFFERS, this->framebuffers_msaa); + glGenRenderbuffers(FrameBuffer::NR_FRAMEBUFFERS, this->rbo_msaa_color); + glGenRenderbuffers(FrameBuffer::NR_FRAMEBUFFERS, this->rbo_msaa_depth_stencil); + } + for (unsigned int i = 0; i < FrameBuffer::NR_FRAMEBUFFERS; ++i) { glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers[i]); glBindTexture(GL_TEXTURE_2D, this->texture_color_buffers[i]); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, render_width, render_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, render_width, render_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this->texture_color_buffers[i], 0); @@ -666,8 +806,32 @@ void AnaglyphWidget::build_framebuffers() { if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { qWarning() << "Framebuffer is not complete."; } + + if (this->msaa_enabled) { + glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers_msaa[i]); + + glBindRenderbuffer(GL_RENDERBUFFER, this->rbo_msaa_color[i]); + QOpenGLContext::currentContext()->extraFunctions()->glRenderbufferStorageMultisample(GL_RENDERBUFFER, this->msaa_samples, GL_RGBA8, render_width, render_height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, this->rbo_msaa_color[i]); + + glBindRenderbuffer(GL_RENDERBUFFER, this->rbo_msaa_depth_stencil[i]); + QOpenGLContext::currentContext()->extraFunctions()->glRenderbufferStorageMultisample(GL_RENDERBUFFER, this->msaa_samples, GL_DEPTH24_STENCIL8, render_width, render_height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, this->rbo_msaa_depth_stencil[i]); + + if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + qWarning() << "MSAA framebuffer is not complete, disabling MSAA."; + this->msaa_enabled = false; + break; + } + } + } + + if (this->msaa_enabled) { + qDebug() << "MSAA enabled with" << this->msaa_samples << "samples."; } + this->framebuffers_initialized = true; + glBindFramebuffer(GL_FRAMEBUFFER, 0); // create screen quad vao @@ -685,19 +849,71 @@ void AnaglyphWidget::build_framebuffers() { 1.0f, 1.0f, 1.0f, 1.0f }; - this->quad_vao.create(); - this->quad_vao.bind(); + if (!this->quad_vao.isCreated()) { + this->quad_vao.create(); + this->quad_vao.bind(); - this->quad_vbo.create(); - this->quad_vbo.setUsagePattern(QOpenGLBuffer::StaticDraw); - this->quad_vbo.bind(); - this->quad_vbo.allocate(quadvecs, sizeof(quadvecs)); - f->glEnableVertexAttribArray(0); - f->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); - f->glEnableVertexAttribArray(1); - f->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); + this->quad_vbo.create(); + this->quad_vbo.setUsagePattern(QOpenGLBuffer::StaticDraw); + this->quad_vbo.bind(); + this->quad_vbo.allocate(quadvecs, sizeof(quadvecs)); + f->glEnableVertexAttribArray(0); + f->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); + f->glEnableVertexAttribArray(1); + f->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); - this->quad_vao.release(); + this->quad_vao.release(); + } +} + + +void AnaglyphWidget::bind_render_framebuffer(FrameBuffer buffer) { + if (this->msaa_enabled) { + glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers_msaa[buffer]); + } else { + glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers[buffer]); + } +} + +void AnaglyphWidget::resolve_framebuffer(FrameBuffer buffer) { + if (!this->msaa_enabled) { + return; + } + + glBindFramebuffer(GL_READ_FRAMEBUFFER, this->framebuffers_msaa[buffer]); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, this->framebuffers[buffer]); + + const QSize target = render_size(); + QOpenGLContext::currentContext()->extraFunctions()->glBlitFramebuffer(0, 0, + target.width(), target.height(), + 0, 0, + target.width(), target.height(), + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers[buffer]); +} + +void AnaglyphWidget::destroy_framebuffers() { + if (!this->framebuffers_initialized) { + return; + } + + glDeleteFramebuffers(FrameBuffer::NR_FRAMEBUFFERS, this->framebuffers); + glDeleteTextures(FrameBuffer::NR_FRAMEBUFFERS, this->texture_color_buffers); + glDeleteRenderbuffers(FrameBuffer::NR_FRAMEBUFFERS, this->rbo); + + glDeleteFramebuffers(FrameBuffer::NR_FRAMEBUFFERS, this->framebuffers_msaa); + glDeleteRenderbuffers(FrameBuffer::NR_FRAMEBUFFERS, this->rbo_msaa_color); + glDeleteRenderbuffers(FrameBuffer::NR_FRAMEBUFFERS, this->rbo_msaa_depth_stencil); + + std::fill(std::begin(this->framebuffers), std::end(this->framebuffers), 0u); + std::fill(std::begin(this->texture_color_buffers), std::end(this->texture_color_buffers), 0u); + std::fill(std::begin(this->rbo), std::end(this->rbo), 0u); + std::fill(std::begin(this->framebuffers_msaa), std::end(this->framebuffers_msaa), 0u); + std::fill(std::begin(this->rbo_msaa_color), std::end(this->rbo_msaa_color), 0u); + std::fill(std::begin(this->rbo_msaa_depth_stencil), std::end(this->rbo_msaa_depth_stencil), 0u); + + this->framebuffers_initialized = false; } @@ -709,6 +925,7 @@ void AnaglyphWidget::reset_matrices() { this->scene->rotation_matrix.rotate(20.0, QVector3D(1,0,0)); this->scene->rotation_matrix.rotate(30.0, QVector3D(0,0,1)); this->scene->arcball_rotation.setToIdentity(); + this->pan_offset = QVector3D(0.0f, 0.0f, 0.0f); } /** @@ -723,17 +940,18 @@ void AnaglyphWidget::paint_regular() { glBlendEquation(GL_FUNC_ADD); // set view matrix - QVector3D lookat = QVector3D(0.0f, 1.0f, 0.0f); + QVector3D lookat = QVector3D(0.0f, 1.0f, 0.0f) + this->pan_offset; this->scene->view.setToIdentity(); - this->scene->view.lookAt(this->scene->camera_position, lookat, QVector3D(0.0, 0.0, 1.0)); + this->scene->view.lookAt(this->scene->camera_position + this->pan_offset, lookat, QVector3D(0.0, 0.0, 1.0)); // regular draw call to the STRUCTURE_NORMAL framebuffer - glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers[FrameBuffer::STRUCTURE_NORMAL]); + this->bind_render_framebuffer(FrameBuffer::STRUCTURE_NORMAL); this->set_render_viewport(); glEnable(GL_DEPTH_TEST); glClearColor(this->tint, this->tint, this->tint, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); this->draw_structure(); + this->resolve_framebuffer(FrameBuffer::STRUCTURE_NORMAL); glBindFramebuffer(GL_FRAMEBUFFER, 0); // bundle both framebuffers and draw on the canvas @@ -773,30 +991,33 @@ void AnaglyphWidget::paint_stereographic() { glBlendEquation(GL_FUNC_ADD); // set convergence point and intra-ocular separation - QVector3D lookat = QVector3D(0.0f, 1.0f, 0.0f); + QVector3D lookat = QVector3D(0.0f, 1.0f, 0.0f) + this->pan_offset; + const QVector3D camera_position = this->scene->camera_position + this->pan_offset; float dist = 1.0f - this->scene->camera_position[1]; float eye_sep = dist / 30.0f; // draw structure for left eye this->scene->view.setToIdentity(); - this->scene->view.lookAt(this->scene->camera_position - QVector3D(eye_sep / 2.0, 0.0, 0.0), lookat, QVector3D(0.0, 0.0, 1.0)); - glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers[FrameBuffer::STRUCTURE_LEFT]); + this->scene->view.lookAt(camera_position - QVector3D(eye_sep / 2.0, 0.0, 0.0), lookat, QVector3D(0.0, 0.0, 1.0)); + this->bind_render_framebuffer(FrameBuffer::STRUCTURE_LEFT); this->set_render_viewport(); glEnable(GL_DEPTH_TEST); glClearColor(this->tint, this->tint, this->tint, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); this->draw_structure(); + this->resolve_framebuffer(FrameBuffer::STRUCTURE_LEFT); glBindFramebuffer(GL_FRAMEBUFFER, 0); // draw structure for right eye this->scene->view.setToIdentity(); - this->scene->view.lookAt(this->scene->camera_position + QVector3D(eye_sep / 2.0, 0.0, 0.0), lookat, QVector3D(0.0, 0.0, 1.0)); - glBindFramebuffer(GL_FRAMEBUFFER, this->framebuffers[FrameBuffer::STRUCTURE_RIGHT]); + this->scene->view.lookAt(camera_position + QVector3D(eye_sep / 2.0, 0.0, 0.0), lookat, QVector3D(0.0, 0.0, 1.0)); + this->bind_render_framebuffer(FrameBuffer::STRUCTURE_RIGHT); this->set_render_viewport(); glEnable(GL_DEPTH_TEST); glClearColor(this->tint, this->tint, this->tint, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); this->draw_structure(); + this->resolve_framebuffer(FrameBuffer::STRUCTURE_RIGHT); glBindFramebuffer(GL_FRAMEBUFFER, 0); // combine L+L diff --git a/src/gui/anaglyph_widget.h b/src/gui/anaglyph_widget.h index 31a5077..fe3ba2e 100644 --- a/src/gui/anaglyph_widget.h +++ b/src/gui/anaglyph_widget.h @@ -72,8 +72,11 @@ class AnaglyphWidget : public QOpenGLWidget, protected QOpenGLFunctions { private: static constexpr int supersample_scale = 2; + int msaa_samples = 4; + int sphere_tesselation_level = 4; QPoint m_lastPos; + QVector3D pan_offset = QVector3D(0.0f, 0.0f, 0.0f); QString root_path; @@ -82,9 +85,15 @@ class AnaglyphWidget : public QOpenGLWidget, protected QOpenGLFunctions { // default background color static constexpr float tint = 235.0f / 255.0f; - unsigned int framebuffers[FrameBuffer::NR_FRAMEBUFFERS]; - unsigned int texture_color_buffers[FrameBuffer::NR_FRAMEBUFFERS]; - unsigned int rbo[FrameBuffer::NR_FRAMEBUFFERS]; + unsigned int framebuffers[FrameBuffer::NR_FRAMEBUFFERS] = {}; + unsigned int framebuffers_msaa[FrameBuffer::NR_FRAMEBUFFERS] = {}; + unsigned int texture_color_buffers[FrameBuffer::NR_FRAMEBUFFERS] = {}; + unsigned int rbo[FrameBuffer::NR_FRAMEBUFFERS] = {}; + unsigned int rbo_msaa_color[FrameBuffer::NR_FRAMEBUFFERS] = {}; + unsigned int rbo_msaa_depth_stencil[FrameBuffer::NR_FRAMEBUFFERS] = {}; + + bool msaa_enabled = false; + bool framebuffers_initialized = false; QOpenGLVertexArrayObject quad_vao; QOpenGLVertexArrayObject quad_vao_small; @@ -92,6 +101,7 @@ class AnaglyphWidget : public QOpenGLWidget, protected QOpenGLFunctions { // used for arcball rotation bool arcball_rotation_flag = false; // whether arcball rotation is active + bool pan_flag = false; // whether right-click panning is active QOpenGLVertexArrayObject vao; QOpenGLBuffer vbo[4]; @@ -176,6 +186,11 @@ class AnaglyphWidget : public QOpenGLWidget, protected QOpenGLFunctions { */ void rotate_scene(float angle); + /** + * @brief Reset panning offset. + */ + void reset_panning(); + /** * @brief Sets the camera alignment. * @@ -251,6 +266,35 @@ class AnaglyphWidget : public QOpenGLWidget, protected QOpenGLFunctions { */ LightingSettings get_object_lighting_settings() const; + /** + * @brief Set MSAA sample count (supported values: 1, 2, 4, 8). + */ + void set_msaa_samples(int samples); + + /** + * @brief Get configured MSAA sample count. + */ + int get_msaa_samples() const { + return this->msaa_samples; + } + + /** + * @brief Set sphere tesselation level. + */ + void set_sphere_tesselation_level(int tesselation_level); + + /** + * @brief Get configured sphere tesselation level. + */ + int get_sphere_tesselation_level() const { + return this->sphere_tesselation_level; + } + + /** + * @brief Reset all lighting settings to defaults. + */ + void reset_lighting_settings_to_defaults(); + ~AnaglyphWidget(); public slots: @@ -367,6 +411,21 @@ public slots: */ QSize render_size(); + /** + * @brief Bind render framebuffer (multisampled when available). + */ + void bind_render_framebuffer(FrameBuffer buffer); + + /** + * @brief Resolve multisampled framebuffer content to texture framebuffer. + */ + void resolve_framebuffer(FrameBuffer buffer); + + /** + * @brief Release framebuffer resources. + */ + void destroy_framebuffers(); + signals: /** * @brief Send signal that opengl engine is ready diff --git a/src/gui/interface_window.cpp b/src/gui/interface_window.cpp index 07a93d6..0aa27d8 100644 --- a/src/gui/interface_window.cpp +++ b/src/gui/interface_window.cpp @@ -23,6 +23,42 @@ #include #include #include +#include +#include + +namespace { +QIcon makeToggleIcon(bool enabled) { + constexpr int width = 46; + constexpr int height = 26; + constexpr int margin = 2; + constexpr int knob = height - 2 * margin; + + QPixmap pixmap(width, height); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + + const QColor background = enabled ? QColor(0x63, 0xD9, 0x6A) : QColor(0xD3, 0xD3, 0xD3); + painter.setPen(Qt::NoPen); + painter.setBrush(background); + painter.drawRoundedRect(0, 0, width, height, height / 2.0, height / 2.0); + + const int knob_x = enabled ? (width - margin - knob) : margin; + painter.setBrush(QColor(255, 255, 255)); + painter.drawEllipse(knob_x, margin, knob, knob); + + return QIcon(pixmap); +} + +void setToggleVisual(QToolButton* button, bool enabled) { + if (!button) { + return; + } + + button->setIcon(makeToggleIcon(enabled)); +} +} /** * @brief Constructs the object. @@ -91,21 +127,57 @@ void InterfaceWindow::build_control_interface() { QBoxLayout *horizontalBox = new QHBoxLayout; this->control_interface->setLayout(horizontalBox); - // add toggle axes button to horizontal row - this->btn_toggle_axis = new QPushButton; - this->btn_toggle_axis->setIcon(QIcon(":/assets/icon/axes_32.png")); - this->btn_toggle_axis->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - this->btn_toggle_axis->setToolTip(QString("Toggle coordination axes in viewport.")); - horizontalBox->addWidget(this->btn_toggle_axis); - connect(this->btn_toggle_axis, SIGNAL(clicked()), this, SLOT(toggle_axes())); - - // add toggle axes button to horizontal row - this->btn_rotate_model = new QPushButton; - this->btn_rotate_model->setIcon(QIcon(":/assets/icon/rotation_gray_32.png")); - this->btn_rotate_model->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - this->btn_rotate_model->setToolTip(QString("Toggle rotation of model around z-axis.")); - horizontalBox->addWidget(this->btn_rotate_model); - connect(this->btn_rotate_model, SIGNAL(clicked()), this, SLOT(toggle_rotation())); + this->switch_toggle_axis = new QToolButton(); + this->switch_toggle_axis->setText(tr("Axes")); + this->switch_toggle_axis->setToolTip(QString("Toggle coordinate axes in viewport.")); + this->switch_toggle_axis->setCheckable(true); + this->switch_toggle_axis->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + this->switch_toggle_axis->setChecked(this->flag_axes); + setToggleVisual(this->switch_toggle_axis, this->flag_axes); + horizontalBox->addWidget(this->switch_toggle_axis); + connect(this->switch_toggle_axis, SIGNAL(toggled(bool)), this, SLOT(set_axes_enabled(bool))); + + this->switch_rotate_model = new QToolButton(); + this->switch_rotate_model->setText(tr("Rotate")); + this->switch_rotate_model->setToolTip(QString("Toggle rotation of model around z-axis.")); + this->switch_rotate_model->setCheckable(true); + this->switch_rotate_model->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + this->switch_rotate_model->setChecked(this->flag_rotate); + setToggleVisual(this->switch_rotate_model, this->flag_rotate); + horizontalBox->addWidget(this->switch_rotate_model); + connect(this->switch_rotate_model, SIGNAL(toggled(bool)), this, SLOT(set_rotation_enabled(bool))); + + this->switch_pingpong = new QToolButton(); + this->switch_pingpong->setText(tr("Ping-pong")); + this->switch_pingpong->setToolTip(QString("Toggle back-and-forth frame playback.")); + this->switch_pingpong->setCheckable(true); + this->switch_pingpong->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + this->switch_pingpong->setChecked(this->flag_pingpong); + setToggleVisual(this->switch_pingpong, this->flag_pingpong); + horizontalBox->addWidget(this->switch_pingpong); + connect(this->switch_pingpong, SIGNAL(toggled(bool)), this, SLOT(set_pingpong_enabled(bool))); + + QFrame *toggle_separator = new QFrame(); + toggle_separator->setFrameShape(QFrame::VLine); + toggle_separator->setFrameShadow(QFrame::Sunken); + horizontalBox->addWidget(toggle_separator); + + this->fps_label = new QLabel(tr("FPS")); + horizontalBox->addWidget(this->fps_label); + + this->fps_dropdown = new QComboBox(); + this->fps_dropdown->addItem("1", 1); + this->fps_dropdown->addItem("2", 2); + this->fps_dropdown->addItem("5", 5); + this->fps_dropdown->addItem("10", 10); + this->fps_dropdown->addItem("24", 24); + this->fps_dropdown->addItem("30", 30); + this->fps_dropdown->addItem("60", 60); + this->fps_dropdown->addItem("120", 120); + this->fps_dropdown->setCurrentIndex(this->fps_dropdown->findData(this->playback_fps)); + this->fps_dropdown->setToolTip(tr("Playback FPS")); + horizontalBox->addWidget(this->fps_dropdown); + connect(this->fps_dropdown, SIGNAL(currentIndexChanged(int)), this, SLOT(set_playback_fps(int))); // rotation timer this->rotation_timer = new QTimer(this); @@ -127,18 +199,13 @@ void InterfaceWindow::build_sequence_interface() { QBoxLayout *horizontal_box = new QHBoxLayout; this->frame_player->setLayout(horizontal_box); - // add sequence play button to horizontal row + // add sequence play/pause toggle button to horizontal row this->btn_frame_play = new QPushButton; + this->btn_frame_play->setCheckable(true); this->btn_frame_play->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); this->btn_frame_play->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); horizontal_box->addWidget(this->btn_frame_play); - // add sequence pause button to horizontal row - this->btn_frame_pause = new QPushButton; - this->btn_frame_pause->setIcon(style()->standardIcon(QStyle::SP_MediaPause)); - this->btn_frame_pause->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - horizontal_box->addWidget(this->btn_frame_pause); - // add frame duration to horizontal row this->frame_label = new QLabel(QString::number(cur_frame) + "/" + QString::number(max_frame)); horizontal_box->addWidget(this->frame_label); @@ -163,8 +230,7 @@ void InterfaceWindow::build_sequence_interface() { // connect interface events connect(this->frame_timer, SIGNAL(timeout()), this, SLOT(frame_timer_trigger())); - connect(this->btn_frame_play, SIGNAL(clicked()), this, SLOT(frame_timer_play())); - connect(this->btn_frame_pause, SIGNAL(clicked()), this, SLOT(frame_timer_pause())); + connect(this->btn_frame_play, SIGNAL(clicked()), this, SLOT(frame_timer_play_pause())); connect(this->btn_frame_prev, SIGNAL(clicked()), this, SLOT(frame_backward())); connect(this->btn_frame_next, SIGNAL(clicked()), this, SLOT(frame_forward())); connect(this, SIGNAL(frame_number_update()), this, SLOT(handle_frame_update())); @@ -201,6 +267,23 @@ void InterfaceWindow::open_file(const QString& filename) { return; } + if (this->container && this->container->is_neb_pathway()) { + this->set_axes_enabled(false); + this->set_pingpong_enabled(true); + this->set_rotation_enabled(false); + this->playback_fps = 60; + } else { + this->playback_fps = 1; + } + + if (this->fps_dropdown) { + const int fps_index = this->fps_dropdown->findData(this->playback_fps); + if (fps_index >= 0) { + this->fps_dropdown->setCurrentIndex(fps_index); + } + } + + this->frame_direction = 1; emit new_container_loaded(); } @@ -222,6 +305,10 @@ void InterfaceWindow::set_camera_mode(QAction* action) { this->anaglyph_widget->set_camera_mode(action->data().toInt()); } +void InterfaceWindow::reset_panning() { + this->anaglyph_widget->reset_panning(); +} + /** * @brief Build orbital * @param Orbital identifier @@ -283,6 +370,8 @@ void InterfaceWindow::handle_description_button() { */ void InterfaceWindow::frame_slider_clicked() { this->frame_timer->stop(); + this->btn_frame_play->setChecked(false); + this->btn_frame_play->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); } /** @@ -294,26 +383,47 @@ void InterfaceWindow::frame_slider_moved(int val) { } /** - * @brief Handle pressing play button + * @brief Toggle frame playback (play/pause) */ -void InterfaceWindow::frame_timer_play() { - //this->frame_timer->setInterval(1000 / 60.0); - this->frame_timer->setSingleShot(false); - this->frame_timer->start(1000 / 60.0); -} +void InterfaceWindow::frame_timer_play_pause() { + if (this->frame_timer->isActive()) { + this->frame_timer->stop(); + this->btn_frame_play->setChecked(false); + this->btn_frame_play->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + return; + } -/** - * @brief Handle pressing pause button - */ -void InterfaceWindow::frame_timer_pause() { - this->frame_timer->stop(); + this->frame_timer->setSingleShot(false); + this->frame_timer->start(1000 / static_cast(this->playback_fps)); + this->btn_frame_play->setChecked(true); + this->btn_frame_play->setIcon(style()->standardIcon(QStyle::SP_MediaPause)); } /** * @brief Handle timer trigger */ void InterfaceWindow::frame_timer_trigger() { - this->cur_frame++; + if (this->max_frame <= 1) { + return; + } + + if (this->flag_pingpong) { + this->cur_frame += this->frame_direction; + + if (this->cur_frame >= this->max_frame) { + this->frame_direction = -1; + this->cur_frame = std::max(0, this->max_frame - 2); + } else if (this->cur_frame < 0) { + this->frame_direction = 1; + this->cur_frame = std::min(this->max_frame - 1, 1); + } + } else { + this->cur_frame++; + if (this->cur_frame >= this->max_frame) { + this->cur_frame = 0; + } + } + emit frame_number_update(); } @@ -322,7 +432,7 @@ void InterfaceWindow::frame_timer_trigger() { */ void InterfaceWindow::handle_frame_update() { if(this->cur_frame >= this->max_frame) { - this->cur_frame = 0; + this->cur_frame = this->max_frame - 1; } if(this->cur_frame < 0) { @@ -368,16 +478,18 @@ void InterfaceWindow::handle_new_container() { if(this->container->get_nr_frames() > 1) { this->btn_frame_play->setEnabled(true); - this->btn_frame_pause->setEnabled(true); this->frame_label->show(); this->frame_slider->setEnabled(true); } else { this->btn_frame_play->setEnabled(false); - this->btn_frame_pause->setEnabled(false); this->frame_label->hide(); this->frame_slider->setEnabled(false); } + this->frame_timer->stop(); + this->btn_frame_play->setChecked(false); + this->btn_frame_play->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + float maxdist = this->container->get_max_dim(); qDebug() << "Parsing new frame to Anaglyph Widget class"; qDebug() << "Max distance: " << maxdist; @@ -390,9 +502,18 @@ void InterfaceWindow::handle_new_container() { * @brief Sets whether or not to rotate the model */ void InterfaceWindow::toggle_rotation() { - this->flag_rotate = !this->flag_rotate; + this->set_rotation_enabled(!this->flag_rotate); +} + +void InterfaceWindow::set_rotation_enabled(bool enabled) { + this->flag_rotate = enabled; - if(flag_rotate) { + if(this->switch_rotate_model && this->switch_rotate_model->isChecked() != enabled) { + this->switch_rotate_model->setChecked(enabled); + } + setToggleVisual(this->switch_rotate_model, enabled); + + if(this->flag_rotate) { qDebug() << "Enabling rotation"; this->rotation_timer->start(1000 / 60); } else { @@ -405,10 +526,33 @@ void InterfaceWindow::toggle_rotation() { * @brief Sets whether to display coordinate axes */ void InterfaceWindow::toggle_axes() { - this->flag_axes = !this->flag_axes; + this->set_axes_enabled(!this->flag_axes); +} + +void InterfaceWindow::set_axes_enabled(bool enabled) { + this->flag_axes = enabled; + + if(this->switch_toggle_axis && this->switch_toggle_axis->isChecked() != enabled) { + this->switch_toggle_axis->setChecked(enabled); + } + setToggleVisual(this->switch_toggle_axis, enabled); + this->anaglyph_widget->set_show_axes(this->flag_axes); } +void InterfaceWindow::set_pingpong_enabled(bool enabled) { + this->flag_pingpong = enabled; + + if (this->switch_pingpong && this->switch_pingpong->isChecked() != enabled) { + this->switch_pingpong->setChecked(enabled); + } + setToggleVisual(this->switch_pingpong, enabled); + + if (!this->flag_pingpong) { + this->frame_direction = 1; + } +} + /** * @brief Handles rotation timer */ @@ -416,6 +560,21 @@ void InterfaceWindow::rotation_timer_trigger() { this->anaglyph_widget->rotate_scene(1); } +void InterfaceWindow::set_playback_fps(int index) { + if (!this->fps_dropdown) { + return; + } + + const int fps = this->fps_dropdown->itemData(index).toInt(); + if (fps > 0) { + this->playback_fps = fps; + } + + if (this->frame_timer && this->frame_timer->isActive()) { + this->frame_timer->start(1000 / static_cast(this->playback_fps)); + } +} + /** * @brief Loads a default structure file. */ diff --git a/src/gui/interface_window.h b/src/gui/interface_window.h index 7778df1..3e88629 100644 --- a/src/gui/interface_window.h +++ b/src/gui/interface_window.h @@ -37,6 +37,8 @@ #include #include #include +#include +#include #include #include "anaglyph_widget.h" @@ -64,10 +66,15 @@ class InterfaceWindow : public QWidget { // control interface QWidget *control_interface; - QPushButton *btn_toggle_axis; - QPushButton *btn_rotate_model; + QToolButton *switch_toggle_axis; + QToolButton *switch_rotate_model; + QToolButton *switch_pingpong; + QLabel *fps_label = nullptr; + QComboBox *fps_dropdown = nullptr; + int playback_fps = 60; bool flag_rotate = false; bool flag_axes = true; + bool flag_pingpong = false; AnaglyphWidget *anaglyph_widget; @@ -75,12 +82,12 @@ class InterfaceWindow : public QWidget { QWidget *frame_player; int cur_frame = 0; int max_frame = 1; + int frame_direction = 1; QLabel *frame_label; QSlider *frame_slider; QPushButton *btn_frame_next; QPushButton *btn_frame_prev; QPushButton *btn_frame_play; - QPushButton *btn_frame_pause; QTimer *frame_timer; QTimer *rotation_timer; @@ -161,6 +168,11 @@ public slots: */ void set_camera_mode(QAction* action); + /** + * @brief Reset pan offset in viewer. + */ + void reset_panning(); + /** * @brief Build orbital * @param Orbital identifier @@ -199,14 +211,9 @@ private slots: void frame_slider_moved(int val); /** - * @brief Handle pressing play button + * @brief Toggle frame playback (play/pause). */ - void frame_timer_play(); - - /** - * @brief Handle pressing pause button - */ - void frame_timer_pause(); + void frame_timer_play_pause(); /** * @brief Handle timer trigger @@ -238,16 +245,36 @@ private slots: */ void toggle_rotation(); + /** + * @brief Set rotation enabled state. + */ + void set_rotation_enabled(bool enabled); + /** * @brief Sets whether to display coordinate axes */ void toggle_axes(); + /** + * @brief Set axis display enabled state. + */ + void set_axes_enabled(bool enabled); + + /** + * @brief Set ping-pong playback behavior. + */ + void set_pingpong_enabled(bool enabled); + /** * @brief Handles rotation timer */ void rotation_timer_trigger(); + /** + * @brief Update playback FPS from text field. + */ + void set_playback_fps(int index); + signals: /** * @brief Signal to indicate that a new container has been loaded diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 046c331..4701681 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -75,6 +75,7 @@ MainWindow::MainWindow(const std::shared_ptr _log_messages, QMenu *menu_camera_mode = new QMenu(tr("Mode")); QAction *action_camera_perspective = new QAction(menu_camera_mode); QAction *action_camera_orthographic = new QAction(menu_camera_mode); + QAction *action_camera_reset_panning = new QAction(menu_camera); // camera projections QMenu *menu_projection = new QMenu(tr("Projection")); @@ -92,11 +93,11 @@ MainWindow::MainWindow(const std::shared_ptr _log_messages, // actions for tools menu QAction *action_view_orbitals_menu = new QAction(menu_tools); - QAction *action_lighting_settings = new QAction(menu_tools); + QAction *action_visualisation_settings = new QAction(menu_tools); action_view_orbitals_menu->setText(tr("Orbitals menu")); - action_lighting_settings->setText(tr("Lighting settings")); + action_visualisation_settings->setText(tr("Visualisation settings")); menu_tools->addAction(action_view_orbitals_menu); - menu_tools->addAction(action_lighting_settings); + menu_tools->addAction(action_visualisation_settings); // actions for help menu QAction *action_about = new QAction(menu_help); @@ -144,6 +145,7 @@ MainWindow::MainWindow(const std::shared_ptr _log_messages, action_camera_orthographic->setText(tr("Orthographic")); action_camera_orthographic->setData(QVariant((int)CameraMode::ORTHOGRAPHIC)); action_camera_orthographic->setShortcut(Qt::CTRL | Qt::Key_5); + action_camera_reset_panning->setText(tr("Reset panning")); action_projection_two_dimensional->setText(tr("Two-dimensional")); action_projection_anaglyph_red_cyan->setText(tr("Anaglyph (red/cyan)")); action_projection_interlaced_rows_lr->setText(tr("Interlaced rows (left first)")); @@ -184,6 +186,8 @@ MainWindow::MainWindow(const std::shared_ptr _log_messages, menu_camera->addMenu(menu_camera_mode); menu_camera_mode->addAction(action_camera_perspective); menu_camera_mode->addAction(action_camera_orthographic); + menu_camera->addSeparator(); + menu_camera->addAction(action_camera_reset_panning); menu_projection->addAction(action_projection_two_dimensional); menu_projection->addAction(action_projection_anaglyph_red_cyan); menu_projection->addMenu(menu_projection_interlaced); @@ -196,6 +200,7 @@ MainWindow::MainWindow(const std::shared_ptr _log_messages, // add actions to help menu menu_help->addAction(action_about); + action_about->setShortcut(Qt::Key_F1); // connect actions file menu connect(action_open, &QAction::triggered, this, &MainWindow::open); @@ -214,7 +219,7 @@ MainWindow::MainWindow(const std::shared_ptr _log_messages, // connect actions tools menu connect(action_view_orbitals_menu, &QAction::triggered, this, &MainWindow::toggle_orbitals_menu); - connect(action_lighting_settings, &QAction::triggered, this, &MainWindow::show_lighting_settings); + connect(action_visualisation_settings, &QAction::triggered, this, &MainWindow::show_visualisation_settings); // connect actions about menu connect(action_about, &QAction::triggered, this, &MainWindow::about); @@ -234,6 +239,7 @@ MainWindow::MainWindow(const std::shared_ptr _log_messages, // connect actions camera menu connect(menu_camera_align, SIGNAL(triggered(QAction*)), this->interface_window, SLOT(set_camera_align(QAction*))); connect(menu_camera_mode, SIGNAL(triggered(QAction*)), this->interface_window, SLOT(set_camera_mode(QAction*))); + connect(action_camera_reset_panning, &QAction::triggered, this->interface_window, &InterfaceWindow::reset_panning); // add projection icon to status bar this->statusbar_projection_icon = new QLabel(); @@ -252,7 +258,6 @@ MainWindow::MainWindow(const std::shared_ptr _log_messages, setAcceptDrops(true); // set Window properties - this->setWindowTitle(QString(PROGRAM_NAME) + " " + QString(PROGRAM_VERSION)); this->resize(800,600); qDebug() << "Done building MainWindow"; @@ -322,8 +327,8 @@ void MainWindow::open_library() { } QDir dir(library_dir); - const QStringList subset_names = {"chemistry", "biology"}; - const QStringList subset_label_names = {tr("Chemistry"), tr("Biology")}; + const QStringList subset_names = {"chemistry", "biology", "catalysis"}; + const QStringList subset_label_names = {tr("Chemistry"), tr("Biology"), tr("Catalysis")}; QStringList subset_labels; QStringList subset_dirs; @@ -520,10 +525,19 @@ void MainWindow::about() { "Maintainer
" "Ivo Filot <i.a.w.filot@tue.nl>

" + "Acknowledgements
" + "Yves Hanssen
" + "Joeri van Limpt

" + "License
" PROGRAM_NAME " is free software released under the " "GNU General Public License v3.0 (GPL-3.0).

" + "Repository: " + "" + "github.com/ifilot/managlyph" + "

" + "This software uses the Qt framework, which is available under the " "GNU Lesser General Public License v3.0 (LGPL-3.0).

" @@ -542,7 +556,7 @@ void MainWindow::about() { message_box.setInformativeText(info); message_box.setStyleSheet( - "QLabel { min-width: 320px; font-weight: normal; }" + "QLabel { min-width: 420px; font-weight: normal; }" ); message_box.exec(); @@ -670,18 +684,18 @@ void MainWindow::toggle_orbitals_menu() { /** * @brief Show lighting settings window */ -void MainWindow::show_lighting_settings() { - if (!this->lighting_settings_dialog) { - this->lighting_settings_dialog = std::make_unique( +void MainWindow::show_visualisation_settings() { + if (!this->visualisation_settings_dialog) { + this->visualisation_settings_dialog = std::make_unique( this->interface_window->get_anaglyph_widget(), this ); } - this->lighting_settings_dialog->sync_from_widget(); - this->lighting_settings_dialog->show(); - this->lighting_settings_dialog->raise(); - this->lighting_settings_dialog->activateWindow(); + this->visualisation_settings_dialog->sync_from_widget(); + this->visualisation_settings_dialog->show(); + this->visualisation_settings_dialog->raise(); + this->visualisation_settings_dialog->activateWindow(); } /** @@ -724,7 +738,7 @@ QString MainWindow::find_library_directory() const { return dir.absolutePath(); } - const QStringList subset_names = {"chemistry", "biology"}; + const QStringList subset_names = {"chemistry", "biology", "catalysis"}; for (const QString& subset : subset_names) { QDir subset_dir(dir.filePath(subset)); if (!subset_dir.exists()) { diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index 57b880d..a73a827 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -41,7 +41,7 @@ #include #include "interface_window.h" -#include "lighting_settings_dialog.h" +#include "visualisation_settings_dialog.h" #include "logwindow.h" #include "../config.h" @@ -64,7 +64,7 @@ class MainWindow : public QMainWindow { // window for log messages std::unique_ptr log_window; - std::unique_ptr lighting_settings_dialog; + std::unique_ptr visualisation_settings_dialog; public: /** @@ -137,7 +137,7 @@ private slots: /** * @brief Show lighting settings window */ - void show_lighting_settings(); + void show_visualisation_settings(); private: /** diff --git a/src/gui/shader_program.cpp b/src/gui/shader_program.cpp index a8f0113..89a57e9 100644 --- a/src/gui/shader_program.cpp +++ b/src/gui/shader_program.cpp @@ -83,6 +83,7 @@ void ShaderProgram::add_uniforms() { this->uniforms.emplace("shininess", this->m_program->uniformLocation("shininess")); this->uniforms.emplace("edge_strength", this->m_program->uniformLocation("edge_strength")); this->uniforms.emplace("edge_power", this->m_program->uniformLocation("edge_power")); + this->uniforms.emplace("camera_mode", this->m_program->uniformLocation("camera_mode")); } if (this->type == ShaderProgramType::StereoscopicShader) { diff --git a/src/gui/structure_renderer.cpp b/src/gui/structure_renderer.cpp index 2f69753..a9b4f26 100644 --- a/src/gui/structure_renderer.cpp +++ b/src/gui/structure_renderer.cpp @@ -34,7 +34,7 @@ StructureRenderer::StructureRenderer(const std::shared_ptr& _scene, shader_manager(_shader_manager) { qDebug() << "Constructing Structure Renderer object"; - this->generate_sphere_coordinates(3); + this->generate_sphere_coordinates(this->sphere_tesselation_level); this->generate_cylinder_coordinates(2, 18); this->load_sphere_to_vao(); this->load_cylinder_to_vao(); @@ -45,6 +45,29 @@ StructureRenderer::StructureRenderer(const std::shared_ptr& _scene, this->load_arrow_model(); } + +void StructureRenderer::set_sphere_tesselation_level(unsigned int tesselation_level) { + if (this->sphere_tesselation_level == tesselation_level) { + return; + } + + this->sphere_tesselation_level = tesselation_level; + + this->generate_sphere_coordinates(this->sphere_tesselation_level); + + if (this->vao_sphere.isCreated()) { + this->vao_sphere.destroy(); + } + + for (unsigned int i = 0; i < 3; ++i) { + if (this->vbo_sphere[i].isCreated()) { + this->vbo_sphere[i].destroy(); + } + } + + this->load_sphere_to_vao(); +} + /** * @brief Draw single object * @@ -72,6 +95,7 @@ void StructureRenderer::draw_single_object(const std::shared_ptr& obj, shader->set_uniform("shininess", ls.shininess); shader->set_uniform("edge_strength", ls.edge_strength); shader->set_uniform("edge_power", ls.edge_power); + shader->set_uniform("camera_mode", static_cast(this->scene->camera_mode)); shader->set_uniform("color", obj->get_color()); @@ -342,6 +366,7 @@ void StructureRenderer::set_lighting_uniforms(ShaderProgram* shader, const Light shader->set_uniform("shininess", settings.shininess); shader->set_uniform("edge_strength", settings.edge_strength); shader->set_uniform("edge_power", settings.edge_power); + shader->set_uniform("camera_mode", static_cast(this->scene->camera_mode)); } /** @@ -555,7 +580,7 @@ void StructureRenderer::load_cylinder_to_vao() { this->vbo_cylinder[2].bind(); this->vbo_cylinder[2].allocate(&this->cylinder_indices[0], this->cylinder_indices.size() * sizeof(unsigned int)); - this->vao_sphere.release(); + this->vao_cylinder.release(); } /** diff --git a/src/gui/structure_renderer.h b/src/gui/structure_renderer.h index 6565700..7656e1e 100644 --- a/src/gui/structure_renderer.h +++ b/src/gui/structure_renderer.h @@ -78,6 +78,8 @@ class StructureRenderer { // models std::shared_ptr axis_model; + unsigned int sphere_tesselation_level = 4; + public: /** * @brief Constructs a new instance. @@ -99,6 +101,12 @@ class StructureRenderer { */ void draw_coordinate_axes(); + void set_sphere_tesselation_level(unsigned int tesselation_level); + + unsigned int get_sphere_tesselation_level() const { + return this->sphere_tesselation_level; + } + private: /** * @brief Set lighting uniforms. diff --git a/src/gui/lighting_settings_dialog.cpp b/src/gui/visualisation_settings_dialog.cpp similarity index 79% rename from src/gui/lighting_settings_dialog.cpp rename to src/gui/visualisation_settings_dialog.cpp index 4c6e8f8..84ae881 100644 --- a/src/gui/lighting_settings_dialog.cpp +++ b/src/gui/visualisation_settings_dialog.cpp @@ -19,7 +19,7 @@ * * **************************************************************************/ -#include "lighting_settings_dialog.h" +#include "visualisation_settings_dialog.h" #include @@ -35,21 +35,41 @@ QString to_percent_label(int value) { } } // namespace -LightingSettingsDialog::LightingSettingsDialog(AnaglyphWidget* anaglyph_widget, QWidget* parent) +VisualisationSettingsDialog::VisualisationSettingsDialog(AnaglyphWidget* anaglyph_widget, QWidget* parent) : QDialog(parent), anaglyph_widget(anaglyph_widget) { - setWindowTitle(tr("Lighting settings")); + setWindowTitle(tr("Visualisation settings")); setModal(false); auto *layout = new QVBoxLayout(this); auto *atom_group = new QGroupBox(tr("Atoms and bonds")); auto *object_group = new QGroupBox(tr("Objects (incl. orbitals)")); + auto *rendering_group = new QGroupBox(tr("Rendering")); setup_controls(atom_group, &atom_controls); setup_controls(object_group, &object_controls); + auto *rendering_grid = new QGridLayout(rendering_group); + this->msaa_combo = new QComboBox(); + this->msaa_combo->addItem("1", 1); + this->msaa_combo->addItem("2", 2); + this->msaa_combo->addItem("4", 4); + this->msaa_combo->addItem("8", 8); + + this->sphere_tesselation_spinbox = new QSpinBox(); + this->sphere_tesselation_spinbox->setRange(0, 6); + + this->reset_lighting_button = new QPushButton(tr("Reset lighting defaults")); + + rendering_grid->addWidget(new QLabel(tr("MSAA samples")), 0, 0); + rendering_grid->addWidget(this->msaa_combo, 0, 1); + rendering_grid->addWidget(new QLabel(tr("Sphere tesselation")), 1, 0); + rendering_grid->addWidget(this->sphere_tesselation_spinbox, 1, 1); + rendering_grid->addWidget(this->reset_lighting_button, 2, 0, 1, 2); + layout->addWidget(atom_group); layout->addWidget(object_group); + layout->addWidget(rendering_group); refresh_from_widget(); @@ -64,15 +84,23 @@ LightingSettingsDialog::LightingSettingsDialog(AnaglyphWidget* anaglyph_widget, connect_controls(atom_controls); connect_controls(object_controls); + connect(this->msaa_combo, static_cast(&QComboBox::currentIndexChanged), this, [this]() { apply_settings(); }); + connect(this->sphere_tesselation_spinbox, static_cast(&QSpinBox::valueChanged), this, [this]() { apply_settings(); }); + connect(this->reset_lighting_button, &QPushButton::clicked, this, [this]() { + if (this->anaglyph_widget) { + this->anaglyph_widget->reset_lighting_settings_to_defaults(); + } + refresh_from_widget(); + }); this->resize(640,480); } -void LightingSettingsDialog::sync_from_widget() { +void VisualisationSettingsDialog::sync_from_widget() { refresh_from_widget(); } -void LightingSettingsDialog::apply_settings() { +void VisualisationSettingsDialog::apply_settings() { if (!anaglyph_widget) { return; } @@ -109,11 +137,15 @@ void LightingSettingsDialog::apply_settings() { object_edge_power ); + const int msaa_samples = this->msaa_combo->currentData().toInt(); + anaglyph_widget->set_msaa_samples(msaa_samples); + anaglyph_widget->set_sphere_tesselation_level(this->sphere_tesselation_spinbox->value()); + update_labels(atom_controls); update_labels(object_controls); } -void LightingSettingsDialog::refresh_from_widget() { +void VisualisationSettingsDialog::refresh_from_widget() { if (!anaglyph_widget) { return; } @@ -147,11 +179,23 @@ void LightingSettingsDialog::refresh_from_widget() { object_controls.edge_slider->setValue(static_cast(object_settings.edge_strength * 100.0f)); object_controls.edge_power_slider->setValue(static_cast(object_settings.edge_power * 100.0f)); + const int configured_msaa = anaglyph_widget->get_msaa_samples(); + const int msaa_index = this->msaa_combo->findData(configured_msaa); + if (msaa_index >= 0) { + QSignalBlocker msaa_blocker(this->msaa_combo); + this->msaa_combo->setCurrentIndex(msaa_index); + } + + { + QSignalBlocker tesselation_blocker(this->sphere_tesselation_spinbox); + this->sphere_tesselation_spinbox->setValue(anaglyph_widget->get_sphere_tesselation_level()); + } + update_labels(atom_controls); update_labels(object_controls); } -void LightingSettingsDialog::setup_controls(QGroupBox* group_box, LightingControls* controls) { +void VisualisationSettingsDialog::setup_controls(QGroupBox* group_box, LightingControls* controls) { auto *grid = new QGridLayout(group_box); controls->ambient_slider = new QSlider(Qt::Horizontal); @@ -209,7 +253,7 @@ void LightingSettingsDialog::setup_controls(QGroupBox* group_box, LightingContro grid->addWidget(controls->edge_power_value, 5, 2); } -void LightingSettingsDialog::update_labels(const LightingControls& controls) { +void VisualisationSettingsDialog::update_labels(const LightingControls& controls) { controls.ambient_value->setText(to_percent_label(controls.ambient_slider->value())); controls.diffuse_value->setText(to_percent_label(controls.diffuse_slider->value())); controls.specular_value->setText(to_percent_label(controls.specular_slider->value())); diff --git a/src/gui/lighting_settings_dialog.h b/src/gui/visualisation_settings_dialog.h similarity index 87% rename from src/gui/lighting_settings_dialog.h rename to src/gui/visualisation_settings_dialog.h index 2f9e259..a438b01 100644 --- a/src/gui/lighting_settings_dialog.h +++ b/src/gui/visualisation_settings_dialog.h @@ -27,14 +27,17 @@ #include #include #include +#include +#include +#include #include "anaglyph_widget.h" -class LightingSettingsDialog : public QDialog { +class VisualisationSettingsDialog : public QDialog { Q_OBJECT public: - explicit LightingSettingsDialog(AnaglyphWidget* anaglyph_widget, QWidget* parent = nullptr); + explicit VisualisationSettingsDialog(AnaglyphWidget* anaglyph_widget, QWidget* parent = nullptr); void sync_from_widget(); private: @@ -63,4 +66,8 @@ class LightingSettingsDialog : public QDialog { LightingControls atom_controls; LightingControls object_controls; + + QComboBox* msaa_combo = nullptr; + QSpinBox* sphere_tesselation_spinbox = nullptr; + QPushButton* reset_lighting_button = nullptr; }; diff --git a/src/main.cpp b/src/main.cpp index 518b5b5..3465ff4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -110,7 +110,7 @@ int main(int argc, char *argv[]) { // build main window qInstallMessageHandler(message_output); mainWindow = std::make_unique(log_messages); - mainWindow->setWindowTitle(QString(PROGRAM_NAME) + " " + QString(PROGRAM_VERSION)); + mainWindow->setWindowTitle(QString(PROGRAM_NAME)); mainWindow->set_cli_parser(parser); } catch(const std::exception& e) { std::cerr << "Error detected!" << std::endl;