OpenGL を使って太陽系を作成する

C++Beginner
オンラインで実践に進む

はじめに

このプロジェクトでは、OpenGL を使って太陽系のシミュレーションを作成します。このシミュレーションには、太陽、惑星、そしてそれらの動きと回転が含まれます。ウィンドウと入力関数を扱うために GLUT(OpenGL Utility Toolkit)を使い、レンダリングには OpenGL を使います。

このプロジェクトを完了することで、以下のことが学べます。

  • OpenGL を使ったグラフィックスプログラミングの基本概念
  • 3D モデルを作成し、シミュレーション環境でレンダリングする方法
  • ユーザー入力を扱い、それに応じてシミュレーションを更新する方法
  • シミュレーションの視覚的品質を向上させるための基本的な照明システムを実装する方法
  • オブジェクト指向プログラミングの原則を使ってコードを整理する方法

このプロジェクトでは、C++ プログラミングの基本的な理解と、グラフィックスプログラミングの概念に対するある程度の慣れが前提となっています。OpenGL を使った簡単なグラフィックスアプリケーションを構築する実践的な経験を提供します。

👀 プレビュー

Solar system simulation preview

🎯 タスク

このプロジェクトでは、以下のことが学べます。

  • 必要なライブラリをインストールし、開発環境をセットアップする方法
  • 必要なクラスを作成し、惑星の回転と公転の基本機能を実装する方法
  • 3D シーンの透視と投影を設定する方法
  • シミュレーションの視覚的品質を向上させるための照明システムを実装する方法
  • ユーザー入力を扱い、ユーザーがシミュレーションの透視を制御できるようにする方法
  • シミュレーションをテストし、期待通りに機能するように微調整する方法

🏆 成果

このプロジェクトを完了すると、以下のことができるようになります。

  • OpenGL を使ったグラフィックスプログラミングの基本概念を適用する
  • 3D モデルを作成し、シミュレーション環境でレンダリングする
  • シミュレーションの視覚的品質を向上させるための基本的な照明システムを実装する
  • オブジェクト指向プログラミングの原則を使ってコードを整理する
  • 問題解決とデバッグのスキルを示す

OpenGL と GLUT を理解する

OpenGL には多くのレンダリング関数が含まれていますが、その設計目的はどのウィンドウシステムやオペレーティングシステムとも独立しています。したがって、オープンウィンドウを作成する関数や、キーボードやマウスからイベントを読み取る関数、さらにはウィンドウを表示する最も基本的な機能さえ含まれていません。したがって、OpenGL のみを使って完全なグラフィックスプログラムを作成することは完全に不可能です。また、ほとんどのプログラムはユーザーとの相互作用(キーボードやマウス操作に応答)が必要です。GLUT がこの便利さを提供します。

GLUT は OpenGL Utility Toolkit の略です。これは OpenGL プログラムを扱うためのツールライブラリで、主に基礎となるオペレーティングシステムへの呼び出しと I/O 操作を担当します。GLUT を使うことで、基礎となるオペレーティングシステムの GUI 実装の一部の詳細を隠すことができ、アプリケーションウィンドウを作成したり、マウスやキーボードイベントを処理したりするためにはただ GLUT API が必要です。これにより、クロスプラットフォーム互換性が達成されます。

まず、実験環境で GLUT をインストールしましょう。

sudo apt-get update && sudo apt-get install freeglut3 freeglut3-dev

標準的な GLUT プログラムの構造は以下のコードに示されています。

// GLUT を使用するための基本的なヘッダーファイル
#include <GL/glut.h>

// グラフィカルウィンドウを作成するための基本的なマクロ
#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700

// GLUT に登録されるコールバック関数
void onDisplay(void);
void onUpdate(void);
void onKeyboard(unsigned char key, int x, int y);

int main(int argc, char* argv[]) {

    // GLUT を初期化し、すべてのコマンドライン引数を処理する
    glutInit(&argc, argv);
    // この関数は、表示に RGBA モードまたはカラーインデックスモードを使用するかどうかを指定します。また、単一バッファまたはダブルバッファのウィンドウを使用するかどうかも指定できます。ここでは、RGBA とダブルバッファのウィンドウを使用します。
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    // 画面上で作成されるときのウィンドウの左上隅の位置を設定する
    glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
    // 作成時のウィンドウの幅と高さを設定する。単純化のため
    glutInitWindowSize(WIDTH, HEIGHT);
    // ウィンドウを作成し、入力文字列はウィンドウのタイトルになる
    glutCreateWindow("SolarSystem at LabEx");

    // glutDisplayFunc のプロトタイプは glutDisplayFunc(void (*func)(void)) です
    // これはコールバック関数で、GLUT がウィンドウの内容を更新して表示する必要があると判断するたびに実行されます。
    //
    // glutIdleFunc(void (*func)(void)) は、イベントループがアイドルのときに実行する関数を指定します。このコールバック関数は、関数ポインタを唯一のパラメータとして取ります。
    //
    // glutKeyboardFunc(void (*func)(unsigned char key, int x, int y)) は、キーボード上のキーと関数を関連付けます。この関数は、キーが押されたり離されたりするときに呼び出されます。
    //
    // したがって、以下の 3 行は実際に GLUT に 3 つのキーコールバック関数を登録している
    glutDisplayFunc(onDisplay);
    glutIdleFunc(onUpdate);
    glutKeyboardFunc(onKeyboard);

    glutMainLoop();
    return 0;

}

~/project/ディレクトリにmain.cppファイルを作成し、以下のコードを書きます。

//
//  main.cpp
//  solarsystem
//
#include <GL/glut.h>
#include "solarsystem.hpp"

#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700

SolarSystem solarsystem;

void onDisplay(void) {
    solarsystem.onDisplay();
}
void onUpdate(void) {
    solarsystem.onUpdate();
}
void onKeyboard(unsigned char key, int x, int y) {
    solarsystem.onKeyboard(key, x, y);
}

int main(int argc, char* argv[]) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
    glutCreateWindow("SolarSystem at LabEx");
    glutDisplayFunc(onDisplay);
    glutIdleFunc(onUpdate);
    glutKeyboardFunc(onKeyboard);
    glutMainLoop();
    return 0;
}

ヒント

  • シングルバッファリングは、ウィンドウ上に直接すべての描画コマンドを実行することを意味し、低速です。コンピュータの処理能力が不十分でシングルバッファリングを使用する場合、画面が点滅する可能性があります。
  • ダブルバッファリングは、メモリ内のバッファに描画コマンドを実行することを意味し、はるかに高速です。描画コマンドが完了すると、結果がバッファスワップを通じて画面にコピーされます。このアプローチの利点は、描画操作をグラフィックスカードでリアルタイムに実行させる場合、描画タスクが複雑になると I/O 操作が複雑になり、パフォーマンスが低下することです。対照的に、ダブルバッファリングでは、完了した描画結果がバッファをスワップする際に直接グラフィックスカードに送信されてレンダリングされるため、I/O が大幅に削減されます。

