Criando o Sistema Solar em OpenGL

C++Beginner
Pratique Agora

Introdução

Neste projeto, criaremos uma simulação do sistema solar usando OpenGL. A simulação incluirá o sol, os planetas e seus movimentos e rotações. Usaremos GLUT (OpenGL Utility Toolkit) para lidar com as funções de janela e entrada, e OpenGL para renderização.

Ao concluir este projeto, você aprenderá:

  • Conceitos básicos de programação gráfica usando OpenGL
  • Como criar modelos 3D e renderizá-los em um ambiente simulado
  • Como lidar com a entrada do usuário e atualizar a simulação de acordo
  • Como implementar um sistema de iluminação básico para aprimorar a qualidade visual da simulação
  • Como organizar o código usando princípios de programação orientada a objetos

Este projeto pressupõe uma compreensão básica da programação C++ e alguma familiaridade com conceitos de programação gráfica. Ele fornecerá uma experiência prática na construção de uma aplicação gráfica simples usando OpenGL.

👀 Pré-visualização

Solar system simulation preview

🎯 Tarefas

Neste projeto, você aprenderá:

  • Como instalar as bibliotecas necessárias e configurar o ambiente de desenvolvimento.
  • Como criar as classes necessárias e implementar a funcionalidade básica para rotação e revolução dos planetas.
  • Como configurar a perspectiva e a projeção para a cena 3D.
  • Como implementar o sistema de iluminação para aprimorar a qualidade visual da simulação.
  • Como lidar com a entrada do usuário para permitir que o usuário controle a perspectiva da simulação.
  • Como testar e refinar a simulação para garantir que ela funcione conforme o esperado.

🏆 Conquistas

Após concluir este projeto, você será capaz de:

  • Aplicar conceitos básicos de programação gráfica usando OpenGL.
  • Criar modelos 3D e renderizá-los em um ambiente simulado.
  • Implementar um sistema de iluminação básico para aprimorar a qualidade visual da simulação.
  • Organizar o código usando princípios de programação orientada a objetos.
  • Demonstrar habilidades de resolução de problemas e depuração.

Entendendo OpenGL e GLUT

OpenGL contém muitas funções de renderização, mas seu propósito de design é independente de qualquer sistema de janelas ou sistema operacional. Portanto, não inclui funções para criar janelas abertas, ler eventos do teclado ou mouse, ou mesmo a funcionalidade mais básica de exibição de janelas. Assim, é completamente impossível criar um programa gráfico completo usando apenas OpenGL. Além disso, a maioria dos programas precisa interagir com o usuário (responder às operações do teclado e do mouse). GLUT fornece essa conveniência.

GLUT significa OpenGL Utility Toolkit. É uma biblioteca de ferramentas para lidar com programas OpenGL, principalmente responsável por lidar com chamadas para o sistema operacional subjacente e operações de E/S. Usar GLUT pode proteger alguns dos detalhes da implementação da GUI do sistema operacional subjacente, e apenas a API GLUT é necessária para criar janelas de aplicativos, lidar com eventos de mouse e teclado, e assim por diante, alcançando assim compatibilidade multiplataforma.

Vamos primeiro instalar GLUT no ambiente do Experimento:

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

A estrutura de um programa GLUT padrão é mostrada no código abaixo:

// 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;

}

Crie um arquivo main.cpp no diretório ~/project/ e escreva o seguinte código:

//
//  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;
}

Dicas

  • O buffer único envolve a execução de todos os comandos de desenho diretamente na janela, o que é lento. Se o poder de processamento do computador não for suficiente e o buffer único for usado, a tela pode piscar.
  • O buffer duplo envolve a execução dos comandos de desenho em um buffer na memória, o que é muito mais rápido. Após a conclusão dos comandos de desenho, os resultados são copiados para a tela por meio de uma troca de buffer. A vantagem dessa abordagem é que, se deixarmos as operações de desenho serem executadas em tempo real com a placa gráfica, quando as tarefas de desenho forem complexas, as operações de E/S se tornarão complexas, resultando em menor desempenho. Em contraste, com o buffer duplo, os resultados de desenho concluídos são enviados diretamente para a placa gráfica para renderização quando o buffer é trocado, o que reduz significativamente a E/S.

Em OpenGL, é recomendado usar GLfloat para representar números de ponto flutuante.

✨ Verificar Solução e Praticar

Design da Classe

