Войти
ПрограммированиеСтатьиГрафика

Реализация glow-эффекта вокруг трёхмерного объекта.

Автор:

В 3D графике далеко не последнее место занимают разного рода эффекты: отражения, тени, свет. Последнее является едва ли не основной головной болью всех программистов мира — уж больно неопределенная и ресурсоемкая задача. Я бы хотел рассказать о реализации одного из световых эффектов — glow, или свечение вокруг объекта. Все, в принципе, достаточно просто, необходимо только знать некоторые нюансы 3Д программирования и порядок их использования.

Итак, начнем. Общий принцип таков:

1. Сначала отрисовываем светящиеся объекты в текстуру размером 64х64 или 128х128. Тут необходимо придерживаться двух правил - светящиеся объекты рисовать белым (или любым другим ярким цветом), а фон очистить в черный цвет.

2. Применяем Blur-эффект к полученной текстуре, то есть размываем ее. Делается это так - для каждого пикселя текстуры находится среднее арифметическое значений цветов его соседей. Размыть текстуру можно несколько раз, в зависимости от того, насколько размытые края вы хотите получить.

3. Рисуем квад (2 треугольника) размером с экран, и накладываем на него полученную текстуру с применением альфа блендинга.

Еще я бы хотел обратить внимание на следующие моменты:
Объекты, которые находятся ближе к камере, чем светящиеся также должны быть отрисованы в текстуру, но с черным цветом, чтобы возникал эффект перекрывания светящимися объектами не светящихся.

Советы по оптимизации:
-Рисуйте в маленькую текстуру, чтоб сократить время рендеринга.
-Уменьшайте размеры viewport'а до размеров текстуры, во время рендеринга в текстуру.
-Делайте меньше проходов при размывании текстуры (хотя при этом и пострадает качество).
-Отрисовывайте в текстуру только те объекты, которые находятся в поле зрения камеры.
-Отрисовывайте сразу все объекты в одну текстуру.
-Если вычислительной скорости компьютера не хватает, то операцию программного размывания можно опустить, и вместо нее поставить текстуру низкого разрешения, растянутая по экрану она сама по себе создаст блюр-эффект.

Теперь каждый шаг рассмотрим поподробнее. Для быстроты и удобства, код я решил писать с помощью D3DAppWizard.

1. Подготавливаем трехмерный чайник (а куда без него!) и текстуру размером 128х128.

//
// Шаг подготовки - создает чайник и текстуру
//

LPD3DXMESH gTeapot;
LPDIRECT3DTEXTURE9 gRenderTexture;
LPDIRECT3DTEXTURE9 gResTexture;

void Prepare()
{
  // Создаем чайник
  D3DXCreateTeapot( m_pd3dDevice, &gTeapot, NULL);

  // Создаем текстуру, в которую будем рендерить
  D3DXCreateTexture(m_pd3dDevice,       // Дескриптор устройства
                128, 128,               // Размеры
                1,                      // Количество mip-уровней
                D3DUSAGE_RENDERTARGET,  // Параметр указывает, что текстура может
                                        // использоваться в качестве Render Target
                D3DFMT_A8R8G8B8,        // Формат текстуры (32 бита)
                D3DPOOL_DEFAULT,        // Храним в обычной памяти
                &gRenderTexture);       // Указатель на переменную-дескриптор

  // Результирующая текстура, которую мы будем накладывать на экранный квад
  D3DXCreateTexture( m_pd3dDevice, 128, 128, 0, 0, D3DFMT_A8R8G8B8, 
                     D3DPOOL_MANAGED, &gResTexture);
}

Итак у нас есть две текстуры - одна предназначена для рендеринга в нее сцены (объектов, которые должны быть засвеченны), вторую мы будем накладывать на экранный квад. Для этого нам нужно будет переписать всю информацию из RenderTarget во временный массив, посчитать размытое изображение, и записать его в готовую текстуру.

Тут есть один момент - для того, чтобы получить доступ к данным в RenderTarget, необходимо поставить флаг D3DPRESENTFLAG_LOCKABLE_BACKBUFFER в поле Flags структуры D3DPRESENT_PARAMETERS при инициализации.

//
// Шаг второй - рендеринг в текстуру, ф-я должна вызываться 
// в цикле прорисовки после BeginScene
//

// Наш материал
D3DMATERIAL9 mtrl;

// Матрица смещения несветящегося чайника
D3DXMATRIX worldMat;

