Creando el sistema solar con OpenGL

C++C++Beginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este proyecto, crearemos una simulación del sistema solar utilizando OpenGL. La simulación incluirá el sol, los planetas y sus movimientos y rotaciones. Utilizaremos GLUT (OpenGL Utility Toolkit) para manejar las funciones de ventana y entrada, y OpenGL para la representación.

Al completar este proyecto, aprenderás:

  • Conceptos básicos de programación gráfica utilizando OpenGL
  • Cómo crear modelos 3D y representarlos en un entorno simulado
  • Cómo manejar la entrada del usuario y actualizar la simulación en consecuencia
  • Cómo implementar un sistema de iluminación básico para mejorar la calidad visual de la simulación
  • Cómo organizar el código utilizando principios de programación orientada a objetos

Este proyecto asume un conocimiento básico de programación en C++ y alguna familiaridad con conceptos de programación gráfica. Proporcionará una experiencia práctica en la construcción de una aplicación gráfica simple utilizando OpenGL.

👀 Vista previa

Vista previa de la simulación del sistema solar

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo instalar las bibliotecas necesarias y configurar el entorno de desarrollo.
  • Cómo crear las clases necesarias e implementar la funcionalidad básica de rotación y revolución de los planetas.
  • Cómo configurar la perspectiva y la proyección para la escena 3D.
  • Cómo implementar el sistema de iluminación para mejorar la calidad visual de la simulación.
  • Cómo manejar la entrada del usuario para permitir que el usuario controle la perspectiva de la simulación.
  • Cómo probar y mejorar la simulación para asegurarse de que funcione como se espera.

🏆 Logros

Después de completar este proyecto, serás capaz de:

  • Aplicar conceptos básicos de programación gráfica utilizando OpenGL.
  • Crear modelos 3D y representarlos en un entorno simulado.
  • Implementar un sistema de iluminación básico para mejorar la calidad visual de la simulación.
  • Organizar el código utilizando principios de programación orientada a objetos.
  • Demostrar habilidades de resolución de problemas y depuración.

Comprendiendo OpenGL y GLUT

OpenGL contiene muchas funciones de representación, pero su propósito de diseño es independiente de cualquier sistema de ventanas o sistema operativo. Por lo tanto, no incluye funciones para crear ventanas abiertas, leer eventos del teclado o del mouse, o incluso la funcionalidad más básica de mostrar ventanas. Por lo tanto, es completamente imposible crear un programa gráfico completo utilizando solo OpenGL. Además, la mayoría de los programas deben interactuar con el usuario (reaccionar a las operaciones del teclado y del mouse). GLUT ofrece esta conveniencia.

GLUT significa OpenGL Utility Toolkit. Es una biblioteca de herramientas para manejar programas OpenGL, principalmente responsable de manejar las llamadas al sistema operativo subyacente y las operaciones de E/S. Utilizar GLUT puede ocultar algunos de los detalles de la implementación de la interfaz gráfica de usuario del sistema operativo subyacente, y solo se necesita la API de GLUT para crear ventanas de aplicación, manejar eventos del mouse y del teclado, etc., logrando así la compatibilidad multiplataforma.

Vamos a instalar primero GLUT en el entorno de Experimentación:

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

La estructura de un programa GLUT estándar se muestra en el código siguiente:

// Encabezado básico para utilizar GLUT
#include <GL/glut.h>

// Macros básicas para crear una ventana gráfica
#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700

// Funciones de devolución de llamada registradas con GLUT
void onDisplay(void);
void onUpdate(void);
void onKeyboard(unsigned char key, int x, int y);

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

    // Inicializar GLUT y procesar todos los argumentos de línea de comandos
    glutInit(&argc, argv);
    // Esta función especifica si se utiliza el modo RGBA o el modo de índice de color para la representación. También se puede especificar si se utiliza una ventana simple o doble búfer. Aquí, usamos el RGBA y una ventana doble búfer.
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    // Establece la posición de la esquina superior izquierda de la ventana cuando se crea en la pantalla
    glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
    // Establece el ancho y el alto de la ventana cuando se crea, por simplicidad
    glutInitWindowSize(WIDTH, HEIGHT);
    // Crea una ventana, y la cadena de entrada es el título de la ventana
    glutCreateWindow("SolarSystem at LabEx");

    // El prototipo de glutDisplayFunc es glutDisplayFunc(void (*func)(void))
    // Esta es una función de devolución de llamada, y se ejecutará cada vez que GLUT determine que el contenido de una ventana necesita ser actualizado y representado.
    //
    // glutIdleFunc(void (*func)(void)) especifica una función a ejecutar cuando el bucle de eventos está inactivo. Esta función de devolución de llamada toma un puntero a función como único parámetro.
    //
    // glutKeyboardFunc(void (*func)(unsigned char key, int x, int y)) asocia una tecla del teclado con una función. Esta función se llama cuando se presiona o suelta la tecla.
    //
    // Por lo tanto, las tres líneas siguientes están en realidad registrando las tres funciones de devolución de llamada de teclado con GLUT
    glutDisplayFunc(onDisplay);
    glutIdleFunc(onUpdate);
    glutKeyboardFunc(onKeyboard);

    glutMainLoop();
    return 0;

}