Na programação orientada a objetos (POO), é essencial esclarecer com quais objetos estamos lidando primeiro. Obviamente, em todo o sistema celeste, todos são planetas (Estrelas), e a distinção entre planetas e estrelas só requer se eles têm um nó pai. Em segundo lugar, para diferentes planetas, eles geralmente têm seus próprios materiais, e diferentes materiais demonstrarão se eles emitem luz. Portanto, temos um modelo de objeto preliminar. Portanto, dividimos os planetas em: planetas comuns que podem girar e orbitar em torno de um ponto (Estrela), planetas com materiais especiais (Planeta) e planetas que podem emitir luz (PlanetaLuz).

Além disso, para a conveniência da implementação da programação, precisamos fazer algumas suposições sobre o modelo de programação real no mundo real:

  1. A órbita do planeta é circular;
  2. A velocidade de rotação permanece a mesma;
  3. Cada vez que a tela é atualizada, presume-se que um dia se passou.

Primeiramente, podemos considerar as seguintes etapas para implementar a lógica:

  1. Inicializar os objetos planetários;
  2. Inicializar o mecanismo OpenGL, implementar onDraw e onUpdate;
  3. Cada planeta deve ser responsável por lidar com suas próprias propriedades, relação de revolução e desenho relacionado à transformação. Portanto, ao projetar a classe planeta, um método de desenho draw() deve ser fornecido;
  4. O planeta também deve lidar com suas próprias atualizações relacionadas à rotação e revolução para exibição. Portanto, ao projetar a classe planeta, um método de atualização update() também deve ser fornecido;
  5. Chamar o método draw() do planeta em onDraw();
  6. Chamar o método update() do planeta em onUpdate();
  7. Ajustar a exibição de todo o sistema solar com o teclado em onKeyboard().

Além disso, para cada planeta, eles têm os seguintes atributos:

  1. Cor color
  2. Raio de revolução radius
  3. Velocidade de rotação selfSpeed
  4. Velocidade de revolução speed
  5. Distância ao centro do sol distance
  6. Planeta pai em revolução parentStar
  7. Ângulo de rotação atual alphaSelf
  8. Ângulo de revolução atual alpha

No diretório ~/project/, crie um arquivo stars.hpp. Com base na análise acima, podemos projetar o seguinte código de classe:

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(); }
};

Além disso, também precisamos considerar o projeto da classe SolarSystem. No sistema solar, é óbvio que o sistema solar consiste em vários planetas. Para o sistema solar, a atualização da visualização após o movimento do planeta deve ser tratada pelo sistema solar. Portanto, as variáveis membro de SolarSystem devem incluir variáveis contendo os planetas, e as funções membro devem lidar com a atualização da visualização e os eventos de resposta do teclado dentro do sistema solar. Portanto, no diretório ~/project/, crie um arquivo solarsystem.hpp e, nele, podemos projetar a classe 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;
};

Dicas

  • Aqui, usamos a forma tradicional de array para gerenciar todos os planetas em vez de usar o vector em C++, porque a forma tradicional de array é suficiente.
  • Definir o ângulo de visão em OpenGL é um conceito complexo que requer um certo tempo para explicar. Mencionaremos brevemente que definir o ângulo de visão requer pelo menos nove parâmetros aqui. Explicaremos suas funções em detalhes quando os implementarmos mais tarde na próxima seção.

Finalmente, também precisamos considerar as configurações básicas de parâmetros e variáveis.

Em SolarSystem, incluindo o sol, há um total de nove planetas (excluindo Plutão), mas na classe Star que projetamos, cada objeto Star tem os atributos de uma Star, então podemos implementar adicionalmente os satélites desses planetas, como a lua orbitando a Terra. Portanto, consideramos implementar um total de dez planetas. Portanto, podemos definir o seguinte enum para indexar os planetas em um array:

#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];

Também presumimos que as velocidades de rotação são as mesmas, então definimos sua velocidade usando uma macro:

#define TIMEPAST 1
#define SELFROTATE 3

Neste ponto, mova as funções membro não implementadas para o arquivo .cpp correspondente e concluímos o experimento nesta seção.

✨ Verificar Solução e Praticar

Resumo do Código

Vamos resumir o código que precisa ser concluído nos experimentos acima:

Primeiro, em main.cpp, criamos um objeto SolarSystem e, em seguida, delegamos a atualização da exibição, a atualização ociosa e o tratamento de eventos do teclado para glut:

Clique para ver o código completo
//
//  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;
}

Em segundo lugar, em stars.hpp, criamos as classes Star, Planet e LightPlanet:

Clique para ver o código completo
//
//  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 */

No diretório ~/project/, crie um arquivo stars.cpp e preencha as implementações das funções membro correspondentes de stars.hpp:

Clique para ver o código completo
//
//  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:
}

Em solarsystem.hpp, a classe SolarSystem é projetada:

//
// 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;
};

No diretório ~/project/, crie um arquivo solarsystem.cpp e implemente as funções membro correspondentes de solarsystem.hpp:

Clique para ver o código completo
//
// 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:
}

No diretório ~/project/, crie um arquivo Makefile e adicione o seguinte código a ele:

Por favor, insira-o manualmente, não copie e cole diretamente, e lembre-se de usar a tecla <tab> em vez da tecla de espaço.

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

Ao escrever o comando de compilação, preste atenção à colocação de -lglut -lGLU -lGL. Isso ocorre porque o uso da opção -l no compilador g++ é ligeiramente especial.

Por exemplo: foo1.cpp -lz foo2.cpp, se o arquivo de destino foo2.cpp usa funções da biblioteca z, essas funções não serão carregadas diretamente. No entanto, se foo1.o usa funções da biblioteca z, nenhum erro de compilação ocorrerá.

Em outras palavras, todo o processo de vinculação vai da esquerda para a direita. Quando um símbolo de função não resolvido é encontrado em foo1.cpp, ele procura a biblioteca de link do lado direito. Quando encontra a opção z, ele pesquisa em z e encontra a função, completando assim o link sem problemas. Portanto, a biblioteca com a opção -l deve ser colocada à direita de todos os arquivos compilados.

Finalmente, execute no terminal:

make && ./solarsystem

Você pode ver que a janela foi criada, mas não há nada nela (ela exibe o que está atrás da janela). Isso ocorre porque ainda não implementamos o mecanismo de atualização para os gráficos na janela. Continuaremos a concluir o código restante no próximo experimento para fazer com que toda a simulação do sistema solar seja executada.

✨ Verificar Solução e Praticar

O Conceito de Matrizes em OpenGL

Em álgebra linear, estamos familiarizados com o conceito de matrizes, mas nossa compreensão de seu propósito e função específicos pode ser limitada. Então, o que exatamente é uma matriz?

Vamos começar analisando a seguinte equação:

x = Ab

Onde A é uma matriz, e x, b são vetores.

Perspectiva Um:

Se x e b são ambos vetores em nosso espaço tridimensional, o que A faz? Ele transforma b em (ou para) x. Desta perspectiva, uma matriz A pode ser entendida como uma transformação.

Agora, vamos considerar outra equação:

Ax = By

Onde A, B são matrizes, e x, y são vetores.

Perspectiva Dois:

Para dois vetores diferentes x e y, eles são essencialmente os mesmos porque podem ser tornados iguais multiplicando-os com as matrizes A e B. Desta perspectiva, uma matriz A pode ser entendida como um sistema de coordenadas. Em outras palavras, os próprios vetores são únicos, mas definimos um sistema de coordenadas para descrevê-los. Como diferentes sistemas de coordenadas são usados, as coordenadas dos vetores variam. Nesse caso, para o mesmo vetor, ele tem coordenadas diferentes em diferentes sistemas de coordenadas.
A matriz A descreve precisamente um sistema de coordenadas, e a matriz B descreve outro sistema de coordenadas. Quando esses dois sistemas de coordenadas são aplicados a x e y, eles produzem o mesmo resultado, significando que x e y são essencialmente o mesmo vetor, mas com diferentes sistemas de coordenadas.

Combinando essas duas perspectivas, podemos concluir que a essência de uma matriz é descrever o movimento.

No contexto do OpenGL, existe uma matriz responsável pelas transformações de renderização, conhecida como o modo de matriz em OpenGL.

Como mencionado anteriormente, uma matriz pode descrever tanto a transformação de um objeto quanto o sistema de coordenadas em que ele existe. Portanto, ao lidar com diferentes operações, precisamos definir diferentes modos de matriz em OpenGL. Isso é feito usando a função glMatrixMode().

Esta função aceita três modos diferentes: GL_PROJECTION para operações de projeção, GL_MODELVIEW para operações de modelo-visualização e GL_TEXTURE para operações de textura.

GL_PROJECTION informa ao OpenGL que as operações de projeção serão executadas, projetando o objeto em um plano. Quando este modo está ativado, a matriz precisa ser definida como a matriz identidade usando glLoadIdentity(), seguido por operações como definir a perspectiva usando gluPerspective (explicaremos esta função em mais detalhes mais tarde, quando discutirmos o conceito de visualização em OpenGL).

