
Pendahuluan
Di satu sisi, mesin OpenSceneGraph sendiri memiliki subsistem canggih untuk mengelola windows, memproses peristiwa input pengguna, mengirim dan menerima pesan pengguna. Kami membicarakan hal ini secara mendetail dalam artikel-artikel sebelumnya dari seri ini. Secara umum, bersama dengan kemampuan C ++ / STL, ini cukup untuk mengembangkan aplikasi yang kompleks dan semena-mena.
Contoh mengintegrasikan OSG ke dalam aplikasi yang dikembangkan oleh QtDesigner. Contoh ini akan dibahas secara rinci di bawah ini.Di sisi lain, untuk mempercepat pengembangan di C ++, kedua pustaka pihak ketiga digunakan yang memperluas kemampuan bahasa ini (seperti boost), dan seluruh kerangka kerja memungkinkan Anda untuk dengan mudah dan alami mengembangkan aplikasi lintas platform dari tujuan fungsional yang luas. Salah satu kerangka kerja tersebut adalah Qt yang sangat populer. Tidak peduli bagaimana mereka memarahi Qt untuk kompiler meta-objek dan kekurangan dan ketidaknyamanan lainnya, kekuatan Qt ada di perpustakaan kelas yang luas yang menyelesaikan semua tugas yang mungkin terjadi dalam pengembangan lintas-platform, serta dalam konsep "sinyal - slot" yang mengimplementasikan subsistem pesan antar kelas. Sinyal dan slot juga didasarkan pada metode interaksi antara aplikasi dan sistem operasi, serta komunikasi antarproses.
Dan sungguh, akan sangat menarik untuk menggabungkan dua teknologi: Qt dan OSG. Tim saya harus menyelesaikan masalah serupa, yang sudah saya tulis di
salah satu publikasi saya . Namun, saya ingin membuka pertanyaan ini sedikit lebih luas, dan artikel ini akan membahas topik ini.
Ada dua opsi untuk mengintegrasikan OSG dan Qt:
- Menggunakan Qt Sinyal dan Slot untuk Berinteraksi Objek dalam Aplikasi OSG
- Integrasi penampil OSG ke antarmuka grafis yang dikembangkan dalam C ++ / Qt, termasuk penggunaan perancang formulir QtDesigner
Opsi pertama berlaku ketika Anda tidak perlu menggunakan elemen GUI yang disediakan oleh Qt, tetapi Anda ingin memastikan interaksi komponen aplikasi melalui sinyal dan slot. Misalnya, kebutuhan seperti itu muncul bagi saya untuk mengintegrasikan aplikasi OSG dengan perpustakaan komunikasi antarproses melalui soket TCP menggunakan Qt.
Opsi kedua diperlukan ketika integrasi mesin OSG dan aplikasi grafis yang dikembangkan menggunakan Qt diperlukan. Sinyal dan slot tersedia untuk kita, dan selain itu, seluruh jajaran elemen GUI standar yang disediakan oleh Qt.
1. Sinyal Qt dalam Sistem Jendela OSG
Contoh pertama akan agak sintetik: kita menulis aplikasi OSG sederhana dengan adegan primitif; buat dua kelas, yang satu akan menangani penekanan tombol, dan yang lainnya untuk menampilkan pesan ke konsol tentang tombol mana yang ditekan. Dalam hal ini, pawang akan menghasilkan sinyal Qt, dengan pesan tentang tombol yang ditekan sebagai parameter.
Untuk berintegrasi dengan Qt, cukup untuk memenuhi tiga kondisi berikut
- Mewarisi kelas yang berinteraksi dari QObject
- Atur loop pemrosesan sinyal
- Buat turunan dari kelas QApplication (atau QCoreApplication) yang ada di memori selama operasi aplikasi
Kode contoh lengkap dapat dilihat di
sini di
repositori OSG-lesson saya , yang berisi semua pelajaran untuk siklus ini.
Pertama, buat kelas yang akan "memutar" antrian pemrosesan sinyal.
qt-events.h#ifndef QT_EVENTS_H #define QT_EVENTS_H #include <osgGA/GUIEventHandler> #include <QCoreApplication> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class QtEventsHandler : public osgGA::GUIEventHandler { public: QtEventsHandler(); virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: }; #endif // QT_EVENTS_H
qt-events.cpp #include "qt-events.h"
Semuanya sangat sederhana - kelas ini adalah pengendali event OSG standar yang, ketika merender bingkai, memulai pemrosesan antrian sinyal Qt dengan memanggil
QCoreApplication::processEvents(QEventLoop::AllEvents);
Sekarang mari kita buat kelas yang memproses keyboard, sekali lagi, menggunakan mekanisme yang dibangun ke dalam OSG, tetapi pada saat yang sama dapat mengirim sinyal Qt. Untuk melakukan ini, kita akan menggunakan multiple inheritance yang baru-baru ini dianalisa
keyhandler.h #ifndef KEY_HANDLER_H #define KEY_HANDLER_H #include <osgGA/GUIEventHandler> #include <QObject> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class KeyboardHandler : public QObject, public osgGA::GUIEventHandler { Q_OBJECT public: KeyboardHandler(QObject *parent = Q_NULLPTR) : QObject(parent) , osgGA::GUIEventHandler () { } bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { switch (ea.getEventType()) { case osgGA::GUIEventAdapter::KEYDOWN: emit sendMessage("Pressed key " + QString(ea.getKey())); break; default: break; } return false; } signals: void sendMessage(QString msg); private: }; #endif // KEY_HANDLER_H
Kelas akan memproses pesan tentang tombol yang ditekan dan mengirim sinyal dengan pesan yang berisi kode tombol yang ditekan. Sinyal ini akan diterima oleh kelas yang tidak terhubung dengan OSG, yang merupakan turunan dari QObject dan berisi satu slot tunggal yang mencetak pesan ke output standar
penerima. h #ifndef RECEIVER_H #define RECEIVER_H #include <QObject> #include <iostream> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class Receiver : public QObject { Q_OBJECT public: Receiver(QObject *parent = Q_NULLPTR) : QObject(parent) {} public slots: void printMessage(QString msg) { std::cout << msg.toStdString() << std::endl; } }; #endif // RECEIVER_H
Sekarang kumpulkan semuanya dengan menulis aplikasi OSG
main.h #ifndef MAIN_H #define MAIN_H #include <osgViewer/Viewer> #include <osgDB/ReadFile> #include <QCoreApplication> #include "qt-events.h" #include "keyhandler.h" #include "receiver.h" #endif
main.cpp #include "main.h"
Pertama, kami membuat turunan dari kelas QCoreApplication.
QCoreApplication app(argc, argv);
Ini diperlukan agar teknologi yang dijelaskan itu berfungsi. Namun, kami
tidak akan memanggil metode QCoreApplication :: exec () ! Sebagai gantinya, loop pemrosesan sinyal akan berputar di dalam loop osgViewer :: Viewer :: run () kami, tempat kami membuat dan mendaftarkan handler yang sesuai
viewer.addEventHandler(new QtEventsHandler);
Kami membuat instance kelas yang akan berinteraksi melalui sinyal Qt, menghubungkan sinyal satu dengan slot lainnya
KeyboardHandler *keyboardHandler = new KeyboardHandler; Receiver *receiver = new Receiver; QObject::connect(keyboardHandler, &KeyboardHandler::sendMessage, receiver, &Receiver::printMessage);
Daftarkan pengendali keyboard
viewer.addEventHandler(keyboardHandler);
Semua menjalankan penampil
return viewer.run();
dan melihat gambar seperti itu
Ya, contohnya agak dibuat-buat, tetapi itu menggambarkan prinsip-prinsip utama mengintegrasikan kode menggunakan mekanisme Qt ke dalam aplikasi menggunakan OSG. Gagasan ini, diperoleh dari
OpenSceneGraph 3. Cookbook , menghemat banyak waktu dan keberanian bagi saya dan tim pengembangan saya, memungkinkan saya untuk menggunakan modul berbasis Qt yang didebug dan dibakukan dalam basis kode kami di basis kode kami.
Tetapi bagaimana jika kita masih ingin menggunakan OSG di dalam aplikasi Qt GUI?
2. pustaka osgQt
osgQt adalah perpustakaan integrasi yang dirancang untuk:
- Menanamkan adegan 3D diimplementasikan pada OSG dalam antarmuka grafis dari aplikasi yang dikembangkan di Qt
- Menyematkan widget Qt pada permukaan geometri 3D di dalam adegan OSG. Ya, Anda dengar - Widget Qt dapat bekerja dengan tenang di dalam dunia virtual. Suatu hari saya pasti akan menunjukkannya
Ada beberapa masalah dengan perpustakaan ini, yang kami atasi dengan mempelajari contoh-contoh terlampir dan membaca
OpenSceneGraph 3. Cookbook yang telah disebutkan sebelumnya
.Perpustakaan harus dikumpulkan, dan proses ini mirip dengan perakitan mesin itu sendiri, dijelaskan secara rinci dalam
artikel pertama siklus . Satu-satunya komentar adalah bahwa -DCMAKE_INSTALL_PREFIX harus dipilih dengan yang sama yang ditentukan saat membangun mesin - jadi osgQt akan dipasang di sebelah mesin, dan akan nyaman digunakan selama pengembangan.
3. Integrasi osgViewer :: Viewer ke dalam GUI Qt
Contoh berikut akan sangat berguna. Kami akan menulis penampil yang memungkinkan Anda memuat model format * .osg menggunakan kontrol Qt standar. Selain itu, untuk mengembangkan antarmuka grafis, kami menggunakan QtDeisgner.
Mari kita buat proyek baru seperti "Aplikasi Qt Widgets"

