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 @@
[](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.
-
- 
-- **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.
-
- 
-- **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.
-
- 
-
-## 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.
+
+
+
+### 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.
+
+
+
+### Atomic Orbitals
+The application can generate and render atomic orbitals up to the 4f subshell,
+including rotation and inspection in 3D.
+
+
+
+### 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.
+
+
+
+## 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;