GL_MODELVIEW informa ao OpenGL que as instruções subsequentes serão usadas para representar uma operação baseada em modelo, como definir o ponto de vista da câmera. Da mesma forma, após ativar este modo, precisamos definir o modo de matriz OpenGL como a matriz identidade.

GL_TEXTURE é usado para operações relacionadas à textura, nas quais não nos aprofundaremos neste momento.

Se você não está familiarizado com o conceito de matrizes, pode simplesmente entender glMatrixMode() como declarações para o OpenGL sobre as próximas operações. Antes de renderizar ou rotacionar um objeto, devemos usar glPushMatrix para salvar o ambiente de matriz atual; caso contrário, erros de desenho misteriosos podem ocorrer.

✨ Verificar Solução e Praticar

APIs Comuns de Desenho de Imagens em OpenGL

OpenGL fornece muitas APIs comumente usadas relacionadas ao desenho de gráficos. Aqui, apresentaremos brevemente algumas delas e as usaremos no código a seguir:

  • glEnable(GLenum cap): Esta função é usada para ativar várias funcionalidades fornecidas pelo OpenGL. O parâmetro cap é uma macro interna ao OpenGL, que fornece efeitos como iluminação, névoa e dithering.
  • glPushMatrix() e glPopMatrix(): Essas funções salvam a matriz atual no topo da pilha (salvando a matriz atual).
  • glRotatef(alpha, x, y, z): Representa a rotação da forma atual no sentido anti-horário por alpha graus ao longo do eixo (x, y, z).
  • glTranslatef(distance, x, y): Representa a translação da forma atual por distance ao longo da direção (x, y).
  • glutSolidSphere(GLdouble radius, GLint slices, GLint stacks): Desenha uma esfera, onde radius é o raio, slices é o número de linhas longitudinais e stacks é o número de linhas latitudinais.
  • glBegin() e glEnd(): Quando queremos desenhar uma forma, precisamos chamar essas duas funções antes e depois do desenho. glBegin() especifica o tipo da forma a ser desenhada. Por exemplo, GL_POINTS representa o desenho de pontos, GL_LINES representa o desenho de pontos conectados por linhas, GL_TRIANGLES completa um triângulo com cada três pontos e GL_POLYGON desenha um polígono do primeiro ponto ao n-ésimo ponto, e assim por diante. Por exemplo, quando precisamos desenhar um círculo, podemos usar um polígono com muitos lados para simulá-lo:
// 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();
✨ Verificar Solução e Praticar

Coordenadas de Perspectiva em OpenGL

Na seção anterior, definimos nove variáveis membro na classe SolarSystem em OpenGL:

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

Para entender essas nove variáveis, primeiro precisamos estabelecer o conceito de perspectiva da câmera na programação 3D com OpenGL.

Imagine que as cenas que geralmente assistimos em filmes são, na verdade, filmadas da perspectiva da câmera. Portanto, OpenGL também tem um conceito semelhante. Se imaginarmos a câmera como nossa própria cabeça, então:

  1. viewX, viewY, viewZ correspondem às coordenadas da cabeça (câmera) no sistema de coordenadas do mundo OpenGL;
  2. centerX, centerY, centerZ correspondem às coordenadas do objeto sendo visualizado (visto pela câmera);
  3. upX, upY, upZ correspondem ao vetor de direção apontando para cima a partir do topo da cabeça (topo da câmera) (como podemos inclinar a cabeça para observar um objeto).

Com essa compreensão, você agora tem um conceito do sistema de coordenadas em OpenGL.

Para este experimento, vamos supor que a perspectiva inicial está nas coordenadas (x, -x, x). Portanto, temos:

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

A posição do objeto observado (sol) está em (0,0,0), então no construtor da classe SolarSystem, inicializamos a perspectiva da seguinte forma:

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

Então, podemos definir os nove parâmetros da perspectiva usando a função gluLookAt:

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

Em seguida, vamos dar uma olhada em gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar).

Esta função criará um volume de visualização de perspectiva simétrico, e antes de usar esta função, o modo de matriz do OpenGL precisa ser definido como GL_PROJECTION.

Como mostrado na figura a seguir:

Diagrama de projeção de perspectiva OpenGL

