📜 ⬆️ ⬇️

My planet Earth



Hello everyone who reads this! I want to tell you how to draw such a globe using the OpenGL library.
Here I will not dwell on the creation of a window and a display device context, since with this is beyond the scope of this narrative.

BASED ON SPHERE


First, you need to build a geometric frame, which will be a sphere.

There is nothing difficult to open any textbook on geometry and find a parametric equation of a sphere there. It is written like this:


')
Using this equation, you can calculate the vertices for constructing the framework. At once I will make a reservation that for visualization of the framework I used the mechanism of vertex arrays, which allows to reduce the number of calls to the display functions of polygons to one. This reduction will help to significantly increase the speed of visualization.
Something else. Vertices have a peculiarity: vertices can belong to different polygons. But in order not to store it separately for each polygon, they decided to store the indices of these vertices. In this particular case, the indices are calculated based on the fact that polygons will be drawn using the following order of vertices:



In OpenGL, this order corresponds to the constant GL_QUAD_STRIP , which is used in the display functions.

Now programming. First, you need to calculate the number of vertices and the number of indices.

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

where step is the interpolation step between two points. I took 8 degrees (or 0.14 radians).

Next, we declare an array of vertices and an array of indices:

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

And here is the code in which the values ​​of these arrays are calculated.

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

Note that instead of the cos and sin functions, the g_tableOfCosines and g_tableOfSinus tables are used, in which the cosine and sine values ​​are pre-recorded. This is done to increase the speed of computing vertices. Instead of using the slow cos and sin functions each time, the ready-made values ​​are used.

Before you start rendering, you must pass the data of the vertex arrays in OpenGL . The following function will be used for this:

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 - the gap in memory (in bytes) between the first coordinate of the previous and the first coordinate of the next vertex.

Note! That vertices are used as normal vectors. The fact is that for smooth filling I used the normal vector at the vertices. And for a sphere, the direction of the normal vector corresponds to the coordinates of this vertex (of course, if the sphere is at the origin).

Now let's draw a sphere.

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

And that's what happened as a result:



NOW TEXTURE


Now that the sphere is ready. It is necessary to impose a texture on it in order to betray similarities with the globe.
Where to get the texture? Everything is very simple. Open images.google.com and enter the query there: " Earth map ".



Stop your choice here on this texture:



The texture will be stored in the resource file:

//
// Texture
//


IDR_TEXTURE_EARTH TEXTURE "Earth.jpg"

Experienced programmers probably noticed that instead of the usual BITMAP I used TEXTURE . Yes, and the file itself is taken with the extension JPEG , not BMP . This is done in order to reduce the size of the executable.
But, in the future, we need to work with a bitmap . For this, I wrote a function that will load textures from resources into a 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


Now you need to load the texture and calculate the texture coordinates. To do this, change the declaration of the array of vertices and declare a variable in which the texture will be stored

class CScene {
private :

...

struct {
GLfloat x,y,z; //
GLfloat u,v; //
} m_vertices[numberOfVertices]; //

...

struct {
GLuint id; //
HBITMAP hBitmap; //
} m_textureOfEarth;

...

}; // class CScene

After that, we will change the previously described functions by adding functionality that works with textures. And draw a globe.

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

Click the compilation ... And this is what the program gives us:



RELIEF


It seems everything looks good, but something is still missing. What? Not enough relief. Those. for greater realism, you must add mountains and valleys. How to do it?

To transfer irregularities, it is necessary to impose several textures on one plane (make several semitransparent planes parallel to this one, but located “slightly below” it or use multitexturing included in the OpenGL extension).

Here, two textures are used: the original texture of the surface of the globe and the texture that reflects surface irregularities (black and white), which, relatively speaking, defines a height map (we will call it a surface map).



Add it to the resource file.

//
// Texture
//


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

First you need to load the original texture (1), and from the surface map we create two textures: the first (2) is exactly the same, but we reduce the brightness twice; the second (3) is an inverted (by brightness value) surface map, for which we also reduce the brightness value twice.

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

The output occurs in the following sequence: first, a plane with a superimposed texture (2) is displayed, before this the following parameters must be set in OpenGL:

glDisable(GL_BLEND);
glDisable(GL_LIGHTING);

Before displaying the next plane, the following parameters are set:

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

The plane itself is displayed in the same place as the previous one, but “slightly” raised relative to the previous one (so that there are no overlaps) and a superimposed texture (3).

Before displaying the last plane, the following OpenGL parameters must be set:

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

It also displays a plane that is parallel to the previous one, but “slightly” raised relative to them, in order to avoid intersections. A texture is superimposed on the plane (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

The result is an image on which the relief is visible.



The project binaries and sources can be downloaded here.

Source: https://habr.com/ru/post/77985/


All Articles