Этот урок описывает создание объемных теней по технологии «Stencil». Здесь нам уже не понадобится матрица теней из предыдущего урока. Фактически на этот раз нам понадобится отрендерить только силуэт обьекта, образующийся из выступающих рёбер в зависимости от источника и направления света.

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

В этом уроке нам понадобятся исходники урока инициализации + класс вектор.

Сначала немного изменим файл Init.cpp.
Найдите в нём функцию bSetupPixelFormat(), и значение pfd.cStencilBits измените на 16:

    pfd.cStencilBits = 32;

 

Теперь редактируем main.cpp:

// Добавим glaux для быстрого рисования примитивов:
#include <gl/glaux.h>// Подключим класс вектора:
#include «CVector.h»// Переменные для вращения сцены
float xRotation = 0.0f;
float yRotation = 0.0f;// Обьект, отбрасывающий тень в этой сцене — простой квадрат. Эта переменная просто
// хранит макс. число точек квадрата…
#define MAX_SHADOW_CASTER_POINTS 4

// Позиция источника освещения:
CVector4 lightPosition(2.0f, 5.0f, 0.0f, 1.0f);

// DisplayVolume используется для зримого отображения обьёмности тени.
// ExtendetAmount — для облегчения создания силуэта обьекта, который будет
// отбрасывать тень.
bool displayVolume = false;
float ExtendAmount = 5.0f;

// Это обьект, который будет отбрасывать тень:
CVector4 ShadowObject[MAX_SHADOW_CASTER_POINTS];

// Прототипы необходимых функций:
void Render();
void DrawSilhouette(CVector4 lightPos, float Extend);

///////////////////////////////////////////////////////////////
//
//         Изменим функцию Init():
//
////////////////////////////////////////////////////////////////
void Init(HWND hWnd)
{
g_hWnd = hWnd;
GetClientRect(g_hWnd, &g_rRect);
InitializeOpenGL(g_rRect.right, g_rRect.bottom);
glClearColor(0.5f, 0.5f, 1.0f, 1.0f);

// Здесь устанавливаем необходимые значения для квадрата, отбрасывающего тень
ShadowObject[0] = CVector4(1.0f, 2.5f, 1.0f);
ShadowObject[1] = CVector4(1.0f, 2.5f,  1.0f);
ShadowObject[2] = CVector4(1.0f, 2.5f,  1.0f);
ShadowObject[3] = CVector4(1.0f, 2.5f, 1.0f);

float ambientLight[] = {1.0f, 1.0f, 1.0f, 1.0f};
float diffuseLight[] = {1.0, 1.0, 1.0, 1.0};
float specularLight[] = {1.0, 1.0, 1.0, 1.0};

glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
glLightfv(GL_LIGHT0, GL_SPECULAR, specularLight);
glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
glEnable(GL_LIGHT0);
}

///////////////////////////////////////////////////////////////
//
//      Теперь изменим RenderScene:
//
///////////////////////////////////////////////////////////////

