{{notification.text}}

MirGames

Красивые водные поверхности в современных играх очень украшают пейзаж, и основным способом их рендеринга является EMBM (Environment bump-mapping). Для понимания этой статьи Вам потребуются некоторый уровень владения D3D 9 и начальные знания хотя бы в использовании HLSL шейдеров.

Для начала – что это вообще за Environment bump-mapping? EMBM – одна из разновидностей bump mapping, но в отличии от большинства техник bump-mapping’а, EMBM основан не на картах нормалей в касательном пространстве геометрии, а на картах смещений текстурных координат карты отражения (простите уж за тавтологию). И выглядит это примерно так:


Карты отражений
Первым делом Вам потребуется каким-либо образом получить карту отражений (т.е. текстуру с отражением того, что находится над поверхностью воды).

В идеале необходимо произвести рендеринг всей сцены отраженной относительно поверхности воды в текстуру. Это будет смотреться прекрасно – если, конечно, у Вас еще есть достаточный запас FPS. Тут может помочь снижение разрешения текстуры с отражением, что часто может остаться незамеченным при обильной ряби на водной поверхности, например. При рендеринге крупных водных поверхностей можно использовать LOD (т.е. разрешение текстуры отражений на удаленных участках водной поверхности можно сделать ниже, чем на фрагментах, лежащих перед камерой). Также можно не рендерить отражения каждый фрейм (как сделано, например, в “NFS: Most wanted” с поверхностью автомобилей) если карта отражений не будет существенно изменяться за время между рендерами (а вот в NFS такие изменения как раз очень бросаются в глаза, давая ощущение, что весь рендеринг “притормаживает”). Также можно разделить текстуру отражений на составляющие – часто/редко изменяющуюся. Можно также, например, рендерить лишь редко изменяющиеся объекты.

Но если у Вас, все же, нет подобного запаса в производительности можно воспользоваться фэйковым (фальшивым) отражением (как, например, сделано у нас в проекте “Ступени Бога”, скриншот откуда и был представлен выше). Лучше всего тогда в качестве карты отражений выбирать облака (а ночью, возможно, звезды) и очертания гор на горизонте, если таковые предусмотрены в Вашей сцене. Тогда фальшивость отражения не так сильно бросается в глаза.

Собственно, EMBM
Отлично, с получением карты отражений мы разобрались. Преступим к следующей части – собственно EMBM. Эту технику можно реализовывать с использованием фиксированного конвейера рендеринга (т.е. без использования шейдеров). Такой способ Вы найдете в примере “BumpWaves”, идущего в комплекте с DirectX 9 SDK. Но Вы сразу ставите себя в рамки возможностей, которые любезно предоставляет Вам фиксированный конвейер рендеринга. В данной статье же мы рассмотрим кое-какие “надстройки” над обычным EMBM (изменение цветов во время дождя и вообще в зависимости от окружающего освещения). Но в любом случае, следует ознакомиться с этим примером хотя бы потому, что метод генерации и анимации карты смещений текстурных координат отражения (проще говоря – ряби на воде) здесь позаимствован оттуда.

Но мы все же будет реализовывать свой EMBM с использованием HLSL шейдеров (а именно – одного пиксельного шейдера). Это дает нам дополнительные возможности, к тому же, с приходом DirectX 10 мы вообще лишаемся настоящего фиксированного конвейера рендеринга (он эмулируется шейдерами), так что пора уже привыкать :-)

1. Карта ряби
Для начала нам понадобится карта ряби. Мы будем делать это способом наподобие как в примере “BumpWaves” из DX 9 SDK:

IDirect3DTexture9* bumpMap = 0;  // Собственно, наша карта смещений текстурных координат

  // Функция возвращает либо указатель на успешно сгенерированную bumpMap,
  // либо 0, если генерация не удалась.
  // (Генерация успешно происходит лишь один раз, все последующие вызовы функции вернут
  //    уже созданную карту)
  // Разрешение текстуры влияет, разумеется, на качество ряби и возможность ее “измельчения”
 IDirect3DTexture9* GetBumpMap(DWORD dwHeight = 2048, DWORD dwWidth = 2048)
{
       if (!bumpMap) {
             // Создать текстуру для хранения bump-карты
             // Формат тексутры D3DFMT_V8U8 (16 бит) соответствует карты смещений
             // по одному байту по оси V и оси U текстурных координат
             if( FAILED( Device->CreateTexture( dwWidth, dwHeight, 1, 0 /* Usage */,
                                                                  D3DFMT_V8U8, D3DPOOL_MANAGED,
                                                                  &bumpMap, NULL ) ) )
                   return NULL;

             // Заблокируем поверхность текстуры для возможности внесения изменений
             //   в нее покомпонентно:
             D3DLOCKED_RECT d3dlr;
             bumpMap->LockRect( 0, &d3dlr, 0, 0 );
             CHAR* pDst = (CHAR*)d3dlr.pBits;
             CHAR  iDu, iDv;

             for( DWORD y = 0; y < dwHeight; y++ )
             {
                   CHAR* pPixel = pDst;

                   for( DWORD x = 0; x < dwWidth; x++ )
                   {
                         // Попробуйте поиграть с константами в этом фрагменте кода – 
                         //  увидите, как меняется ваша рябь (тут указаны значения,
                         //  которые используются у нас)
                         FLOAT fx = x/(FLOAT)dwWidth - 0.5f;
                         FLOAT fy = y/(FLOAT)dwHeight - 0.2f;

                         FLOAT r = sqrtf( fx*fx + fy*fy );

                         iDu  = (CHAR)( 64 * cosf( 1600.0f * r ) * expf( -r * 5.0f ) );
                         iDu += (CHAR)( 32 * cosf( 800.0f * ( fx + fy ) ) );
                         iDu += (CHAR)( 16 * cosf( 880 * ( fx * 0.85f - fy ) ) );

                         iDv  = (CHAR)( 64 * sinf( 1600.0f * r ) * expf( -r * 5.0f ) );
                         iDv += (CHAR)( 32 * sinf( 800.0f * ( fx + fy ) ) );
                         iDv += (CHAR)( 16 * sinf( 880.0f * ( fx * 0.85f - fy ) ) );

                         *pPixel++ = iDu;
                         *pPixel++ = iDv;
                   }
                   pDst += d3dlr.Pitch;
             }
             // Разблокируем текстуру для возможности ее использования
             bumpMap->UnlockRect(0);
       }
       return bumpMap;
}

Мы получим рябь, идущую из точечного источника в разные стороны. Также можно, например, использовать готовую текстуру смещений текстурных координат разного типа для получения разной ряби. Такие текстуры можно найти в Интернете.

Отлично. Карта ряби у нас есть. Но пока она – статическая. Проанимируем ее матрицей (пока матрица – сама по себе, далее будем использовать ее в пиксельном шейдере):

D3DXMATRIXA16 bumpMat;
  // Вызывается каждый фрейм. 
  // timeDelta – количество секунд (в виде числа с плавающей точкой),
  //  прошедших с предыдущего вызова этой функции.
void Update(float timeDelta)
{
     static float shdTime = 0;
     shdTime += timeDelta;
       // Тут ваш апдейт

     // Также попробуйте поиграть с константами – они влияют на скорость и
     //  направление движения ряби.
     const float r = 0.8f;
     bumpMat._11 =  r * cosf( shdTime * 20.0f );
     bumpMat._12 = -r * sinf( shdTime * 20.0f );
     bumpMat._21 =  r * sinf( shdTime * 20.0f );
     bumpMat._22 =  r * cosf( shdTime * 20.0f );
}

2. Шейдер EMBM
Перед тем, как приступить к рендерингу, рассмотрим, наконец главное: шейдер EMBM.

Да, для начала небольшое замечание – шейдер применяется в обычном world-пространстве поверхности воды, а не в касательном пространстве. Дело в том, что наша вода – плоскость и ее tangent space с ортогональным базисом пространства текстурных координат, и ее касательное пространство совпадает с мировым.