OpenGL では、浮動小数点数を表すためにGLfloatを使用することが推奨されます。

クラスの設計

オブジェクト指向プログラミングでは、まず扱うオブジェクトを明確にすることが不可欠です。明らかに、天体システム全体では、それらはすべて惑星(Star)であり、惑星と恒星の違いは、親ノードがあるかどうかだけです。其次に、異なる惑星は通常独自の素材を持っており、異なる素材は光を発するかどうかを示します。したがって、初期的なオブジェクトモデルがあります。したがって、惑星を以下のように分類します。点を中心に回転し公転できる通常の惑星(Star)、特殊な素材を持つ惑星(Planet)、光を発することができる惑星(LightPlanet)。

また、プログラミング実装の便宜上、現実世界の実際のプログラミングモデルについていくつかの仮定を行います。

  1. 惑星の軌道は円形です。
  2. 回転速度は一定です。
  3. 画面が更新されるたびに、1 日が経過したものと仮定します。

まず、以下の手順でロジックを実装することができます。

  1. 惑星オブジェクトを初期化する。
  2. OpenGL エンジンを初期化し、onDraw と onUpdate を実装する。
  3. 各惑星は独自のプロパティ、公転関係、および変換に関連する描画を処理する責任があります。したがって、惑星クラスを設計する際には、描画メソッド draw() を提供する必要があります。
  4. 惑星はまた、表示用の回転と公転に関連する自身の更新を処理する必要があります。したがって、惑星クラスを設計する際には、更新メソッド update() も提供する必要があります。
  5. onDraw() で惑星の draw() メソッドを呼び出す。
  6. onUpdate() で惑星の update() メソッドを呼び出す。
  7. onKeyboard() でキーボードを使って太陽系全体の表示を調整する。

さらに、各惑星には以下の属性があります。

  1. 色 color
  2. 公転半径 radius
  3. 回転速度 selfSpeed
  4. 公転速度 speed
  5. 太陽の中心までの距離 distance
  6. 公転する親惑星 parentStar
  7. 現在の回転角度 alphaSelf
  8. 現在の公転角度 alpha

~/project/ディレクトリにstars.hppファイルを作成します。上記の分析に基づき、以下のようなクラスコードを設計できます。

class Star {
public:
    // 惑星の公転半径
    GLfloat radius;
    // 惑星の公転と回転速度
    GLfloat speed, selfSpeed;
    // 惑星の中心から親惑星の中心までの距離
    GLfloat distance;
    // 惑星の色
    GLfloat rgbaColor[4];

    // 親惑星
    Star* parentStar;

    // コンストラクタ、惑星を構築する際に、回転半径、回転速度、回転速度、および公転(親惑星)を指定する必要があります
    Star(GLfloat radius, GLfloat distance,
         GLfloat speed,  GLfloat selfSpeed,
         Star* parentStar);
    // 通常の惑星の動き、回転、その他の活動を描画する
    void drawStar();
    // drawStar() を呼び出すデフォルトの実装を提供する
    virtual void draw() { drawStar(); }
    // パラメータは各画面更新の時間間隔です
    virtual void update(long timeSpan);
protected:
    GLfloat alphaSelf, alpha;
};
class Planet : public Star {
public:
    // コンストラクタ
    Planet(GLfloat radius, GLfloat distance,
           GLfloat speed,  GLfloat selfSpeed,
           Star* parentStar, GLfloat rgbColor[3]);
    // 独自の素材を持つ惑星に素材を追加する
    void drawPlanet();
    // サブクラスに対して再書き込み関数を継続して開く
    virtual void draw() { drawPlanet(); drawStar(); }
};
class LightPlanet : public Planet {
public:
    LightPlanet(GLfloat Radius, GLfloat Distance,
                GLfloat Speed,  GLfloat SelfSpeed,
                Star* ParentStar, GLfloat rgbColor[]);
    // 光源を提供する恒星に照明を追加する
    void drawLight();
    virtual void draw() { drawLight(); drawPlanet(); drawStar(); }
};

また、SolarSystem クラスの設計も考慮する必要があります。太陽系では、明らかに太陽系は様々な惑星で構成されています。太陽系において、惑星の動きに伴うビューの更新は太陽系によって処理される必要があります。したがって、SolarSystem のメンバ変数には惑星を含む変数が含まれ、メンバ関数は太陽系内のビューの更新とキーボード応答イベントを処理する必要があります。したがって、~/project/ディレクトリにsolarsystem.hppファイルを作成し、その中にSolarSystemクラスを設計することができます。

class SolarSystem {

public:

    SolarSystem();
    ~SolarSystem();

    void onDisplay();
    void onUpdate();
    void onKeyboard(unsigned char key, int x, int y);

private:
    Star *stars[STARS_NUM];

    // 視野角のパラメータを定義する
    GLdouble viewX, viewY, viewZ;
    GLdouble centerX, centerY, centerZ;
    GLdouble upX, upY, upZ;
};

ヒント

  • ここでは、C++ の vector ではなく、従来の配列形式を使ってすべての惑星を管理しています。なぜなら、従来の配列形式で十分だからです。
  • OpenGL における視野角の定義は複雑な概念で、説明にはある程度の長さが必要です。ここでは、視野角の定義には少なくとも 9 つのパラメータが必要であることを簡単に触れます。次のセクションで実装する際に、それらの機能について詳細に説明します。

最後に、基本パラメータと変数の設定も考慮する必要があります。

SolarSystem には、太陽を含めて合計 9 つの惑星(冥王星を除く)がありますが、私たちが設計した Star クラスでは、各 Star オブジェクトが Star の属性を持っているため、これらの惑星の衛星、たとえば地球を中心に回る月を追加で実装することができます。したがって、合計 10 個の惑星を実装することを考えています。したがって、以下の enum を設定して、配列内の惑星をインデックス付けすることができます。

#define STARS_NUM 10
enum STARS {
    Sun,        // 太陽
    Mercury,    // 水星
    Venus,      // 金星
    Earth,      // 地球
    Moon,       // 月
    Mars,       // 火星
    Jupiter,    // 木星
    Saturn,     // 土星
    Uranus,     // 天王星
    Neptune     // 海王星
};
Star * stars[STARS_NUM];

また、回転速度が同じと仮定しているため、マクロを使って速度を設定します。

#define TIMEPAST 1
#define SELFROTATE 3