Crea un archivo main.cpp en el directorio ~/project/ y escribe el siguiente 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;
}

Consejos

  • El buffer simple implica ejecutar todos los comandos de dibujo directamente en la ventana, lo que es lento. Si la capacidad de procesamiento de la computadora no es suficiente y se utiliza el buffer simple, la pantalla puede parpadear.
  • El buffer doble implica ejecutar los comandos de dibujo en un buffer en memoria, lo que es mucho más rápido. Una vez completados los comandos de dibujo, los resultados se copian a la pantalla a través de un intercambio de buffers. La ventaja de este enfoque es que si hacemos que las operaciones de dibujo se ejecuten en tiempo real con la tarjeta gráfica, cuando las tareas de dibujo son complejas, las operaciones de E/S se vuelven complejas, lo que resulta en un rendimiento más bajo. En contraste, con el buffer doble, los resultados de dibujo completados se envían directamente a la tarjeta gráfica para su representación cuando se intercambia el buffer, lo que reduce significativamente la E/S.

En OpenGL, se recomienda utilizar GLfloat para representar números de punto flotante.

✨ Revisar Solución y Practicar

Diseño de clases

En la programación orientada a objetos (OOP), es esencial aclarar primero qué objetos estamos manejando. Obviamente, en todo el sistema celestial, todos son planetas (Star), y la distinción entre planetas y estrellas solo depende de si tienen un nodo padre. Segundo, para diferentes planetas, generalmente tienen sus propios materiales, y diferentes materiales demostrarán si emiten luz o no. Por lo tanto, tenemos un modelo de objeto preliminar. Por lo tanto, dividimos los planetas en: planetas ordinarios que pueden rotar y girar alrededor de un punto (Star), planetas con materiales especiales (Planet) y planetas que pueden emitir luz (LightPlanet).

Además, para la conveniencia de la implementación de la programación, necesitamos hacer algunas suposiciones sobre el modelo de programación real en el mundo real:

  1. La órbita del planeta es circular;
  2. La velocidad de rotación permanece constante;
  3. Cada vez que se actualiza la pantalla, se asume que ha pasado un día.

En primer lugar, podemos considerar los siguientes pasos para implementar la lógica:

  1. Inicializar los objetos de los planetas;
  2. Inicializar el motor OpenGL, implementar onDraw y onUpdate;
  3. Cada planeta debe ser responsable de manejar sus propias propiedades, relación de revolución y dibujo relacionado con la transformación. Por lo tanto, al diseñar la clase del planeta, se debe proporcionar un método de dibujo draw();
  4. El planeta también debe manejar sus propias actualizaciones relacionadas con la rotación y la revolución para la visualización. Por lo tanto, al diseñar la clase del planeta, también se debe proporcionar un método de actualización update();
  5. Llamar al método draw() del planeta en onDraw();
  6. Llamar al método update() del planeta en onUpdate();
  7. Ajustar la visualización del sistema solar completo con el teclado en onKeyboard().

Además, para cada planeta, tienen los siguientes atributos:

  1. Color color
  2. Radio de revolución radius
  3. Velocidad de rotación selfSpeed
  4. Velocidad de revolución speed
  5. Distancia al centro del sol distance
  6. Planeta padre de revolución parentStar
  7. Ángulo de rotación actual alphaSelf
  8. Ángulo de revolución actual alpha

En el directorio ~/project/, cree un archivo stars.hpp. Basado en el análisis anterior, podemos diseñar el siguiente código de clase:

class Star {
public:
    // Radio de revolución del planeta
    GLfloat radius;
    // Velocidad de revolución y rotación del planeta
    GLfloat speed, selfSpeed;
    // Distancia desde el centro del planeta hasta el centro del planeta padre
    GLfloat distance;
    // Color del planeta
    GLfloat rgbaColor[4];

    // Planeta padre
    Star* parentStar;

