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 프로그램의 구조는 아래 코드에 나와 있습니다:

// Basic header file for using GLUT
#include <GL/glut.h>

// Basic macros for creating a graphical window
#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700

// Callback functions registered with GLUT
void onDisplay(void);
void onUpdate(void);
void onKeyboard(unsigned char key, int x, int y);

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

    // Initialize GLUT and process all command-line arguments
    glutInit(&argc, argv);
    // This function specifies whether to use the RGBA mode or the color index mode for display. It can also specify whether to use a single-buffered or double-buffered window. Here, we use the RGBA and double-buffered window.
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    // Set the position of the top-left corner of the window when created on the screen
    glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
    // Set the width and height of the window when created, for simplicity
    glutInitWindowSize(WIDTH, HEIGHT);
    // Create a window, and the input string is the title of the window
    glutCreateWindow("SolarSystem at LabEx");

    // The prototype of glutDisplayFunc is glutDisplayFunc(void (*func)(void))
    // This is a callback function, and it will be executed whenever GLUT determines that the contents of a window need to be updated and displayed.
    //
    // glutIdleFunc(void (*func)(void)) specifies a function to be executed when the event loop is idle. This callback function takes a function pointer as its only parameter.
    //
    // glutKeyboardFunc(void (*func)(unsigned char key, int x, int y)) associates a key on the keyboard with a function. This function is called when the key is pressed or released.
    //
    // Therefore, the three lines below are actually registering the three key callback functions with GLUT
    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를 사용하는 것이 좋습니다.

클래스 설계

객체 지향 프로그래밍 (OOP programming) 에서는 먼저 다루려는 객체가 무엇인지 명확히 하는 것이 중요합니다. 분명히, 전체 천체 시스템에서 그들은 모두 행성 (Star) 이며, 행성과 별의 구분은 부모 노드가 있는지 여부에 따라 결정됩니다. 둘째, 서로 다른 행성의 경우, 일반적으로 자체 재료를 가지고 있으며, 서로 다른 재료는 빛을 방출하는지 여부를 보여줍니다. 따라서, 우리는 예비 객체 모델을 가지고 있습니다. 따라서, 우리는 행성을 다음과 같이 나눕니다: 회전하고 한 점을 중심으로 공전할 수 있는 일반 행성 (Star), 특수 재료를 가진 행성 (Planet), 빛을 방출할 수 있는 행성 (LightPlanet).

또한, 프로그래밍 구현의 편의를 위해 실제 프로그래밍 모델에 대한 몇 가지 가정을 해야 합니다:

  1. 행성의 궤도는 원형입니다;
  2. 회전 속도는 동일하게 유지됩니다;
  3. 화면이 새로 고쳐질 때마다 하루가 지났다고 가정합니다.

먼저, 논리를 구현하기 위해 다음 단계를 고려할 수 있습니다:

  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:
    // Planet's revolution radius
    GLfloat radius;
    // Planet's revolution and rotation speed
    GLfloat speed, selfSpeed;
    // Distance from the center of the planet to the parent planet's center
    GLfloat distance;
    // Planet's color
    GLfloat rgbaColor[4];

    // Parent planet
    Star* parentStar;

    // Constructor, when constructing a planet, the rotation radius, rotation speed, rotation speed, and revolving (parent planet) should be provided
    Star(GLfloat radius, GLfloat distance,
         GLfloat speed,  GLfloat selfSpeed,
         Star* parentStar);
    // Draw common planet's movement, rotation, and other activities
    void drawStar();
    // Provide default implementation to call drawStar()
    virtual void draw() { drawStar(); }
    // The parameter is the time span for each screen refresh
    virtual void update(long timeSpan);