この時点で、未実装のメンバ関数を対応する.cppファイルに移動し、このセクションの実験を完了しました。

コードの概要

上記の実験で完成させる必要のあるコードをまとめましょう。

まず、main.cppでは、SolarSystemオブジェクトを作成し、その後、表示更新、アイドル時の更新、およびキーボードイベントの処理をglutに委譲します。

コードを表示するにはクリックしてください
//
//  main.cpp
//  solarsystem
//
#include <GL/glut.h>
#include "solarsystem.hpp"

#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700

SolarSystem solarsystem;

void onDisplay(void) {
    solarsystem.onDisplay();
}
void onUpdate(void) {
    solarsystem.onUpdate();
}
void onKeyboard(unsigned char key, int x, int y) {
    solarsystem.onKeyboard(key, x, y);
}

int main(int argc, char* argv[]) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
    glutCreateWindow("SolarSystem at LabEx");
    glutDisplayFunc(onDisplay);
    glutIdleFunc(onUpdate);
    glutKeyboardFunc(onKeyboard);
    glutMainLoop();
    return 0;
}

其次に、stars.hppでは、StarPlanet、およびLightPlanetクラスを作成します。

コードを表示するにはクリックしてください
//
//  stars.hpp
//  solarsystem
//
#ifndef stars_hpp
#define stars_hpp

#include <GL/glut.h>

class Star {
public:
    GLfloat radius;
    GLfloat speed, selfSpeed;
    GLfloat distance;
    GLfloat rgbaColor[4];

    Star* parentStar;

    Star(GLfloat radius, GLfloat distance,
         GLfloat speed, GLfloat selfSpeed,
         Star* parentStar);
    void drawStar();
    virtual void draw() { drawStar(); }
    virtual void update(long timeSpan);
protected:
    GLfloat alphaSelf, alpha;
};

class Planet : public Star {
public:
    Planet(GLfloat radius, GLfloat distance,
           GLfloat speed, GLfloat selfSpeed,
           Star* parentStar, GLfloat rgbColor[3]);
    void drawPlanet();
    virtual void draw() { drawPlanet(); drawStar(); }
};

class LightPlanet : public Planet {
public:
    LightPlanet(GLfloat Radius, GLfloat Distance,
                GLfloat Speed, GLfloat SelfSpeed,
                Star* parentStar, GLfloat rgbColor[]);
    void drawLight();
    virtual void draw() { drawLight(); drawPlanet(); drawStar(); }
};

#endif /* star_hpp */

~/project/ディレクトリにstars.cppファイルを作成し、stars.hppから対応するメンバ関数の実装を記述します。

コードを表示するにはクリックしてください
//
//  stars.cpp
//  solarsystem
//
#include "stars.hpp"

#define PI 3.1415926535

Star::Star(GLfloat radius, GLfloat distance,
           GLfloat speed, GLfloat selfSpeed,
           Star* parentStar) {
    // TODO:
}

void Star::drawStar() {
    // TODO:
}

void Star::update(long timeSpan) {
    // TODO:
}

Planet::Planet(GLfloat radius, GLfloat distance,
               GLfloat speed, GLfloat selfSpeed,
               Star* parentStar, GLfloat rgbColor[3]) :
Star(radius, distance, speed, selfSpeed, parentStar) {
    // TODO:
}

void Planet::drawPlanet() {
    // TODO:
}

LightPlanet::LightPlanet(GLfloat radius, GLfloat distance, GLfloat speed,
                         GLfloat selfSpeed, Star* parentStar, GLfloat rgbColor[3]) :
Planet(radius, distance, speed, selfSpeed, parentStar, rgbColor) {
    // TODO:
}

void LightPlanet::drawLight() {
    // TODO:
}

solarsystem.hppでは、SolarSystemクラスが設計されています。

//
// solarsystem.hpp
// solarsystem
//
#include <GL/glut.h>

#include "stars.hpp"

#define STARS_NUM 10

class SolarSystem {

public:

    SolarSystem();
    ~SolarSystem();

    void onDisplay();
    void onUpdate();
    void onKeyboard(unsigned char key, int x, int y);

private:
    Star *stars[STARS_NUM];

    // Define the parameters of the viewing angle
    GLdouble viewX, viewY, viewZ;
    GLdouble centerX, centerY, centerZ;
    GLdouble upX, upY, upZ;
};

~/project/ディレクトリにsolarsystem.cppファイルを作成し、solarsystem.hppから対応するメンバ関数を実装します。

コードを表示するにはクリックしてください
//
// solarsystem
//
#include "solarsystem.hpp"

#define TIMEPAST 1
#define SELFROTATE 3

enum STARS {Sun, Mercury, Venus, Earth, Moon,
    Mars, Jupiter, Saturn, Uranus, Neptune};

void SolarSystem::onDisplay() {
    // TODO:
}
void SolarSystem::onUpdate() {
    // TODO:
}
void SolarSystem::onKeyboard(unsigned char key, int x, int y) {
    // TODO:
}
SolarSystem::SolarSystem() {
    // TODO:

}
SolarSystem::~SolarSystem() {
    // TODO:
}

~/project/ディレクトリにMakefileファイルを作成し、その中に以下のコードを追加します。

直接コピー&ペーストせずに手動で入力してください。<tab>キーを使ってください。スペースキーは使わないでください。

CXX = g++
EXEC = solarsystem
SOURCES = main.cpp stars.cpp solarsystem.cpp
OBJECTS = main.o stars.o solarsystem.o
LDFLAGS = -lglut -lGL -lGLU

all :
    $(CXX) $(SOURCES) $(LDFLAGS) -o $(EXEC)

clean:
    rm -f $(EXEC) *.gdb *.o

コンパイルコマンドを書く際には、-lglut -lGLU -lGLの配置に注意してください。これは、g++ コンパイラにおける-lオプションの使い方が少し特殊だからです。

例えば:foo1.cpp -lz foo2.cppの場合、対象ファイルfoo2.cppがライブラリzの関数を使っていても、これらの関数は直接読み込まれません。ただし、foo1.ozライブラリの関数を使っている場合、コンパイルエラーは発生しません。

言い換えると、全体のリンク処理は左から右に行われます。foo1.cppで解決できない関数シンボルが見つかると、右側のリンクライブラリを探します。zオプションに遭遇すると、zの中を検索して関数を見つけ、円滑にリンクが完了します。したがって、-lオプション付きのライブラリは、すべてのコンパイル済みファイルの右側に配置する必要があります。

最後に、ターミナルで以下を実行します。

make && ./solarsystem