    // Constructor, cuando se construye un planeta, se deben proporcionar el radio de rotación, la velocidad de rotación, la velocidad de rotación y la revolución (planeta padre)
    Star(GLfloat radius, GLfloat distance,
         GLfloat speed,  GLfloat selfSpeed,
         Star* parentStar);
    // Dibujar los movimientos, rotaciones y otras actividades del planeta común
    void drawStar();
    // Proporcionar una implementación predeterminada para llamar a drawStar()
    virtual void draw() { drawStar(); }
    // El parámetro es el intervalo de tiempo para cada actualización de pantalla
    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]);
    // Agregar materiales a los planetas con sus propios materiales
    void drawPlanet();
    // Continuar abriendo la función de reescritura a sus subclases
    virtual void draw() { drawPlanet(); drawStar(); }
};
class LightPlanet : public Planet {
public:
    LightPlanet(GLfloat Radius, GLfloat Distance,
                GLfloat Speed,  GLfloat SelfSpeed,
                Star* ParentStar, GLfloat rgbColor[]);
    // Agregar iluminación a las estrellas que proporcionan fuentes de luz
    void drawLight();
    virtual void draw() { drawLight(); drawPlanet(); drawStar(); }
};

Además, también necesitamos considerar el diseño de la clase SolarSystem. En el sistema solar, es obvio que el sistema solar está compuesto por varios planetas. Para el sistema solar, la actualización de la vista después del movimiento del planeta debe ser manejada por el sistema solar. Por lo tanto, las variables miembro de SolarSystem deben incluir variables que contengan los planetas, y las funciones miembro deben manejar la actualización de la vista y los eventos de respuesta del teclado dentro del sistema solar. Por lo tanto, en el directorio ~/project/, cree un archivo solarsystem.hpp, y en él, podemos diseñar la clase SolarSystem:

class SolarSystem {

public:

    SolarSystem();
    ~SolarSystem();

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

private:
    Star *stars[STARS_NUM];

    // Definir los parámetros del ángulo de visión
    GLdouble viewX, viewY, viewZ;
    GLdouble centerX, centerY, centerZ;
    GLdouble upX, upY, upZ;
};

Consejos

  • Aquí usamos la forma tradicional de matriz para administrar todos los planetas en lugar de usar el vector en C++, porque la forma tradicional de matriz es suficiente.
  • Definir el ángulo de visión en OpenGL es un concepto complejo que requiere cierta extensión para explicar. Simplemente mencionaremos brevemente que definir el ángulo de visión requiere al menos nueve parámetros aquí. Explicaremos sus funciones en detalle cuando las implementemos más adelante en la siguiente sección.

Finalmente, también necesitamos considerar la configuración de los parámetros y variables básicos.

En SolarSystem, incluyendo el sol, hay un total de nueve planetas (excluyendo Plutón), pero en la clase Star que diseñamos, cada objeto Star tiene los atributos de un Star, por lo que podemos implementar adicionalmente los satélites de estos planetas, como la luna que gira alrededor de la tierra. Por lo tanto, consideramos implementar un total de diez planetas. Por lo tanto, podemos establecer el siguiente enum para indexar los planetas en una matriz:

#define STARS_NUM 10
enum STARS {
    Sun,        // Sol
    Mercury,    // Mercurio
    Venus,      // Venus
    Earth,      // Tierra
    Moon,       // Luna
    Mars,       // Marte
    Jupiter,    // Júpiter
    Saturn,     // Saturno
    Uranus,     // Urano
    Neptune     // Neptuno
};
Star * stars[STARS_NUM];

También asumimos que las velocidades de rotación son las mismas, por lo que establecemos su velocidad usando una macro:

#define TIMEPAST 1
#define SELFROTATE 3

En este momento, mueva las funciones miembro no implementadas al archivo .cpp correspondiente, y hemos completado el experimento en esta sección.

✨ Revisar Solución y Practicar

Resumen del código

Vamos a resumir el código que se necesita completar en los experimentos anteriores:

Primero, en main.cpp, creamos un objeto SolarSystem y luego delegamos la actualización de la visualización, la actualización en el estado ocioso y el manejo de eventos del teclado a glut:

Haga clic para ver el 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;
}

Segundo, en stars.hpp, creamos las clases Star, Planet y LightPlanet:

Haga clic para ver el 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 */

En el directorio ~/project/, cree un archivo stars.cpp y complete las implementaciones de las funciones miembro correspondientes de stars.hpp:

Haga clic para ver el 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:
}

En solarsystem.hpp, se diseña la clase 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;
};

En el directorio ~/project/, cree un archivo solarsystem.cpp e implemente las funciones miembro correspondientes de solarsystem.hpp:

Haga clic para ver el 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:
}

En el directorio ~/project/, cree un archivo Makefile y agregue el siguiente código a él:

Por favor, introduzca manualmente, no copie y pegue directamente, y recuerde usar la tecla <tab> en lugar de la tecla de espacio.

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

Cuando escriba el comando de compilación, preste atención a la ubicación de -lglut -lGLU -lGL. Esto es porque el uso de la opción -l en el compilador g++ es un poco especial.