void RenderToTexture()
{
  LPDIRECT3DSURFACE9 OLD_RT, RT;

  // Получаем текущий сюрфейс, находящийся в Render Target 
  // и сохраняем его в переменную OLD_RT
  m_pd3dDevice->GetRenderTarget(0, &OLD_RT);

  // Получаем сюрфейс нашей текстуры
  gRenderTexture->GetSurfaceLevel(0, &RT);

  // Устанавливаем сюрфейс текстуры в Render Target
  m_pd3dDevice->SetRenderTarget(0, RT);

  // Очищаем Render Target и закрашиваем его черным цветом
  m_pd3dDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
  m_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);
  m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
  m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
  m_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
  m_pd3dDevice->SetTexture(0, 0);

  m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
                       0x00000000, 1.0f, 0L );

  // Ставим чайник в центр координат
  D3DXMATRIX matTrans;
  D3DXMatrixIdentity(&matTrans);
  D3DXMatrixTranslation( &matTrans, 0, 0, 0 );
  m_pd3dDevice->SetTransform( D3DTS_WORLD, &matTrans );

  // Рисуем чайник белым цветом - это будет светящийся объект
  SetupMatrialForGlow(mtrl);  // Эта ф-ия рассмотрена в конце статьи
  m_pd3dDevice->SetMaterial( &mtrl );
  m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, 0);
  m_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
  gTeapot->DrawSubset(0);

  // Для того, чтоб показать эффект перекрывания обычным чайником светящегося,
  // рисуем смещенный вправо чайник полностью черным цветом
  D3DXMatrixTranslation(&worldMat, 1, 0, 0);
  m_pd3dDevice->SetTransform( D3DTS_WORLD, &matTrans );

  D3DUtil_InitMaterial(&mtrl, 0, 0, 0);
  gTeapot->DrawSubset(0);


  // Восстанавливаем предыдущую RenderTarget, 
  // в новой у нас теперь хранится вся необходимая информация
  m_pd3dDevice->SetRenderTarget(0, OLD_RT);
}
//
// Шаг третий - копируем содержимое RenderTarget в текстуру и размываем ее
//

// Эти массивы нужны для хранения цветовых компонентов пикселей текстуры,
// С их помощью мы рассчитаем размытое изображение
BYTE gColorR[128][128];
BYTE gColorG[128][128];
BYTE gColorB[128][128];

// Макросы для получения компонента из цвета
#define _r(x) (BYTE)(x>>16)
#define _g(x) (BYTE)(x>> 8)
#define _b(x) (BYTE)(x>> 0)
#define rgb(r, g, b) (DWORD)((0xff<<24) + (r<<16) + (g<<8) + (b<<0))

void CopyFromRT()
{
  D3DLOCKED_RECT     d3dlr;
  D3DSURFACE_DESC    d3dsd;
  DWORD res;

  // Размер нулевого сюрфейса текстуры
  gRenderTexture->GetLevelDesc(0, &d3dsd);

  // Локаем текстуру 
  gRenderTexture->LockRect(0, &d3dlr, 0, 0);
  DWORD* pSrcPixel = (DWORD*)d3dlr.pBits;

  // Проходим по всем пикселям, и заполняем массивы
  DWORD color;
  for (register i=0; iUnlockRect(0);
}

// Эта ф-я размывает пиксели текстуры 'times' количество раз

void Blur(int times)
{
  int r, g, b;
  float factor = 1.3f;

  for (register p=0; p<times; p++)
  {
     for (register i=1; i<128; i++)
     {
        for (register j=1; j<128; j++)
        {
          // Получаем среднее арифметическое 8-ми соседних для текущего пикселей
          // умножаем на factor для увеличения "яркости" 
          r = ( gColorR[i+1][j+1] + gColorR[i+1][j] + gColorR[i][j+1] +
                gColorR[i-1][j-1] + gColorR[i-1][j] + gColorR[i][j-1] +
                gColorR[i-1][j+1] + gColorR[i+1][j-1] )/8 * factor;
  
          g = ( gColorG[i+1][j+1] + gColorG[i+1][j] + gColorG[i][j+1] +
                gColorG[i-1][j-1] + gColorG[i-1][j] + gColorG[i][j-1] +
                gColorG[i-1][j+1] + gColorG[i+1][j-1] )/8 * factor;

          b = ( gColorB[i+1][j+1] + gColorB[i+1][j] + gColorB[i][j+1] +
                gColorB[i-1][j-1] + gColorB[i-1][j] + gColorB[i][j-1] +
                gColorB[i-1][j+1] + gColorB[i+1][j-1] )/8 * factor;
  
          gColorR[i][j] = r;
          gColorG[i][j] = g;
          gColorB[i][j] = b;
  
          if (r > 255)
            gColorR[i][j] = 255;
          if (g > 255)
            gColorG[i][j] = 255;
          if (b > 255)
            gColorB[i][j] = 255;
        }
     }
  }

  // Локаем результирующую текстуру и копируем туда информацию из массивов
  gResTexture->LockRect( 0, &d3dlr, 0, 0 );
  DWORD* pSrcPixel = (DWORD*)d3dlr.pBits;

  for (i=0; i<128; i++)
  {
    for (j=0; j<128; j++)
    {
      *pSrcPixel++ = rgb(smr[i][j], smg[i][j], smb[i][j]);
    }
  }

  gResTexture->UnlockRect(0);
}
//
// Шаг четвертый - рисуем 2 полигона на экране 
// и накладываем на них полученную текстуру с прозрачностью
//

// Структура экранной вершины и ее FVF (Flexible vertex format)
typedef struct 
{
  D3DXVECTOR4   pos;
  D3DXVECTOR2   tex;
} ScreenVertex;

#define FVF_SCREEN (D3DFVF_XYZRHW | D3DFVF_TEX1)

void RenderGlow()
{
  D3DVIEWPORT9 vp;
  m_pd3dDevice->GetViewport(&vp);
  
  // Заполняем структуру, мы используем параметры viewport'a 
  // для определения размеров экрана
  ScreenVertex v[4];
  v[0].pos = D3DXVECTOR4(0.0f,     vp.Height, 1.0f, 1.0f);
  v[1].pos = D3DXVECTOR4(0.0f,     0.0f,      1.0f, 1.0f);
  v[2].pos = D3DXVECTOR4(vp.Width, vp.Height, 1.0f, 1.0f);
  v[3].pos = D3DXVECTOR4(0.0f,     vp.Height, 1.0f, 1.0f);
  
  // текстура должна быть растянута по всему кваду
  v[0].tex = D3DXVECTOR2(0, 1);
  v[1].tex = D3DXVECTOR2(0, 0);
  v[2].tex = D3DXVECTOR2(1, 1);
  v[3].tex = D3DXVECTOR2(1, 0);

  // Накладываем полученную в результате рендеринга текстуру
  // И настраиваем параметры прозрачности
  m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
  m_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
  m_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
  m_pd3dDevice->SetTexture(0, gResTexture);
  
  // Рисуем вершины стрипом, метод DrawPrimitiveUP не слишком быстрый, 
  // но в нашем случае это несущественно
  m_pd3dDevice->SetFVF(FVF_SCREEN);
  m_pd3dDevice->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v, sizeof(ScreenVertex));
}
//
// Шаг пятый - рендеринг конечной сцены
//

