Привет всем кто это читает! Хочу рассказать, как с помощью библиотеки 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
В итоге получается изображение, на котором виден рельеф.
Бинарник и исходники проекта можно скачать здесь