Pull to refresh

Моя планета Земля

Reading time13 min
Views3.8K


Привет всем кто это читает! Хочу рассказать, как с помощью библиотеки OpenGL нарисовать вот такой земной шар.
Здесь я не буду останавливаться на создании окна и контекста устройства отображения, т.к. с это выходит за рамки данного повествования.

В ОСНОВЕ СФЕРА


Сперва, необходимо построить геометрический каркас, которым будет являться сфера.

Тут ничего сложного открываем любой учебник по геометрии и находим там параметрическое уравнение сферы. Оно записывается вот так:



С помощью этого уравнения можно вычислить вершины для построения каркаса. Сразу оговорюсь, что для визуализации каркаса я использовал механизм вершинных массивов, который позволяет сократить число вызовов функций отображения полигонов до одного. Такое сокращение поможет значительно повысить скорость визуализации.
Кое, что еще. У вершин есть особенность: вершина может принадлежать разным полигонам. Но чтобы не хранить её отдельно для каждого полигона, придумали хранить индексы этих вершин. В данном конкретном случае, индексы вычисляются исходя из того, что полигоны будут рисоваться, используя следующий порядок вершин:



В OpenGL такому порядку соответствует константа GL_QUAD_STRIP, которая используется в функциях отображения.

Теперь программирование. Для начала, необходимо вычислить число вершин и число индексов.

numberOfVertices = ((180 / step) + 1) * ((360 / step) + 1)
numberOfIndices = 2 * (numberOfVertices - (360 / step) - 1)

где step шаг интерполяции между двумя точками. Я взял 8 градусов (или 0,14 радиана).

Далее объявим массив вершин и массив индексов:

class CScene {
private:

  enum {
    step = 8,
    numberOfVertices = ((180 / step) + 1) * ((360 / step) + 1),
    numberOfIndices  = 2 * (numberOfVertices - (360 / step) - 1)
  }; // enum

  struct {
    GLfloat x,y,z; // координаты точки
  } m_vertices[numberOfVertices]; // массив вершин

  GLuint m_indices[numberOfIndices]; // массив индексов

  ...

}; // class CScene

А вот код, в котором вычисляются значения этих массивов.

CScene::CScene(void) {
  /// вычисляем вершины сферы и заносим их в массив
  const GLfloat fRadius = 1.0;

  for (int alpha = 90, index = 0; alpha <= 270; alpha += step) {
    const int angleOfVertical = alpha % 360;

    for (int phi = 0; phi <= 360; phi += step, ++index) {
      const int angleOfHorizontal = phi % 360;

      /// вычисляем координаты точки
      m_vertices[index].x = fRadius * g_tableOfCosines[angleOfVertical] * g_tableOfCosines[angleOfHorizontal];
      m_vertices[index].y = fRadius * g_tableOfSinus[angleOfVertical];
      m_vertices[index].z = fRadius * g_tableOfCosines[angleOfVertical] * g_tableOfSinus[angleOfHorizontal];
    } // for
  } // for

  /// вычисляем индексы вершин и заносим их в массив
  for (int index = 0; index < numberOfIndices; index += 2) {
    m_indices[index] = index >> 1;
    m_indices[index + 1] = m_indices[index] + (360 / step) + 1;
  } // for
} // constructor CScene

Обратите внимание, что вместо функций cos и sin используются таблицы g_tableOfCosines и g_tableOfSinus, в которые заранее занесены значения косинуса и синуса. Это сделано для повышения скорости вычисления вершин. Вместо того, чтобы каждый раз пользоваться медленными функциями cos и sin используються уже готовые значения.

Прежде чем приступить к визуализации, необходимо передать данные вершинных массивов в OpenGL. Для этого будет использоваться следующая функция:

inline void CScene::InitArrays(void) {
  const GLsizei stride = 3 * sizeof(GLfloat);

  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(3, GL_FLOAT, stride, m_vertices);

  glEnableClientState(GL_NORMAL_ARRAY);
  glNormalPointer(GL_FLOAT, stride, m_vertices);
} // InitArrays