Por ejemplo: foo1.cpp -lz foo2.cpp, si el archivo objetivo foo2.cpp utiliza funciones de la biblioteca z, estas funciones no se cargarán directamente. Sin embargo, si foo1.o utiliza funciones de la biblioteca z, no se producirá ningún error de compilación.

En otras palabras, todo el proceso de enlace va de izquierda a derecha. Cuando se encuentra un símbolo de función no resuelto en foo1.cpp, busca la biblioteca de enlace derecha. Cuando encuentra la opción z, la busca en z y encuentra la función, completando así el enlace sin problemas. Por lo tanto, la biblioteca con la opción -l debe estar ubicada a la derecha de todos los archivos compilados.

Finalmente, ejecute en la terminal:

make && ./solarsystem

Podrá ver que se ha creado la ventana, pero no hay nada en ella (muestra lo que hay detrás de la ventana). Esto es porque aún no hemos implementado el mecanismo de actualización de la gráfica en la ventana. Continuaremos completando el código restante en el siguiente experimento para hacer que funcione la simulación completa del sistema solar.

✨ Revisar Solución y Practicar

El concepto de matrices en OpenGL

En álgebra lineal, estamos familiarizados con el concepto de matrices, pero nuestra comprensión de su propósito y función específicos puede ser limitada. Entonces, ¿qué es exactamente una matriz?

Comencemos viendo la siguiente ecuación:

x = Ab

Donde A es una matriz, y x, b son vectores.

Perspectiva Uno:

Si x y b son ambos vectores en nuestro espacio tridimensional, ¿qué hace A? Transforma b en (o hacia) x. Desde esta perspectiva, una matriz A se puede entender como una transformación.

Ahora considere otra ecuación:

Ax = By

Donde A, B son matrices, y x, y son vectores.

Perspectiva Dos:

Para dos vectores diferentes x y y, son esencialmente iguales porque se pueden hacer iguales multiplicándolos por las matrices A y B. Desde esta perspectiva, una matriz A se puede entender como un sistema de coordenadas. En otras palabras, los vectores por sí mismos son únicos, pero definimos un sistema de coordenadas para describirlos. Dado que se usan diferentes sistemas de coordenadas, las coordenadas de los vectores variarán. En este caso, para el mismo vector, tiene diferentes coordenadas en diferentes sistemas de coordenadas.
La matriz A describe precisamente un sistema de coordenadas, y la matriz B describe otro sistema de coordenadas. Cuando estos dos sistemas de coordenadas se aplican a x y y, producen el mismo resultado, lo que significa que x y y son esencialmente el mismo vector pero con diferentes sistemas de coordenadas.

Combinando estas dos perspectivas, podemos concluir que la esencia de una matriz es describir movimiento.

En el contexto de OpenGL, hay una matriz responsable de las transformaciones de representación, conocida como el modo de matriz en OpenGL.

Como se mencionó anteriormente, una matriz puede describir tanto la transformación de un objeto como el sistema de coordenadas en el que existe. Por lo tanto, cuando se tratan diferentes operaciones, necesitamos establecer diferentes modos de matriz en OpenGL. Esto se logra usando la función glMatrixMode().

Esta función acepta tres modos diferentes: GL_PROJECTION para operaciones de proyección, GL_MODELVIEW para operaciones de vista de modelo y GL_TEXTURE para operaciones de textura.

GL_PROJECTION le dice a OpenGL que se realizarán operaciones de proyección, proyectando el objeto sobre un plano. Cuando se habilita este modo, la matriz debe ser establecida como la matriz identidad usando glLoadIdentity(), seguido de operaciones como establecer la perspectiva usando gluPerspective (explicaremos esta función en más detalle más adelante cuando discutamos el concepto de vista en OpenGL).

GL_MODELVIEW le dice a OpenGL que las siguientes declaraciones se usarán para describir una operación basada en un modelo, como establecer la perspectiva de la cámara. Del mismo modo, después de habilitar este modo, necesitamos establecer el modo de matriz de OpenGL como la matriz identidad.

GL_TEXTURE se utiliza para operaciones relacionadas con texturas, que no profundizaremos en este momento.

Si no está familiarizado con el concepto de matrices, puede entender simplemente glMatrixMode() como declaraciones a OpenGL sobre las operaciones que vendrán. Antes de representar o rotar un objeto, debemos usar glPushMatrix para guardar el entorno de la matriz actual; de lo contrario, pueden ocurrir errores de representación misteriosos.

✨ Revisar Solución y Practicar

APIs comunes de dibujo de imágenes en OpenGL

