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

Создание зеркал с использованием stencil буфера.

Автор:

Итак, что нам надо, чтобы нарисовать отражение объекта?
1)  Сам объект
2)  Отражающая поверхность (зеркало)

Тогда для прорисовки отражения надо развернуть камеру относительно зеркала и повторно нарисовать объект. Всё. Отражение нарисовано. Сложность только в том, а что делать, когда зеркало конечно, т.е. когда возникает необходимость обрезки отражения? Ведь оно не должно выходить за пределы зеркала.

Есть несколько вариантов решения данной проблемы:
1)  С помощью плоскостей отсечения (clipping planes)
2)  С помощью рендера в текстуру.
3)  С помощью stencil буфера (трафарета)

В первом случае перед прорисовкой отражения нам надо создать столько плоскостей отсечения, сколько у зеркала граней, причём расположить плоскости надо перпендикулярно каждой из граней зеркала. Кроме того, существует ограничение на форму зеркала, из-за ограниченного количества плоскостей отсечения.

Здесь рассматривается способ использования метода stencil буфера (буфер трафарета), который позволяет выводить только те пикселы, которые прошли тест трафарета. В общих словах мы рисуем в трафарет зеркало (на месте зеркала трафарет заполняется единицами), а затем, до вывода отражения, мы указываем трафарету, что отображать следует только те пикселы отражения, которые лежат на зеркале (т.е. которым в трафарете соответствуют единицы). Если же конкретнее, то нам надо:
1)  Создать трафарет
2)  Очистить его
3)  Нарисовать отражение:
a)  Нарисовать зеркало с выключенным тестом трафарета
b)  Отразить камеру относительно зеркала
c)  Нарисовать отражение с включенным тестом трафарета
4)  Повторить пункт 3 каждый раз, когда производим рендеринг

Следует заметить, что, если вы хотите, чтобы рисовалось зеркало, вам это надо выполнить отдельно, несмотря на пункт 3.a, т.к. он не влияет на отображение зеркала на экране.

Создание трафарета.

Для создания трафарета надо установить член EnableAutoDepthStencil структуры D3DPRESENT_PARAMETERS, передаваемой при создании устройства (т.е. методу IDirect3D8::CreateDevice), в true, а в качестве AutoDepthStencilFormat задать один из действительных форматов буфера глубины – трафарета, поддерживающих трафарет (например, D3DFMT_D24S8).

Приведённый фрагмент демонстрирует сказанное.

  D3DPRESENT_PARAMETERS d3dpp; 

  ZeroMemory( &d3dpp, sizeof(d3dpp) );
  d3dpp.Windowed               = TRUE;
  d3dpp.SwapEffect             = D3DSWAPEFFECT_COPY_VSYNC;
  d3dpp.EnableAutoDepthStencil = TRUE;
  d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8;

  if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
                                  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                  &d3dpp, &p_d3d_Device ) ) )
    return E_FAIL;

Очистка трафарета.

Перед тем, как рисовать в трафарет, его, как и Z – буфер, надо очищать. Делается это тем же методом IDirect3DDevice8::Clear. Только теперь надо передать в качестве третьего параметра D3DCLEAR_STENCIL, а в пятом параметре передаётся значение, которым заполнится весь трафарет. В приведённом ниже фрагменте это 0.

  p_d3d_Device->Clear( 0L, NULL, D3DCLEAR_STENCIL , 0L, 0L, 0 );

Прорисовка отражения.