Dalam hal ini, jendela aplikasi utama akan dihasilkan dengan bilah menu, bilah alat dan bilah status. Di QtDesigner, tambahkan komponen QFrame ke jendela ini.

Kami akan menempatkan penampil OSG di bingkai ini. Penampil OSG pada dasarnya akan menjadi widget Qt, untuk implementasinya kita akan menulis kelas QViewerWidget. Saya akan meletakkan sumber lengkap di spoiler, agar tidak mengaburkan presentasi dengan lembar kode
qviewerwidget.h #ifndef QVIEWER_WIDGET_H #define QVIEWER_WIDGET_H #include <QWidget> #include <osgViewer/Viewer> #include <osgQt/GraphicsWindowQt> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class QViewerWidget : public QWidget { public: QViewerWidget(const QRect &geometry); virtual ~QViewerWidget(); osg::Group *getScene(); osgViewer::Viewer *getViewer(); protected: osg::ref_ptr<osg::Group> scene; osgViewer::Viewer viewer; private: osgQt::GraphicsWindowQt *createGraphicsWindow(const QRect &geometry); void initCamera(const QRect &geometry); void paintEvent(QPaintEvent *); }; #endif // QVIEWER_WIDGET_H
qviewerwidget.cpp include "qviewerwidget.h" #include <osgViewer/ViewerEventHandlers> #include <osgGA/TrackballManipulator> #include <QGridLayout> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ QViewerWidget::QViewerWidget(const QRect &geometry) : QWidget() , scene(new osg::Group) { initCamera(geometry); viewer.setSceneData(scene); viewer.addEventHandler(new osgViewer::StatsHandler); viewer.setCameraManipulator(new osgGA::TrackballManipulator); viewer.setThreadingModel(osgViewer::Viewer::SingleThreaded); osgQt::GraphicsWindowQt *gw = static_cast<osgQt::GraphicsWindowQt *>(viewer.getCamera()->getGraphicsContext()); QGridLayout *layout = new QGridLayout; if (layout != Q_NULLPTR) { layout->addWidget(gw->getGLWidget()); this->setLayout(layout); } } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ QViewerWidget::~QViewerWidget() { } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osg::Group *QViewerWidget::getScene() { return scene.get(); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osgViewer::Viewer *QViewerWidget::getViewer() { return &viewer; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osgQt::GraphicsWindowQt *QViewerWidget::createGraphicsWindow(const QRect &geometry) { osg::DisplaySettings *ds = osg::DisplaySettings::instance().get(); osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits; traits->windowName = ""; traits->windowDecoration = false; traits->x = geometry.x(); traits->y = geometry.y(); traits->width = geometry.width(); traits->height = geometry.height(); if (traits->height == 0) traits->height = 1; traits->doubleBuffer = true; traits->alpha = ds->getMinimumNumAlphaBits(); traits->stencil = ds->getMinimumNumStencilBits(); traits->sampleBuffers = ds->getMultiSamples(); traits->samples = ds->getNumMultiSamples(); return new osgQt::GraphicsWindowQt(traits.get()); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void QViewerWidget::initCamera(const QRect &geometry) { osg::Camera *camera = viewer.getCamera(); osg::ref_ptr<osgQt::GraphicsWindowQt> gw = createGraphicsWindow(geometry); gw->setTouchEventsEnabled(true); camera->setGraphicsContext(gw.get()); const osg::GraphicsContext::Traits *traits = gw->getTraits(); camera->setClearColor(osg::Vec4(0.7f, 0.7f, 0.7f, 1.0f)); camera->setViewport(0, 0, traits->width, traits->height); double aspect = static_cast<double>(traits->width) / static_cast<double>(traits->height); camera->setProjectionMatrixAsPerspective(30.0, aspect, 1.0, 1000.0); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void QViewerWidget::paintEvent(QPaintEvent *) { viewer.frame(); }
Gagasan utama penerapannya adalah menggunakan kelas osgQt :: GraphicsWindow, yang menciptakan jendela grafis berdasarkan kelas QGLWidget. Untuk membuat jendela ini, gunakan metode ini
osgQt::GraphicsWindowQt *QViewerWidget::createGraphicsWindow(const QRect &geometry) { osg::DisplaySettings *ds = osg::DisplaySettings::instance().get(); osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits; traits->windowName = ""; traits->windowDecoration = false; traits->x = geometry.x(); traits->y = geometry.y(); traits->width = geometry.width(); traits->height = geometry.height(); if (traits->height == 0) traits->height = 1; traits->doubleBuffer = true; traits->alpha = ds->getMinimumNumAlphaBits(); traits->stencil = ds->getMinimumNumStencilBits(); traits->sampleBuffers = ds->getMultiSamples(); traits->samples = ds->getNumMultiSamples(); return new osgQt::GraphicsWindowQt(traits.get()); }
Jendela dikonfigurasikan sesuai dengan parameter geometri yang dikirim ke input dan pengaturan yang diperlukan untuk rendering 3D OSG. Pointer yang dikembalikan adalah konteks grafis OSG yang harus diteruskan ke kamera. Karena itu, langkah selanjutnya adalah menginisialisasi kamera
void QViewerWidget::initCamera(const QRect &geometry) { osg::Camera *camera = viewer.getCamera(); osg::ref_ptr<osgQt::GraphicsWindowQt> gw = createGraphicsWindow(geometry); gw->setTouchEventsEnabled(true); camera->setGraphicsContext(gw.get()); const osg::GraphicsContext::Traits *traits = gw->getTraits(); camera->setClearColor(osg::Vec4(0.7f, 0.7f, 0.7f, 1.0f)); camera->setViewport(0, 0, traits->width, traits->height); double aspect = static_cast<double>(traits->width) / static_cast<double>(traits->height); camera->setProjectionMatrixAsPerspective(30.0, aspect, 1.0, 1000.0); }
Panggilan yang sebenarnya
camera->setGraphicsContext(gw.get());
dan meneruskan konteks yang diinginkan terkait dengan widget QGLWidget ke kamera. Kami menempatkan seluruh widget rutin di konstruktor kelas
QViewerWidget::QViewerWidget(const QRect &geometry) : QWidget() , scene(new osg::Group) { initCamera(geometry); viewer.setSceneData(scene); viewer.addEventHandler(new osgViewer::StatsHandler); viewer.setCameraManipulator(new osgGA::TrackballManipulator); viewer.setThreadingModel(osgViewer::Viewer::SingleThreaded); osgQt::GraphicsWindowQt *gw = static_cast<osgQt::GraphicsWindowQt *>(viewer.getCamera()->getGraphicsContext()); QGridLayout *layout = new QGridLayout; if (layout != Q_NULLPTR) { layout->addWidget(gw->getGLWidget()); this->setLayout(layout); } }
Di sini kita mengonfigurasi pemirsa dan memberi perhatian khusus pada tantangan
viewer.setThreadingModel(osgViewer::Viewer::SingleThreaded);
Mengalihkan penampil ke mode single-threaded. Ini adalah langkah yang perlu ketika mengintegrasikan OSG ke Qt, karena pada beberapa distribusi Linux program akan macet ketika menggunakan rendering multi-threaded yang digunakan oleh OSG secara default. Alasan untuk ini memerlukan pembekalan terpisah, jadi lanjutkan dan perhatikan kode ini
osgQt::GraphicsWindowQt *gw = static_cast<osgQt::GraphicsWindowQt *>(viewer.getCamera()->getGraphicsContext()); QGridLayout *layout = new QGridLayout; if (layout != Q_NULLPTR) { layout->addWidget(gw->getGLWidget()); this->setLayout(layout); }
di mana kami membuat layer, mengganggu QGLWidget yang dikembalikan dari konteks grafik kamera, dikonversi ke pointer osgQt :: GraphicsWindows. Kami menambahkan layer yang dibuat ke widget QViewerWidget kami dengan menelepon
this->setLayout(layout);
Agar widget kami, dan dengan itu, adegan harus diperbarui ketika jendela diperbarui, Anda perlu mendefinisikan kembali pengendali event QPaintEvent
void QViewerWidget::paintEvent(QPaintEvent *) { viewer.frame(); }
di mana kita memulai rendering frame dengan memanggil metode osgViewer :: Viewer :: frame ().
Oke, kode widget kami sudah siap, sekarang kami menyematkannya dalam bingkai yang terletak di formulir. Untuk melakukan ini, dalam konstruktor kelas MainWindow kita menulis kode seperti itu
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , qviewer(Q_NULLPTR) { ui->setupUi(this); QGridLayout *layout = new QGridLayout; qviewer = new QViewerWidget(QRect(0, 0, ui->frame->width(), ui->frame->height())); layout->addWidget(qviewer); ui->frame->setLayout(layout); this->setCentralWidget(ui->frame); connect(&timer, &QTimer::timeout, this, &MainWindow::update); timer.start(40); connect(ui->actionQuit, &QAction::triggered, this, &MainWindow::quit); connect(ui->actionClean, &QAction::triggered, this, &MainWindow::clean); connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::open); this->setWindowTitle("QViewerWidget example"); }
atau lebih tepatnya, kami masih tertarik pada bagian ini
QGridLayout *layout = new QGridLayout; qviewer = new QViewerWidget(QRect(0, 0, ui->frame->width(), ui->frame->height())); layout->addWidget(qviewer); ui->frame->setLayout(layout); this->setCentralWidget(ui->frame);
tempat kami membuat layer, buat widget kami, dengan dimensi yang sama dengan ukuran frame, tambahkan widget yang dibuat ke layer, dan lampirkan layer ke frame. Dan, agar tidak repot dengan tata letak dalam contoh ini, kami meregangkan bingkai ke seluruh area klien dari jendela, menjadikannya widget pusat.
Untuk merender, Anda harus mengatur pembaruan jendela secara periodik oleh penghitung waktu. Untuk melakukan ini, buat timer dengan interval 40 milidetik (25 frame per detik) dan kaitkan sinyal timeout-nya dengan slot pembaruan jendela. Saya melakukannya dengan cara ini menggunakan sintaks Qt5
connect(&timer, &QTimer::timeout, this, &MainWindow::update); timer.start(40);
setelah sebelumnya menetapkan slot pembaruan untuk kelas jendela dengan cara ini
void MainWindow::update() { QMainWindow::update(this->geometry()); }
Mengapa demikian, karena Anda dapat secara langsung mengaitkan sinyal timer dengan slot QMainWindow :: update dengan cara yang sama seperti yang ditunjukkan oleh sebagian besar contoh osgQt
connect(&timer, SIGNAL(timeout), this, SLOT(update));
Faktanya adalah bahwa sintaks dengan makro SIGNAL () dan SLOT () sudah usang, dan untuk mengantisipasi transisi ke Qt6, itu harus ditinggalkan. Pada saat yang sama, kelas QMainWindow tidak memiliki kelebihan pembaruan () slot tanpa parameter, yang akan menyebabkan kesalahan pada panggilan yang mengikat selama kompilasi. Untuk melakukan ini, saya harus mendefinisikan slot update () saya tanpa parameter, memanggil basis QMainWindow :: update () di dalamnya, melewati area klien dari jendela di sana.
Menambahkan ke tempat ini dan menjalankan program, kami akan mendapatkan hasil tertentu