protected:
    GLfloat alphaSelf, alpha;
};
class Planet : public Star {
public:
    // Constructor
    Planet(GLfloat radius, GLfloat distance,
           GLfloat speed,  GLfloat selfSpeed,
           Star* parentStar, GLfloat rgbColor[3]);
    // Add materials to planets with their own materials
    void drawPlanet();
    // Continue to open the rewrite function to its subclasses
    virtual void draw() { drawPlanet(); drawStar(); }
};
class LightPlanet : public Planet {
public:
    LightPlanet(GLfloat Radius, GLfloat Distance,
                GLfloat Speed,  GLfloat SelfSpeed,
                Star* ParentStar, GLfloat rgbColor[]);
    // Add lighting to stars that provide light sources
    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];

    // Define the parameters of the viewing angle
    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,        // Sun
    Mercury,    // Mercury
    Venus,      // Venus
    Earth,      // Earth
    Moon,       // Moon
    Mars,       // Mars
    Jupiter,    // Jupiter
    Saturn,     // Saturn
    Uranus,     // Uranus
    Neptune     // 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에서 Star, Planet, 및 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는 벡터입니다.

관점 1:

만약 xb가 모두 3 차원 공간의 벡터라면, A는 무엇을 할까요? bx로 변환합니다. 이 관점에서, 행렬 A는 변환으로 이해될 수 있습니다.

이제 다른 방정식을 고려해 봅시다:

Ax = By

여기서 A, B는 행렬이고, x, y는 벡터입니다.

관점 2:

두 개의 서로 다른 벡터 xy는 본질적으로 동일합니다. 왜냐하면 행렬 AB를 곱하여 같게 만들 수 있기 때문입니다. 이 관점에서, 행렬 A는 좌표계로 이해될 수 있습니다. 즉, 벡터 자체는 고유하지만, 우리는 그들을 설명하기 위해 좌표계를 정의합니다. 서로 다른 좌표계가 사용되므로, 벡터의 좌표는 달라집니다. 이 경우, 동일한 벡터는 서로 다른 좌표계에서 서로 다른 좌표를 갖습니다. 행렬 A는 정확히 좌표계를 설명하고, 행렬 B는 다른 좌표계를 설명합니다. 이 두 좌표계가 xy에 적용되면, 동일한 결과를 생성합니다. 즉, xy는 본질적으로 동일한 벡터이지만 서로 다른 좌표계를 갖습니다.

이 두 관점을 결합하면, 행렬의 본질은 움직임을 설명하는 것이라고 결론 내릴 수 있습니다.

OpenGL 의 맥락에서, 렌더링 변환을 담당하는 행렬이 있으며, 이를 OpenGL 에서 행렬 모드 (matrix mode) 라고 합니다.

앞서 언급했듯이, 행렬은 객체의 변환과 객체가 존재하는 좌표계를 모두 설명할 수 있습니다. 따라서, 서로 다른 연산을 처리할 때, OpenGL 에서 서로 다른 행렬 모드를 설정해야 합니다. 이는 glMatrixMode() 함수를 사용하여 수행됩니다.

이 함수는 세 가지 다른 모드를 허용합니다: 투영 연산을 위한 GL_PROJECTION, 모델 - 뷰 연산을 위한 GL_MODELVIEW, 텍스처 연산을 위한 GL_TEXTURE.

GL_PROJECTION은 OpenGL 에 투영 연산이 수행될 것이며, 객체를 평면에 투영할 것이라고 알려줍니다. 이 모드가 활성화되면, glLoadIdentity()를 사용하여 행렬을 항등 행렬로 설정해야 하며, 그 다음 gluPerspective를 사용하여 원근감을 설정하는 등의 연산을 수행합니다 (OpenGL 에서 뷰잉 (viewing) 의 개념을 논의할 때 이 함수에 대해 자세히 설명하겠습니다).

GL_MODELVIEW는 OpenGL 에 후속 문이 모델 기반 연산, 예를 들어 카메라의 시점을 설정하는 데 사용될 것이라고 알려줍니다. 마찬가지로, 이 모드를 활성화한 후, OpenGL 행렬 모드를 항등 행렬로 설정해야 합니다.

GL_TEXTURE는 텍스처 관련 연산에 사용되며, 현재는 자세히 다루지 않겠습니다.

행렬의 개념에 익숙하지 않다면, glMatrixMode()를 OpenGL 에 대한 다가오는 연산에 대한 선언으로 간단히 이해할 수 있습니다. 객체를 렌더링하거나 회전하기 전에, 현재 행렬 환경을 저장하기 위해 glPushMatrix를 사용해야 합니다; 그렇지 않으면, 알 수 없는 그리기 오류가 발생할 수 있습니다.