A imagem na janela é capturada pela câmera, e o conteúdo real sendo capturado está no plano distante, enquanto o conteúdo exibido está no plano próximo. Portanto, esta função requer quatro parâmetros:

  • O primeiro parâmetro é o tamanho do ângulo de perspectiva.
  • O segundo parâmetro é a proporção da janela real, como mostrado na figura aspect=w/h.
  • O terceiro parâmetro é a distância para o plano próximo.
  • O quarto parâmetro é a distância para o plano distante.
✨ Verificar Solução e Praticar

Efeitos de Iluminação em OpenGL

OpenGL divide o sistema de iluminação em três partes: fontes de luz, materiais e ambiente de iluminação.

Como o nome sugere, uma fonte de luz é a origem da luz, como o sol;
Materiais referem-se às superfícies de vários objetos que recebem luz, como planetas e satélites no sistema solar, exceto o sol;
O ambiente de iluminação inclui parâmetros adicionais que determinam o efeito de iluminação final, como a reflexão dos raios de luz, que pode ser controlada definindo um parâmetro chamado "brilho ambiente" para tornar a imagem final mais próxima da realidade.

Na física, quando a luz paralela atinge uma superfície lisa, a luz refletida permanece paralela. Esse tipo de reflexão é chamado de "reflexão especular". Por outro lado, a reflexão causada por uma superfície irregular é chamada de "reflexão difusa".

Exemplo de efeitos de iluminação OpenGL

Fontes de Luz

Para implementar o sistema de iluminação em OpenGL, a primeira coisa que precisamos fazer é configurar as fontes de luz. Vale a pena mencionar que OpenGL suporta um número limitado de fontes de luz (oito no total), representadas pelas macros GL_LIGHT0 a GL_LIGHT7. Elas podem ser habilitadas com a função glEnable e desabilitadas com a função glDisable. Por exemplo: glEnable(GL_LIGHT0);

A posição da fonte de luz é definida usando a função glLightfv, por exemplo:

GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // Especifica a posição da fonte de luz 0

A posição é representada por quatro valores, (x, y, z, w). Quando w é 0, significa que a fonte de luz está infinitamente distante. Os valores de x, y e z especificam a direção dessa fonte de luz infinitamente distante.
Quando w não é 0, representa uma fonte de luz posicional, e sua posição é (x/w, y/w, z/w).

Materiais

Para definir o material de um objeto, geralmente são necessários cinco atributos:

  1. A intensidade da luz restante no ambiente após múltiplas reflexões.
  2. A intensidade da luz após a reflexão difusa.
  3. A intensidade da luz após a reflexão especular.
  4. A intensidade da luz emitida por objetos não emissores de luz em OpenGL, que é fraca e não afeta outros objetos.
  5. O expoente especular, que representa a rugosidade do material. Um valor menor significa um material mais áspero, e quando a luz emitida por uma fonte pontual de luz incide sobre ele, são produzidos pontos brilhantes maiores. Por outro lado, um valor maior significa que o material é mais parecido com uma superfície especular, produzindo pontos brilhantes menores.

OpenGL fornece duas versões de funções para definir materiais:

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

A diferença entre elas é que apenas um valor precisa ser definido para o expoente especular, então glMaterialf é usado. Para outras configurações de material que exigem vários valores, uma matriz é usada, e a versão com parâmetros de vetor de ponteiro, glMaterialfv, é usada. Por exemplo:

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);

Ambiente de Iluminação

Por padrão, OpenGL não lida com iluminação. Para habilitar a funcionalidade de iluminação, você precisa usar a macro GL_LIGHTING, ou seja, glEnable(GL_LIGHTING);

✨ Verificar Solução e Praticar

Desenhando Planetas

Ao desenhar um planeta, primeiro precisamos considerar seu ângulo de revolução e ângulo de rotação. Portanto, podemos primeiro implementar a função membro Star::update(long timeSpan) da classe Star no arquivo ~/project/stars.cpp:

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

Após atualizar os ângulos de revolução e rotação, podemos desenhar o planeta específico com base nos parâmetros:

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();

}

Este código usa as funções sin() e cos(), que exigem a inclusão de #include<cmath>.

✨ Verificar Solução e Praticar

Desenho da Iluminação

Para a classe Planet, que representa um corpo celeste não luminoso, precisamos desenhar seu efeito de iluminação. Adicione o seguinte código ao arquivo ~/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);
}