Dengan menekan "S" kita dapat mengaktifkan monitor statistik OSG dan memastikan bahwa widget kita berfungsi sebagaimana mestinya, menggambar adegan kosong.
Statistik apa yang dipantau?Agar tidak membebani artikel saya akan menulis di sini. OSG memiliki monitor internal yang menampilkan statistik mesin secara real time. Untuk menambahkannya ke penampil, kami menyertakan file header
#include <osgViewer/ViewerEventHandlers>
dan menambahkan penangan ke penampil
viewer.addEventHandler(new osgViewer::StatsHandler);
lalu kapan saja dengan menekan "S" untuk menampilkan banyak informasi berguna.
4. Selesai pemirsa kami: tambahkan menu
Di perancang formulir, kami mengubahsuaikan menu menggunakan pemrograman "berorientasi mouse" (yang saya acuhkan, tapi ya, terkadang itu nyaman). Pada akhirnya kita akan mendapatkan sesuatu seperti ini

Sekarang kita akan memulai slot prosesor yang sesuai, memungkinkan Anda untuk memuat model di sepanjang jalur yang dipilih dari kotak dialog, menghapus adegan dan keluar dari aplikasi
Setelah itu, kita akan mendapatkan viewer model format * .osg yang sangat nyaman.

Peragaan karyanya ditampilkan dalam video di awal artikel. Kode sumber lengkap untuk contoh ini
tersedia di sini.Kesimpulan
Seperti yang telah kita lihat, integrasi OSG dan Qt tidak terlalu sulit baik dalam pemahaman maupun implementasi. Ini sangat membantu untuk membuat aplikasi lintas platform untuk visualisasi teknis, dan mungkin game.
Artikel ini membuka kelanjutan dari seri OSG, yang akan menguraikan teknik pengembangan yang kompleks. Saya pikir dia berhasil keluar. Terima kasih dan sampai jumpa lagi!