Итак, вот фрагмент, отвечающий за прорисовку отражения.

    p_d3d_Device->SetRenderState( D3DRS_STENCILENABLE, TRUE );
    p_d3d_Device->SetRenderState( D3DRS_STENCILFUNC,  D3DCMP_ALWAYS );
    p_d3d_Device->SetRenderState( D3DRS_STENCILREF,      0x1 );
    p_d3d_Device->SetRenderState( D3DRS_STENCILMASK,     0xffffffff );
    p_d3d_Device->SetRenderState( D3DRS_STENCILWRITEMASK,0xffffffff );
    p_d3d_Device->SetRenderState( D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP );
    p_d3d_Device->SetRenderState( D3DRS_STENCILFAIL,  D3DSTENCILOP_KEEP );
    p_d3d_Device->SetRenderState( D3DRS_STENCILPASS,  D3DSTENCILOP_REPLACE );

    // Отключаем запись в z – буфер и включаем прозрачность
    p_d3d_Device->SetRenderState( D3DRS_ZWRITEENABLE,  FALSE );
    p_d3d_Device->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE );
    p_d3d_Device->SetRenderState( D3DRS_SRCBLEND,  D3DBLEND_ZERO );
    p_d3d_Device->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE );

    // Рисуем зеркало в трафарет
    p_d3d_Device->SetTexture( 0, NULL);
    //p_d3d_Device->SetTransform( D3DTS_WORLD, &m_matMirrorMatrix );
    p_d3d_Device->SetVertexShader( D3DFVF_MIRRORVERTEX );
    p_d3d_Device->SetStreamSource( 0, m_pMirrorVB, sizeof(MIRRORVERTEX) );
    p_d3d_Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );

    p_d3d_Device->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );

    // Сохраняем видовую матрицу
    D3DXMATRIX matViewSaved;
    p_d3d_Device->GetTransform( D3DTS_VIEW, &matViewSaved );

    // Отражаем камеру относительно зеркала
    D3DXMATRIX matView, matReflect;
    D3DXPLANE plane;
    D3DXPlaneFromPointNormal( &plane, &D3DXVECTOR3(0,31,0), &D3DXVECTOR3(0,32,0) );
    D3DXMatrixReflect( &matReflect, &plane );
    D3DXMatrixMultiply( &matView, &matReflect, &matViewSaved );
    p_d3d_Device->SetTransform( D3DTS_VIEW, &matView );

    // Задаём плоскость отсечения, чтобы отражалось только то, что выше зеркала
    p_d3d_Device->SetClipPlane( 0, plane );
    p_d3d_Device->SetRenderState( D3DRS_CLIPPLANEENABLE, 0x01 );

    // Т.к. видовая матрица изменилась, необходимо изменить порядок вывода полигонов.
    p_d3d_Device->SetRenderState( D3DRS_ZWRITEENABLE, TRUE );
    p_d3d_Device->SetRenderState( D3DRS_STENCILFUNC,  D3DCMP_EQUAL );
    p_d3d_Device->SetRenderState( D3DRS_STENCILPASS,  D3DSTENCILOP_KEEP );
    p_d3d_Device->SetRenderState( D3DRS_SRCBLEND,     D3DBLEND_DESTCOLOR );
    p_d3d_Device->SetRenderState( D3DRS_DESTBLEND,    D3DBLEND_ZERO );
    p_d3d_Device->SetRenderState( D3DRS_CULLMODE,     D3DCULL_CW );

    // Очищаем z - буфер
    p_d3d_Device->Clear( 0L, NULL, D3DCLEAR_ZBUFFER, 0L, 1.0f, 0L );

    // Рисуем отражение
    mUser.Render();

    // Восстанавливаем исходное состояние рендера
    p_d3d_Device->SetRenderState( D3DRS_CULLMODE,         D3DCULL_CCW );
    p_d3d_Device->SetRenderState( D3DRS_STENCILENABLE,    FALSE );
    p_d3d_Device->SetRenderState( D3DRS_CLIPPLANEENABLE,  0x00 );
    p_d3d_Device->SetTransform( D3DTS_VIEW, &matViewSaved );

Сначала мы делаем доступным трафарет (иначе, как мы его будем использовать?), затем задаётся функция сравнения для теста трафарета. Константа D3DCMP_ALWAYS указывает, что он будет всегда проходить успешно. Затем задаётся значение, которое будет писаться в трафарет (у нас это 0x01 – т.е. единица). Строка

p_d3d_Device->SetRenderState( D3DRS_STENCILPASS,  D3DSTENCILOP_REPLACE );

указывает, что когда пикселы будут проходить тест трафарета (а они будут проходить его всегда, т.к. мы задали D3DCMP_ALWAYS – см. предложения выше), место, где будут находиться эти пикселы заполнится единицами. Что это нам даёт? Теперь, когда мы нарисуем в зеркало, ему в трафарете будут соответствовать единицы, но об этом чуть позже.

Сейчас же отключается тест глубины, чтобы зеркало ничего не загораживало и включается alpha blending, чтобы зеркало имело прозрачность (подробнее об этом – в статье MegaMax’а [gurl=http://www.gamedev.ru/articles/read.shtml?id=10104]“Прозрачные объекты”[/url]).

    p_d3d_Device->SetRenderState( D3DRS_ZWRITEENABLE,  FALSE );
    p_d3d_Device->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE );
    p_d3d_Device->SetRenderState( D3DRS_SRCBLEND,  D3DBLEND_ZERO );
    p_d3d_Device->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE );

Только после этого рисуется зеркало (помните, что я говорил насчёт единиц в трафарете)

    // Рисуем зеркало в трафарет
    p_d3d_Device->SetTexture( 0, NULL);
    //p_d3d_Device->SetTransform( D3DTS_WORLD, &m_matMirrorMatrix );
    p_d3d_Device->SetVertexShader( D3DFVF_MIRRORVERTEX );
    p_d3d_Device->SetStreamSource( 0, m_pMirrorVB, sizeof(MIRRORVERTEX) );
    p_d3d_Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );

Т.к. после того, как мы нарисовали зеркало, прозрачность нам не нужна, мы её отключаем.