OpenGL proporciona muchas APIs comúnmente utilizadas relacionadas con el dibujo gráfico. Aquí, presentaremos brevemente algunas de ellas y las usaremos en el código siguiente:

  • glEnable(GLenum cap): Esta función se utiliza para activar las diversas funcionalidades proporcionadas por OpenGL. El parámetro cap es una macro interna a OpenGL, que proporciona efectos como iluminación, niebla y dithering.
  • glPushMatrix() y glPopMatrix(): Estas funciones guardan la matriz actual en la cima de la pila (guardando la matriz actual).
  • glRotatef(alpha, x, y, z): Representa la rotación de la forma actual en sentido antihorario por alpha grados alrededor del eje (x, y, z).
  • glTranslatef(distance, x, y): Representa la traslación de la forma actual por distance en la dirección (x, y).
  • glutSolidSphere(GLdouble radius, GLint slices, GLint stacks): Dibuja una esfera, donde radius es el radio, slices es el número de líneas longitudinales y stacks es el número de líneas latitudinales.
  • glBegin() y glEnd(): Cuando queremos dibujar una forma, necesitamos llamar a estas dos funciones antes y después del dibujo. glBegin() especifica el tipo de forma que se va a dibujar. Por ejemplo, GL_POINTS representa el dibujo de puntos, GL_LINES representa el dibujo de puntos conectados por líneas, GL_TRIANGLES completa un triángulo con cada tres puntos y GL_POLYGON dibuja un polígono desde el primer punto hasta el n-ésimo punto, y así sucesivamente. Por ejemplo, cuando necesitamos dibujar un círculo, podemos simularlo con un polígono con muchos lados:
// r es el radio, n es el número de lados
glBegin(GL_POLYGON);
    for(i=0; i<n; ++i)
        glVertex2f(r*cos(2*PI/n*i), r*sin(2*PI/n*i));
glEnd();
✨ Revisar Solución y Practicar

Coordenadas de perspectiva en OpenGL

En la sección anterior, definimos nueve variables miembro en la clase SolarSystem en OpenGL:

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

Para entender estas nueve variables, primero necesitamos establecer el concepto de perspectiva de la cámara en la programación 3D con OpenGL.

Imagina que las escenas que normalmente vemos en las películas se graban desde la perspectiva de la cámara. Por lo tanto, OpenGL también tiene un concepto similar. Si imaginamos la cámara como nuestra propia cabeza, entonces:

  1. viewX, viewY, viewZ corresponden a las coordenadas de la cabeza (cámara) en el sistema de coordenadas mundial de OpenGL;
  2. centerX, centerY, centerZ corresponden a las coordenadas del objeto que se está viendo (tal como se ve desde la cámara);
  3. upX, upY, upZ corresponden al vector de dirección que apunta hacia arriba desde la parte superior de la cabeza (parte superior de la cámara) (ya que podemos inclinar la cabeza para observar un objeto).

Con esta comprensión, ahora tienes un concepto del sistema de coordenadas en OpenGL.

Para este experimento, asumamos que la perspectiva inicial está en las coordenadas (x, -x, x). Por lo tanto, tenemos:

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

La posición del objeto observado (sol) está en (0,0,0), por lo que en el constructor de la clase SolarSystem, inicializamos la perspectiva de la siguiente manera:

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

Luego, podemos establecer los nueve parámetros de la perspectiva usando la función gluLookAt:

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

A continuación, echemos un vistazo a gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar).

Esta función creará un volumen de visualización de perspectiva simétrico, y antes de usar esta función, el modo de matriz de OpenGL debe ser establecido a GL_PROJECTION.

Como se muestra en la siguiente figura:

Diagrama de proyección de perspectiva de OpenGL

La imagen en la ventana es capturada por la cámara, y el contenido real que se está capturando está en el plano lejano, mientras que el contenido mostrado está en el plano cercano. Por lo tanto, esta función requiere cuatro parámetros:

  • El primer parámetro es el tamaño del ángulo de perspectiva.
  • El segundo parámetro es la relación de aspecto de la ventana real, como se muestra en la figura aspect=w/h.
  • El tercer parámetro es la distancia al plano cercano.
  • El cuarto parámetro es la distancia al plano lejano.
✨ Revisar Solución y Practicar

Efectos de iluminación en OpenGL

OpenGL divide el sistema de iluminación en tres partes: fuentes de luz, materiales y entorno de iluminación.

Como su nombre indica, una fuente de luz es la fuente de luz, como el sol;
Los materiales se refieren a las superficies de varios objetos que reciben luz, como los planetas y satélites del sistema solar aparte del sol;
El entorno de iluminación incluye parámetros adicionales que determinan el efecto de iluminación final, como la reflexión de los rayos de luz, que se puede controlar estableciendo un parámetro llamado "brillo ambiente" para que la imagen final se acerque más a la realidad.