ウィンドウが作成されていることがわかりますが、中身は何もありません(ウィンドウの後ろに表示されるものが表示されます)。これは、ウィンドウ内のグラフィックスの更新メカニズムをまだ実装していないためです。次の実験で残りのコードを完成させ、太陽系のシミュレーション全体を動かします。

OpenGL における行列の概念

線形代数では、行列の概念には馴染みがありますが、その具体的な目的や機能についての理解は限られているかもしれません。では、行列とは何でしょうか?

まず、以下の方程式を見てみましょう。

x = Ab

ここで、Aは行列であり、x, bはベクトルです。

視点一

xbがともに 3 次元空間内のベクトルである場合、Aは何をするのでしょうか?それはbxに変換します。この視点から見ると、行列Aは変換として理解できます。

次に、もう一つの方程式を考えてみましょう。

Ax = By

ここで、A, Bは行列であり、x, yはベクトルです。

視点二

2 つの異なるベクトルxyは、実質的に同じであることがわかります。なぜなら、行列ABを掛けることで等しくすることができるからです。この視点から見ると、行列Aは座標系として理解できます。言い換えると、ベクトル自体は一意ですが、それを記述するために座標系を定義します。異なる座標系を使用するため、ベクトルの座標は異なります。この場合、同じベクトルは異なる座標系で異なる座標を持ちます。 行列Aは正確に座標系を記述し、行列Bは別の座標系を記述します。これら 2 つの座標系をxyに適用すると、同じ結果が得られます。つまり、xyは実質的に同じベクトルであり、ただ座標系が異なるだけです。

これら 2 つの視点を結合すると、行列の本質は動きを記述することであると結論付けることができます。

OpenGL のコンテキストでは、レンダリング変換を担当する行列があり、これは OpenGL の行列モードと呼ばれます。

前述の通り、行列はオブジェクトの変換とその存在する座標系の両方を記述できます。したがって、異なる操作を行う際には、OpenGL で異なる行列モードを設定する必要があります。これは、glMatrixMode()関数を使用して達成されます。

この関数は 3 つの異なるモードを受け付けます。投影操作にはGL_PROJECTION、モデルビュー操作にはGL_MODELVIEW、テクスチャ操作にはGL_TEXTUREです。

GL_PROJECTIONは OpenGL に対して投影操作が行われることを伝え、オブジェクトを平面に投影します。このモードが有効になっている場合、行列を単位行列に設定するためにglLoadIdentity()を使用し、その後、gluPerspectiveを使用して透視を設定するなどの操作を行います(OpenGL の視点の概念について後で詳しく説明します)。

GL_MODELVIEWは OpenGL に対して、後続の文がモデルベースの操作、たとえばカメラの視点を設定するために使用されることを伝えます。同様に、このモードを有効にした後、OpenGL の行列モードを単位行列に設定する必要があります。

GL_TEXTUREはテクスチャ関連の操作に使用されますが、ここでは詳しくは触れません。

行列の概念に慣れていない場合は、glMatrixMode()を OpenGL に対する次の操作の宣言として単純に理解することができます。オブジェクトを描画または回転させる前に、必ずglPushMatrixを使用して現在の行列環境を保存する必要があります。そうしないと、奇妙な描画エラーが発生する可能性があります。

一般的な OpenGL 画像描画 API

OpenGL は、グラフィック描画に関連する多くの一般的な API を提供しています。ここでは、それらのうちいくつかを簡単に紹介し、以下のコードで使用します。

  • glEnable(GLenum cap): この関数は、OpenGL が提供するさまざまな機能を有効にするために使用されます。パラメータ cap は、OpenGL 内部のマクロであり、照明、フォグ、ディザリングなどの効果を提供します。
  • glPushMatrix() および glPopMatrix(): これらの関数は、現在の行列をスタックのトップに保存します(現在の行列を保存します)。
  • glRotatef(alpha, x, y, z): 現在の形状を、(x, y, z)軸に沿って時計回りに alpha 度回転させます。
  • glTranslatef(distance, x, y): 現在の形状を、(x, y)方向に distance だけ平行移動させます。
  • glutSolidSphere(GLdouble radius, GLint slices, GLint stacks): 球体を描画します。ここで、radius は半径、slices は経線の数、stacks は緯線の数です。
  • glBegin() および glEnd(): 形状を描画したい場合、描画の前後でこれら 2 つの関数を呼び出す必要があります。glBegin()は、描画する形状の種類を指定します。たとえば、GL_POINTSは点を描画することを表し、GL_LINESは線で結ばれた点を描画することを表し、GL_TRIANGLESは 3 つの点で三角形を完成させ、GL_POLYGONは最初の点から n 番目の点まで多角形を描画します。たとえば、円を描画する必要がある場合、多辺形を使ってそれをシミュレートすることができます。
// r は半径、n は辺の数
glBegin(GL_POLYGON);
    for(i=0; i<n; ++i)
        glVertex2f(r*cos(2*PI/n*i), r*sin(2*PI/n*i));
glEnd();

OpenGL における透視座標

前節では、OpenGL のSolarSystemクラスに 9 つのメンバ変数を定義しました。

GLdouble viewX, viewY, viewZ;
GLdouble centerX, centerY, centerZ;
GLdouble upX, upY, upZ;

これら 9 つの変数を理解するには、まず OpenGL を使った 3D プログラミングにおけるカメラ透視の概念を確立する必要があります。

映画で見るシーンは、実際にはカメラの視点から撮影されています。したがって、OpenGL にも同様の概念があります。カメラを自分自身の頭に想像すると、次のようになります。

  1. viewXviewYviewZは、OpenGL のワールド座標系における頭(カメラ)の座標に対応します。
  2. centerXcenterYcenterZは、視覚対象の物体の座標(カメラから見たもの)に対応します。
  3. upXupYupZは、頭の上(カメラの上)から上向きを指す方向ベクトルに対応します(物体を観察するために頭を傾けることができます)。

この理解を通じて、OpenGL における座標系の概念を把握することができます。

この実験では、初期の透視が座標 (x, -x, x) にあると仮定します。したがって、次のようになります。

#define REST 700
#define REST_Y (-REST)
#define REST_Z (REST)

観察対象の物体(太陽)の位置は (0,0,0) です。したがって、SolarSystemクラスのコンストラクタでは、透視を次のように初期化します。

viewX = 0;
viewY = REST_Y;
viewZ = REST_Z;
centerX = centerY = centerZ = 0;
upX = upY = 0;
upZ = 1;

その後、gluLookAt関数を使って透視の 9 つのパラメータを設定することができます。

gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);

次に、gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar)を見てみましょう。

この関数は、対称的な透視視野を作成します。この関数を使用する前に、OpenGL の行列モードをGL_PROJECTIONに設定する必要があります。

次の図のようになります。