Quanto à classe LightPlanet, que representa um corpo celeste luminoso, não apenas precisamos definir seu material de iluminação, mas também precisamos definir a posição de sua fonte de luz:

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); // Especifica a posição da fonte de luz 0
    glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ambient);  // Representa a intensidade dos raios de luz de várias fontes que atingem o material, após múltiplas reflexões e rastreamento
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_diffuse);  // Intensidade da luz após a reflexão difusa
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // Intensidade da luz após a reflexão especular

}
✨ Verificar Solução e Praticar

Desenhando a Janela

Na seção anterior, mencionamos as duas funções mais importantes para lidar com a exibição de imagens: glutDisplayFunc e glutIdleFunc. glutDisplayFunc executa a função de retorno de chamada quando GLUT determina que o conteúdo da janela precisa ser atualizado, enquanto glutIdleFunc lida com o retorno de chamada quando o loop de eventos está ocioso.

Para fazer todo o sistema solar se mover, precisamos considerar quando atualizar as posições dos planetas e quando atualizar a visualização.

Obviamente, glutDisplayFunc deve se concentrar na atualização da visualização, e quando o evento estiver ocioso, podemos começar a atualizar as posições dos planetas. Depois que as posições são atualizadas, podemos então chamar a função de atualização da visualização para atualizar a exibição.

Portanto, podemos primeiro implementar a função membro SolarSystem::onUpdate() chamada em glutDisplayFunc:

#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
}

Em seguida, a atualização da visualização da exibição é implementada em 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();
}
✨ Verificar Solução e Praticar

Construtores e Destrutores de Classe

Os construtores das classes definidas em stars.hpp precisam inicializar as variáveis membro das classes. Esta parte é relativamente simples e até mesmo destrutores padrão podem ser usados, então implemente esses construtores por conta própria:

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]);

Dica: Observe que, ao inicializar a variável de velocidade, converta-a para velocidade angular. A fórmula de conversão é: alpha_speed = 360/speed.

Para o construtor de solarsystem.cpp, precisamos inicializar todos os planetas. Aqui fornecemos os parâmetros entre os planetas para conveniência:

// 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};

Dica:

Definimos uma macro SET_VALUE_3 aqui. Você pode pensar que podemos escrever uma função para atingir o mesmo propósito.

Na verdade, as macros completam o trabalho geral de substituição no processo de compilação, enquanto definir funções

Isso requer operações de pilha de funções durante a chamada, o que é muito menos eficiente do que o processamento de macro durante o processo de compilação.

Portanto, as macros podem ser mais eficientes.

No entanto, vale a pena notar que, embora as macros possam ser mais eficientes, o uso excessivo pode levar a um código feio e menos legível. O uso apropriado de macros, por outro lado, é incentivado.

Portanto, podemos implementar o construtor da classe SolarSystem, onde as cores dos planetas são selecionadas aleatoriamente. Os leitores podem alterar as cores dos planetas por conta própria:

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);

}

Além disso, não se esqueça de liberar a memória alocada no destrutor:

SolarSystem::~SolarSystem() {
    for(int i = 0; i<STARS_NUM; i++)
        delete stars[i];
}
✨ Verificar Solução e Praticar

Implementação da Mudança de Perspectiva com Teclas do Teclado

Para controlar a mudança de perspectiva, podemos usar as cinco teclas w, a, s, d, x no teclado e usar a tecla r para redefinir a perspectiva. Primeiro, precisamos determinar a magnitude da mudança de perspectiva após cada pressionamento de tecla. Aqui, definimos uma macro OFFSET. Em seguida, podemos determinar o comportamento de pressionamento de tecla do usuário inspecionando o parâmetro key passado.

#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;
    }

}
✨ Verificar Solução e Praticar

Executar e Testar

Implementamos principalmente o código nos arquivos stars.cpp e solarsystem.cpp.

O código para stars.cpp é o seguinte:

Clique para ver o código completo
//
//  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);

}

O código para solarsystem.cpp é o seguinte:

Clique para ver o código completo
//
// 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];
}

Execute no terminal:

make && ./solarsystem

O resultado é mostrado na figura:

Visualização da simulação do sistema solar

Como as cores dos planetas são únicas, o efeito de iluminação não é muito óbvio, mas ainda é visível. Por exemplo, no lado direito de Júpiter, que é amarelo, há uma aparência esbranquiçada.

✨ Verificar Solução e Praticar

Resumo

Neste projeto, alcançamos um modelo simples do sistema solar. Ao usar o sistema de iluminação em OpenGL, somos capazes de ver os efeitos de iluminação dos planetas enquanto eles giram em torno do sol. Além disso, podemos ajustar a perspectiva usando o teclado para observar o sistema solar de diferentes ângulos.