En física, cuando la luz paralela golpea una superficie lisa, la luz reflejada sigue siendo paralela. Este tipo de reflexión se llama "reflexión especular". Por otro lado, la reflexión causada por una superficie irregular se llama "reflexión difusa".

Ejemplo de efectos de iluminación de OpenGL

Fuentes de luz

Para implementar el sistema de iluminación en OpenGL, lo primero que debemos hacer es configurar las fuentes de luz. Cabe mencionar que OpenGL admite un número limitado de fuentes de luz (ocho en total), representadas por las macros GL_LIGHT0 a GL_LIGHT7. Pueden ser habilitadas con la función glEnable y deshabilitadas con la función glDisable. Por ejemplo: glEnable(GL_LIGHT0);

La posición de la fuente de luz se establece usando la función glLightfv, por ejemplo:

GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // Especifica la posición de la fuente de luz 0

La posición está representada por cuatro valores, (x, y, z, w). Cuando w es 0, significa que la fuente de luz está a una distancia infinita. Los valores de x, y y z especifican la dirección de esta fuente de luz a distancia infinita.
Cuando w no es 0, representa una fuente de luz posicional, y su posición es (x/w, y/w, z/w).

Materiales

Para configurar el material de un objeto, generalmente se requieren cinco atributos:

  1. La intensidad de la luz que queda en el entorno después de múltiples reflexiones.
  2. La intensidad de la luz después de la reflexión difusa.
  3. La intensidad de la luz después de la reflexión especular.
  4. La intensidad de la luz emitida por objetos no emisores de luz en OpenGL, que es débil y no afecta a otros objetos.
  5. El exponente especular, que representa la rugosidad del material. Un valor más pequeño significa un material más rugoso, y cuando la luz emitida por una fuente puntual de luz brilla sobre él, se producen manchas brillantes más grandes. Por el contrario, un valor más grande significa que el material es más similar a una superficie especular, produciendo manchas brillantes más pequeñas.

OpenGL proporciona dos versiones de funciones para configurar materiales:

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

La diferencia entre ellas es que solo se necesita establecer un valor para el exponente especular, por lo que se utiliza glMaterialf. Para otras configuraciones de materiales que requieren múltiples valores, se utiliza una matriz, y se utiliza la versión con parámetros de vector de puntero, glMaterialfv. Por ejemplo:

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

Entorno de iluminación

Por defecto, OpenGL no maneja la iluminación. Para habilitar la funcionalidad de iluminación, se debe usar la macro GL_LIGHTING, es decir, glEnable(GL_LIGHTING);

✨ Revisar Solución y Practicar

Dibujando planetas

Cuando se dibuja un planeta, primero debemos considerar su ángulo de revolución y ángulo de rotación. Por lo tanto, podemos primero implementar la función miembro Star::update(long timeSpan) de la clase Star en el archivo ~/project/stars.cpp:

void Star::update(long timeSpan) {
    alpha += timeSpan * speed;  // actualizar el ángulo de revolución
    alphaSelf += selfSpeed;     // actualizar el ángulo de rotación
}

Después de actualizar los ángulos de revolución y rotación, podemos dibujar el planeta específico según los parámetros:

void Star::drawStar() {

    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_BLEND);

    int n = 1440;

    // Guardar el entorno de la matriz OpenGL actual
    glPushMatrix();
    {
        // Revolución

        // Si es un planeta y la distancia no es 0, entonces trasladarlo al origen por un radio
        // Esta parte se utiliza para los satélites
        if (parentStar!= 0 && parentStar->distance > 0) {
            // Rotar la gráfica de dibujo alrededor del eje z por alpha
            glRotatef(parentStar->alpha, 0, 0, 1);
            // Transladar en la dirección del eje x por distance, mientras que las direcciones y y z permanecen inalteradas
            glTranslatef(parentStar->distance, 0.0, 0.0);
        }
        // Dibujar la órbita
        glBegin(GL_LINES);
        for(int i=0; i<n; ++i)
            glVertex2f(distance * cos(2 * PI * i / n),
                       distance * sin(2 * PI * i / n));
        glEnd();
        // Rotar alrededor del eje z por alpha
        glRotatef(alpha, 0, 0, 1);
        // Transladar en la dirección del eje x por distance, mientras que las direcciones y y z permanecen inalteradas
        glTranslatef(distance, 0.0, 0.0);

        // Rotación
        glRotatef(alphaSelf, 0, 0, 1);

        // Dibujar el color del planeta
        glColor3f(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
        glutSolidSphere(radius, 40, 32);
    }
    // Restaurar el entorno de la matriz antes del dibujo
    glPopMatrix();

}

Este código utiliza las funciones sin() y cos(), lo que requiere incluir #include<cmath>.