void SetupCamera()
{
  D3DXMATRIX view, proj, world;
  D3DXMatrixPerspectiveFovLH(&proj, D3DXToRadian(60), 1.33333f, 1.0f, 200.0f);
  D3DXMatrixLookAtLH(&view, &D3DXVECTOR3(0.0f, 0.0f, -10.0f), 
                     &D3DXVECTOR3(0.0f, 0.0f, 0.0f), &D3DXVECTOR(0.0f, 1.0f, 0.0f));
  D3DXMatrixIdentity(&world, &world);
  
  m_pd3dDevice->SetTransform(D3DTS_PROJECTION,   &proj);
  m_pd3dDevice->SetTransform(D3DTS_VIEW,   &view);
  m_pd3dDevice->SetTransform(D3DTS_WORLD,   &world);
}

void FinalRender()
{
  // Вызываем поочередно все шаги, а потом рисуем сцену
  m_pd3dDevice->BeginScene();

  RenderToTexture();
  CopyFromRT();
  Blur(2);
  SetupCamera();

  // Чистим задний буфер и заливаем его в синий цвет
  m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, 
                       0xff0000ff, 1.0f, 0L ); 

  // Рисуем чайник с некой текстурой, смещенный вправо на 1
  m_pd3dDevice->SetTexture(0, gSomeTexture); // Считаем, что текстура уже загружена
  gTeapot->DrawSubset(0);

  // Ну, и последний шаг, рисуем экранный квад в самом конце
  RenderGlow();

  m_pd3dDevice->EndScene();
  m_pd3dDevice->Present(0,0,0,0);
}

В результате применения этих операций, Вы увидите это:

Изображение

Конечно, с параметрами отрисовки можно еще поиграть, например поменять местами отрисовку чайника в FinalRender() и RenderGlow(), тогда получиться чайник, с белым ореолом вокруг него:

Изображение

Еще можно поэкспериментировать с параметрами материала чайника при рендеринге в текстуру.

Начальные значения у нас были такие:

void SetupMaterialForGlow(D3DMATERIAL9& mat)
{
  D3DUtil_InitMaterial( mat, 1, 1, 1 );
}

А если сделать цвет черным, а спекулярный цвет очень ярким -

void SetupMaterialForGlow(D3DMATERIAL9& mat)
{
  mat.Ambient = D3DXCOLOR(0, 0, 0, 0);
  mat.Diffuse = D3DXCOLOR(0, 0, 0, 0);
  mat.Specular = D3DXCOLOR(10, 10, 10, 0);
  mat.Power = 20;
}

то получится такой эффект:

Изображение

Вот, собственно, и все. С помощью подобных действий можно рисовать сцены с красивыми и реалистичными световыми эффектами.

#эффекты, #glow

17 апреля 2003 (Обновление: 18 сен 2009)

Комментарии [12]