OpenGL 이미지 그리기 API (API)

OpenGL 은 그래픽 그리기와 관련된 많은 일반적으로 사용되는 API 를 제공합니다. 여기서는 그 중 몇 가지를 간략하게 소개하고 다음 코드에서 사용하겠습니다:

  • glEnable(GLenum cap): 이 함수는 OpenGL 에서 제공하는 다양한 기능을 활성화하는 데 사용됩니다. 매개변수 cap 은 OpenGL 내부의 매크로로, 조명, 안개, 디더링 (dithering) 과 같은 효과를 제공합니다.
  • 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(): 도형을 그리려면, 그리기 전후에 이 두 함수를 호출해야 합니다. glBegin()은 그려질 도형의 유형을 지정합니다. 예를 들어, GL_POINTS는 점을 그리는 것을 나타내고, GL_LINES는 선으로 연결된 점을 그리는 것을 나타내며, GL_TRIANGLES는 각 세 점으로 삼각형을 완성하고, GL_POLYGON은 첫 번째 점에서 n 번째 점까지의 다각형을 그리는 등입니다. 예를 들어, 원을 그릴 필요가 있을 때, 많은 변을 가진 다각형을 사용하여 시뮬레이션할 수 있습니다:
// r is the radius, n is the number of sides
glBegin(GL_POLYGON);
    for(i=0; i<n; ++i)
        glVertex2f(r*cos(2*PI/n*i), r*sin(2*PI/n*i));
glEnd();

OpenGL 원근 좌표 (Perspective Coordinates) 이해

이전 섹션에서, 우리는 OpenGL 에서 SolarSystem 클래스에 아홉 개의 멤버 변수를 정의했습니다:

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

이 아홉 개의 변수를 이해하려면, 먼저 OpenGL 을 사용한 3D 프로그래밍에서 카메라 원근법의 개념을 확립해야 합니다.

우리가 영화에서 일반적으로 보는 장면은 실제로 카메라의 시점에서 촬영된 것이라고 상상해 보세요. 따라서 OpenGL 에도 유사한 개념이 있습니다. 카메라를 우리 자신의 머리라고 상상하면 다음과 같습니다:

  1. viewX, viewY, viewZ는 OpenGL 세계 좌표계에서 머리 (카메라) 의 좌표에 해당합니다;
  2. centerX, centerY, centerZ는 관찰 대상 (카메라가 보는) 의 좌표에 해당합니다;
  3. upX, upY, upZ는 머리 (카메라 상단) 의 위쪽을 가리키는 방향 벡터에 해당합니다 (우리가 머리를 기울여 객체를 관찰할 수 있는 것처럼).

이러한 이해를 바탕으로, 이제 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 함수를 사용하여 원근법의 아홉 가지 매개변수를 설정할 수 있습니다:

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

창의 이미지는 카메라에 의해 캡처되며, 실제로 캡처되는 내용은 원거리 평면에 있고, 표시되는 내용은 근거리 평면에 있습니다. 따라서 이 함수에는 네 개의 매개변수가 필요합니다:

  • 첫 번째 매개변수는 원근 각도의 크기입니다.
  • 두 번째 매개변수는 실제 창의 종횡비이며, 그림에서 aspect=w/h입니다.
  • 세 번째 매개변수는 근거리 평면까지의 거리입니다.
  • 네 번째 매개변수는 원거리 평면까지의 거리입니다.

OpenGL 조명 효과 (Lighting Effects) 구현 방법

OpenGL 은 조명 시스템을 광원, 재료, 조명 환경의 세 부분으로 나눕니다.

이름에서 알 수 있듯이, 광원은 태양과 같은 빛의 근원입니다; 재료는 태양을 제외한 태양계의 행성과 위성과 같이 빛을 받는 다양한 물체의 표면을 나타냅니다; 조명 환경에는 최종 조명 효과를 결정하는 추가 매개변수가 포함됩니다. 예를 들어, "주변 밝기"라는 매개변수를 설정하여 최종 이미지를 현실에 더 가깝게 만들 수 있는 광선의 반사가 있습니다.