stride – промежуток в памяти (в байтах) между первой координатой предыдущей и первой координатой следующей вершины.

Обратите внимание! Что в качестве векторов нормалей используются вершины. Дело в том, что для плавного закрашивания я использовал вектора нормалей в вершинах. А для сферы направлению вектора нормали соответствуют координаты этой вершины (конечно если сфера находиться в начале координат).

Теперь давайте нарисуем сферу.

inline void CScene::InitLights(void) {
  const GLfloat pos[] = {1.0, 1.0, 1.0, 0.0};
  const GLfloat clr[] = {1.0, 1.0, 1.0, 1.0};

  glEnable(GL_LIGHT0);

  glLightfv(GL_LIGHT0, GL_POSITION, pos);
  glLightfv(GL_LIGHT0, GL_DIFFUSE,  clr);
  glLightfv(GL_LIGHT0, GL_SPECULAR, clr);
} // InitLights

void CScene::Init(const int _nWidth, const int _nHeight) {
  // устанавливаем порт отображения
  glViewport(0, 0, _nWidth, _nHeight);
  /// устанавливаем перспективную проекцию
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(80.0, (GLdouble)_nWidth / (GLdouble)_nHeight, 0.1, 3.0);
  /// устанавливаем камеру
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt( 0.0,1.5,1.5,  0.0,0.0,0.0,  0.0,1.0,0.0);
  /// разрешаем отсечение невидемых граней
  glCullFace(GL_FRONT);
  glEnable(GL_CULL_FACE);
  // рзрешаем тест буфера глубины
  glEnable(GL_DEPTH_TEST);
  // разрешаем нормализацию
  glEnable(GL_NORMALIZE);
  // устанавливаем плавное закрашивание
  glShadeModel(GL_SMOOTH);
  // устанавливаем метод отображения - закрашивание
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
  /// устанавливаем источники света
  this->InitLights();
  glEnable(GL_LIGHTING);
  // задаем цвет фона - черный
  glClearColor(0.0, 0.0, 0.0, 1.0);
  /// задаем свойства материала объекта
  const GLfloat clr[] = {1.0, 1.0, 1.0, 1.0};
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, clr);
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 50.0);
  // инициализируем вершинные массивы
  this->InitArrays();
} // Init

inline void CScene::DrawEarth(void) {
  glDrawElements(GL_QUAD_STRIP, numberOfIndices, GL_UNSIGNED_INT, m_indices);
} // DrawEarth

void CScene::Redraw(void) {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  this->DrawEarth();

  glFinish();
} // Redraw

И вот, что получилось в результате:



ТЕПЕРЬ ТЕКСТУРА


Теперь когда сфера готова. Нужно наложить на нее текстуру, чтобы предать схожести с земным шаром.
Где взять текстуру? Все очень просто. Открываем images.google.ru и вводим там запрос: "Earth map".



Остановим свой выбор вот на этой текстуре:



Текстуру будем хранить в файле ресурсов:

//
// Texture
//


IDR_TEXTURE_EARTH     TEXTURE     "Earth.jpg"

Опытные программисты, наверное, заметили, что вместо привычного BITMAP я использовал TEXTURE. Да и сам файл берется с расширением JPEG, а не BMP. Это сделано для того, чтобы уменьшить размер исполняемого модуля.
Но, в дальнейшем, нам понадобиться работать с битовой картой (Bitmap). Для этого я написал функцию, которая будет загружать текстуры из ресурсов в Bitmap:

inline HBITMAP LoadTextureFromResource(HMODULE _hModule, LPCTSTR _lpName, LPCTSTR _lpType) {
  HRSRC hRsrc = FindResource(_hModule, _lpName, _lpType);
  if (NULL == hRsrc) return NULL;

  HGLOBAL hGlobal = LoadResource(_hModule, hRsrc);
  if (NULL == hGlobal) return NULL;

  HBITMAP hBitmap = NULL;
  DWORD dwSize = SizeofResource(_hModule, hRsrc);

  HGLOBAL hData = GlobalAlloc(GMEM_MOVEABLE, dwSize);
  CopyMemory(GlobalLock(hData), LockResource(hGlobal), dwSize);
  GlobalUnlock(hData);

  IStream *pStream = NULL;
  HRESULT hr = CreateStreamOnHGlobal(hData, FALSE, &pStream);
  if (SUCCEEDED(hr)) {
    IPicture *pPicture = NULL;
    hr = OleLoadPicture(pStream, dwSize, TRUE, IID_IPicture, (LPVOID*)&pPicture);

    if (SUCCEEDED(hr)) {
      pPicture->get_Handle((OLE_HANDLE *)&hBitmap);
      hBitmap = (HBITMAP)CopyImage(hBitmap, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);

      pPicture->Release(); pPicture = NULL;
    } // if
    pStream->Release(); pStream = NULL;
  } // if

  GlobalFree(hData); hData = NULL;
  UnlockResource(hGlobal); hGlobal = NULL;

  return hBitmap;
} // LoadTextureFromResource


Теперь нужно загрузить текстуру и вычислить текстурные координаты. Для этого изменим объявление массива вершин и объявим переменную в которой будет храниться текстура

class CScene {
private:

  ...

  struct {
    GLfloat x,y,z; // координаты точки
    GLfloat u,v; // текстурные координаты
  } m_vertices[numberOfVertices]; // массив вершин

  ...

  struct {
    GLuint  id; // идентификатор текстуры
    HBITMAP hBitmap; // дискриптор битовой карты
  } m_textureOfEarth;

  ...

}; // class CScene

После чего изменим ранее описанные функции, добавив в них функционал работающий с текстурами. И нарисуем земной шар.

CScene::CScene(void) {
  /// вычисляем вершины сферы и заносим их в массив
  const GLfloat fRadius = 1.0;

  for (int alpha = 90, index = 0; alpha <= 270; alpha += step) {
    const int angleOfVertical = alpha % 360;

    for (int phi = 0; phi <= 360; phi += step, ++index) {
      const int angleOfHorizontal = phi % 360;

      /// вычисляем координаты точки
      ...

      /// вычисляем текстурные координаты
      m_vertices[index].u = (360 - phi) / 360.0f;
      m_vertices[index].v = (270 - alpha) / 180.0f;
    } // for
  } // for

  /// вычисляем индексы вершин и заносим их в массив
  ...

  /// загрузка текстуры из ресурсов
  HINSTANCE hModule = GetModuleHandle(NULL);
  m_textureOfEarth.hBitmap = LoadTextureFromResource(hModule, MAKEINTRESOURCE(IDR_TEXTURE_EARTH), _T("TEXTURE"));
} // constructor CScene

CScene::~CScene(void) {
  /// удаляем текстуру
  if (NULL != m_textureOfEarth.hBitmap) {
    DeleteObject(m_textureOfEarth.hBitmap);
  } // if
} // destructor CScene

inline void CScene::InitArrays(void) {
  const GLsizei stride = 5 * sizeof(GLfloat);

  ...

  glEnableClientState(GL_TEXTURE_COORD_ARRAY);
  glTexCoordPointer(2, GL_FLOAT, stride, (GLfloat *)m_vertices + 3);
} // InitArrays

inline void CScene::InitTextures(void) {
  /// создаем текстуру
  if (NULL != m_textureOfEarth.hBitmap) {
    glGenTextures(1, &m_textureOfEarth.id);
    glBindTexture(GL_TEXTURE_2D, m_textureOfEarth.id);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    BITMAP bitmap = { 0 };
    GetObject(m_textureOfEarth.hBitmap, sizeof(BITMAP), &bitmap);

    glTexImage2D(GL_TEXTURE_2D, 0, 3, bitmap.bmWidth, bitmap.bmHeight, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, bitmap.bmBits);
  } // if
} // InitTextures

void CScene::Init(const int _nWidth, const int _nHeight) {
  ...
  // инициализируем текстуру
  this->InitTextures();
  // разрешаем текстурирование
  glEnable(GL_TEXTURE_2D);
} // Init