✨ Revisar Solución y Practicar

Dibujo de iluminación

Para la clase Planet, que representa un cuerpo celeste no luminoso, necesitamos dibujar su efecto de iluminación. Agregue el siguiente código al archivo ~/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);
}

En cuanto a la clase LightPlanet, que representa un cuerpo celeste luminoso, no solo necesitamos configurar su material de iluminación, sino también establecer la posición de su fuente 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 la posición de la fuente de luz 0
    glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ambient);  // Representa la intensidad de los rayos de luz de diversas fuentes que alcanzan el material, después de múltiples reflexiones y trazado
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_diffuse);  // Intensidad de la luz después de la reflexión difusa
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // Intensidad de la luz después de la reflexión especular

}
✨ Revisar Solución y Practicar

Dibujando la ventana

En la sección anterior, mencionamos las dos funciones más importantes para manejar la visualización de imágenes: glutDisplayFunc y glutIdleFunc. glutDisplayFunc ejecuta la función de devolución de llamada cuando GLUT determina que el contenido de la ventana necesita ser actualizado, mientras que glutIdleFunc maneja la función de devolución de llamada cuando el bucle de eventos está inactivo.

Para hacer que todo el sistema solar se mueva, necesitamos considerar cuándo actualizar las posiciones de los planetas y cuándo actualizar la vista.

Obviamente, glutDisplayFunc debe centrarse en actualizar la vista, y cuando el evento está inactivo, podemos comenzar a actualizar las posiciones de los planetas. Una vez actualizadas las posiciones, luego podemos llamar a la función de actualización de vista para actualizar la visualización.

Por lo tanto, primero podemos implementar la función miembro SolarSystem::onUpdate() llamada en glutDisplayFunc:

#define TIMEPAST 1 // Asumir que cada actualización toma un día
void SolarSystem::onUpdate() {

    for (int i=0; i<STARS_NUM; i++)
        stars[i]->update(TIMEPAST); // Actualizar las posiciones de las estrellas

    this->onDisplay(); // Actualizar la visualización
}

Luego, la actualización de la vista se implementa en SolarSystem::onDisplay():

void SolarSystem::onDisplay() {

    // Limpiar el búfer del viewport
    glClear(GL_COLOR_BUFFER_BIT  |  GL_DEPTH_BUFFER_BIT);
    // Limpiar y establecer el búfer de color
    glClearColor(.7f,.7f,.7f,.1f);
    // Especificar la matriz actual como la matriz de proyección
    glMatrixMode(GL_PROJECTION);
    // Especificar la matriz especificada como una matriz identidad
    glLoadIdentity();
    // Especificar el volumen de vista actual
    gluPerspective(75.0f, 1.0f, 1.0f, 40000000);
    // Especificar la matriz actual como la matriz de vista y modelo
    glMatrixMode(GL_MODELVIEW);
    // Especificar la matriz actual como una matriz identidad
    glLoadIdentity();
    // Definir la matriz de vista y multiplicarla por la matriz actual
    gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);

    // Establecer la primera fuente de luz (fuente de luz 0)
    glEnable(GL_LIGHT0);
    // Habilitar la iluminación
    glEnable(GL_LIGHTING);
    // Habilitar la prueba de profundidad, automáticamente ocultar gráficos cubiertos basados en las coordenadas
    glEnable(GL_DEPTH_TEST);

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

    // Usamos GLUT_DOUBLE cuando inicializamos el modo de visualización en la función principal
    // Necesitamos usar glutSwapBuffers para implementar el intercambio de búferes para el doble búfer después de terminar el dibujo
    glutSwapBuffers();
}
✨ Revisar Solución y Practicar

Constructores y destructores de clases

Los constructores de las clases definidas en stars.hpp deben inicializar las variables miembro de las clases. Esta parte es relativamente simple e incluso se pueden usar los destructores predeterminados, así que implemente estos constructores por su cuenta:

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

Consejo: Tenga en cuenta que al inicializar la variable de velocidad, conviértala a velocidad angular. La fórmula de conversión es: alpha_speed = 360/speed.

Para el constructor de solarsystem.cpp, necesitamos inicializar todos los planetas. Aquí se proporcionan los parámetros entre los planetas para mayor conveniencia:

// Radio de revolución
#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

// Distancia al sol
#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

// Velocidad de movimiento
#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

// Velocidad de autorrotación
#define SELFROTATE 3

// Definir una macro para facilitar la configuración de una matriz multidimensional
#define SET_VALUE_3(name, value0, value1, value2) \
                   ((name)[0])=(value0), ((name)[1])=(value1), ((name)[2])=(value2)

// En el experimento anterior, definimos el enum de los planetas
enum STARS {Sun, Mercury, Venus, Earth, Moon,
    Mars, Jupiter, Saturn, Uranus, Neptune};