uniform float3 cameraPos;
 uniform float4 lightColor;
 uniform matrix offsetMat;    // Она же – наша bumpMat
  
 sampler tex : register (s0);       // Texture stage 0 – Карта отражения
 sampler bumpMap : register (s1);   // Texture stage 1 – Карта смещений текстурных корд.
  
 float4 main(float2 texCoord: TEXCOORD0): COLOR
 {
       // Для начала получим смещение наших текстурных координат
       // согласно карте смещений
       float4 texOffset = tex2D(bumpMap, texCoord);
       // Проанимируем эти смещения
       texOffset = mul(texOffset, offsetMat);
       // Сместим полученные на входе текстурные координаты
             // Замечание: текстурные координаты нашего фрагмента воды – квадрат от (0, 0)
           // до (1, 1). Слагаемые (-cameraPos.x/512 + 0.5f) и (-cameraPos.z/512 + 0.5f)
             // есть смещения карты отражения вместе с камерой, чтобы отражение казалось
             // отражением (ИСПОЛЬЗУЕТСЯ ЛИШЬ ДЛЯ ФЕЙКОВОГО ОТРАЖЕНИЯ, ибо рендеринг в
             // текстуру отражения заведомо учитывает такое смещение).
       texCoord[0] += (-cameraPos.x/512 + 0.5f) + texOffset[0];
       texCoord[1] += (-cameraPos.z/512 + 0.5f) + texOffset[1];
       // Получение цвета текселя в уже смещенных текстурных координатах
       float4 pixColor = tex2D(tex, texCoord);
       
       // Этот фрагмент используется для изменения цвета воды (здесь – на более зеленый)
       // при недостатке освещения, придавая больше зеленого цвета поверхности воды
       // в вечернее и ночное время, или же, у нас, во время дождя. Вы можете опустить
       // Последующие 3 строки кода или поиграть с этим кодом как вам вздумается.
       float bright = -(lightColor.r + lightColor.g + lightColor.b - 1.3f)/3;
       if (bright > 0)
             lightColor += float4(0.0f, bright, bright, bright);
             
       // Ну и, наконец, понятно, что вода блестит цветом окружающего освещения.
       // Умножаем результат на этот цвет:
       return pixColor * lightColor;
 }

В слагаемых (-cameraPos.x/512 + 0.5f) и (-cameraPos.z/512 + 0.5f) /512 — нужно для пересчета смещения камеры относительно поверхности воды в текстурные координаты.

Вам же нужно делить на длину и ширину прямоугольника с поверхностью воды (после применения к нему мировой матрицы).

Смещение происходит по x и z т.к. у нас вода лежит в плоскости xz.

При расчете конечного результата можно умножить полученный цвет еще и на скалярное произведение нормали на минус вектор света для правильного освещения поверхности.

Здесь же этого не сделано для получения «блестящей» воды в темное время. На яркость поверхности при таком рассчете влияет ярость цвета освещения.

3. Отрисовка воды
Что ж – осталось лишь отрисовать воду. Считается, что буфер вершин уже есть. Пиксельный шейдер воды также уже скомпилирован и создан. Компилируется он, начиная с версии “ps_2_0”, т.е. на видеокартах, поддерживающих shader model 2.0 или выше.

void Draw(IDirect3DDevice9* Device)
{
       // Ставим мировую матрицу воды
  
       // Если у Вашей воды есть прозрачность:
       DWORD dwBlendFactor = /* Вычисление бленд-фактора */;
       Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
       Device->SetRenderState(D3DRS_BLENDFACTOR, dwBlendFactor);
       Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_BLENDFACTOR);
       Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
       // Установка материалов и текстур:
       Device->SetMaterial(/* Указатель на белый материал */);
       Device->SetTexture(0, reflectionMap);
       Device->SetTexture(1, GetBumpMap(2048, 2048) ); // Ваша карта смещений текс. корд.
  
       // Что-то наподобие (в зависимости от вашей обертки шейдеров и методов получения
       //  информации о камере, освещении и т.п.)
       if (waterPixelShader) {
             waterPixelShader->SetVector("lightColor", &lightColor);
             waterPixelShader->SetFloatArray("playerPos",
                                          (const FLOAT*)(GODCamera::GetPosition()), 3);
             waterPixelShader->SetMatrix("offsetMat", &bumpMat);
             Device->SetPixelShader(waterPixelShader);
       }
  
       // Установка буфера вершин (используем неиндексированный буфер – чего
       // там индексировать – всего 2 треугольника)
       Device->SetStreamSource(0, vbWater, 0, sizeof(Vertex));
       
       // Собственно, рендеринг водной поверхности
       Device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
  
       // Восстановим важные состояния D3D
       Device->SetPixelShader(0);
       Device->SetTexture(1, 0);
       Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
}

Итак, если Вы все делали правильно (естественно, заменяя все имена и вообще архитектуру на Ваши собственные) и ваша видеокарта поддерживает 2-е шейдеры – Вы получите красивую анимированную водную поверхность.

При реализации очень советую подглядывать также в пример “BumpWaves” от Microsoft.

Специально для www.mirgames.ru
13.05.06 05:39