Реализация glow-эффекта вокруг трёхмерного объекта.
Автор: Resolver
В 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)); }