Sepsání dnešního pojednání jsem se výjimečně ujal já, protože téma přibližně odpovídá programu, na němž minulý semestr závisel můj zápočet. Další díly seriálu (o kterých jsem už něco zaslechl :)) budou od původního autora.
Hned na úvod bych rád zdůraznil, že účelem článku není demonstrovat nejlepší algoritmus simulace pohybu vesmírných těles, a skutečnost, že se planety v příkladu pohybují po spirále, může být zapříčiněna nejen použitou metodou, ale i nevhodně zvolenými vlastnostmi prostředí (gravitační konstanta, hmotnost, výchozí vektor rychlosti planet atd.).
Senzory v Open Inventoru
Obíhání těles bylo v předchozích ukázkách vyřešeno pomocí noduSoRotationXYZ, jeho připojením k engine SoElapsedTime a posunem ze středu scény. My si vytvoříme třídu planet, kde budou uchovány všechny podstatné informace (poloha, hmotnost, vektor rychlosti a ukazatel na node SoTranslation zajišťující přesun planety na správné místo). Dále potřebujeme funkci, která vypočítá novou pozici planet a bude jednou za čas volána. Právě k tomu se hodí senzory.
Senzory jsou objekty, které sledují určité události a v případě jejich výskytu zavolají programátorem vytvořenou funkci. Mezi možné sledované události patří změna hodnoty pole (SoFieldSensor), uplynutí časového kvanta (SoAlarmSensor, SoTimerSensor), nečinnost aplikace (SoIdleSensor) a další.
Pro potomky abstraktní třídy SoDelayQueueSensor platí, že jakmile se objeví sledovaná událost, jsou přidáni do fronty Delay queue, kde jsou seřazeni podle priority (tu je možné ovlivnit), a někdy v budoucnu budou zpracováni (bude zavolána jejich callback funkce). Zařazení do fronty je možné dosáhnout i bez výskytu události metodou void schedule().
Senzory odvozené od abstraktní třídy SoTimerQueueSensor (SoAlarmSensor a SoTimerSensor) musí být do frontyTimer queue přidány některou z jejich metod. Také nejsou řazeny podle prioritym ale podle času, kdy se mají zpracovat.
My do scény přidáme SoOneShotSensor. Prvním parametrem v konstruktoru je ukazatel na callback funkci, druhým jsou data jí předávaná (typ void *).
SoOneShotSensor *sensor = new SoOneShotSensor(sensorCallback, data);
sensor->schedule();
Ve funkci sensorCallback zjistíme čas uplynulý od jejího minulého volání, provedeme výpočet nových pozic těles a metodou schedule() opět zařadíme senzor do fronty (v proměnné sensor je ukazatel na senzor, který funkci zavolal). Tím máme zajištěno, že bude dosaženo maximálního možného počtu vyrenderovaných snímků za sekundu.
void sensorCallback(void *data, SoSensor *sensor) { ... sensor->schedule(); }
Tvorba UI
Práci s Qt zde nebudu popisovat nijak důkladně, omezím se jen na základní principy potřebné k pochopení příkladu. Zájemcům o podrobnější informace doporučuji tutoriál a dokumentaci na stránkách Trolltechu.
V každém programu využívajícím toolkit Qt musí být vytvořena právě jedna instance třídy QApplication. Je přístupná přes globální ukazatel qApp a stará se o počáteční inicializaci, zpracování událostí, ukončení aplikace a další záležitosti.
Okna jsou v nejjednodušším případě odvozena od třídy QWidget, přestože existují i specializované varianty (QMainWindow, QDockWindow…). Prvním parametrem konstruktoru je ukazatel na rodiče, druhým jméno widgetu. Vztah rodič-potomek v tomto případě nemá nic společného s dědičností. Jestliže je widget renderWindow potomkemmainWindow, znamená to, že renderWindow je umístěn uvnitř widgetu mainWindow. Při zrušení rodiče jsou zrušeni i všichni jeho potomci. Prvky Qt tedy vytvářejí stromovou hierarchii, jejímž kořenem (tzv. top-level window) je instance třídy, které byl jako rodič předán nulový ukazatel. Pokud se aplikace skládá z několika oken, potom existuje i několik takovýchto stromů.
Metoda int QApplication::exec() spustí hlavní smyčku programu, během níž jsou zpracovávány a obsluhovány příchozí události. Stejný princip je použit v knihovně GLUT (funkce void glutMainLoop()) nebo GTK+ (void gtk_main()).
int main(int argc, char **argv) { QApplication a(argc, argv); mainWindow w; w.setGeometry(100, 100, 650, 500); a.setMainWidget(&w); w.show(); return a.exec(); }
Ke komunikaci mezi objekty v Qt slouží mechanismus slotů a signálů. Když uživatel klikne na tlačítko, dojde k vyslání signálu clicked(). Ten je možné např. metodou bool QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member) spojit se slotem – funkcí, jež je při vyslání signálu zavolána. Takto lze předávat i parametry. Hodně objektů má množství zabudovaných slotů i signálů, ale nic nám nebrání vytvořit si vlastní (tímto způsobem můžeme propojit i dva signály – při vyslání prvního signálu je vyslán i signál druhý).
QPopupMenu *file = new QPopupMenu(this);
file->insertItem("&Konec", qApp, SLOT(quit()), CTRL+Key_Q);
V příkladu je slider propojen s widgetem renderWindow, který obsahuje slot void setIntensity(int) sloužící k nastavování intenzity ambientního světla prostředí. Ve všech objektech, které obsahují signály nebo sloty, musí být uvedeno makro Q_OBJECT.
// deklarace tridy renderWindow
class renderWindow: public QWidget {
Q_OBJECT
private:
SoEnvironment *envir;
public:
renderWindow(QWidget *parent = NULL, const char *name = NULL);
~renderWindow();
public slots:
void setIntensity(int);
};
void renderWindow::setIntensity(int i) {
envir->ambientIntensity.setValue(i / 99.0f);
}
QSlider *slider = new QSlider(Vertical, this, "slider");
renderWindow *render = new renderWindow(this, "render");
connect(slider, SIGNAL(valueChanged(int)), render, SLOT(setIntensity(int)));
Dosud jsme aplikaci tvořili bez ohledu na knihovnu Coin. Hlavní okno jsme za použití QGridLayout rozdělili na čtyři části (dva řádky, dva sloupce), do nichž jsme umístili menu, slider a okno s 3D scénou (renderWindow). V konstruktoru třídy renderWindow musíme inicializovat knihovnu SoQt (void SoQt::init(QWidget *toplevelwidget)) a klasickým způsobem vytvoříme scénu. Ukazatel na instanci renderWindow je nutné předat i konstruktoru třídySoQtFlyViewer ve funkci void createScene(QWidget *parent, SoEnvironment **envir).
renderWindow::renderWindow(QWidget *parent, const char *name): QWidget(parent, name) {
SoQt::init(this);
createScene(this, &envir);
}
Na závěr přikládám zdrojové kódy příkladu. Kdybyste měli problémy s jejich kompilací, zkuste vygenerovat nástrojem qmake nový Makefile (mezi knihovny je potom potřeba doplnit soqt-config --ldflags
soqt-config -libs
).
- main.h, main.cpp
- hlavní okno
- render.h, render.cpp
- renderovací okno
- scene.h, scene.cpp
- vytvoření a zrušení scény
- planet.h, planet.cpp
- třída planet
- freeFlyer.h, freeFlyer.cpp
- třída freeFlyer (vesmírná loď)