void RenderScene()
{

//!
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glLoadIdentity();

// Как и в других случаях рендера в stencil-буффер, сначала нужно
// включить цветовую маску, а затем отрендерить сцену.
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
Render();

// Устанавливаем опции stencil-буфера.
glEnable(GL_CULL_FACE);
glEnable(GL_STENCIL_TEST);
glDepthMask(GL_FALSE);
glStencilFunc(GL_ALWAYS, 0, 0);

// Ключевой момент. Мы рендерим обьемную тени и увеличиваем «трафарет» каждый раз,
// когда передняя поверхность окажется пересечена. Обьёмная тень — это силуэт обьекта,
// основанный на позиции источника света и значении Extend.
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
glCullFace(GL_BACK);
DrawSilhouette(lightPosition, ExtendAmount);

// Теперь рендерим обьемную тень ещё раз, и на этот раз уменьшаем «трафарет» везде,
// где он не в отрисовываемой зоне. Другими словами — везде, где он вне затенённой области.
glStencilOp(GL_KEEP, GL_KEEP, GL_DECR);
glCullFace(GL_FRONT);
DrawSilhouette(lightPosition, ExtendAmount);

// Теперь сбрасываем всё в значения по умолчанию.
glDepthMask(GL_TRUE);
glDepthFunc(GL_LEQUAL);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glCullFace(GL_BACK);
glDisable(GL_CULL_FACE);

// Рендерим тёмные участки сцены. Другими словами, рисуем все затенённые зоны с тёмным оттенком.
glDisable(GL_LIGHT0);
glStencilFunc(GL_EQUAL, 1, 1);
Render();

// Наконец рисуем светлые участки сцены. В отличие от предыдущего раза, теперь мы
// рисуем участки, не попадающие в затенённую область.
glEnable(GL_LIGHT0);
glStencilFunc(GL_EQUAL, 0, 1);
Render();

// Ну и наконец исправим тест глубины и выключим stensil-test.
glDepthFunc(GL_LESS);
glDisable(GL_STENCIL_TEST);

// Опционально. Если вы хотите видеть обьёмную тень, значение=true, иначе — false.
if(displayVolume == true)
{
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
DrawSilhouette(lightPosition, ExtendAmount);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

SwapBuffers(g_hDC);
}

///////////////////////////////////////////////////////////////////////////////////
//
//      Эта функция будет рисовать все обьекты, поверхность и источник света.
//
///////////////////////////////////////////////////////////////////////////////////

void Render()
{
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glClearColor(0.5f,0.5f,0.5f,1.0f);

glTranslatef(0.0f, 5.0f, 15.0f);
glRotatef(yRotation, 1.0f, 0.0f, 0.0f);
glRotatef(xRotation, 0.0f, 1.0f, 0.0f);

// Рисуем «пол»
//glDisable(GL_LIGHTING);
//glBindTexture(GL_TEXTURE_2D, NULL);

glPushMatrix();

glColor3f(0.5f,0.8f,0.5f);
// Один большой квадрат
glBegin(GL_QUADS);
glNormal3f(0.0f, 1.0f, 0.0f);
glVertex3f(15.0f, 0.01f, 15.0f);
glVertex3f(15.0f, 0.01f, 15.0f);
glVertex3f(15.0f, 0.01f, 15.0f);
glVertex3f(15.0f, 0.01f, 15.0f);
glEnd();

glPopMatrix();
glEnable(GL_LIGHTING);

// Теперь рисуем остальные обьекты, начиная с тора.
// Эти обьекты не настроены на отбрасывание тени, но они будут затенены кубом.
glPushMatrix();

glTranslatef(2.0f, 0.8f, 0.2f);
glColor3f(1.0f, 1.0f ,1.0f);
auxSolidTorus(0.2f, 0.4f);

glPopMatrix();

// Рисуем куб
glPushMatrix();

glTranslatef(0.0f, 0.8f, 0.2f );
glColor3f(1.0f, 1.0f ,1.0f);
auxSolidCube(1.0f);

glPopMatrix();

// После куба рисуем сферу
glPushMatrix();

glTranslatef(2.0f, 0.8f, 0.2f);
glColor3f(1.0f, 1.0f ,1.0f);
auxSolidSphere(0.7f);

glPopMatrix();

// Рисуем обьект, отбрасывающий тень
glPushMatrix();

glBegin(GL_QUADS);

glNormal3f(0, 1, 0);
glTexCoord2f(0, 1);
glVertex3f(ShadowObject[0].x, ShadowObject[0].y, ShadowObject[0].z);

glTexCoord2f(0, 0);
glVertex3f(ShadowObject[1].x, ShadowObject[1].y, ShadowObject[1].z);

glTexCoord2f(1, 0);
glVertex3f(ShadowObject[2].x, ShadowObject[2].y, ShadowObject[2].z);

glTexCoord2f(1, 1);
glVertex3f(ShadowObject[3].x, ShadowObject[3].y, ShadowObject[3].z);

glEnd();

glPopMatrix();

// И наконец, рисуем источник света.
glDisable(GL_LIGHTING);
glPushMatrix();

// Устанавливаем позицию света. мы сможем перемещать источник клавишами,
// поэтому координаты нужно обновлять в цикле.
float lp[4];
lp[0] = lightPosition.x; lp[1] = lightPosition.y;
lp[2] = lightPosition.z; lp[3] = lightPosition.w;
glLightfv(GL_LIGHT0, GL_POSITION, lp);

glTranslatef(lightPosition.x, lightPosition.y, lightPosition.z);
glColor3f(1.0f, 1.0f, 0.5f);
GLUquadricObj *Sph1 = gluNewQuadric();
gluSphere(Sph1, 0.3, 16,16);

glPopMatrix();
glEnable(GL_LIGHTING);
}

//////////////////////////////////////////////////////////////////////////////////////
//
//      Эта функция вычисляет силуэт обьекта, создаёт обьемную тень,
//      и рисует её (вызывается после установки stencil-буфера)
//
//////////////////////////////////////////////////////////////////////////////////////

void DrawSilhouette(CVector4 lightPos, float Extend)
{
CVector4 Extended;
int A, B;

// Выключаем освещение
glDisable(GL_LIGHTING);

glColor3f(0.5f, 0.7f, 0.5f);

// Проходим циклом через все точки для создания обьемной тени. Вообще-то нужно
// вычислять силуэт, но т.к. в нашем случае это простой квадрат, это не имеет большого
// смысла.
for(int i = 0; i < MAX_SHADOW_CASTER_POINTS; i++)
{
// Переменные указывают, с какими точками мы сейчас работаем.
A = i; B = i + 1;

// Проверка на ошибки: не превысили ли мы лимит точек
if(B >= MAX_SHADOW_CASTER_POINTS) B = 0;

// Рисуем наш обьект
glBegin(GL_QUADS);

// Начинаем с первой точки.
glVertex3f(ShadowObject[A].x, ShadowObject[A].y, ShadowObject[A].z);

// Затем «увеличиваем» эту точку, основываясь на позиции источника света
// и позиции обьекта, чтобы сформировать вторую часть невидимого квадрата.
// Мы используем функцию класса вектора «ExtendVertexPos», принимающую
// точку, позицию источника света и значение, на которое нужно увеличивать.
// Возвращает функция новую позицию.
Extended.ExtendVertexPos(ShadowObject[A], lightPos, Extend);
glVertex3f(Extended.x, Extended.y, Extended.z);

// Так же, как и выше, берём вторую точку и «увеличиваем» её, чтобы
// получить третью часть квадрата.
Extended.ExtendVertexPos(ShadowObject[B], lightPos, Extend);
glVertex3f(Extended.x, Extended.y, Extended.z);

// И наконец рисуем вторую точку как последнюю часть квадрата.
glVertex3f(ShadowObject[B].x, ShadowObject[B].y, ShadowObject[B].z);

glEnd();
}

glEnable(GL_LIGHTING);
}

/////////////////////////////////////////////////////////////////////
//
//            И наконец изменим обработку клавиш. Мы хотим управлять
//            положением источника света.
//

case VK_SPACE:
displayVolume = !displayVolume;
break;

case VK_UP:
lightPosition.z -= 0.05f;
break;

case VK_DOWN:
lightPosition.z += 0.05f;
break;

case VK_LEFT:
lightPosition.x -= 0.05f;
break;

case VK_RIGHT:
lightPosition.x += 0.05f;
break;

 

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

Итак, давайте обобщим. Сначала создаётся и настраивается stencil-буфер. Затем рисуем в буфер только объёмную тень. По сути, объёмная тень — это выступающие рёбра объекта, спроецированные в направлении падения света. После рендера объема тени в буфер мы рендерим сцену дважды для каждой затрагиваемой зоны.

Исходные коды к уроку