물리학에서, 평행광이 매끄러운 표면에 부딪히면 반사된 빛은 평행을 유지합니다. 이러한 유형의 반사를 "경면 반사 (specular reflection)"라고 합니다. 반면에, 고르지 않은 표면으로 인한 반사는 "확산 반사 (diffuse reflection)"라고 합니다.

OpenGL lighting effects example

광원

OpenGL 에서 조명 시스템을 구현하려면, 먼저 광원을 설정해야 합니다. OpenGL 은 GL_LIGHT0 에서 GL_LIGHT7 까지의 매크로로 표시되는 제한된 수의 광원 (총 8 개) 을 지원한다는 점에 유의해야 합니다. glEnable 함수로 활성화하고 glDisable 함수로 비활성화할 수 있습니다. 예를 들어: glEnable(GL_LIGHT0);

광원의 위치는 glLightfv 함수를 사용하여 설정합니다. 예를 들어:

GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // Specify the position of light source 0

위치는 네 개의 값 (x, y, z, w) 으로 표시됩니다. w 가 0 이면 광원이 무한히 멀리 떨어져 있음을 의미합니다. x, y, z 의 값은 이 무한히 먼 광원의 방향을 지정합니다. w 가 0 이 아니면 위치 광원을 나타내며, 그 위치는 (x/w, y/w, z/w) 입니다.

재료

객체의 재료를 설정하려면 일반적으로 다섯 가지 속성이 필요합니다:

  1. 여러 반사 후 환경에 남아있는 빛의 강도.
  2. 확산 반사 후 빛의 강도.
  3. 경면 반사 후 빛의 강도.
  4. OpenGL 에서 빛을 방출하지 않는 객체에서 방출되는 빛의 강도, 이는 약하며 다른 객체에 영향을 미치지 않습니다.
  5. 경면 지수 (specular exponent), 이는 재료의 거칠기를 나타냅니다. 값이 작을수록 재료가 거칠어지고, 점 광원에서 방출되는 빛이 비추면 더 큰 밝은 점이 생성됩니다. 반대로, 값이 클수록 재료가 경면 표면과 더 유사해져 더 작은 밝은 점이 생성됩니다.

OpenGL 은 재료를 설정하는 두 가지 버전의 함수를 제공합니다:

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

그들 간의 차이점은 경면 지수에 대해 하나의 값만 설정해야 하므로 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) 을 사용해야 합니다.

행성 그리기 (Drawing Planets) - OpenGL 튜토리얼

행성을 그릴 때, 우리는 먼저 그 공전 각도와 자전 각도를 고려해야 합니다. 따라서, ~/project/stars.cpp 파일에서 Star 클래스의 멤버 함수 Star::update(long timeSpan)을 먼저 구현할 수 있습니다:

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

공전 및 자전 각도를 업데이트한 후, 매개변수를 기반으로 특정 행성을 그릴 수 있습니다:

void Star::drawStar() {

    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_BLEND);

    int n = 1440;

    // Save the current OpenGL matrix environment
    glPushMatrix();
    {
        // Revolution

        // If it is a planet and the distance is not 0, then translate it to the origin by a radius
        // This part is used for satellites
        if (parentStar != 0 && parentStar->distance > 0) {
            // Rotate the drawing graphics around the z-axis by alpha
            glRotatef(parentStar->alpha, 0, 0, 1);
            // Translate in the x-axis direction by distance, while y and z directions remain unchanged
            glTranslatef(parentStar->distance, 0.0, 0.0);
        }
        // Draw the orbit
        glBegin(GL_LINES);
        for(int i=0; i<n; ++i)
            glVertex2f(distance * cos(2 * PI * i / n),
                       distance * sin(2 * PI * i / n));
        glEnd();
        // Rotate around the z-axis by alpha
        glRotatef(alpha, 0, 0, 1);
        // Translate in the x-axis direction by distance, while y and z directions remain unchanged
        glTranslatef(distance, 0.0, 0.0);

        // Rotation
        glRotatef(alphaSelf, 0, 0, 1);

        // Draw the planet color
        glColor3f(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
        glutSolidSphere(radius, 40, 32);
    }
    // Restore the matrix environment before drawing
    glPopMatrix();

}