p_d3d_Device->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );

Теперь надо отразить видовую камеру относительно зеркала, изменив видовую матрицу. Для этого, во-первых, сохраняется старая видовая матрица, чтобы вернуть её по завершению работы с отражением. Т.к. у меня зеркало проходит через точку (0; 31; 0) перпендикулярно её нормали (0; 32; 0), то я строю поверхность зеркала заданным образом, получаю матрицу отражения осей координат относительно зеркала и умножаю видовую матрицу на матрицу отражения осей координат, получая искомую матрицу отражения камеры относительно зеркала. Остаётся только сделать искомую камеру видовой.

    // Сохраняем видовую матрицу
    D3DXMATRIX matViewSaved;
    p_d3d_Device->GetTransform( D3DTS_VIEW, &matViewSaved );

    // Отражаем камеру относительно плоскости зеркала
    D3DXMATRIX matView, matReflect;
    D3DXPLANE plane;
    D3DXPlaneFromPointNormal( &plane, &D3DXVECTOR3(0,31,0), &D3DXVECTOR3(0,32,0) );
    D3DXMatrixReflect( &matReflect, &plane );
    D3DXMatrixMultiply( &matView, &matReflect, &matViewSaved );
    p_d3d_Device->SetTransform( D3DTS_VIEW, &matView );

Всё-таки одна плоскость отсечения нам необходима. Ею будет являться плоскость зеркала (см. выше, как мы создали plane), чтобы отражалось только то, что расположено надо зеркалом. Сделать это необходимо, потому что трафарет рисует то, что находится в плоскости зеркала, не взирая на то, как расположено зеркало и отражаемый объект. Поясню сказанное на примере. Создайте зеркало и поместите под него отражаемый объект и закомментируйте эти две строчки. Как ни странно, но тогда вы увидите отражение, чего быть не может, т.к. объект находится сзади отражающей поверхности зеркала.

// Задаём плоскость отсечения, чтобы отражалось только то, что выше зеркала
    p_d3d_Device->SetClipPlane( 0, plane );
    p_d3d_Device->SetRenderState( D3DRS_CLIPPLANEENABLE, 0x01 );

Теперь осталось только нарисовать отражение. Для этого мы включаем запись в Z – буфер, чтобы правильно рисовалось отражение. Затем в качестве функции теста трафарета устанавливаем эквивалентность. После этого из всех пикселов рисоваться будут только те, которые прошли тест трафарета, т.е. которым в трафарете соответствует значение, заданное ранее в D3DRS_STENCILREF. Т.е. рисоваться будет только то, что попадает на зеркало.

    p_d3d_Device->SetRenderState( D3DRS_ZWRITEENABLE, TRUE );
    p_d3d_Device->SetRenderState( D3DRS_STENCILFUNC,  D3DCMP_EQUAL );
    p_d3d_Device->SetRenderState( D3DRS_STENCILPASS,  D3DSTENCILOP_KEEP );
    p_d3d_Device->SetRenderState( D3DRS_SRCBLEND,     D3DBLEND_DESTCOLOR );
    p_d3d_Device->SetRenderState( D3DRS_DESTBLEND,    D3DBLEND_ZERO );
Также строкой
    p_d3d_Device->SetRenderState( D3DRS_CULLMODE,     D3DCULL_CW );

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

Ну и последнее, что делается перед отрисовкой отражения, это очистка z – буфера, чтобы отражение ничем не загораживалось. Например, если отражение находится ниже уровня земли, а вы хотите, чтобы оно рисовалось поверх земли, имеет смысл очистить z – буфер (только учтите, что в этом случае прорисовка отражений должна выполняться последней, иначе у вас поверх всего нарисуется ещё что-нибудь, кроме отражений). Если же вам этого не надо, чтобы отражение рисовалось таким образом, удалите строку очистки.

    p_d3d_Device->Clear( 0L, NULL, D3DCLEAR_ZBUFFER, 0L, 1.0f, 0L );

А вот, собственно, и отрисовка отражения.

    mUser.Render();

После этого мы возвращаем изначальное состояние рендера. Устанавливаем обычный порядок отрисовки полигонов, выключаем трафарет, убираем плоскость отсечения и возвращаем старую видовую матрицу.

    // Восстанавливаем исходное состояние рендера
    p_d3d_Device->SetRenderState( D3DRS_CULLMODE,         D3DCULL_CCW );
    p_d3d_Device->SetRenderState( D3DRS_STENCILENABLE,    FALSE );
    p_d3d_Device->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );
    p_d3d_Device->SetRenderState( D3DRS_CLIPPLANEENABLE,  0x00 );
    p_d3d_Device->SetTransform( D3DTS_VIEW, &matViewSaved );

Вот и всё.

#Direct3D, #stencil

27 сентября 2003 (Обновление: 5 окт. 2009)

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