inline void CScene::DrawEarth(void) {
  glBindTexture(GL_TEXTURE_2D, m_textureOfEarth.id);
  glDrawElements(GL_QUAD_STRIP, numberOfIndices, GL_UNSIGNED_INT, m_indices);
} // DrawEarth

Жмем компиляцию… И вот что выдает нам программа:



РЕЛЬЕФ


Вроде все выглядит хорошо, но чего-то все таки не хватает. Чего? Не хватает рельефа. Т.е. для большей реалистичности необходимо добавить горы и впадины. Как это сделать?

Для передачи неровностей необходимо накладывать на одну плоскость несколько текстур (делать несколько полупрозрачных плоскостей, параллельных данной, но находящихся «немного ниже» нее или использовать мультитекстурирование, входящие в расширение OpenGL).

Здесь используются две текстуры: исходная текстура поверхности земного шара и текстура, отражающая неровности поверхности (черно белая), которая условно говоря, задает карту высот (будем называть ее картой поверхности).



Добавим ее в файл ресурсов.

//
// Texture
//


IDR_TEXTURE_EARTH     TEXTURE     "Earth.jpg"
IDR_TEXTURE_BUMP      TEXTURE     "bump.jpg"

Сначала нужно загрузить исходную текстуру (1), а из карты поверхности создаем две текстуры: первая (2) — точно такая же, но уменьшаем яркость в два раза; вторая (3) — это инвертированная (по значению яркости) карта поверхности, для которой также значение яркости уменьшаем в два раза.

class CScene {
private:

  ...

  struct {
    GLuint  id; // идентификатор текстуры
    HBITMAP hBitmap; // дискриптор битовой карты
  } m_textureOfEarth, m_textureOfBump, m_textureOfBumpInvert;

  ...

}; // class CScene
  
...

CScene::CScene(void) {
  /// вычисляем вершины сферы и заносим их в массив
  ...

  /// вычисляем индексы вершин и заносим их в массив
  ...

  /// загрузка текстур из ресурсов
  HINSTANCE hModule = GetModuleHandle(NULL);

  m_textureOfEarth.hBitmap = LoadTextureFromResource(hModule, MAKEINTRESOURCE(IDR_TEXTURE_EARTH), _T("TEXTURE"));

  m_textureOfBump.hBitmap = LoadTextureFromResource(hModule, MAKEINTRESOURCE(IDR_TEXTURE_BUMP), _T("TEXTURE"));

  m_textureOfBumpInvert.hBitmap = LoadTextureFromResource(hModule, MAKEINTRESOURCE(IDR_TEXTURE_BUMP), _T("TEXTURE"));
  if (NULL != m_textureOfBumpInvert.hBitmap) {
    BITMAP bitmap = { 0 };
    GetObject(m_textureOfBumpInvert.hBitmap, sizeof(BITMAP), &bitmap);

    // Проинвертируем растр
    LPBYTE ptr = (LPBYTE)bitmap.bmBits + (3 * bitmap.bmHeight * bitmap.bmWidth);
    while (--ptr >= bitmap.bmBits) (*ptr) = 0xFF - (*ptr);
  } // if
} // constructor CScene

CScene::~CScene(void) {
  /// удаляем текстуру
  if (NULL != m_textureOfEarth.hBitmap) {
    DeleteObject(m_textureOfEarth.hBitmap);
  } // if
  /// удаляем текстуру
  if (NULL != m_textureOfBump.hBitmap) {
    DeleteObject(m_textureOfBump.hBitmap);
  } // if
  /// удаляем текстуру
  if (NULL != m_textureOfBumpInvert.hBitmap) {
    DeleteObject(m_textureOfBumpInvert.hBitmap);
  } // if
} // destructor CScene
  