이 코드는 sin() 및 cos() 함수를 사용하며, #include<cmath>를 포함해야 합니다.

조명 렌더링 (Illumination) 구현 - OpenGL 강좌

비발광 천체를 나타내는 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); // Specify the position of light source 0
    glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ambient);  // Represents the intensity of light rays from various sources that reach the material, after multiple reflections and tracing
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_diffuse);  // Intensity of light after diffuse reflection
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // Intensity of light after specular reflection

}

윈도우 그리기 (Drawing the Window) - OpenGL 기초

이전 섹션에서 이미지 표시를 처리하는 데 가장 중요한 두 가지 함수인 glutDisplayFuncglutIdleFunc에 대해 언급했습니다. glutDisplayFunc는 GLUT 가 윈도우 내용을 업데이트해야 한다고 판단할 때 콜백 함수를 실행하고, glutIdleFunc는 이벤트 루프가 유휴 상태일 때 콜백을 처리합니다.

전체 태양계를 움직이게 하려면, 행성의 위치를 언제 업데이트하고 뷰를 언제 새로 고칠지 고려해야 합니다.

분명히, glutDisplayFunc는 뷰를 새로 고치는 데 집중해야 하며, 이벤트가 유휴 상태일 때 행성의 위치 업데이트를 시작할 수 있습니다. 위치가 업데이트된 후에는 디스플레이를 새로 고치기 위해 뷰 새로 고침 함수를 호출할 수 있습니다.

따라서, 먼저 glutDisplayFunc에서 호출되는 멤버 함수 SolarSystem::onUpdate()를 구현할 수 있습니다:

#define TIMEPAST 1 // Assume that each update takes one day
void SolarSystem::onUpdate() {

    for (int i=0; i<STARS_NUM; i++)
        stars[i]->update(TIMEPAST); // Update the positions of the stars

    this->onDisplay(); // Refresh the display
}

다음으로, 디스플레이 뷰의 새로 고침은 SolarSystem::onDisplay()에서 구현됩니다:

void SolarSystem::onDisplay() {

    // Clear the viewport buffer
    glClear(GL_COLOR_BUFFER_BIT  |  GL_DEPTH_BUFFER_BIT);
    // Clear and set the color buffer
    glClearColor(.7f, .7f, .7f, .1f);
    // Specify the current matrix as the projection matrix
    glMatrixMode(GL_PROJECTION);
    // Specify the specified matrix as an identity matrix
    glLoadIdentity();
    // Specify the current view volume
    gluPerspective(75.0f, 1.0f, 1.0f, 40000000);
    // Specify the current matrix as the model view matrix
    glMatrixMode(GL_MODELVIEW);
    // Specify the current matrix as an identity matrix
    glLoadIdentity();
    // Define the view matrix and multiply it with the current matrix
    gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);

    // Set the first light source (light source 0)
    glEnable(GL_LIGHT0);
    // Enable lighting
    glEnable(GL_LIGHTING);
    // Enable depth testing, automatically hide covered graphics based on the coordinates
    glEnable(GL_DEPTH_TEST);

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

    // We used GLUT_DOUBLE when initializing the display mode in the main function
    // We need to use glutSwapBuffers to implement buffer swapping for double buffering after drawing is finished
    glutSwapBuffers();
}

클래스 생성자 및 소멸자 - C++ 강좌

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의 생성자의 경우, 모든 행성을 초기화해야 합니다. 편의를 위해 행성 간의 매개변수를 제공합니다:

// Revolution radius
#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

// Distance from the sun
#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

// Movement speed
#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

// Self-rotation speed
#define SELFROTATE 3

// Define a macro to facilitate the setting of a multi-dimensional array
#define SET_VALUE_3(name, value0, value1, value2) \
                   ((name)[0])=(value0), ((name)[1])=(value1), ((name)[2])=(value2)

// In the previous experiment, we defined the enum of the planets
enum STARS {Sun, Mercury, Venus, Earth, Moon,
    Mars, Jupiter, Saturn, Uranus, Neptune};

힌트:

여기서는 매크로 SET_VALUE_3을 정의합니다. 동일한 목적을 달성하기 위해 함수를 작성할 수 있다고 생각할 수 있습니다.