OpenGL perspective projection diagram

ウィンドウ内の画像はカメラによってキャプチャされ、実際にキャプチャされている内容は遠平面上にあり、表示される内容は近平面上にあります。したがって、この関数には 4 つのパラメータが必要です。

  • 最初のパラメータは、透視角のサイズです。
  • 2 番目のパラメータは、実際のウィンドウのアスペクト比です。図に示すようにaspect=w/hです。
  • 3 番目のパラメータは、近平面までの距離です。
  • 4 番目のパラメータは、遠平面までの距離です。

OpenGL における照明効果

OpenGL は照明システムを光源、材質、照明環境の 3 つに分けています。

名前の通り、光源は光の源であり、太陽のようなものです。 材質は、光を受けるさまざまな物体の表面を指し、太陽以外の太陽系の惑星や衛星などです。 照明環境には、最終的な照明効果を決定する追加のパラメータが含まれており、たとえば光線の反射で、「環境輝度」と呼ばれるパラメータを設定することで制御でき、最終的な画像を現実に近づけることができます。

物理学では、平行光が滑らかな表面に当たるとき、反射光は依然として平行になります。この種の反射を「鏡面反射」と呼びます。一方、表面が凹凸していることによる反射を「拡散反射」と呼びます。

OpenGL lighting effects example

光源

OpenGL で照明システムを実装するには、まず光源を設定する必要があります。言うまでもなく、OpenGL は有限数の光源(合計 8 つ)をサポートしており、GL_LIGHT0 から GL_LIGHT7 のマクロで表されます。これらは、glEnable 関数で有効にし、glDisable 関数で無効にすることができます。たとえば:glEnable(GL_LIGHT0);

光源の位置は、glLightfv 関数を使って設定します。たとえば:

GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // 光源 0 の位置を指定

位置は 4 つの値 (x, y, z, w) で表されます。w が 0 のときは、光源が無限遠であることを意味します。x、y、z の値は、この無限遠の光源の方向を指定します。 w が 0 でないときは、位置付き光源を表し、その位置は (x/w, y/w, z/w) です。

材質

物体の材質を設定するには、一般的に 5 つの属性が必要です。

  1. 複数回反射した後に環境に残る光の強度。
  2. 拡散反射後の光の強度。
  3. 鏡面反射後の光の強度。
  4. OpenGL での非発光物体が発する光の強度で、これは弱く、他の物体には影響しません。
  5. 鏡面指数で、材質の粗さを表します。値が小さいほど材質が粗く、点光源が発する光が当たるときに大きな輝点が生じます。逆に、値が大きいほど材質が鏡面に近く、小さな輝点が生じます。

OpenGL は、材質を設定するための 2 つのバージョンの関数を提供しています。

void glMaterialf(GLenum face, GLenum pname, TYPE param);
void glMaterialfv(GLenum face, GLenum pname, TYPE *param);

それらの違いは、鏡面指数には 1 つの値のみを設定する必要があるため、glMaterialf が使用されます。他の材質設定では複数の値が必要なので、配列を使い、ポインタベクタパラメータのあるバージョンである glMaterialfv が使用されます。たとえば:

GLfloat mat_ambient[]  = {0.0f, 0.0f, 0.5f, 1.0f};
GLfloat mat_diffuse[]  = {0.0f, 0.0f, 0.5f, 1.0f};
GLfloat mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
GLfloat mat_emission[] = {0.5f, 0.5f, 0.5f, 0.5f};
GLfloat mat_shininess  = 90.0f;
glMaterialfv(GL_FRONT, GL_AMBIENT,   mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE,   mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR,  mat_specular);
glMaterialfv(GL_FRONT, GL_EMISSION,  mat_emission);
glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);

照明環境

デフォルトでは、OpenGL は照明を処理しません。照明機能を有効にするには、マクロ GL_LIGHTING を使用します。つまり、glEnable(GL_LIGHTING);

惑星を描画する

惑星を描画する際、まずその公転角度と自転角度を考慮する必要があります。したがって、まず~/project/stars.cppファイルの Star クラスのメンバ関数Star::update(long timeSpan)を実装することができます。

void Star::update(long timeSpan) {
    alpha += timeSpan * speed;  // 公転角度を更新
    alphaSelf += selfSpeed;     // 自転角度を更新
}

公転と自転角度を更新した後、パラメータに基づいて特定の惑星を描画することができます。

void Star::drawStar() {

    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_BLEND);

    int n = 1440;

    // 現在の OpenGL 行列環境を保存
    glPushMatrix();
    {
        // 公転

        // 惑星で距離が 0 でない場合、半径分だけ原点に平行移動させる
        // この部分は衛星用
        if (parentStar!= 0 && parentStar->distance > 0) {
            // 描画グラフィックを z 軸を中心に alpha だけ回転させる
            glRotatef(parentStar->alpha, 0, 0, 1);
            // x 軸方向に距離だけ平行移動させる一方、y と z 方向は変更しない
            glTranslatef(parentStar->distance, 0.0, 0.0);
        }
        // 軌道を描画
        glBegin(GL_LINES);
        for(int i=0; i<n; ++i)
            glVertex2f(distance * cos(2 * PI * i / n),
                       distance * sin(2 * PI * i / n));
        glEnd();
        // z 軸を中心に alpha だけ回転させる
        glRotatef(alpha, 0, 0, 1);
        // x 軸方向に距離だけ平行移動させる一方、y と z 方向は変更しない
        glTranslatef(distance, 0.0, 0.0);

        // 自転
        glRotatef(alphaSelf, 0, 0, 1);

        // 惑星の色を描画
        glColor3f(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
        glutSolidSphere(radius, 40, 32);
    }
    // 描画前の行列環境を復元
    glPopMatrix();

}

このコードでは sin() と cos() 関数を使用していますので、#include<cmath>を含める必要があります。

照明の描画

非発光天体を表すPlanetクラスに対して、その照明効果を描画する必要があります。~/project/stars.cppファイルに次のコードを追加します。

void Planet::drawPlanet() {
    GLfloat mat_ambient[]  = {0.0f, 0.0f, 0.5f, 1.0f};
    GLfloat mat_diffuse[]  = {0.0f, 0.0f, 0.5f, 1.0f};
    GLfloat mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
    GLfloat mat_emission[] = {rgbaColor[0], rgbaColor[1], rgbaColor[2], rgbaColor[3]};
    GLfloat mat_shininess  = 90.0f;

    glMaterialfv(GL_FRONT, GL_AMBIENT,   mat_ambient);
    glMaterialfv(GL_FRONT, GL_DIFFUSE,   mat_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR,  mat_specular);
    glMaterialfv(GL_FRONT, GL_EMISSION,  mat_emission);
    glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
}