inline void CScene::InitTextures(void) {
  /// создаем текстуру
  if (NULL != m_textureOfEarth.hBitmap) {
    ...
  } // if
  /// создаем текстуру
  if (NULL != m_textureOfBump.hBitmap) {
    glGenTextures(1, &m_textureOfBump.id);
    glBindTexture(GL_TEXTURE_2D, m_textureOfBump.id);

    glPixelTransferf(GL_RED_SCALE, 0.5);   // промасштабируем яркость до 50%,
    glPixelTransferf(GL_GREEN_SCALE, 0.5); // поскольку нам нужна половинная интенсивность
    glPixelTransferf(GL_BLUE_SCALE, 0.5);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    BITMAP bitmap = { 0 };
    GetObject(m_textureOfBump.hBitmap, sizeof(BITMAP), &bitmap);

    glTexImage2D(GL_TEXTURE_2D, 0, 3, bitmap.bmWidth, bitmap.bmHeight, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, bitmap.bmBits);
  } // if
  /// создаем текстуру
  if (NULL != m_textureOfBumpInvert.hBitmap) {
    glGenTextures(1, &m_textureOfBumpInvert.id);
    glBindTexture(GL_TEXTURE_2D, m_textureOfBumpInvert.id);

    glPixelTransferf(GL_RED_SCALE, 0.5);   // промасштабируем яркость до 50%,
    glPixelTransferf(GL_GREEN_SCALE, 0.5); // поскольку нам нужна половинная интенсивность
    glPixelTransferf(GL_BLUE_SCALE, 0.5);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    BITMAP bitmap = { 0 };
    GetObject(m_textureOfBumpInvert.hBitmap, sizeof(BITMAP), &bitmap);

    glTexImage2D(GL_TEXTURE_2D, 0, 3, bitmap.bmWidth, bitmap.bmHeight, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, bitmap.bmBits);
  } // if
} // InitTextures

Вывод происходит в следующей последовательности: сначала выводится плоскость с наложенной текстурой (2), перед этим в OpenGL должны быть установлены следующие параметры:

glDisable(GL_BLEND);
glDisable(GL_LIGHTING);

Перед выводом следующей плоскости устанавливаются следующие параметры:

glBlendFunc(GL_ONE, GL_ONE);
glDepthFunc(GL_LEQUAL);
glEnable(GL_BLEND);

Сама плоскость выводится на том же месте, что и предыдущая, но «чуть-чуть» приподнята относительно предыдущей (чтобы не было перекрытий) и наложенной текстурой (3).

Перед выводом последней плоскости необходимо установить следующие параметры OpenGL:

glEnable(GL_LIGHTING);
glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);

Здесь также выводится плоскость, которая параллельна предыдущим, но «чуть-чуть» приподнята относительно них, для избежания пересечений. На плоскость накладывается текстура (1).

inline void CScene::DrawEarth(void) {
  /// рисуем первый слой
  glDisable(GL_BLEND);
  glDisable(GL_LIGHTING);

  glBindTexture(GL_TEXTURE_2D, m_textureOfBump.id);
  glPushMatrix();
    glScalef(0.99f, 0.99f, 0.99f);
    glDrawElements(GL_QUAD_STRIP, numberOfIndices, GL_UNSIGNED_INT, m_indices);
  glPopMatrix();

  /// рисуем второй слой
  glBlendFunc(GL_ONE, GL_ONE);
  glDepthFunc(GL_LEQUAL);
  glEnable(GL_BLEND);

  glBindTexture(GL_TEXTURE_2D, m_textureOfBumpInvert.id);
  glPushMatrix();
    glScalef(0.995f, 0.995f, 0.995f);
    glDrawElements(GL_QUAD_STRIP, numberOfIndices, GL_UNSIGNED_INT, m_indices);
  glPopMatrix();

  /// рисуем третий слой
  glEnable(GL_LIGHTING);
  glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);

  glBindTexture(GL_TEXTURE_2D, m_textureOfEarth.id);
  glDrawElements(GL_QUAD_STRIP, numberOfIndices, GL_UNSIGNED_INT, m_indices);
} // DrawEarth

В итоге получается изображение, на котором виден рельеф.



Бинарник и исходники проекта можно скачать здесь
Tags:
Hubs:
+48
Comments21

Articles