Easy3D 2.6.1
Loading...
Searching...
No Matches
Tutorial_204_Viewer_Qt/main.cpp

This example shows how to create a simple viewer using Qt.

The header file of the viewer class:

1/********************************************************************
2 * Copyright (C) 2015 Liangliang Nan <liangliang.nan@gmail.com>
3 * https://3d.bk.tudelft.nl/liangliang/
4 *
5 * This file is part of Easy3D. If it is useful in your research/work,
6 * I would be grateful if you show your appreciation by citing it:
7 * ------------------------------------------------------------------
8 * Liangliang Nan.
9 * Easy3D: a lightweight, easy-to-use, and efficient C++ library
10 * for processing and rendering 3D data.
11 * Journal of Open Source Software, 6(64), 3255, 2021.
12 * ------------------------------------------------------------------
13 *
14 * Easy3D is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License Version 3
16 * as published by the Free Software Foundation.
17 *
18 * Easy3D is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ********************************************************************/
26
27#ifndef EASY3D_QT_VIEWER_H
28#define EASY3D_QT_VIEWER_H
29
30#include <easy3d/core/types.h>
31
32#include <QOpenGLWidget>
33#include <QOpenGLFunctions>
34#include <QElapsedTimer>
35
36
37class QWidget;
38
39namespace easy3d {
40
41 class Camera;
42 class Model;
43 class Drawable;
44 class TrianglesDrawable;
45 class TextRenderer;
46
47 class Viewer : public QOpenGLWidget, public QOpenGLFunctions {
48 Q_OBJECT
49 public:
50 explicit Viewer(QWidget *parent = nullptr);
51 ~Viewer() override;
52
53 virtual std::string usage() const;
54
55 // the actual samples received
56 int samples() const { return samples_; }
57 // Scaling factor for high DPI devices
58 float dpiScaling() const { return dpi_scaling_; }
59
60 const easy3d::vec4 &backGroundColor() const { return background_color_; }
61 void setBackgroundColor(const easy3d::vec4 &c);
62
63 void addModel(easy3d::Model *model);
64 void deleteModel(easy3d::Model *model);
65
66 const std::vector<easy3d::Model *> &models() const { return models_; }
67 easy3d::Model *currentModel() const;
68
82 bool addDrawable(Drawable* drawable);
83 bool deleteDrawable(Drawable* drawable);
84 const std::vector<Drawable*>& drawables() const { return drawables_; }
85
86 // the camera
87 easy3d::Camera *camera() const { return camera_; }
88
89 // moves the camera so that the 'model' is centered on the screen.
90 // if 'model' is NULL, it centers the entire scene (all models).
91 void fitScreen(const easy3d::Model *model = nullptr);
92
93 // Returns the coordinates of the 3D point located at pixel (x,y) on screen.
94 // x, y: screen point expressed in pixel units with an origin in the upper left corner.
95 // found: indicates whether a point was found or not.
96 // NOTE: This method assumes that a GL context is available, and that its
97 // content was drawn using the Camera (i.e. using its projection and modelview
98 // matrices). This method hence cannot be used for offscreen Camera computations.
99 // Use cameraCoordinatesOf() and worldCoordinatesOf() to perform similar
100 // operations in that case.
101 // The precision of the z-Buffer highly depends on how the zNear() and zFar()
102 // values are fitted to your scene. Loose boundaries will result in imprecision
103 // along the viewing direction.
104 easy3d::vec3 pointUnderPixel(const QPoint &p, bool &found);
105
106 // Save snapshot
107 // file_name: the image file name
108 bool saveSnapshot(const QString &file_name);
109
110 protected:
111
112 /* Set up required OpenGL resources/state and then calls user-defined init().
113 * This method is called once before the first call to paintGL() or resizeGL().
114 * Note:
115 * - Overload init() instead of this method to modify specific OpenGL state;
116 * - The framebuffer is not yet available at this stage.
117 */
118 void initializeGL() override;
119
120 /* User-defined initialization method.
121 * This method is called within initializeGL() and should be overloaded to
122 * initialize OpenGL flags/resources, e.g.,
123 * - OpenGL state modification;
124 * - shader program/texture/VAOs creation;
125 * - camera initialization;
126 * - previous viewer state restoration;
127 * - ...
128 * All OpenGL specific initializations must be done in this method.
129 * OpenGL context is not yet available in your viewer constructor.
130 * NOTE:
131 * - If you derive you own viewer from this class, don't forget to call
132 * Viewer::init() at the beginning of your inherited function.
133 * - Do not call updateGL() in this method (resulting in an infinite loop).
134 */
135 virtual void init();
136
137 /* Sets up the OpenGL viewport, projection, etc. Gets called whenever the
138 * widget has been resized (and also when it is shown for the first time
139 * because all newly created widgets get a resize event automatically).
140 * If you overload this method, first call the inherited method in which
141 * the projection matrix is updated.
142 */
143 void resizeGL(int width, int height) override;
144
145 /* Renders the OpenGL scene. Gets called whenever the widget needs to
146 * be updated. Internally, it calls the following methods in order:
147 * - preDraw(): places the camera in the world coordinate system;
148 * - draw(): main drawing method. Should be overloaded.
149 * - postDraw(): display of visual hints (world axis, FPS...)
150 * Note: For normal rendering, i.e., drawing triggered by the
151 * paintEvent(), the clearing of the color and depth buffers is
152 * done by the widget before entering paintGL(). However, if you
153 * want to reuse the paintGL() method for offscreen rendering,
154 * you have to clear both buffers before calling paintGL().
155 */
156 void paintGL() override;
157
158 /* This function will be called before the main draw procedure.
159 */
160 virtual void preDraw();
161
162 /* The core method of the viewer, that draws the scene.
163 */
164 virtual void draw();
165
166 /* Called after draw() to draw viewer visual hints.
167 * By default, it displays axis and visual hints if the respective flags are set.
168 */
169 virtual void postDraw();
170
171 // OpenGL resources (e.g., shaders, textures, VAOs) can only be destroyed when there exists a valid
172 // rendering context. It is (usually) a bad idea to clean up OpenGL in a destructor because the OpenGL
173 // context may not exist (e.g., destroyed already) or the visible one is not *current*. This cleanup()
174 // function is to ensure you have a valid rendering context. See also init().
175 // NOTE: Don't forget to call Viewer::cleanup() at the end of your inherited function.
176 void cleanup();
177
178 protected:
179 void mousePressEvent(QMouseEvent *) override; // Mouse button press event handler
180 void mouseMoveEvent(QMouseEvent *) override;
181
182 void mouseReleaseEvent(QMouseEvent *) override; // Mouse button release event handler
183 void mouseDoubleClickEvent(QMouseEvent *) override;
184
185 void wheelEvent(QWheelEvent *) override; // Mouse scroll event handler
186 void keyPressEvent(QKeyEvent *) override; // Keyboard press event handler.
187 void keyReleaseEvent(QKeyEvent *) override; // Keyboard press event handler.
188 void timerEvent(QTimerEvent *) override;
189
190 void closeEvent(QCloseEvent *) override;
191
192 protected:
193
194 void drawCornerAxes();
195
196 signals:
197
198 void currentModelChanged();
199
200 protected:
201 float dpi_scaling_;
202 int samples_;
203
204 QElapsedTimer timer_;
205 easy3d::TextRenderer *texter_;
206
207 easy3d::Camera *camera_;
208 easy3d::vec4 background_color_;
209
210 Qt::MouseButton pressed_button_;
211 QPoint mouse_pressed_pos_;
212 QPoint mouse_previous_pos_;
213
214 bool show_pivot_point_;
215
216 //----------------- viewer data -------------------
217
218 // corner axes
219 easy3d::TrianglesDrawable *drawable_axes_;
220 std::vector<easy3d::Model *> models_;
221 int model_idx_;
222
223 // drawables independent of any model
224 std::vector<Drawable*> drawables_;
225 };
226
227}
228
229#endif // EASY3D_QT_VIEWER_H
virtual ~Viewer()
The destructor.
Definition viewer.cpp:553
const std::vector< std::shared_ptr< Drawable > > & drawables() const
Query the drawables managed by this viewer.
Definition viewer.h:337
const std::vector< std::shared_ptr< Model > > & models() const
Query the models managed by this viewer.
Definition viewer.h:287
int samples() const
Query the actual samples of the viewer.
Definition viewer.h:160
Viewer(const std::string &title="Easy3D Viewer", int samples=4, int gl_major=3, int gl_minor=2, bool full_screen=false, bool resizable=true, int depth_bits=24, int stencil_bits=8, int width=800, int height=600)
Constructor.
Definition viewer.cpp:75
Camera * camera()
Returns the camera used by the viewer. See Camera.
Definition viewer.h:181
int height() const
Returns the height of the viewer/window.
Definition viewer.h:134
int width() const
Returns the width of the viewer/window.
Definition viewer.h:132
Definition collider.cpp:182
Vec< 3, float > vec3
A 3D point/vector of float type.
Definition types.h:44
Vec< 4, float > vec4
A 4D point/vector of float type.
Definition types.h:46

The source file of the viewer class:

1/********************************************************************
2 * Copyright (C) 2015 Liangliang Nan <liangliang.nan@gmail.com>
3 * https://3d.bk.tudelft.nl/liangliang/
4 *
5 * This file is part of Easy3D. If it is useful in your research/work,
6 * I would be grateful if you show your appreciation by citing it:
7 * ------------------------------------------------------------------
8 * Liangliang Nan.
9 * Easy3D: a lightweight, easy-to-use, and efficient C++ library
10 * for processing and rendering 3D data.
11 * Journal of Open Source Software, 6(64), 3255, 2021.
12 * ------------------------------------------------------------------
13 *
14 * Easy3D is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License Version 3
16 * as published by the Free Software Foundation.
17 *
18 * Easy3D is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ********************************************************************/
26
27#include "viewer.h"
28
29#include <QKeyEvent>
30#include <QPainter>
31#include <QOpenGLContext>
32#include <QOpenGLFunctions>
33#include <QOpenGLFramebufferObjectFormat>
34#include <QApplication>
35
36#include <easy3d/core/surface_mesh.h>
37#include <easy3d/core/point_cloud.h>
38#include <easy3d/core/graph.h>
39#include <easy3d/renderer/drawable_points.h>
40#include <easy3d/renderer/drawable_lines.h>
41#include <easy3d/renderer/drawable_triangles.h>
42#include <easy3d/renderer/shader_program.h>
43#include <easy3d/renderer/shader_manager.h>
44#include <easy3d/renderer/shape.h>
45#include <easy3d/renderer/transform.h>
46#include <easy3d/renderer/camera.h>
47#include <easy3d/renderer/manipulated_camera_frame.h>
48#include <easy3d/renderer/key_frame_interpolator.h>
49#include <easy3d/renderer/opengl_util.h>
50#include <easy3d/renderer/opengl_error.h>
51#include <easy3d/renderer/texture_manager.h>
52#include <easy3d/renderer/text_renderer.h>
53#include <easy3d/renderer/renderer.h>
54#include <easy3d/util/resource.h>
55#include <easy3d/fileio/surface_mesh_io.h>
56#include <easy3d/util/logging.h>
57#include <easy3d/util/file_system.h>
58#include <easy3d/util/setting.h>
59
60
61namespace easy3d {
62
63 // \cond
64
65 Viewer::Viewer(QWidget *parent /* = nullptr*/)
66 : QOpenGLWidget(parent), texter_(nullptr), pressed_button_(Qt::NoButton),
67 mouse_pressed_pos_(0, 0), mouse_previous_pos_(0, 0), show_pivot_point_(false), drawable_axes_(nullptr),
68 model_idx_(-1) {
69 // like Qt::StrongFocus plus the widget accepts focus by using the mouse wheel.
70 setFocusPolicy(Qt::WheelFocus);
71 setMouseTracking(true);
72
73 camera_ = new Camera;
74 camera_->setType(Camera::PERSPECTIVE);
75 camera_->setUpVector(vec3(0, 0, 1)); // Z pointing up
76 camera_->setViewDirection(vec3(-1, 0, 0)); // X pointing out
77 camera_->showEntireScene();
78
79 easy3d::connect(&camera_->frame_modified, this, static_cast<void (Viewer::*)(void)>(&Viewer::update));
80 }
81
82
84 // Make sure the context is current and then explicitly
85 // destroy all underlying OpenGL resources.
86 makeCurrent();
87
88 cleanup();
89
90 doneCurrent();
91
92 LOG(INFO) << "viewer terminated. Bye!";
93 }
94
95
96 void Viewer::cleanup() {
97 delete camera_;
98 delete drawable_axes_;
99 delete texter_;
100
101 for (auto m: models_)
102 delete m;
103 models_.clear();
104
105 for (auto d : drawables_)
106 delete d;
107 drawables_.clear();
108
111 }
112
113
114 void Viewer::init() {
115 const std::string file_name = resource::directory() + "/data/easy3d.ply";
116 auto mesh = SurfaceMeshIO::load(file_name);
117 if (mesh)
118 addModel(mesh);
119
120 // We always want to look at the front of the easy3d logo.
121 camera()->setViewDirection(vec3(0, 0, -1));
122 camera()->setUpVector(vec3(0, 1, 0));
123 fitScreen(mesh);
124 }
125
126
127 void Viewer::initializeGL() {
128 QOpenGLWidget::initializeGL();
129 initializeOpenGLFunctions();
130
132#ifndef NDEBUG
134#endif
135
136 if (!hasOpenGLFeature(QOpenGLFunctions::Multisample))
137 throw std::runtime_error("Multisample not supported on this machine!!! Viewer may not run properly");
138 if (!hasOpenGLFeature(QOpenGLFunctions::Framebuffers))
139 throw std::runtime_error(
140 "Framebuffer Object is not supported on this machine!!! Viewer may not run properly");
141
142 background_color_ = setting::background_color;
143
144 glEnable(GL_DEPTH_TEST);
145 glClearDepthf(1.0f);
146 glClearColor(background_color_[0], background_color_[1], background_color_[2], background_color_[3]);
147
148 int major_requested = QSurfaceFormat::defaultFormat().majorVersion();
149 int minor_requested = QSurfaceFormat::defaultFormat().minorVersion();
150 VLOG(1) << "OpenGL vendor: " << glGetString(GL_VENDOR);
151 VLOG(1) << "OpenGL renderer: " << glGetString(GL_RENDERER);
152 VLOG(1) << "OpenGL version requested: " << major_requested << "." << minor_requested;
153 VLOG(1) << "OpenGL version received: " << glGetString(GL_VERSION);
154 VLOG(1) << "GLSL version received: " << glGetString(GL_SHADING_LANGUAGE_VERSION);
155
156 int major = 0;
157 glGetIntegerv(GL_MAJOR_VERSION, &major);
158 int minor = 0;
159 glGetIntegerv(GL_MINOR_VERSION, &minor);
160 if (major * 10 + minor < 32) {
161 throw std::runtime_error("Viewer requires at least OpenGL 3.2");
162 }
163
164#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
165 dpi_scaling_ = static_cast<float>(devicePixelRatioF());
166#else
167 dpi_scaling_ = static_cast<float>(devicePixelRatio());
168#endif
169 VLOG(1) << "DPI scaling: " << dpiScaling();
170
171 // This won't work because QOpenGLWidget draws everything in framebuffer and
172 // the framebuffer has not been created in the initializeGL() method. We
173 // will query the actual samples in the paintGL() method.
174 //int samples_received = 0;
175 //func_->glgetintegerv(gl_samples, &samples_received);
176
177 // create TextRenderer renderer and load default fonts
178 texter_ = new TextRenderer(dpiScaling());
179 texter_->add_font(resource::directory() + "/fonts/en_Earth-Normal.ttf");
180 texter_->add_font(resource::directory() + "/fonts/en_Roboto-Medium.ttf");
181
182 // Calls user defined method.
183 init();
184
185 // print usage
186 std::cout << usage() << std::endl;
187
188 timer_.start();
189 }
190
191
192 void Viewer::resizeGL(int w, int h) {
193 QOpenGLWidget::resizeGL(w, h);
194
195 // The viewport is set up by QOpenGLWidget before drawing.
196 // So I don't need to set up.
197 // func_->glViewport(0, 0, static_cast<int>(w * highdpi_), static_cast<int>(h * highdpi_));
198
200 }
201
202
203 void Viewer::setBackgroundColor(const vec4 &c) {
204 background_color_ = c;
205
206 makeCurrent();
207 glClearColor(background_color_[0], background_color_[1], background_color_[2], background_color_[3]);
208 doneCurrent();
209 }
210
211
212 void Viewer::mousePressEvent(QMouseEvent *e) {
213 pressed_button_ = e->button();
214 mouse_previous_pos_ = e->pos();
215 mouse_pressed_pos_ = e->pos();
216
217 camera_->frame()->action_start();
218 if (e->modifiers() == Qt::ShiftModifier) {
219 if (e->button() == Qt::LeftButton) {
220 bool found = false;
221 const vec3 &p = pointUnderPixel(e->pos(), found);
222 if (found) {
224 camera_->setPivotPoint(p);
225
226 // show, but hide the visual hint of pivot point after \p delay milliseconds.
227 show_pivot_point_ = true;
228 Timer<>::single_shot(10000, [&]() {
229 show_pivot_point_ = false;
230 update();
231 });
232 } else {
233 camera_->setPivotPoint(camera_->sceneCenter());
234 show_pivot_point_ = false;
235 }
236 } else if (e->button() == Qt::RightButton) {
238 camera_->setPivotPoint(camera_->sceneCenter());
239 show_pivot_point_ = false;
240 }
241 }
242
243 QOpenGLWidget::mousePressEvent(e);
244 update();
245 }
246
247
248 void Viewer::mouseReleaseEvent(QMouseEvent *e) {
249 if (e->button() == Qt::LeftButton && e->modifiers() == Qt::ControlModifier) { // ZOOM_ON_REGION
250 int xmin = std::min(mouse_pressed_pos_.x(), e->pos().x());
251 int xmax = std::max(mouse_pressed_pos_.x(), e->pos().x());
252 int ymin = std::min(mouse_pressed_pos_.y(), e->pos().y());
253 int ymax = std::max(mouse_pressed_pos_.y(), e->pos().y());
254 camera_->fitScreenRegion(xmin, ymin, xmax, ymax);
255 } else
256 camera_->frame()->action_end();
257
258 pressed_button_ = Qt::NoButton;
259 mouse_pressed_pos_ = QPoint(0, 0);
260
261 QOpenGLWidget::mouseReleaseEvent(e);
262 update();
263 }
264
265
266 void Viewer::mouseMoveEvent(QMouseEvent *e) {
267 int x = e->pos().x(), y = e->pos().y();
268 // Restrict the cursor to be within the client area during dragging
269 if (x < 0 || x > width() || y < 0 || y > height()) {
270 e->ignore();
271 return;
272 }
273
274 if (pressed_button_ != Qt::NoButton) { // button pressed
275 if (e->modifiers() == Qt::ControlModifier) {
276 // zoom on region
277 } else {
278 int dx = x - mouse_previous_pos_.x();
279 int dy = y - mouse_previous_pos_.y();
280 if (pressed_button_ == Qt::LeftButton)
281 camera_->frame()->action_rotate(x, y, dx, dy, camera_, ManipulatedFrame::NONE);
282 else if (pressed_button_ == Qt::RightButton)
283 camera_->frame()->action_translate(x, y, dx, dy, camera_, ManipulatedFrame::NONE);
284 else if (pressed_button_ == Qt::MiddleButton) {
285 if (dy != 0)
286 camera_->frame()->action_zoom(dy > 0 ? 1 : -1, camera_);
287 }
288 }
289 }
290
291 mouse_previous_pos_ = e->pos();
292 QOpenGLWidget::mouseMoveEvent(e);
293 }
294
295
296 void Viewer::mouseDoubleClickEvent(QMouseEvent *e) {
297 QOpenGLWidget::mouseDoubleClickEvent(e);
298 update();
299 }
300
301
302 void Viewer::wheelEvent(QWheelEvent *e) {
303 const int delta = e->angleDelta().y();
304 if (delta <= -1 || delta >= 1) {
305 int dy = e->angleDelta().y() > 0 ? 1 : -1;
306 camera_->frame()->action_zoom(dy, camera_);
307 }
308
309 QOpenGLWidget::wheelEvent(e);
310 update();
311 }
312
313
314 bool Viewer::saveSnapshot(const QString &file_name) {
315 makeCurrent();
316
317 int w = static_cast<int>(static_cast<float>(width()) * dpiScaling());
318 int h = static_cast<int>(static_cast<float>(height()) * dpiScaling());
319
320 QOpenGLFramebufferObjectFormat format;
321 format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
322 format.setSamples(4);
323 auto fbo = new QOpenGLFramebufferObject(w, h, format);
324 fbo->addColorAttachment(w, h);
325
326 fbo->bind();
327 glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
328 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
329
330 draw();
331
332 fbo->release();
333
334 const QImage &image = fbo->toImage();
335
336 // restore the clear color
337 glClearColor(background_color_[0], background_color_[1], background_color_[2], background_color_[3]);
338
339 doneCurrent();
340
341 return image.save(file_name);
342 }
343
344
345 Model *Viewer::currentModel() const {
346 if (models_.empty())
347 return nullptr;
348 if (model_idx_ < models_.size())
349 return models_[model_idx_];
350 return nullptr;
351 }
352
353 void Viewer::keyPressEvent(QKeyEvent *e) {
354 if (e->key() == Qt::Key_F1 && e->modifiers() == Qt::NoModifier)
355 std::cout << usage() << std::endl;
356 else if (e->key() == Qt::Key_Left && e->modifiers() == Qt::KeypadModifier) {
357 auto angle = static_cast<float>(1 * M_PI / 180.0); // turn left, 1 degrees each step
358 camera_->frame()->action_turn(angle, camera_);
359 } else if (e->key() == Qt::Key_Right && e->modifiers() == Qt::KeypadModifier) {
360 auto angle = static_cast<float>(1 * M_PI / 180.0); // turn right, 1 degrees each step
361 camera_->frame()->action_turn(-angle, camera_);
362 } else if (e->key() == Qt::Key_Up && e->modifiers() == Qt::KeypadModifier) { // move camera forward
363 float step = 0.05f * camera_->sceneRadius();
364 camera_->frame()->translate(camera_->frame()->inverseTransformOf(vec3(0.0, 0.0, -step)));
365 } else if (e->key() == Qt::Key_Down && e->modifiers() == Qt::KeypadModifier) {// move camera backward
366 float step = 0.05f * camera_->sceneRadius();
367 camera_->frame()->translate(camera_->frame()->inverseTransformOf(vec3(0.0, 0.0, step)));
368 } else if (e->key() == Qt::Key_Left &&
369 e->modifiers() == (Qt::KeypadModifier | Qt::ControlModifier)) { // move camera left
370 float step = 0.05f * camera_->sceneRadius();
371 camera_->frame()->translate(camera_->frame()->inverseTransformOf(vec3(-step, 0.0, 0.0)));
372 } else if (e->key() == Qt::Key_Right &&
373 e->modifiers() == (Qt::KeypadModifier | Qt::ControlModifier)) { // move camera right
374 float step = 0.05f * camera_->sceneRadius();
375 camera_->frame()->translate(camera_->frame()->inverseTransformOf(vec3(step, 0.0, 0.0)));
376 } else if (e->key() == Qt::Key_Up &&
377 e->modifiers() == (Qt::KeypadModifier | Qt::ControlModifier)) { // move camera up
378 float step = 0.05f * camera_->sceneRadius();
379 camera_->frame()->translate(camera_->frame()->inverseTransformOf(vec3(0.0, step, 0.0)));
380 } else if (e->key() == Qt::Key_Down &&
381 e->modifiers() == (Qt::KeypadModifier | Qt::ControlModifier)) { // move camera down
382 float step = 0.05f * camera_->sceneRadius();
383 camera_->frame()->translate(camera_->frame()->inverseTransformOf(vec3(0.0, -step, 0.0)));
384 } else if (e->key() == Qt::Key_A && e->modifiers() == Qt::NoModifier) {
385 if (drawable_axes_)
386 drawable_axes_->set_visible(!drawable_axes_->is_visible());
387 } else if (e->key() == Qt::Key_C && e->modifiers() == Qt::NoModifier) {
388 if (currentModel())
389 fitScreen(currentModel());
390 } else if (e->key() == Qt::Key_F && e->modifiers() == Qt::NoModifier) {
391 fitScreen();
392 } else if (e->key() == Qt::Key_P && e->modifiers() == Qt::NoModifier) {
393 if (camera_->type() == Camera::PERSPECTIVE)
394 camera_->setType(Camera::ORTHOGRAPHIC);
395 else
396 camera_->setType(Camera::PERSPECTIVE);
397 } else if (e->key() == Qt::Key_Space && e->modifiers() == Qt::NoModifier) {
398 // Aligns camera
399 Frame frame;
400 frame.setTranslation(camera_->pivotPoint());
401 camera_->frame()->alignWithFrame(&frame, true);
402
403 // Aligns frame
404 //if (manipulatedFrame())
405 // manipulatedFrame()->alignWithFrame(camera_->frame());
406 } else if (e->key() == Qt::Key_Minus && e->modifiers() == Qt::ControlModifier)
407 camera_->frame()->action_zoom(-1, camera_);
408 else if (e->key() == Qt::Key_Equal && e->modifiers() == Qt::ControlModifier)
409 camera_->frame()->action_zoom(1, camera_);
410
411 else if (e->key() == Qt::Key_K && e->modifiers() == Qt::AltModifier) { // add key frame
412 easy3d::Frame *frame = camera()->frame();
414 // update scene bounding box to make sure the path is within the view frustum
415 float old_radius = camera()->sceneRadius();
416 float candidate_radius = distance(camera()->sceneCenter(), frame->position());
417 camera()->setSceneRadius(std::max(old_radius, candidate_radius));
418 } else if (e->key() == Qt::Key_D && e->modifiers() == Qt::ControlModifier) { // delete path
420
421 // update scene bounding box
422 Box3 box;
423 for (auto m: models_)
424 box.grow(m->bounding_box());
425 camera_->setSceneBoundingBox(box.min_point(), box.max_point());
426 } else if (e->key() == Qt::Key_K && e->modifiers() == Qt::ControlModifier) { // play the path
427 if (camera()->keyframe_interpolator()->is_interpolation_started())
429 else
431 } else if (e->key() == Qt::Key_BracketLeft && e->modifiers() == Qt::NoModifier) {
432 for (auto m: models_) {
433 for (auto d: m->renderer()->lines_drawables()) {
434 float size = d->line_width() - 1.0f;
435 if (size < 1)
436 size = 1;
437 d->set_line_width(size);
438 }
439 }
440 } else if (e->key() == Qt::Key_BracketRight && e->modifiers() == Qt::NoModifier) {
441 for (auto m: models_) {
442 for (auto d: m->renderer()->lines_drawables()) {
443 float size = d->line_width() + 1.0f;
444 d->set_line_width(size);
445 }
446 }
447 } else if (e->key() == Qt::Key_Minus && e->modifiers() == Qt::NoModifier) {
448 for (auto m: models_) {
449 for (auto d: m->renderer()->points_drawables()) {
450 float size = d->point_size() - 1.0f;
451 if (size < 1)
452 size = 1;
453 d->set_point_size(size);
454 }
455 }
456 } else if (e->key() == Qt::Key_Equal && e->modifiers() == Qt::NoModifier) {
457 for (auto m: models_) {
458 for (auto d: m->renderer()->points_drawables()) {
459 float size = d->point_size() + 1.0f;
460 d->set_point_size(size);
461 }
462 }
463 } else if (e->key() == Qt::Key_Comma && e->modifiers() == Qt::NoModifier) {
464 int pre_idx = model_idx_;
465 if (models_.empty())
466 model_idx_ = -1;
467 else
468 model_idx_ = int((model_idx_ - 1 + models_.size()) % models_.size());
469 if (model_idx_ != pre_idx) {
470 if (model_idx_ >= 0)
471 LOG(INFO) << "current model: " << model_idx_ << ", " << models_[model_idx_]->name();
472 }
473 } else if (e->key() == Qt::Key_Period && e->modifiers() == Qt::NoModifier) {
474 int pre_idx = model_idx_;
475 if (models_.empty())
476 model_idx_ = -1;
477 else
478 model_idx_ = int((model_idx_ + 1) % models_.size());
479 if (model_idx_ != pre_idx) {
480 if (model_idx_ >= 0)
481 LOG(INFO) << "current model: " << model_idx_ << ", " << models_[model_idx_]->name();
482 }
483 } else if (e->key() == Qt::Key_Delete && e->modifiers() == Qt::NoModifier) {
484 if (currentModel())
485 deleteModel(currentModel());
486 } else if (e->key() == Qt::Key_E && e->modifiers() == Qt::NoModifier) {
487 if (currentModel()) {
488 auto *drawable = currentModel()->renderer()->get_lines_drawable("edges");
489 if (drawable)
490 drawable->set_visible(!drawable->is_visible());
491 }
492 } else if (e->key() == Qt::Key_V && e->modifiers() == Qt::NoModifier) {
493 if (currentModel()) {
494 auto drawable = currentModel()->renderer()->get_points_drawable("vertices");
495 if (drawable)
496 drawable->set_visible(!drawable->is_visible());
497 }
498 } else if (e->key() == Qt::Key_B && e->modifiers() == Qt::NoModifier) {
499 auto mesh = dynamic_cast<SurfaceMesh *>(currentModel());
500 if (mesh) {
501 auto drawable = mesh->renderer()->get_lines_drawable("borders");
502 if (drawable)
503 drawable->set_visible(!drawable->is_visible());
504 }
505 } else if (e->key() == Qt::Key_L && e->modifiers() == Qt::NoModifier) { // locked vertices
506 auto mesh = dynamic_cast<SurfaceMesh *>(currentModel());
507 if (mesh) {
508 auto drawable = mesh->renderer()->get_points_drawable("locks");
509 if (drawable)
510 drawable->set_visible(!drawable->is_visible());
511 }
512 } else if (e->key() == Qt::Key_M && e->modifiers() == Qt::NoModifier) {
513 if (dynamic_cast<SurfaceMesh *>(currentModel())) {
514 auto drawable = currentModel()->renderer()->get_triangles_drawable("faces");
515 if (drawable) {
516 drawable->set_smooth_shading(!drawable->smooth_shading());
517 }
518 }
519 } else if (e->key() == Qt::Key_D && e->modifiers() == Qt::NoModifier) {
520 if (currentModel()) {
521 std::ostream &output = std::cout;
522
523 output << "----------- " << file_system::simple_name(currentModel()->name()) << " -----------\n";
524 if (dynamic_cast<SurfaceMesh *>(currentModel())) {
525 auto model = dynamic_cast<SurfaceMesh *>(currentModel());
526 output << "model is a surface mesh. #face: " << std::to_string(model->n_faces())
527 << ", #vertex: " + std::to_string(model->n_vertices())
528 << ", #edge: " + std::to_string(model->n_edges()) << std::endl;
529 } else if (dynamic_cast<PointCloud *>(currentModel())) {
530 auto model = dynamic_cast<PointCloud *>(currentModel());
531 output << "model is a point cloud. #vertex: " + std::to_string(model->n_vertices()) << std::endl;
532 } else if (dynamic_cast<Graph *>(currentModel())) {
533 auto model = dynamic_cast<Graph *>(currentModel());
534 output << "model is a graph. #vertex: " + std::to_string(model->n_vertices())
535 << ", #edge: " + std::to_string(model->n_edges()) << std::endl;
536 }
537
538 if (!currentModel()->renderer()->points_drawables().empty()) {
539 output << "points drawables:\n";
540 for (auto d: currentModel()->renderer()->points_drawables())
541 d->buffer_stats(output);
542 }
543 if (!currentModel()->renderer()->lines_drawables().empty()) {
544 output << "lines drawables:\n";
545 for (auto d: currentModel()->renderer()->lines_drawables())
546 d->buffer_stats(output);
547 }
548 if (!currentModel()->renderer()->triangles_drawables().empty()) {
549 output << "triangles drawables:\n";
550 for (auto d: currentModel()->renderer()->triangles_drawables())
551 d->buffer_stats(output);
552 }
553
554 currentModel()->property_stats(output);
555 }
556 } else if (e->key() == Qt::Key_R && e->modifiers() == Qt::NoModifier) {
557 // Reload the shader(s) - useful for writing/debugging shader code.
559 }
560
561 QOpenGLWidget::keyPressEvent(e);
562 update();
563 }
564
565
566 void Viewer::keyReleaseEvent(QKeyEvent *e) {
567 QOpenGLWidget::keyReleaseEvent(e);
568 update();
569 }
570
571
572 void Viewer::timerEvent(QTimerEvent *e) {
573 QOpenGLWidget::timerEvent(e);
574 update();
575 }
576
577
578 void Viewer::closeEvent(QCloseEvent *e) {
579 cleanup();
580 QOpenGLWidget::closeEvent(e);
581 }
582
583
584 std::string Viewer::usage() const {
585 return " ------------------------------------------------------------------\n"
586 " Easy3D viewer usage: \n"
587 " ------------------------------------------------------------------\n"
588 " F1: Help \n"
589 " ------------------------------------------------------------------\n"
590 " Ctrl + 'o': Open file \n"
591 " Ctrl + 's': Save file \n"
592 " Fn + Delete: Delete current model \n"
593 " '<' or '>': Switch between models \n"
594 " 's': Snapshot \n"
595 " ------------------------------------------------------------------\n"
596 " 'p': Toggle perspective/orthographic projection) \n"
597 " Left: Orbit-rotate the camera \n"
598 " Right: Move up/down/left/right \n"
599 " Middle or Wheel: Zoom in/out \n"
600 " Ctrl + '+'/'-': Zoom in/out \n"
601 " Alt + Left: Orbit-rotate the camera (screen based) \n"
602 " Alt + Right: Move up/down/left/right (screen based) \n"
603 " Left/Right Turn camera left/right \n"
604 " Ctrl + Left/Right: Move camera left/right \n"
605 " Up/Down: Move camera forward/backward \n"
606 " Ctrl + Up/Down: Move camera up/down \n"
607 " ------------------------------------------------------------------\n"
608 " 'f': Fit screen (all models) \n"
609 " 'c': Fit screen (current model only) \n"
610 " Shift + Left/Right: Zoom to target/Zoom to fit screen \n"
611 " ------------------------------------------------------------------\n"
612 " '+'/'-': Increase/Decrease point size (line width) \n"
613 " 'a': Toggle axes \n"
614 " 'b': Toggle borders \n"
615 " 'e': Toggle edges \n"
616 " 'v': Toggle vertices \n"
617 " 'm': Toggle smooth shading (for SurfaceMesh) \n"
618 " 'd': Print model info (drawables, properties) \n"
619 " ------------------------------------------------------------------\n";
620 }
621
622
623 void Viewer::addModel(Model *model) {
624 if (!model) {
625 LOG(WARNING) << "model is nullptr";
626 return;
627 }
628 for (auto m: models_) {
629 if (model == m) {
630 LOG(WARNING) << "model has already been added to the viewer.";
631 return;
632 }
633 }
634
635 if (model->empty()) {
636 LOG(WARNING) << "model does not have vertices. Only complete model can be added to the viewer.";
637 return;
638 }
639
640 makeCurrent();
641 model->set_renderer(std::make_shared<Renderer>(model));
642 doneCurrent();
643
644 int pre_idx = model_idx_;
645 models_.push_back(model);
646 model_idx_ = static_cast<int>(models_.size()) - 1; // make the last one current
647
648 if (model_idx_ != pre_idx) {
649 emit currentModelChanged();
650 if (model_idx_ >= 0)
651 LOG(INFO) << "current model: " << model_idx_ << ", " << models_[model_idx_]->name();
652 }
653 }
654
655
656 void Viewer::deleteModel(Model *model) {
657 if (!model) {
658 LOG(WARNING) << "model is nullptr";
659 return;
660 }
661
662 int pre_idx = model_idx_;
663 auto pos = std::find(models_.begin(), models_.end(), model);
664 if (pos != models_.end()) {
665 const std::string name = model->name();
666 models_.erase(pos);
667 makeCurrent(); // make sure the context is current
668 delete model; // internally the renderer and manipulator will be deleted
669 doneCurrent();
670 model_idx_ = static_cast<int>(models_.size()) - 1; // make the last one current
671
672 std::cout << "model deleted: " << name << std::endl;
673 } else
674 LOG(WARNING) << "no such model: " << model->name();
675
676 if (model_idx_ != pre_idx) {
677 emit currentModelChanged();
678 if (model_idx_ >= 0)
679 LOG(INFO) << "current model: " << model_idx_ << ", " << models_[model_idx_]->name();
680 }
681 }
682
683
684 bool Viewer::addDrawable(Drawable *drawable) {
685 if (!drawable) {
686 LOG(WARNING) << "drawable is nullptr";
687 return false;
688 }
689 for (auto d : drawables_) {
690 if (drawable == d) {
691 LOG(WARNING) << "drawable has already been added to the viewer.";
692 return false;
693 }
694 }
695
696 drawables_.push_back(drawable);
697 return true;
698 }
699
700
701 bool Viewer::deleteDrawable(Drawable *drawable) {
702 if (!drawable) {
703 LOG(WARNING) << "drawable is nullptr";
704 return false;
705 }
706
707 for (auto it = drawables_.begin(); it != drawables_.end(); ++it) {
708 if (*it == drawable) {
709 delete drawable;
710 drawables_.erase(it);
711 return true;
712 }
713 }
714
715 // if the drawable was not found
716 LOG(WARNING) << "no such drawable: " << drawable->name();
717 return false;
718 }
719
720
721 void Viewer::fitScreen(const Model *model) {
722 if (!model && models_.empty() && drawables_.empty())
723 return;
724
725 Box3 box;
726 if (model)
727 box = model->bounding_box();
728 else {
729 for (auto m: models_)
730 box.grow(m->bounding_box());
731 for (auto d : drawables_)
732 box.grow(d->bounding_box());
733 }
734 camera_->setSceneBoundingBox(box.min_point(), box.max_point());
735 camera_->showEntireScene();
736 update();
737 }
738
739
740 vec3 Viewer::pointUnderPixel(const QPoint &p, bool &found) {
741 // Liangliang: Qt6's QOpenGLWidget implementation is a black box. Using glReadPixels to read the depth buffer
742 // outside the paintGL() function has some undefined behaviors, even I made sure the context is made
743 // current and the defaultFramebufferObject() is bound. Below is what I observed:
744 // - it works well on macOS and Ubuntu, but not on Windows; it works on all three platforms if Qt5 is used;
745 // - on Windows, it works if the glReadPixels is called within the paintGL() function;
746 // - on Windows, when the function is called outside paintGL(), if saving the obtained depth buffer to an
747 // image, only some edge(s) of the image show correct depth information, and the majority part of the
748 // depth image shows black and very sparse white pixels.
749 // I guess the reason might be:
750 // - Depth buffer clearing: on some platforms (e.g., Windows), the depth buffer may be cleared or invalidated
751 // when Qt performs post rendering operations (e.g., compositing).
752 // - Driver specific implementation: Windows drivers may optimize FBO usage differently, especially if
753 // rendering results are only needed for display. This could affect the depth buffer availability outside
754 // paintGL().
755 // These platform-specific behaviors are hard to resolve without fully understanding the source code of the
756 // QOpenGLWidget class. It is bad the Qt keeps such implementation private, and it may change over the time.
757 //
758 // Solution: rendering into my own framebuffer object with only a depth attachment and then reading the depth
759 // values. This ensures we have full control over the depth buffer and avoids relying on the QOpenGLWidget's
760 // internal framebuffer.
761
762 makeCurrent();
763
764 int viewport[4];
765 glGetIntegerv(GL_VIEWPORT, viewport);
766 const int w = viewport[2];
767 const int h = viewport[3];
768
769 // Create an FBO with a depth attachment
770 QOpenGLFramebufferObjectFormat format;
771 format.setAttachment(QOpenGLFramebufferObject::Depth);
772 format.setSamples(0); // No multisampling
773 QOpenGLFramebufferObject fbo(w, h, format);
774
775 // Bind the framebuffer
776 fbo.bind();
777 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
778 std::cerr << "Framebuffer is not complete!" << std::endl;
779 return vec3(0, 0, 0);
780 }
781
782 // Configure OpenGL state for rendering
783 GLboolean last_enable_depth_test = glIsEnabled(GL_DEPTH_TEST);
784 if (!last_enable_depth_test)
785 glEnable(GL_DEPTH_TEST);
786 glClear(GL_DEPTH_BUFFER_BIT);
787
788 // Render the scene
789 draw();
790
791 // Read the depth buffer
792 // Qt (same as GLFW) uses upper corner for its origin while GL uses the lower corner.
793 int glx = p.x();
794 int gly = height() - 1 - p.y();
795 // when dealing with OpenGL, all positions are relative to the viewer port. So we have to handle highdpi displays.
796 glx = static_cast<int>(static_cast<float>(glx) * dpiScaling());
797 gly = static_cast<int>(static_cast<float>(gly) * dpiScaling());
798 float depth = std::numeric_limits<float>::max();
799 glReadPixels(glx, gly, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
800
801 // Unbind the framebuffer
802 fbo.release();
803
804 // Restore previous depth test states
805 if (!last_enable_depth_test)
806 glDisable(GL_DEPTH_TEST);
807
808 doneCurrent();
809
810 found = depth < 1.0f;
811 if (found) {
812 // The input to unprojectedCoordinatesOf() is defined in the screen coordinate system
813 return camera_->unprojectedCoordinatesOf(vec3(static_cast<float>(p.x()), static_cast<float>(p.y()), depth));
814 }
815
816 return {};
817}
818
819
820 void Viewer::paintGL() {
821 easy3d_debug_log_gl_error
822
823#if 1
824 // QOpenGLWidget renders everything into an FBO. Internally it changes
825 // QSurfaceFormat to always have samples = 0 and the OpenGL context is
826 // not a multisample context. So we have to query the render-buffer
827 // to know if it is using multisampling. At initializeGL() we were not
828 // able to query the actual samples because the internal FBO has not
829 // been created yet, so we do it here.
830 static bool queried = false;
831 if (!queried) {
832#if 1
833 glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_SAMPLES, &samples_);
834 easy3d_debug_log_frame_buffer_error
835#else // the samples can also be retrieved using glGetIntegerv()
836 glGetIntegerv(GL_SAMPLES, &samples_); easy3d_debug_log_gl_error
837#endif
838 // warn the user if the expected request was not satisfied
839 int samples = QSurfaceFormat::defaultFormat().samples();
840 int max_num = 0;
841 glGetIntegerv(GL_MAX_SAMPLES, &max_num);
842 if (samples > 0 && samples_ != samples) {
843 if (samples_ == 0)
844 LOG(WARNING) << "MSAA is not available (" << samples << " samples requested)";
845 else
846 LOG(WARNING) << "MSAA is available with " << samples_ << " samples (" << samples
847 << " requested but max support is " << max_num << ")";
848 } else VLOG(1) << "Samples received: " << samples_ << " (" << samples << " requested, max support is "
849 << max_num << ")";
850
851 queried = true;
852 }
853#endif
854
855 preDraw();
856
857 draw();
858
859 // Add visual hints: axis, camera, grid...
860 postDraw();
861 }
862
863
864 void Viewer::drawCornerAxes() {
865 ShaderProgram *program = ShaderManager::get_program("surface/surface");
866 if (!program) {
867 std::vector<ShaderProgram::Attribute> attributes;
868 attributes.emplace_back(ShaderProgram::Attribute(ShaderProgram::POSITION, "vtx_position"));
869 attributes.emplace_back(ShaderProgram::Attribute(ShaderProgram::TEXCOORD, "vtx_texcoord"));
870 attributes.emplace_back(ShaderProgram::Attribute(ShaderProgram::COLOR, "vtx_color"));
871 attributes.emplace_back(ShaderProgram::Attribute(ShaderProgram::NORMAL, "vtx_normal"));
872 program = ShaderManager::create_program_from_files("surface/surface", attributes);
873 }
874 if (!program)
875 return;
876
877 if (!drawable_axes_) {
878 const float base = 0.5f; // the cylinder length, relative to the allowed region
879 const float head = 0.2f; // the cone length, relative to the allowed region
880 std::vector<vec3> points, normals, colors;
881 shape::create_cylinder(0.03, 10, vec3(0, 0, 0), vec3(base, 0, 0), vec3(1, 0, 0), points, normals, colors);
882 shape::create_cylinder(0.03, 10, vec3(0, 0, 0), vec3(0, base, 0), vec3(0, 1, 0), points, normals, colors);
883 shape::create_cylinder(0.03, 10, vec3(0, 0, 0), vec3(0, 0, base), vec3(0, 0, 1), points, normals, colors);
884 shape::create_cone(0.06, 20, vec3(base, 0, 0), vec3(base + head, 0, 0), vec3(1, 0, 0), points, normals,
885 colors);
886 shape::create_cone(0.06, 20, vec3(0, base, 0), vec3(0, base + head, 0), vec3(0, 1, 0), points, normals,
887 colors);
888 shape::create_cone(0.06, 20, vec3(0, 0, base), vec3(0, 0, base + head), vec3(0, 0, 1), points, normals,
889 colors);
890 shape::create_sphere(vec3(0, 0, 0), 0.06, 20, 20, vec3(0, 1, 1), points, normals, colors);
891 drawable_axes_ = new TrianglesDrawable("corner_axes");
892 drawable_axes_->update_vertex_buffer(points);
893 drawable_axes_->update_normal_buffer(normals);
894 drawable_axes_->update_color_buffer(colors);
895 drawable_axes_->set_property_coloring(State::VERTEX);
896 }
897 if (!drawable_axes_->is_visible())
898 return;
899
900 // The viewport is changed to fit the lower left corner.
901 int viewport[4];
902 glGetIntegerv(GL_VIEWPORT, viewport);
903
904 static const int corner_frame_size = static_cast<int>(100 * dpiScaling());
905 glViewport(0, 0, corner_frame_size, corner_frame_size);
906
907 // To make the axis appear over other objects: reserve a tiny bit of the
908 // front depth range. NOTE: do remember to restore it later.
909 glDepthRangef(0, 0.01f);
910
911 const mat4 &proj = transform::ortho(-1, 1, -1, 1, -1, 1);
912 const mat4 &view = camera_->orientation().inverse().matrix();
913 const mat4 &MVP = proj * view;
914
915 // camera position is defined in world coordinate system.
916 const vec3 &wCamPos = camera_->position();
917 // it can also be computed as follows:
918 //const vec3& wCamPos = invMV * vec4(0, 0, 0, 1);
919 const mat4 &MV = camera_->modelViewMatrix();
920 const vec4 &wLightPos = inverse(MV) * setting::light_position;
921
922 program->bind();
923 program->set_uniform("MVP", MVP)
924 ->set_uniform("MANIP", mat4::identity())
925 ->set_uniform("NORMAL", mat3::identity()) // needs be padded when using uniform blocks
926 ->set_uniform("lighting", true)
927 ->set_uniform("two_sides_lighting", false)
928 ->set_uniform("smooth_shading", true)
929 ->set_uniform("wLightPos", wLightPos)
930 ->set_uniform("wCamPos", wCamPos)
931 ->set_uniform("ssaoEnabled", false)
932 ->set_uniform("per_vertex_color", true)
933 ->set_uniform("distinct_back_color", false)
934 ->set_block_uniform("Material", "ambient", setting::material_ambient)
935 ->set_block_uniform("Material", "specular", setting::material_specular)
936 ->set_block_uniform("Material", "shininess", &setting::material_shininess)
937 ->set_uniform("highlight", false)
938 ->set_uniform("clippingPlaneEnabled", false)
939 ->set_uniform("selected", false)
940 ->set_uniform("highlight_color", setting::highlight_color)
941 ->set_uniform("use_texture", false);
942 drawable_axes_->gl_draw();
943 program->release();
944
945 // restore the viewport
946 glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
947 glDepthRangef(0.0f, 1.0f);
948 }
949
950
951 void Viewer::preDraw() {
952 // The Qt6 documentation says (https://doc.qt.io/qt-6/qopenglwidget.html#paintGL):
953 // Default implementation performs a glClear(). Subclasses are not expected to invoke
954 // the base class implementation and should perform clearing on their own.
955 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
956 }
957
958
959 void Viewer::postDraw() {
960 // draw the Easy3D logo and GPU time
961 if (texter_ && texter_->num_fonts() >= 2) {
962 const float font_size = 15.0f;
963 const float offset = 20.0f * dpiScaling();
964 texter_->draw("Easy3D", offset, offset, font_size, 0);
965
966 // FPS computation
967 static unsigned int fps_count = 0;
968 static double fps = 0.0;
969 static const unsigned int max_count = 40;
970 static QString fps_string("fps: ??");
971 if (++fps_count == max_count) {
972 fps = 1000.0 * max_count / static_cast<double>(timer_.restart());
973 fps_string = tr("fps: %1").arg(fps, 0, 'f', ((fps < 10.0) ? 1 : 0));
974 fps_count = 0;
975 }
976 texter_->draw(fps_string.toStdString(), offset, 50.0f * dpiScaling(), 16, 1);
977 }
978
979 if (show_pivot_point_) {
980 ShaderProgram *program = ShaderManager::get_program("lines/lines_plain_color");
981 if (!program) {
982 std::vector<ShaderProgram::Attribute> attributes;
983 attributes.emplace_back(ShaderProgram::Attribute(ShaderProgram::POSITION, "vtx_position"));
984 attributes.emplace_back(ShaderProgram::Attribute(ShaderProgram::COLOR, "vtx_color"));
985 program = ShaderManager::create_program_from_files("lines/lines_plain_color", attributes);
986 }
987 if (!program)
988 return;
989
990 const float size = 10;
991 LinesDrawable drawable("pivot_point");
992 const vec3 &pivot = camera()->projectedCoordinatesOf(camera()->pivotPoint());
993 std::vector<vec3> points = {
994 vec3(pivot.x - size, pivot.y, 0.5f), vec3(pivot.x + size, pivot.y, 0.5f),
995 vec3(pivot.x, pivot.y - size, 0.5f), vec3(pivot.x, pivot.y + size, 0.5f)
996 };
997 drawable.update_vertex_buffer(points);
998
999 const mat4 &proj = transform::ortho(0.0f, static_cast<float>(width()), static_cast<float>(height()), 0.0f,
1000 0.0f,
1001 -1.0f);
1002 glDisable(GL_DEPTH_TEST); // always on top
1003 program->bind();
1004 program->set_uniform("MVP", proj);
1005 program->set_uniform("per_vertex_color", false);
1006 program->set_uniform("default_color", vec4(0.0f, 0.0f, 1.0f, 1.0f));
1007 drawable.gl_draw();
1008 program->release();
1009 glEnable(GL_DEPTH_TEST); // restore
1010 }
1011
1012 drawCornerAxes();
1013 }
1014
1015
1016 void Viewer::draw() {
1017#if 0
1018 // Example code: split the view to have different rendering styles for the same object/scene.
1019 glEnable(GL_SCISSOR_TEST);
1020 int viewport[4];
1021 glGetIntegerv(GL_VIEWPORT, viewport);
1022 glScissor(viewport[0], viewport[1], viewport[2] * 0.5f, viewport[3]);
1023 for (const auto m : models_) {
1024 for (auto d : m->renderer()->triangles_drawables())
1025 d->draw(camera());
1026 }
1027 glScissor(viewport[2] * 0.5f, viewport[1], viewport[2] * 0.5f, viewport[3]);
1028 for (const auto m : models_) {
1029 for (auto d : m->renderer()->lines_drawables())
1030 d->draw(camera());
1031 }
1032 glScissor(viewport[0], viewport[1], viewport[2], viewport[3]);
1033
1034#else
1035
1036 easy3d_debug_log_gl_error
1037
1038 for (const auto m: models_) {
1039 if (!m->renderer()->is_visible())
1040 continue;
1041
1042 // temporarily change the depth range and depth comparison method to properly render edges.
1043 glDepthRange(0.001, 1.0);
1044 for (auto d: m->renderer()->triangles_drawables()) {
1045 if (d->is_visible())
1046 d->draw(camera());
1047 easy3d_debug_log_gl_error
1048 }
1049
1050 glDepthRange(0.0, 1.0);
1051 glDepthFunc(GL_LEQUAL);
1052 for (auto d: m->renderer()->lines_drawables()) {
1053 if (d->is_visible())
1054 d->draw(camera());
1055 easy3d_debug_log_gl_error
1056 }
1057 glDepthFunc(GL_LESS);
1058
1059 for (auto d: m->renderer()->points_drawables()) {
1060 if (d->is_visible())
1061 d->draw(camera());
1062 easy3d_debug_log_gl_error
1063 }
1064 }
1065
1066 for (auto d : drawables_) {
1067 if (d->is_visible())
1068 d->draw(camera());
1069 }
1070#endif
1071 }
1072
1073 // \endcond
1074
1075}
KeyFrameInterpolator * keyframe_interpolator() const
Get the keyframe interpolator.
Definition camera.h:336
@ PERSPECTIVE
Perspective camera.
Definition camera.h:144
@ ORTHOGRAPHIC
Orthographic camera.
Definition camera.h:145
void setSceneRadius(float radius)
Set the radius of the scene.
float sceneRadius() const
Returns the radius of the scene observed by the Camera.
Definition camera.h:565
void interpolateToLookAt(const vec3 &point)
Perform keyframe interpolation to look at a point.
vec3 projectedCoordinatesOf(const vec3 &src, const Frame *frame=nullptr) const
Returns the screen projected coordinates of a 3D point defined in the frame coordinate system....
void setScreenWidthAndHeight(int width, int height)
Set the screen width and height.
void setUpVector(const vec3 &up, bool noMove=true) const
Set the up vector of the camera.
void interpolateToFitScene()
Perform keyframe interpolation to fit the scene.
void setViewDirection(const vec3 &direction) const
Set the view direction of the camera.
ManipulatedCameraFrame * frame() const
Returns the ManipulatedFrame attached to the Camera.
Definition camera.h:623
The Frame class represents a coordinate system, defined by a position and an orientation.
Definition frame.h:78
vec3 position() const
Returns the position of the Frame.
void grow(const Point &p)
Add a point to this box. This will compute its new extent.
Definition box.h:276
void start_interpolation()
Starts the interpolation process.
Definition key_frame_interpolator.cpp:213
void stop_interpolation()
Stops an interpolation started with start_interpolation().
Definition key_frame_interpolator.cpp:245
void delete_path()
Removes all keyframes from the path.
Definition key_frame_interpolator.cpp:140
bool add_keyframe(const Frame &frame)
Appends a new keyframe to the path.
Definition key_frame_interpolator.cpp:74
@ NONE
No constraint.
Definition manipulated_frame.h:165
static Mat< N, M, T > identity()
Returns an identity matrix.
Definition mat.h:697
static bool init()
Initialize OpenGL.
Definition opengl_util.cpp:49
static void terminate()
Destroy all shader programs.
Definition shader_manager.cpp:271
static ShaderProgram * create_program_from_files(const std::string &file_base_name, const std::vector< ShaderProgram::Attribute > &attributes=std::vector< ShaderProgram::Attribute >(), const std::vector< std::string > &outputs=std::vector< std::string >(), bool geom_shader=false)
Create a shader program from shader source files specified by the shader file's base name.
Definition shader_manager.cpp:49
static ShaderProgram * get_program(const std::string &shader_name)
Get the shader program if it exists and is working.
Definition shader_manager.cpp:41
static void reload()
Reload all shader programs.
Definition shader_manager.cpp:282
@ TEXCOORD
Texture coordinates.
Definition shader_program.h:82
@ NORMAL
Normal.
Definition shader_program.h:81
@ COLOR
Color.
Definition shader_program.h:80
@ POSITION
Position.
Definition shader_program.h:79
std::pair< AttribType, std::string > Attribute
Attribute: a pair of attribute type and attribute name.
Definition shader_program.h:85
@ VERTEX
Property defined on vertices.
Definition state.h:69
static SurfaceMesh * load(const std::string &file_name)
Reads a surface mesh from a file.
Definition surface_mesh_io.cpp:37
static void terminate()
Destroy all textures and release their memory.
Definition texture_manager.cpp:147
static void single_shot(int delay, std::function< void(Args...)> const &func, Args... args)
Executes function func after delay milliseconds.
Definition timer.h:202
void update() const
Update the display (i.e., repaint).
Definition viewer.cpp:630
std::string simple_name(const std::string &path)
Gets file name without path but with extension (e.g, /a/b/c.Ext => c.Ext)
void setup_gl_debug_callback()
Set up a debug callback for OpenGL.
std::string directory()
Returns the resource directory (containing color maps, shaders, textures, fonts, etc....
EASY3D_UTIL_EXPORT vec4 highlight_color
Default highlight color for highlighted/selected primitives.
EASY3D_UTIL_EXPORT float material_shininess
Default shininess of the material.
EASY3D_UTIL_EXPORT vec4 material_specular
Default specular color of the material.
EASY3D_UTIL_EXPORT vec4 light_position
Default light position defined in the camera coordinate system.
EASY3D_UTIL_EXPORT vec4 background_color
Background color of the viewer.
EASY3D_UTIL_EXPORT vec4 material_ambient
Default ambient color of the material.
void create_sphere(const vec3 &center, double radius, int slices, int stacks, const vec3 &color, std::vector< vec3 > &points, std::vector< vec3 > &normals, std::vector< vec3 > &colors)
Generates data (points, normals, and colors) for a 3D sphere.
Definition shape.cpp:767
void create_cylinder(double radius, int slices, const vec3 &s, const vec3 &t, const vec3 &color, std::vector< vec3 > &points, std::vector< vec3 > &normals, std::vector< vec3 > &colors)
Prepares data (points, normals, and colors) for a 3D cylinder defined by two 3D points s and t.
Definition shape.cpp:839
void create_cone(double radius, int slices, const vec3 &s, const vec3 &t, const vec3 &color, std::vector< vec3 > &points, std::vector< vec3 > &normals, std::vector< vec3 > &colors)
Prepares data (points, normals, and colors) for a 3D cone defined by two 3D points b and t.
Definition shape.cpp:884
mat4 ortho(float left, float right, float bottom, float top, float zNear, float zFar)
Definition transform.cpp:36
T distance(const Vec< N, T > &v1, const Vec< N, T > &v2)
Computes the distance between two vectors/points.
Definition vec.h:295
GenericBox< 3, float > Box3
A 3D axis-aligned bounding box of float type.
Definition types.h:108
Mat< N, N, T > inverse(const Mat< N, N, T > &m)
Returns the inverse of an N x N (square) matrix.
Definition mat.h:1190
Mat4< float > mat4
A 4 by 4 matrix of float type.
Definition types.h:67
int connect(SIGNAL *signal, FUNCTION const &slot)
Connects a function to the signal.
Definition signal.h:200

The header file of the window class:

1/********************************************************************
2 * Copyright (C) 2015 Liangliang Nan <liangliang.nan@gmail.com>
3 * https://3d.bk.tudelft.nl/liangliang/
4 *
5 * This file is part of Easy3D. If it is useful in your research/work,
6 * I would be grateful if you show your appreciation by citing it:
7 * ------------------------------------------------------------------
8 * Liangliang Nan.
9 * Easy3D: a lightweight, easy-to-use, and efficient C++ library
10 * for processing and rendering 3D data.
11 * Journal of Open Source Software, 6(64), 3255, 2021.
12 * ------------------------------------------------------------------
13 *
14 * Easy3D is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License Version 3
16 * as published by the Free Software Foundation.
17 *
18 * Easy3D is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ********************************************************************/
26
27#ifndef EASY3D_QT_VIEWER_WINDOW_H
28#define EASY3D_QT_VIEWER_WINDOW_H
29
30#include <string>
31#include <QMainWindow>
32
33
34namespace Ui {
35 class Window;
36}
37
38namespace easy3d {
39
40 class Model;
41 class Viewer;
42
43 class Window : public QMainWindow {
44 Q_OBJECT
45 public:
46 explicit Window(QWidget *parent = nullptr);
47 ~Window() override = default;
48
49 Viewer *viewer() { return viewer_; }
50
51 public slots:
52
53 // file
54 bool onOpen();
55 bool onSave();
56 void onOpenRecentFile();
57 void onClearRecentFiles();
58 void onCurrentModelChanged();
59
60 // view
61 void saveSnapshot();
62 void setBackgroundColor();
63
64 // topology
65 void reportTopologyStatistics();
66
67 // about
68 void onAbout();
69
70 protected:
71 void dragEnterEvent(QDragEnterEvent *) override;
72 void dropEvent(QDropEvent *) override;
73 void closeEvent(QCloseEvent *) override;
74
75 private:
76
77 // open a file (given the file name) and add the model to the viewer
78 // for visualization. It will also create the default drawables for
79 // visualizing the model.
80 easy3d::Model *open(const std::string &file_name);
81
82 private:
83 void createActions();
84 void createActionsForFileMenu();
85 void createActionsForViewMenu();
86 void createActionsForTopologyMenu();
87 bool okToContinue();
88 void readSettings();
89 void writeSettings();
90 void updateWindowTitle();
91 void setCurrentFile(const QString &fileName);
92 void updateRecentFileActions();
93 QString strippedName(const QString &fullFileName);
94
95 private:
96 Viewer *viewer_;
97
98 QStringList recentFiles_;
99 QString curDataDirectory_;
100
101 enum {
102 MaxRecentFiles = 5
103 };
104 QAction *actionsRecentFile[MaxRecentFiles],
105 *actionSeparator;
106
107 private:
108 Ui::Window *ui;
109
110 };
111
112}
113
114#endif // EASY3D_QT_VIEWER_WINDOW_H

The source file of the window class:

1/********************************************************************
2 * Copyright (C) 2015 Liangliang Nan <liangliang.nan@gmail.com>
3 * https://3d.bk.tudelft.nl/liangliang/
4 *
5 * This file is part of Easy3D. If it is useful in your research/work,
6 * I would be grateful if you show your appreciation by citing it:
7 * ------------------------------------------------------------------
8 * Liangliang Nan.
9 * Easy3D: a lightweight, easy-to-use, and efficient C++ library
10 * for processing and rendering 3D data.
11 * Journal of Open Source Software, 6(64), 3255, 2021.
12 * ------------------------------------------------------------------
13 *
14 * Easy3D is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License Version 3
16 * as published by the Free Software Foundation.
17 *
18 * Easy3D is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ********************************************************************/
26
27#include "window.h"
28
29#include <string>
30#include <iostream>
31
32#include <QFileDialog>
33#include <QDropEvent>
34#include <QMimeData>
35#include <QSettings>
36#include <QMessageBox>
37#include <QColorDialog>
38
39#include <easy3d/core/surface_mesh.h>
40#include <easy3d/core/graph.h>
41#include <easy3d/core/point_cloud.h>
42#include <easy3d/core/poly_mesh.h>
43#include <easy3d/fileio/point_cloud_io.h>
44#include <easy3d/fileio/graph_io.h>
45#include <easy3d/fileio/surface_mesh_io.h>
46#include <easy3d/fileio/poly_mesh_io.h>
47#include <easy3d/fileio/ply_reader_writer.h>
48#include <easy3d/fileio/point_cloud_io_ptx.h>
49#include <easy3d/util/file_system.h>
50#include <easy3d/util/logging.h>
51#include <easy3d/util/progress.h>
52#include <easy3d/algo/surface_mesh_components.h>
53#include <easy3d/algo/surface_mesh_topology.h>
54
55#include "viewer.h"
56
57#include <ui_window.h>
58
59
60namespace easy3d {
61
62 // \cond
63
64 Window::Window(QWidget *parent)
65 : QMainWindow(parent), ui(new Ui::Window) {
66 ui->setupUi(this);
67
68 viewer_ = new Viewer(this);
69 connect(viewer_, SIGNAL(currentModelChanged()), this, SLOT(onCurrentModelChanged()));
70 setCentralWidget(viewer_);
71
72 createActions();
73
74 setWindowIcon(QIcon(QString::fromStdString(":/resources/icons/ViewerQt.png")));
75 setFocusPolicy(Qt::StrongFocus);
76 setContextMenuPolicy(Qt::CustomContextMenu);
77 setAcceptDrops(true);
78
79 setBaseSize(1280, 960);
80// setWindowState(Qt::WindowMaximized);
81
82 readSettings();
83 updateWindowTitle();
84 }
85
86
87 void Window::dragEnterEvent(QDragEnterEvent *e) {
88 if (e->mimeData()->hasUrls())
89 e->acceptProposedAction();
90 }
91
92
93 void Window::dropEvent(QDropEvent *e) {
94 if (e->mimeData()->hasUrls())
95 e->acceptProposedAction();
96
97 int count = 0;
98 foreach (const QUrl &url, e->mimeData()->urls()) {
99 const QString &fileName = url.toLocalFile();
100 if (open(fileName.toStdString()))
101 ++count;
102 }
103
104 if (count > 0)
105 viewer_->update();
106 }
107
108
109 bool Window::onOpen() {
110 const QStringList &fileNames = QFileDialog::getOpenFileNames(
111 this,
112 "Open file(s)",
113 curDataDirectory_,
114 "Supported formats (*.ply *.obj *.off *.stl *.sm *.geojson *.trilist *.bin *.las *.laz *.xyz *.bxyz *.vg *.bvg *.ptx *.plm *.pm *.mesh)\n"
115 "Surface Mesh (*.ply *.obj *.off *.stl *.sm *.geojson *.trilist)\n"
116 "Point Cloud (*.ply *.bin *.ptx *.las *.laz *.xyz *.bxyz *.vg *.bvg *.ptx)\n"
117 "Polyhedral Mesh (*.plm *.pm *.mesh)\n"
118 "Graph (*.ply)\n"
119 "All formats (*.*)"
120 );
121
122 // Hide closed dialog
123 QApplication::processEvents();
124
125 if (fileNames.empty())
126 return false;
127
128 int count = 0;
129 ProgressLogger progress(fileNames.size(), false, false);
130 for (const auto &name: fileNames) {
131 if (progress.is_canceled()) {
132 LOG(WARNING) << "opening files cancelled";
133 break;
134 }
135 if (open(name.toStdString()))
136 ++count;
137 progress.next();
138 }
139 if (count > 0)
140 viewer_->update();
141
142 return count > 0;
143 }
144
145
146 bool Window::onSave() {
147 const Model *model = viewer_->currentModel();
148 if (!model) {
149 std::cerr << "no model exists" << std::endl;
150 return false;
151 }
152
153 std::string default_file_name = model->name();
154 if (file_system::extension(default_file_name).empty()) // no extension?
155 default_file_name += ".ply"; // default to ply
156
157 const QString &fileName = QFileDialog::getSaveFileName(
158 this,
159 "Open file(s)",
160 QString::fromStdString(default_file_name),
161 "Supported formats (*.ply *.obj *.off *.stl *.sm *.bin *.las *.laz *.xyz *.bxyz *.vg *.bvg *.plm *.pm *.mesh)\n"
162 "Surface Mesh (*.ply *.obj *.off *.stl *.sm)\n"
163 "Point Cloud (*.ply *.bin *.ptx *.las *.laz *.xyz *.bxyz *.vg *.bvg)\n"
164 "Polyhedral Mesh (*.plm *.pm *.mesh)\n"
165 "Graph (*.ply)\n"
166 "All formats (*.*)"
167 );
168
169 if (fileName.isEmpty())
170 return false;
171
172 bool saved = false;
173 if (dynamic_cast<const PointCloud *>(model)) {
174 const auto cloud = dynamic_cast<const PointCloud *>(model);
175 saved = PointCloudIO::save(fileName.toStdString(), cloud);
176 } else if (dynamic_cast<const SurfaceMesh *>(model)) {
177 const auto mesh = dynamic_cast<const SurfaceMesh *>(model);
178 saved = SurfaceMeshIO::save(fileName.toStdString(), mesh);
179 } else if (dynamic_cast<const Graph *>(model)) {
180 const auto graph = dynamic_cast<const Graph *>(model);
181 saved = GraphIO::save(fileName.toStdString(), graph);
182 }
183
184 if (saved) {
185 std::cout << "model successfully saved to: " << fileName.toStdString();
186 setCurrentFile(fileName);
187 return true;
188 }
189
190 return false;
191 }
192
193
194 Model *Window::open(const std::string &file_name) {
195 auto models = viewer_->models();
196 for (auto m: models) {
197 if (m->name() == file_name) {
198 LOG(WARNING) << "model already loaded: " << file_name;
199 return nullptr;
200 }
201 }
202
203 const std::string &ext = file_system::extension(file_name, true);
204 bool is_ply_mesh = false;
205 if (ext == "ply")
206 is_ply_mesh = (io::PlyReader::num_instances(file_name, "face") > 0);
207
208 Model *model = nullptr;
209 if ((ext == "ply" && is_ply_mesh) || ext == "obj" || ext == "off" || ext == "stl" || ext == "sm" ||
210 ext == "plg") { // mesh
211 model = SurfaceMeshIO::load(file_name);
212 } else if (ext == "ply" && io::PlyReader::num_instances(file_name, "edge") > 0) {
213 model = GraphIO::load(file_name);
214 } else if (ext == "plm" || ext == "pm" || ext == "mesh") {
215 model = PolyMeshIO::load(file_name);
216 } else { // point cloud
217 if (ext == "ptx") {
218 io::PointCloudIO_ptx serializer(file_name);
219 while (auto cloud = serializer.load_next())
220 viewer_->addModel(cloud);
221 } else
222 model = PointCloudIO::load(file_name);
223 }
224
225 if (model) {
226 model->set_name(file_name);
227 viewer_->addModel(model);
228 setCurrentFile(QString::fromStdString(file_name));
229 }
230
231 return model;
232 }
233
234
235 void Window::onCurrentModelChanged() {
236 const Model *m = viewer_->currentModel();
237 if (m) {
238 viewer_->fitScreen(m);
239
240 const std::string &name = m->name();
241 setCurrentFile(QString::fromStdString(name));
242 } else
243 updateWindowTitle();
244 }
245
246
247 void Window::setCurrentFile(const QString &fileName) {
248 QString dir = fileName.left(fileName.lastIndexOf("/"));
249 if (!dir.isEmpty() && file_system::is_directory(dir.toStdString()))
250 curDataDirectory_ = dir;
251
252 setWindowModified(false);
253
254 if (!fileName.isEmpty()) {
255 recentFiles_.removeAll(fileName);
256 recentFiles_.prepend(fileName);
257 updateRecentFileActions();
258 }
259
260 updateWindowTitle();
261 }
262
263
264 void Window::onOpenRecentFile() {
265 if (okToContinue()) {
266 auto action = qobject_cast<QAction *>(sender());
267 if (action) {
268 const QString filename(action->data().toString());
269 if (open(filename.toStdString()))
270 viewer_->update();
271 }
272 }
273 }
274
275
276 void Window::onClearRecentFiles() {
277 recentFiles_.clear();
278 updateRecentFileActions();
279 }
280
281
282 void Window::saveSnapshot() {
283 const Model *model = viewer_->currentModel();
284
285 const bool overwrite = false;
286 std::string default_file_name("untitled.png");
287 if (model)
288 default_file_name = file_system::replace_extension(model->name(), "png");
289 const QString fileName = QFileDialog::getSaveFileName(
290 this,
291 "Please choose a file name",
292 QString::fromStdString(default_file_name),
293 "Image Files (*.png *.jpg *.bmp *.ppm)\n"
294 "PNG (*.png)\n"
295 "JPG (*.jpg)\n"
296 "Windows Bitmap (*.bmp)\n"
297 "24bit RGB Bitmap (*.ppm)\n"
298 "All Files (*.*)",
299 nullptr,
300 overwrite ? QFileDialog::DontConfirmOverwrite : QFileDialog::Option()
301 );
302 // Hide closed dialog
303 QApplication::processEvents();
304
305 if (fileName.isEmpty())
306 return;
307
308 viewer_->saveSnapshot(fileName);
309 }
310
311
312 void Window::setBackgroundColor() {
313 const vec4 &c = viewer_->backGroundColor();
314 QColor orig(static_cast<int>(c.r * 255), static_cast<int>(c.g * 255), static_cast<int>(c.b * 255),
315 static_cast<int>(c.a * 255));
316 const QColor &color = QColorDialog::getColor(orig, this);
317 if (color.isValid()) {
318 const vec4 newColor(static_cast<float>(color.redF()), static_cast<float>(color.greenF()), static_cast<float>(color.blueF()), static_cast<float>(color.alphaF()));
319 viewer_->setBackgroundColor(newColor);
320 viewer_->update();
321 }
322 }
323
324
325 bool Window::okToContinue() {
326 if (isWindowModified()) {
327 QMessageBox::StandardButton r = QMessageBox::warning(
328 this,
329 tr("Viewer"),
330 tr("The model has been modified.\n"
331 "Do you want to save your changes?"),
332 QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
333 QMessageBox::Yes
334 );
335 if (r == QMessageBox::Yes)
336 return onSave();
337 else if (r == QMessageBox::Cancel)
338 return false;
339 }
340 return true;
341 }
342
343
344 void Window::onAbout() {
345 const std::string name = "<h3>" + std::string(EXAMPLE_TITLE) + "</h3>";
346 QString title = QMessageBox::tr(name.c_str());
347
348 QString text = QMessageBox::tr(
349 "<p>This viewer shows how to use Qt for GUI creation and event handling</p>"
350 "<p>Liangliang Nan<br>"
351 "<a href=\"mailto:liangliang.nan@gmail.com\">liangliang.nan@gmail.com</a><br>"
352 "<a href=\"https://3d.bk.tudelft.nl/liangliang/\">https://3d.bk.tudelft.nl/liangliang/</a></p>"
353 );
354
355 //QMessageBox::about(this, title, text);
356 QMessageBox::about(this, "About Viewer", title + text);
357 }
358
359
360 void Window::readSettings() {
361 QSettings settings("liangliang.nan@gmail.com", "Viewer");
362 recentFiles_ = settings.value("recentFiles").toStringList();
363 updateRecentFileActions();
364 curDataDirectory_ = settings.value("currentDirectory").toString();
365 }
366
367
368 void Window::writeSettings() {
369 QSettings settings("liangliang.nan@gmail.com", "Viewer");
370 settings.setValue("recentFiles", recentFiles_);
371 if (!curDataDirectory_.isEmpty() && file_system::is_directory(curDataDirectory_.toStdString()))
372 settings.setValue("currentDirectory", curDataDirectory_);
373 }
374
375
376 void Window::updateWindowTitle() {
377 Model *model = viewer_->currentModel();
378
379 QString title = QString::fromStdString(EXAMPLE_TITLE);
380#ifndef NDEBUG
381 title += " (Debug Version)";
382#endif // NDEBUG
383
384 QString fileName("Untitled");
385 if (model)
386 fileName = QString::fromStdString(model->name());
387
388 title = tr("%1[*] - %2").arg(strippedName(fileName), title);
389 setWindowTitle(title);
390 }
391
392
393 void Window::closeEvent(QCloseEvent *event) {
394 if (okToContinue()) {
395 writeSettings();
396 event->accept();
397 } else {
398 event->ignore();
399 }
400 }
401
402
403 void Window::updateRecentFileActions() {
404 QMutableStringListIterator i(recentFiles_);
405 while (i.hasNext()) {
406 if (!QFile::exists(i.next()))
407 i.remove();
408 }
409
410 for (int j = 0; j < MaxRecentFiles; ++j) {
411 if (j < recentFiles_.count()) {
412 QString text = tr("&%1 %2").arg(j + 1).arg(strippedName(recentFiles_[j]));
413 actionsRecentFile[j]->setText(text);
414 actionsRecentFile[j]->setData(recentFiles_[j]);
415 actionsRecentFile[j]->setVisible(true);
416 } else {
417 actionsRecentFile[j]->setVisible(false);
418 }
419 }
420
421 actionSeparator->setVisible(!recentFiles_.isEmpty());
422 }
423
424
425 QString Window::strippedName(const QString &fullFileName) {
426 return QFileInfo(fullFileName).fileName();
427 }
428
429
430 void Window::createActions() {
431 // file menu
432 createActionsForFileMenu();
433
434 // view menu
435 createActionsForViewMenu();
436
437 // topology menu
438 createActionsForTopologyMenu();
439
440 // about menu
441 connect(ui->actionAbout, SIGNAL(triggered()), this, SLOT(onAbout()));
442 }
443
444
445 void Window::createActionsForFileMenu() {
446 connect(ui->actionOpen, SIGNAL(triggered()), this, SLOT(onOpen()));
447 connect(ui->actionSave, SIGNAL(triggered()), this, SLOT(onSave()));
448
449 actionSeparator = ui->menuFile->addSeparator();
450
451 QList<QAction*> actions;
452 for (auto& action : actionsRecentFile) {
453 action = new QAction(this);
454 action->setVisible(false);
455 connect(action, SIGNAL(triggered()), this, SLOT(onOpenRecentFile()));
456 actions.push_back(action);
457 }
458 ui->menuRecentFiles->insertActions(ui->actionClearRecentFiles, actions);
459 ui->menuRecentFiles->insertSeparator(ui->actionClearRecentFiles);
460 connect(ui->actionClearRecentFiles, SIGNAL(triggered()), this, SLOT(onClearRecentFiles()));
461
462 connect(ui->actionExit, SIGNAL(triggered()), this, SLOT(close()));
463 ui->actionExit->setShortcut(QString("Ctrl+Q"));
464 }
465
466
467 void Window::createActionsForViewMenu() {
468 connect(ui->actionSnapshot, SIGNAL(triggered()), this, SLOT(saveSnapshot()));
469
470 ui->menuView->addSeparator();
471
472 connect(ui->actionSetBackgroundColor, SIGNAL(triggered()), this, SLOT(setBackgroundColor()));
473 }
474
475
476 void Window::createActionsForTopologyMenu() {
477 connect(ui->actionTopologyStatistics, SIGNAL(triggered()), this, SLOT(reportTopologyStatistics()));
478 }
479
480
481 void Window::reportTopologyStatistics() {
482 auto mesh = dynamic_cast<SurfaceMesh *>(viewer()->currentModel());
483 if (!mesh)
484 return;
485
486 const std::string simple_name = file_system::simple_name(mesh->name());
487 if (simple_name.empty())
488 std::cout << "#elements in model (with unknown name): ";
489 else
490 std::cout << "#elements in model '" << file_system::simple_name(mesh->name()) << "': ";
491
492 std::cout << "#face = " << mesh->n_faces() << ", #vertex = " << mesh->n_vertices() << ", #edge = "
493 << mesh->n_edges() << std::endl;
494
495 // count isolated vertices
496 std::size_t count = 0;
497 for (auto v: mesh->vertices()) {
498 if (mesh->is_isolated(v))
499 ++count;
500 }
501 if (count > 0)
502 std::cout << "#isolated vertices: " << count << std::endl;
503
504 const auto &components = SurfaceMeshComponent::extract(mesh);
505 std::cout << "#connected component: " << components.size() << std::endl;
506
507 const std::size_t num = 10;
508 if (components.size() > num)
509 std::cout << "\ttopology of the first " << num << " components:" << std::endl;
510
511 for (std::size_t i = 0; i < std::min(components.size(), num); ++i) {
512 const SurfaceMeshComponent &comp = components[i];
513 SurfaceMeshTopology topo(&comp);
514 std::string type = "unknown";
515 if (topo.is_sphere())
516 type = "sphere";
517 else if (topo.is_disc())
518 type = "disc";
519 else if (topo.is_cylinder())
520 type = "cylinder";
521 else if (topo.is_torus())
522 type = "torus";
523 else if (topo.is_closed())
524 type = "unknown closed";
525
526 std::cout << "\t\t" << i << ": "
527 << type
528 << ", #face = " << comp.n_faces() << ", #vertex = " << comp.n_vertices() << ", #edge = "
529 << comp.n_edges()
530 << ", #border = " << topo.number_of_borders();
531 if (topo.number_of_borders() == 1)
532 std::cout << ", border size = " << topo.largest_border_size();
533 else if (topo.number_of_borders() > 1)
534 std::cout << ", largest border size = " << topo.largest_border_size();
535 std::cout << std::endl;
536 }
537 }
538
539 // \endcond
540
541}
static Graph * load(const std::string &file_name)
Reads a graph from file file_name.
Definition graph_io.cpp:37
static bool save(const std::string &file_name, const Graph *graph)
Saves graph to file file_name.
Definition graph_io.cpp:72
static bool save(const std::string &file_name, const PointCloud *cloud)
Saves a point_cloud to a file.
Definition point_cloud_io.cpp:83
static PointCloud * load(const std::string &file_name)
Reads a point cloud from file file_name.
Definition point_cloud_io.cpp:37
static PolyMesh * load(const std::string &file_name)
Reads a polyhedral mesh from a file.
Definition poly_mesh_io.cpp:37
static std::vector< SurfaceMeshComponent > extract(SurfaceMesh *mesh, bool descending=true)
Extracts connected components from the given mesh.
Definition surface_mesh_components.cpp:187
static bool save(const std::string &file_name, const SurfaceMesh *mesh)
Saves a surface mesh to a file.
Definition surface_mesh_io.cpp:84
static std::size_t num_instances(const std::string &file_name, const std::string &element_name)
A quick check of the number of instances of a type of element.
std::string replace_extension(std::string const &path, const std::string &ext)
Replaces the extension of the given file with 'ext'. If the file name does not have an extension,...
std::string extension(const std::string &path, bool lower=true)
Query the file extension without dot (e.g., /a/b/c.Ext => Ext).
bool is_directory(const std::string &path)
Tests if 'path' is an existing directory.
1/********************************************************************
2 * Copyright (C) 2015 Liangliang Nan <liangliang.nan@gmail.com>
3 * https://3d.bk.tudelft.nl/liangliang/
4 *
5 * This file is part of Easy3D. If it is useful in your research/work,
6 * I would be grateful if you show your appreciation by citing it:
7 * ------------------------------------------------------------------
8 * Liangliang Nan.
9 * Easy3D: a lightweight, easy-to-use, and efficient C++ library
10 * for processing and rendering 3D data.
11 * Journal of Open Source Software, 6(64), 3255, 2021.
12 * ------------------------------------------------------------------
13 *
14 * Easy3D is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License Version 3
16 * as published by the Free Software Foundation.
17 *
18 * Easy3D is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ********************************************************************/
26
27#include <QDir>
28#include <QTime>
29#include <QApplication>
30#include <QSurfaceFormat>
31#include <QStyleFactory>
32
33#include <easy3d/util/initializer.h>
34
35#include "window.h"
36
50
51using namespace easy3d;
52
53int main(int argc, char *argv[]) {
54 // Note: Calling QSurfaceFormat::setDefaultFormat() before constructing the
55 // QApplication instance is mandatory on some platforms (e.g., macOS)
56 // when an OpenGL core profile context is requested. This is to ensure
57 // that resource sharing between contexts stays functional as all internal
58 // contexts are created using the correct version and profile.
59 QSurfaceFormat format = QSurfaceFormat::defaultFormat();
60 format.setVersion(4, 3);
61 format.setProfile(QSurfaceFormat::CoreProfile);
62 format.setDepthBufferSize(24);
63 format.setStencilBufferSize(8);
64 format.setSamples(4);
65 QSurfaceFormat::setDefaultFormat(format);
66
67 // Commented to let Qt choose the most suitable OpenGL implementation
68 QApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
69
70 QApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
71#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) && (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)))
72 QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
73 QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
74#endif
75
76 QApplication app(argc, argv);
77#ifdef _WIN32 // to have similar style as on macOS.
78 app.setStyle(QStyleFactory::create("Fusion"));
79#endif
80
81 QDir workingDir = QCoreApplication::applicationDirPath();
82#ifdef __APPLE__
83 // This makes sure that our "working directory" is not within the application bundle
84 if (workingDir.dirName() == "MacOS") {
85 workingDir.cdUp();
86 workingDir.cdUp();
87 workingDir.cdUp();
88 }
89#endif
90 QDir::setCurrent(workingDir.absolutePath());
91
92 // initialize Easy3D.
93 initialize();
94
95 Window win;
96 win.show();
97 return app.exec();
98}
void initialize(bool info_to_stdout, bool use_log_file, bool use_setting_file, const std::string &resource_dir)
Initialization of Easy3D.
Definition initializer.cpp:39