一方、発光天体を表すLightPlanetクラスに対しては、その照明材質を設定するだけでなく、光源の位置も設定する必要があります。

void LightPlanet::drawLight() {

    GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
    GLfloat light_ambient[]  = {0.0f, 0.0f, 0.0f, 1.0f};
    GLfloat light_diffuse[]  = {1.0f, 1.0f, 1.0f, 1.0f};
    GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};
    glLightfv(GL_LIGHT0, GL_POSITION, light_position); // 光源 0 の位置を指定
    glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ambient);  // 様々な光源からの光線が材質に到達し、複数回反射・追跡された後の強度を表す
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_diffuse);  // 拡散反射後の光の強度
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // 鏡面反射後の光の強度

}

ウィンドウの描画

前節では、画像表示を処理するための 2 つの最も重要な関数glutDisplayFuncglutIdleFuncについて説明しました。glutDisplayFuncは、GLUT がウィンドウの内容を更新する必要があると判断したときにコールバック関数を実行します。一方、glutIdleFuncは、イベントループがアイドル状態のときのコールバックを処理します。

太陽系全体を動かすには、惑星の位置を更新するタイミングとビューを更新するタイミングを考慮する必要があります。

明らかに、glutDisplayFuncはビューの更新に焦点を当てるべきであり、イベントがアイドル状態のときに惑星の位置を更新し始めることができます。位置を更新した後、ビュー更新関数を呼び出して表示を更新することができます。

したがって、まずglutDisplayFuncで呼び出されるメンバ関数SolarSystem::onUpdate()を実装することができます。

#define TIMEPAST 1 // 1 日分の更新とする
void SolarSystem::onUpdate() {

    for (int i=0; i<STARS_NUM; i++)
        stars[i]->update(TIMEPAST); // 星の位置を更新

    this->onDisplay(); // 表示を更新
}

次に、表示ビューの更新をSolarSystem::onDisplay()で実装します。

void SolarSystem::onDisplay() {

    // ビューポートバッファをクリア
    glClear(GL_COLOR_BUFFER_BIT  |  GL_DEPTH_BUFFER_BIT);
    // 色バッファをクリアして設定
    glClearColor(.7f,.7f,.7f,.1f);
    // 現在の行列を射影行列と指定
    glMatrixMode(GL_PROJECTION);
    // 指定された行列を単位行列に設定
    glLoadIdentity();
    // 現在のビューボリューを指定
    gluPerspective(75.0f, 1.0f, 1.0f, 40000000);
    // 現在の行列をモデルビュー行列と指定
    glMatrixMode(GL_MODELVIEW);
    // 現在の行列を単位行列に設定
    glLoadIdentity();
    // ビュー行列を定義し、現在の行列に乗算する
    gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);

    // 最初の光源(光源 0)を設定
    glEnable(GL_LIGHT0);
    // 照明を有効にする
    glEnable(GL_LIGHTING);
    // 深度テストを有効にし、座標に基づいて被覆されたグラフィックを自動的に隠す
    glEnable(GL_DEPTH_TEST);

    // 惑星を描画
    for (int i=0; i<STARS_NUM; i++)
        stars[i]->draw();

    // メイン関数で表示モードを初期化する際に GLUT_DOUBLE を使用しています
    // 描画が終了した後、ダブルバッファリングのためのバッファスワップを実現するには glutSwapBuffers を使用する必要があります
    glutSwapBuffers();
}

クラスのコンストラクタとデストラクタ

stars.hppで定義されたクラスのコンストラクタは、クラスのメンバ変数を初期化する必要があります。この部分は比較的簡単で、デフォルトのデストラクタでも使用できるので、これらのコンストラクタを自分で実装してください。

Star::Star(GLfloat radius, GLfloat distance,
           GLfloat speed,  GLfloat selfSpeed,
           Star* parent);
Planet::Planet(GLfloat radius, GLfloat distance,
               GLfloat speed,  GLfloat selfSpeed,
               Star* parent, GLfloat rgbColor[3]);
LightPlanet::LightPlanet(GLfloat radius, GLfloat distance,
                         GLfloat speed,  GLfloat selfSpeed,
                         Star* parent,   GLfloat rgbColor[3]);

ヒント:速度変数を初期化する際には、それを角速度に変換してください。変換式は:alpha_speed = 360/speedです。

solarsystem.cppのコンストラクタについては、すべての惑星を初期化する必要があります。ここでは、惑星間のパラメータを提供しておきます。

// 公転半径
#define SUN_RADIUS 48.74
#define MER_RADIUS  7.32
#define VEN_RADIUS 18.15
#define EAR_RADIUS 19.13
#define MOO_RADIUS  6.15
#define MAR_RADIUS 10.19
#define JUP_RADIUS 42.90
#define SAT_RADIUS 36.16
#define URA_RADIUS 25.56
#define NEP_RADIUS 24.78

// 太陽からの距離
#define MER_DIS   62.06
#define VEN_DIS  115.56
#define EAR_DIS  168.00
#define MOO_DIS   26.01
#define MAR_DIS  228.00
#define JUP_DIS  333.40
#define SAT_DIS  428.10
#define URA_DIS 848.00
#define NEP_DIS 949.10

// 移動速度
#define MER_SPEED   87.0
#define VEN_SPEED  225.0
#define EAR_SPEED  365.0
#define MOO_SPEED   30.0
#define MAR_SPEED  687.0
#define JUP_SPEED 1298.4
#define SAT_SPEED 3225.6
#define URA_SPEED 3066.4
#define NEP_SPEED 6014.8

// 自転速度
#define SELFROTATE 3

// 多次元配列の設定を容易にするためのマクロを定義
#define SET_VALUE_3(name, value0, value1, value2) \
                   ((name)[0])=(value0), ((name)[1])=(value1), ((name)[2])=(value2)

// 前の実験で、惑星の enum を定義しました
enum STARS {Sun, Mercury, Venus, Earth, Moon,
    Mars, Jupiter, Saturn, Uranus, Neptune};

ヒント

ここではSET_VALUE_3というマクロを定義しています。同じ目的を達成するために関数を書くこともできると思うかもしれません。

実際、マクロはコンパイル時に全体の置換作業を行いますが、関数を定義すると

これは呼び出し時に関数スタック操作が必要になり、コンパイル時のマクロ処理よりもはるかに効率が悪くなります。

したがって、マクロの方が効率的です。

ただし、マクロは効率的であるとはいえ、使いすぎるとコードが醜くなり、読みにくくなることがあります。一方、適切にマクロを使うことが推奨されます。

したがって、SolarSystemクラスのコンストラクタを実装することができます。ここでは、惑星の色をランダムに選択しています。読者は自分で惑星の色を変更できます。