Consejo:

Definimos una macro SET_VALUE_3 aquí. Usted puede pensar que podemos escribir una función para lograr el mismo propósito.

De hecho, las macros completan el trabajo de sustitución general durante el proceso de compilación, mientras que la definición de funciones

Esto requiere operaciones de pila de funciones durante la llamada, lo que es mucho menos eficiente que el procesamiento de macros durante el proceso de compilación.

Por lo tanto, las macros pueden ser más eficientes.

Sin embargo, es importante tener en cuenta que aunque las macros pueden ser más eficientes, el uso excesivo puede conducir a un código feo y menos legible. Por otro lado, se alienta el uso adecuado de macros.

Por lo tanto, podemos implementar el constructor de la clase SolarSystem, donde los colores de los planetas se seleccionan al azar. Los lectores pueden cambiar los colores de los planetas por sí mismos:

SolarSystem::SolarSystem() {

    // Definir la vista en perspectiva, como hemos discutido la inicialización de la vista en perspectiva antes
    viewX = 0;
    viewY = REST_Y;
    viewZ = REST_Z;
    centerX = centerY = centerZ = 0;
    upX = upY = 0;
    upZ = 1;

    // Sol
    GLfloat rgbColor[3] = {1, 0, 0};
    stars[Sun]     = new LightPlanet(SUN_RADIUS, 0, 0, SELFROTATE, 0, rgbColor);
    // Mercurio
    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);
    // Tierra
    SET_VALUE_3(rgbColor, 0, 1, 0);
    stars[Earth]   = new Planet(EAR_RADIUS, EAR_DIS, EAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Luna
    SET_VALUE_3(rgbColor, 1, 1, 0);
    stars[Moon]    = new Planet(MOO_RADIUS, MOO_DIS, MOO_SPEED, SELFROTATE, stars[Earth], rgbColor);
    // Marte
    SET_VALUE_3(rgbColor, 1,.5,.5);
    stars[Mars]    = new Planet(MAR_RADIUS, MAR_DIS, MAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Júpiter
    SET_VALUE_3(rgbColor, 1, 1,.5);
    stars[Jupiter] = new Planet(JUP_RADIUS, JUP_DIS, JUP_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Saturno
    SET_VALUE_3(rgbColor,.5, 1,.5);
    stars[Saturn]  = new Planet(SAT_RADIUS, SAT_DIS, SAT_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Urano
    SET_VALUE_3(rgbColor,.4,.4,.4);
    stars[Uranus]  = new Planet(URA_RADIUS, URA_DIS, URA_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Neptuno
    SET_VALUE_3(rgbColor,.5,.5, 1);
    stars[Neptune] = new Planet(NEP_RADIUS, NEP_DIS, NEP_SPEED, SELFROTATE, stars[Sun], rgbColor);

}

Además, no olvide liberar la memoria asignada en el destructor:

SolarSystem::~SolarSystem() {
    for(int i = 0; i<STARS_NUM; i++)
        delete stars[i];
}
✨ Revisar Solución y Practicar

Implementación del cambio de perspectiva con las teclas del teclado

Para controlar el cambio de perspectiva, podemos utilizar las cinco teclas w, a, s, d, x del teclado y la tecla r para restablecer la perspectiva. Primero, necesitamos determinar la magnitud del cambio de perspectiva después de cada pulsación de tecla. Aquí, definimos una macro OFFSET. Luego, podemos determinar el comportamiento de pulsación de tecla del usuario inspeccionando el parámetro key pasado.

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

    switch (key)    {
        case 'w': viewY += OFFSET; break; // Aumenta la posición del eje Y de la cámara en 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;
    }

}
✨ Revisar Solución y Practicar

Ejecución y prueba

Principalmente implementamos el código en los archivos stars.cpp y solarsystem.cpp.

El código de stars.cpp es el siguiente:

Haga clic para ver el 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);

}

El código de solarsystem.cpp es el siguiente:

Haga clic para ver el 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];
}

Ejecute en la terminal:

make && ./solarsystem

El resultado se muestra en la figura:

Vista previa de la simulación del sistema solar

Dado que los colores de los planetas son sencillos, el efecto de iluminación no es muy obvio pero todavía es visible. Por ejemplo, en el lado derecho del Júpiter amarillo, hay una apariencia blanquecina.

✨ Revisar Solución y Practicar

Resumen

En este proyecto, hemos logrado un modelo simple del sistema solar. Al utilizar el sistema de iluminación de OpenGL, podemos ver los efectos de iluminación de los planetas mientras giran alrededor del sol. Además, podemos ajustar la perspectiva utilizando el teclado para observar el sistema solar desde diferentes ángulos.