사실, 매크로는 컴파일 과정에서 전체 대체 작업을 완료하는 반면, 함수를 정의하는 것은

호출 중에 함수 스택 연산이 필요하며, 이는 컴파일 과정에서 매크로 처리보다 훨씬 덜 효율적입니다.

따라서 매크로가 더 효율적일 수 있습니다.

그러나 매크로가 더 효율적일 수 있지만, 과도한 사용은 보기 흉하고 읽기 어려운 코드로 이어질 수 있다는 점에 유의해야 합니다. 적절한 매크로 사용은 권장됩니다.

따라서, 행성의 색상이 무작위로 선택되는 SolarSystem 클래스의 생성자를 구현할 수 있습니다. 독자는 스스로 행성의 색상을 변경할 수 있습니다:

SolarSystem::SolarSystem() {

    // Define the perspective view, as we have discussed the initialization of the perspective view before
    viewX = 0;
    viewY = REST_Y;
    viewZ = REST_Z;
    centerX = centerY = centerZ = 0;
    upX = upY = 0;
    upZ = 1;

    // Sun
    GLfloat rgbColor[3] = {1, 0, 0};
    stars[Sun]     = new LightPlanet(SUN_RADIUS, 0, 0, SELFROTATE, 0, rgbColor);
    // Mercury
    SET_VALUE_3(rgbColor, .2, .2, .5);
    stars[Mercury] = new Planet(MER_RADIUS, MER_DIS, MER_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Venus
    SET_VALUE_3(rgbColor, 1, .7, 0);
    stars[Venus]   = new Planet(VEN_RADIUS, VEN_DIS, VEN_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Earth
    SET_VALUE_3(rgbColor, 0, 1, 0);
    stars[Earth]   = new Planet(EAR_RADIUS, EAR_DIS, EAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Moon
    SET_VALUE_3(rgbColor, 1, 1, 0);
    stars[Moon]    = new Planet(MOO_RADIUS, MOO_DIS, MOO_SPEED, SELFROTATE, stars[Earth], rgbColor);
    // Mars
    SET_VALUE_3(rgbColor, 1, .5, .5);
    stars[Mars]    = new Planet(MAR_RADIUS, MAR_DIS, MAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Jupiter
    SET_VALUE_3(rgbColor, 1, 1, .5);
    stars[Jupiter] = new Planet(JUP_RADIUS, JUP_DIS, JUP_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Saturn
    SET_VALUE_3(rgbColor, .5, 1, .5);
    stars[Saturn]  = new Planet(SAT_RADIUS, SAT_DIS, SAT_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Uranus
    SET_VALUE_3(rgbColor, .4, .4, .4);
    stars[Uranus]  = new Planet(URA_RADIUS, URA_DIS, URA_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Neptune
    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];
}

키보드 키를 이용한 원근법 변경 구현 - OpenGL 튜토리얼

원근감 변경을 제어하기 위해 키보드의 다섯 개의 키 w, a, s, d, x를 사용할 수 있으며, r 키를 사용하여 원근감을 재설정할 수 있습니다. 먼저, 각 키를 누른 후 원근감 변경의 크기를 결정해야 합니다. 여기서는 매크로 OFFSET을 정의합니다. 그런 다음, 전달된 매개변수 key를 검사하여 사용자의 키 누름 동작을 결정할 수 있습니다.

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

    switch (key)    {
        case 'w': viewY += OFFSET; break; // Increase the camera's Y-axis position by 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

결과는 그림과 같습니다:

태양계 시뮬레이션 미리보기

행성의 색상이 단색이므로 조명 효과가 그다지 뚜렷하지 않지만 여전히 보입니다. 예를 들어, 노란색 목성의 오른쪽에 희끄무레한 모습이 있습니다.

요약

이 프로젝트에서 우리는 태양계의 간단한 모델을 구현했습니다. OpenGL 의 조명 시스템을 사용하여 행성이 태양 주위를 공전할 때의 조명 효과를 볼 수 있습니다. 또한, 키보드를 사용하여 원근감을 조절하여 다양한 각도에서 태양계를 관찰할 수 있습니다.

✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습