SolarSystem::SolarSystem() {

    // 透視投影を定義します。前に透視投影の初期化について説明したので、ここでは省略します
    viewX = 0;
    viewY = REST_Y;
    viewZ = REST_Z;
    centerX = centerY = centerZ = 0;
    upX = upY = 0;
    upZ = 1;

    // 太陽
    GLfloat rgbColor[3] = {1, 0, 0};
    stars[Sun]     = new LightPlanet(SUN_RADIUS, 0, 0, SELFROTATE, 0, rgbColor);
    // 水星
    SET_VALUE_3(rgbColor,.2,.2,.5);
    stars[Mercury] = new Planet(MER_RADIUS, MER_DIS, MER_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // 金星
    SET_VALUE_3(rgbColor, 1,.7, 0);
    stars[Venus]   = new Planet(VEN_RADIUS, VEN_DIS, VEN_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // 地球
    SET_VALUE_3(rgbColor, 0, 1, 0);
    stars[Earth]   = new Planet(EAR_RADIUS, EAR_DIS, EAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // 月
    SET_VALUE_3(rgbColor, 1, 1, 0);
    stars[Moon]    = new Planet(MOO_RADIUS, MOO_DIS, MOO_SPEED, SELFROTATE, stars[Earth], rgbColor);
    // 火星
    SET_VALUE_3(rgbColor, 1,.5,.5);
    stars[Mars]    = new Planet(MAR_RADIUS, MAR_DIS, MAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // 木星
    SET_VALUE_3(rgbColor, 1, 1,.5);
    stars[Jupiter] = new Planet(JUP_RADIUS, JUP_DIS, JUP_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // 土星
    SET_VALUE_3(rgbColor,.5, 1,.5);
    stars[Saturn]  = new Planet(SAT_RADIUS, SAT_DIS, SAT_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // 天王星
    SET_VALUE_3(rgbColor,.4,.4,.4);
    stars[Uranus]  = new Planet(URA_RADIUS, URA_DIS, URA_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // 海王星
    SET_VALUE_3(rgbColor,.5,.5, 1);
    stars[Neptune] = new Planet(NEP_RADIUS, NEP_DIS, NEP_SPEED, SELFROTATE, stars[Sun], rgbColor);

}

また、デストラクタで割り当てられたメモリを解放しないでください。

SolarSystem::~SolarSystem() {
    for(int i = 0; i<STARS_NUM; i++)
        delete stars[i];
}

キーボードキーを使った透視変換の実装

透視変更を制御するには、キーボードのw, a, s, d, xの 5 つのキーを使用し、rキーで透視をリセットします。まず、各キー押下後の透視変更の大きさを決定する必要があります。ここでは、マクロOFFSETを定義します。そして、渡されたパラメータkeyを調べることでユーザーのキー押下動作を判断することができます。

#define OFFSET 20
void SolarSystem::onKeyboard(unsigned char key, int x, int y) {

    switch (key)    {
        case 'w': viewY += OFFSET; break; // カメラのY軸位置をOFFSETだけ増やす
        case 's': viewZ += OFFSET; break;
        case 'S': viewZ -= OFFSET; break;
        case 'a': viewX -= OFFSET; break;
        case 'd': viewX += OFFSET; break;
        case 'x': viewY -= OFFSET; break;
        case 'r':
            viewX = 0; viewY = REST_Y; viewZ = REST_Z;
            centerX = centerY = centerZ = 0;
            upX = upY = 0; upZ = 1;
            break;
        case 27: exit(0); break;
        default: break;
    }

}

実行とテスト

主にstars.cppsolarsystem.cppファイルにコードを実装しました。

stars.cppのコードは以下の通りです。

コード全体を表示するにはここをクリック
//
//  stars.cpp
//  solarsystem
//

#include "stars.hpp"
#include <cmath>

#define PI 3.1415926535

Star::Star(GLfloat radius, GLfloat distance,
           GLfloat speed,  GLfloat selfSpeed,
           Star* parent) {
    this->radius = radius;
    this->selfSpeed = selfSpeed;
    this->alphaSelf = this->alpha = 0;
    this->distance = distance;

    for (int i = 0; i < 4; i++)
        this->rgbaColor[i] = 1.0f;

    this->parentStar = parent;
    if (speed > 0)
        this->speed = 360.0f / speed;
    else
        this->speed = 0.0f;
}

void Star::drawStar() {

    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_BLEND);

    int n = 1440;

    glPushMatrix();
    {
        if (parentStar!= 0 && parentStar->distance > 0) {
            glRotatef(parentStar->alpha, 0, 0, 1);
            glTranslatef(parentStar->distance, 0.0, 0.0);
        }
        glBegin(GL_LINES);
        for(int i=0; i<n; ++i)
            glVertex2f(distance * cos(2 * PI * i / n),
                       distance * sin(2 * PI * i / n));
        glEnd();
        glRotatef(alpha, 0, 0, 1);
        glTranslatef(distance, 0.0, 0.0);

        glRotatef(alphaSelf, 0, 0, 1);

        glColor3f(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
        glutSolidSphere(radius, 40, 32);
    }
    glPopMatrix();

}

void Star::update(long timeSpan) {
    alpha += timeSpan * speed;
    alphaSelf += selfSpeed;
}


Planet::Planet(GLfloat radius, GLfloat distance,
               GLfloat speed,  GLfloat selfSpeed,
               Star* parent, GLfloat rgbColor[3]) :
Star(radius, distance, speed, selfSpeed, parent) {
    rgbaColor[0] = rgbColor[0];
    rgbaColor[1] = rgbColor[1];
    rgbaColor[2] = rgbColor[2];
    rgbaColor[3] = 1.0f;
}

void Planet::drawPlanet() {
    GLfloat mat_ambient[]  = {0.0f, 0.0f, 0.5f, 1.0f};
    GLfloat mat_diffuse[]  = {0.0f, 0.0f, 0.5f, 1.0f};
    GLfloat mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
    GLfloat mat_emission[] = {rgbaColor[0], rgbaColor[1], rgbaColor[2], rgbaColor[3]};
    GLfloat mat_shininess  = 90.0f;

    glMaterialfv(GL_FRONT, GL_AMBIENT,   mat_ambient);
    glMaterialfv(GL_FRONT, GL_DIFFUSE,   mat_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR,  mat_specular);
    glMaterialfv(GL_FRONT, GL_EMISSION,  mat_emission);
    glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
}

LightPlanet::LightPlanet(GLfloat radius,    GLfloat distance, GLfloat speed,
                         GLfloat selfSpeed, Star* parent,   GLfloat rgbColor[3]) :
Planet(radius, distance, speed, selfSpeed, parent, rgbColor) {
    ;
}

void LightPlanet::drawLight() {

    GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
    GLfloat light_ambient[]  = {0.0f, 0.0f, 0.0f, 1.0f};
    GLfloat light_diffuse[]  = {1.0f, 1.0f, 1.0f, 1.0f};
    GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};
    glLightfv(GL_LIGHT0, GL_POSITION, light_position);
    glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ambient);
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_diffuse);
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);

}

solarsystem.cppのコードは以下の通りです。

コード全体を表示するにはここをクリック
//
// solarsystem.cpp
// solarsystem
//

#include "solarsystem.hpp"

#define REST 700
#define REST_Z (REST)
#define REST_Y (-REST)

void SolarSystem::onDisplay() {

    glClear(GL_COLOR_BUFFER_BIT  |  GL_DEPTH_BUFFER_BIT);
    glClearColor(.7f,.7f,.7f,.1f);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(75.0f, 1.0f, 1.0f, 40000000);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);

    glEnable(GL_LIGHT0);
    glEnable(GL_LIGHTING);
    glEnable(GL_DEPTH_TEST);

    for (int i=0; i<STARS_NUM; i++)
        stars[i]->draw();

    glutSwapBuffers();
}

#define TIMEPAST 1
void SolarSystem::onUpdate() {

    for (int i=0; i<STARS_NUM; i++)
        stars[i]->update(TIMEPAST);

    this->onDisplay();
}

#define OFFSET 20
void SolarSystem::onKeyboard(unsigned char key, int x, int y) {

    switch (key)    {
        case 'w': viewY += OFFSET; break;
        case 's': viewZ += OFFSET; break;
        case 'S': viewZ -= OFFSET; break;
        case 'a': viewX -= OFFSET; break;
        case 'd': viewX += OFFSET; break;
        case 'x': viewY -= OFFSET; break;
        case 'r':
            viewX = 0; viewY = REST_Y; viewZ = REST_Z;
            centerX = centerY = centerZ = 0;
            upX = upY = 0; upZ = 1;
            break;
        case 27: exit(0); break;
        default: break;
    }

}

#define SUN_RADIUS 48.74
#define MER_RADIUS  7.32
#define VEN_RADIUS 18.15
#define EAR_RADIUS 19.13
#define MOO_RADIUS  6.15
#define MAR_RADIUS 10.19
#define JUP_RADIUS 42.90
#define SAT_RADIUS 36.16
#define URA_RADIUS 25.56
#define NEP_RADIUS 24.78

#define MER_DIS   62.06
#define VEN_DIS  115.56
#define EAR_DIS  168.00
#define MOO_DIS   26.01
#define MAR_DIS  228.00
#define JUP_DIS  333.40
#define SAT_DIS  428.10
#define URA_DIS 848.00
#define NEP_DIS 949.10

#define MER_SPEED   87.0
#define VEN_SPEED  225.0
#define EAR_SPEED  365.0
#define MOO_SPEED   30.0
#define MAR_SPEED  687.0
#define JUP_SPEED 1298.4
#define SAT_SPEED 3225.6
#define URA_SPEED 3066.4
#define NEP_SPEED 6014.8

#define SELFROTATE 3

enum STARS {Sun, Mercury, Venus, Earth, Moon,
    Mars, Jupiter, Saturn, Uranus, Neptune};

#define SET_VALUE_3(name, value0, value1, value2) \
                   ((name)[0])=(value0), ((name)[1])=(value1), ((name)[2])=(value2)

SolarSystem::SolarSystem() {

    viewX = 0;
    viewY = REST_Y;
    viewZ = REST_Z;
    centerX = centerY = centerZ = 0;
    upX = upY = 0;
    upZ = 1;

    GLfloat rgbColor[3] = {1, 0, 0};
    stars[Sun]     = new LightPlanet(SUN_RADIUS, 0, 0, SELFROTATE, 0, rgbColor);

    SET_VALUE_3(rgbColor,.2,.2,.5);
    stars[Mercury] = new Planet(MER_RADIUS, MER_DIS, MER_SPEED, SELFROTATE, stars[Sun], rgbColor);

    SET_VALUE_3(rgbColor, 1,.7, 0);
    stars[Venus]   = new Planet(VEN_RADIUS, VEN_DIS, VEN_SPEED, SELFROTATE, stars[Sun], rgbColor);

    SET_VALUE_3(rgbColor, 0, 1, 0);
    stars[Earth]   = new Planet(EAR_RADIUS, EAR_DIS, EAR_SPEED, SELFROTATE, stars[Sun], rgbColor);

    SET_VALUE_3(rgbColor, 1, 1, 0);
    stars[Moon]    = new Planet(MOO_RADIUS, MOO_DIS, MOO_SPEED, SELFROTATE, stars[Earth], rgbColor);

    SET_VALUE_3(rgbColor, 1,.5,.5);
    stars[Mars]    = new Planet(MAR_RADIUS, MAR_DIS, MAR_SPEED, SELFROTATE, stars[Sun], rgbColor);

    SET_VALUE_3(rgbColor, 1, 1,.5);
    stars[Jupiter] = new Planet(JUP_RADIUS, JUP_DIS, JUP_SPEED, SELFROTATE, stars[Sun], rgbColor);

    SET_VALUE_3(rgbColor,.5, 1,.5);
    stars[Saturn]  = new Planet(SAT_RADIUS, SAT_DIS, SAT_SPEED, SELFROTATE, stars[Sun], rgbColor);

    SET_VALUE_3(rgbColor,.4,.4,.4);
    stars[Uranus]  = new Planet(URA_RADIUS, URA_DIS, URA_SPEED, SELFROTATE, stars[Sun], rgbColor);

    SET_VALUE_3(rgbColor,.5,.5, 1);
    stars[Neptune] = new Planet(NEP_RADIUS, NEP_DIS, NEP_SPEED, SELFROTATE, stars[Sun], rgbColor);

}

SolarSystem::~SolarSystem() {
    for(int i = 0; i<STARS_NUM; i++)
        delete stars[i];
}

ターミナルで実行します。

make && ./solarsystem

結果は以下の図の通りです。

Solar system simulation preview

惑星の色が単調なため、照明効果はあまり明らかではありませんが、依然として見えます。たとえば、黄色い木星の右側には薄い白色の外観があります。

まとめ

このプロジェクトでは、太陽系のシンプルなモデルを実現しました。OpenGL の照明システムを使用することで、惑星が太陽の周りを公転する際の照明効果を見ることができます。また、キーボードを使って透視を調整することができ、さまざまな角度から太陽系を観察することができます。

✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習