Variance shadow mapping - мягкие тени
Наши тени в текущем виде смотрятся неплохо, но ведут себя не так как те, которые мы видим в реальном мире. Хотя края теней часто бывают четкие, но чаще всего - нет. В пасмурные дни, или когда объекты освещаются рассеянным светом (непрямой источник освещения), тени часто выглядят нечеткими и смазанными. В следующих разделах мы реализуем
мягкие тени (soft shadows) с помощью метода называемого
Variance Shadow Maps (VSM). Большим преимуществом рассеянных теней является то, что мы можем использовать фильтры для текстуры глубины (we can filter the depth texture) как для обычной текстуры - в данном случае размывая ее - не разрушая при этом результирующие тени.
Этот раздел в основном посвящен реализации VSM, но оригинальная статья и презентация данного метода, также как и другой пример реализации, доступны здесь:
http://www.punkuser.net/vsm.
Первым различием между "обычным" проецированием теней и VSM является то, что мы сохраняем глубину и квадрат глубины в самой текстуре глубины. Позже мы используем эти значения для апроксимации теней. Сначала нам потребуется нам надо обновить эффект отрисовки глубинной текстуры (depth texture
rendering effect), чтобы он возвращал оба эти значения:
Код: |
return float4(depth, depth * depth, 0, 1); |
Теперь когда эффект глубинной текстуры возвращает два значения, нам нужно изменить формат поверхности (surface format), который мы используем. В прошлом мы использовали
SurfaceFormat.Single, который является 32-bit'ным форматом, где все 32 бита выделены для красного канала. Это позволяет нам хранить относительно точные значения глубины в красном канале цели отрисовки (render target) - гораздо точнее чем
SurfaceFormat.Color например, который отдает только 8 бит красному каналу.
Так как мы теперь храним два значения, мы будем использовать
SurfaceFormat.HalfVector2. Это тоже 32-bit'ный формат, в котором 16 бит отведены красному каналу, и 16 бит - зеленому каналу. Это даст меньшую точность по сравнению с тем, что мы использовали, но, поскольку мы размываем карту тени, разница не очень заметна. Это позволит нам оставить требования к памяти низкими, особенно с учетом количества целей отрисовки (render targets), которые у нас набрались.
Код: |
shadowDepthTarg = new RenderTarget2D(GraphicsDevice, shadowMapSize,
shadowMapSize, false, SurfaceFormat.HalfVector2,
DepthFormat.Depth24); |
Variance shadow mapping - размывание текстуры глубины
Следующий шаг в технологии VSM - это размывание текстуры глубины, которую мы только что отобразили, используя то, что называется
Gaussian blur. Глава 8 рассматривает гауссово размывание гораздо более детально, но ради простоты мы используем заранее вычисленные параметры для размывания. Размывание глубинной текстуры даст нам мягкие тени. Если мы не будем размывать текстуру глубины, то мы получим тени почти идентичные тем, что были получены нами ранее.
Размывние (blurring) это процесс усреднения пикселя и его соседей для каждого пикселя изображения - "сглаживание" изображения в целом. Gaussian blur улучшает эффект используя специфичные смещения пикселей и в
есы, но это такой же процесс.
Эффект Gaussian blur:
Код: |
// Текстура для размывания
texture ScreenTexture;
sampler2D tex = sampler_state {
texture = <ScreenTexture>;
minfilter = point;
magfilter = point;
mipfilter = point;
};
// Заранее вычисленные весы и смещения
float weights[15] = { 0.1061154, 0.1028506, 0.1028506, 0.09364651,
0.09364651, 0.0801001, 0.0801001, 0.06436224, 0.06436224,
0.04858317, 0.04858317, 0.03445063, 0.03445063, 0.02294906,
0.02294906 };
float offsets[15] = { 0, 0.00125, -0.00125, 0.002916667,
-0.002916667, 0.004583334, -0.004583334, 0.00625, -0.00625,
0.007916667, -0.007916667, 0.009583334, -0.009583334, 0.01125,
-0.01125 };
// Горизонтальное размывание изображения
float4 BlurHorizontal(float4 Position : POSITION0,
float2 UV : TEXCOORD0) : COLOR0
{
float4 output = float4(0, 0, 0, 1);
// Сэмплирование из окружающих пикселей используя заранее вычисленные
// смещения пикселей (pixel offsets) и весы цветов (color weights)
for (int i = 0; i < 15; i++)
output += tex2D(tex, UV + float2(offsets[i], 0)) * weights[i];
return output;
}
// Вертикальное размывание изображения
float4 BlurVertical(float4 Position : POSITION0,
float2 UV : TEXCOORD0) : COLOR0
{
float4 output = float4(0, 0, 0, 1);
for (int i = 0; i < 15; i++)
output += tex2D(tex, UV + float2(0, offsets[i])) * weights[i];
return output;
}
technique Technique1
{
pass Horizontal
{
PixelShader = compile ps_2_0 BlurHorizontal();
}
pass Vertical
{
PixelShader = compile ps_2_0 BlurVertical();
}
}
|
Обратите внимание, что этот эффект содержит два метода - один для горизонтального размывания изображения и один - для вертикального. Мы производим независимые размывания по обоим направлениям чтобы получить более мягкое размывание. Также обратите внимание, что каждый пиксельный шейдер просто добавляет вклад 15-и соседних пикселей и усредняет результат так, как было сказано ранее.
Чтобы класс
PrelightingRenderer выполнил размывание, напотребуется еще несколько экземплярных переменных (instance variables) -
SpriteBatch чтобы отобразить карту глубины (draw depth map) в цели отрисовки (into render targets), эффект Gaussian blur и цель отрисовки (render target), в которой мы будем хранить результат горизонтального размывания. Мы проводим вертикальное размывание используя первоначальный буфер глубины (original depth buffer) в качестве цели, сэмплируя сцену размытую горизонтально из этой вторичной цели отрисовки (render target).
Код: |
SpriteBatch spriteBatch;
RenderTarget2D shadowBlurTarg;
Effect shadowBlurEffect; |
Эти значения должны быть инициализированы в конструкторе:
Код: |
spriteBatch = new SpriteBatch(GraphicsDevice);
shadowBlurEffect = Content.Load<Effect>("GaussianBlur");
shadowBlurTarg = new RenderTarget2D(GraphicsDevice, shadowMapSize,
shadowMapSize, false, SurfaceFormat.Color, DepthFormat.Depth24); |
Затем мы создадим функцию, которая выполнит размывание. Обратите внимание, что мы указываем из какой цели отрисовки (render target) проводится сэмплирование, и в какую цель отрисовки (render target) пойдет результат. Также мы указываем какой из методов применять с помощью параметра
dir - 0 для горизонтального размывания и 1 для вертикального.
Код: |
void blurShadow(RenderTarget2D to, RenderTarget2D from, int dir)
{
// Указываем целевую цель отрисовки (render target)
graphicsDevice.SetRenderTarget(to);
graphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque);
// Начинаем эффект Gaussian blur
shadowBlurEffect.CurrentTechnique.Passes[dir].Apply();
// Отображаем содержимое исходной цели отрисовки, чтобы они
// могли быть размыты пиксельным шейдером gaussian blur
spriteBatch.Draw(from, Vector2.Zero, Color.White);
spriteBatch.End();
// Clean up после sprite batch
graphicsDevice.BlendState = BlendState.Opaque;
graphicsDevice.DepthStencilState = DepthStencilState.Default;
// Удаляем цель отрисовки
graphicsDevice.SetRenderTarget(null);
}
|
В последнем изменении в классе
PrelightingRenderer, нам нужно убедиться, что мы размываем тень в функции
Draw(). Обратите внимание, что мы сначала копируем из глубинной цели (depth target) в цель размывания (blur target) размывая при этом горизонтально, а затем копируем обратно из цели размывания в цель глубины размывая при этом вертикально.
Код: |
public void Draw()
{
drawDepthNormalMap();
drawLightMap();
if (DoShadowMapping)
{
drawShadowDepthMap();
blurShadow(shadowBlurTarg, shadowDepthTarg, 0);
blurShadow(shadowDepthTarg, shadowBlurTarg, 1);
}
prepareMainPass();
}
|
Variance shadow mapping - генерирование теней
Последний шаг в VSM - это генерирование самих теней. Мы проделаем это в том же месте, где мы вычисляли тени в предыдущем примере. Так как мы теперь храним два значения в текстуре глубины, сначала нам надо обновить функцию сэмплирования, чтобы она возвращала значения красного и зеленого:
Код: |
float2 sampleShadowMap(float2 UV)
{
if (UV.x < 0 || UV.x > 1 || UV.y < 0 || UV.y > 1)
return float2(1, 1);
return tex2D(shadowSampler, UV).rg;
} |
Наконец мы можем обновить пиксельный шейдер, чтобы он проводил вычисления для variance shadow mapping. Мы берем сэмпл из текстуры глубины как и обычно чтобы получить значение глубины, которое она содержит, вычисляем расстояние до источника света (light distance) как и обычно внося небольшое смещение (offsetting it with a small bias). После этого мы проводим вычисления теней как было показано в демонстрационном коде VSM:
Код: |
float shadow = 1;
if (DoShadowMapping)
{
float2 shadowTexCoord = postProjToScreen(input.ShadowScreenPosition)
+ halfPixel();
float realDepth = input.ShadowScreenPosition.z / ShadowFarPlane
- ShadowBias;
if (realDepth < 1)
{
// Variance shadow mapping code below from the variance shadow
// mapping demo code @ http://www.punkuser.net/vsm/
// Сэмплируем из текстуры глубины
float2 moments = sampleShadowMap(shadowTexCoord);
// Проверка находимся ли мы в тени
float lit_factor = (realDepth <= moments.x);
// Variance shadow mapping
float E_x2 = moments.y;
float Ex_2 = moments.x * moments.x;
float variance = min(max(E_x2 - Ex_2, 0.0) +
1.0f / 10000.0f, 1.0);
float m_d = (moments.x - realDepth);
float p = variance / (variance + m_d * m_d);
shadow = clamp(max(lit_factor, p), ShadowMult, 1.0f);
}
}
return float4(basicTexture * DiffuseColor * light * shadow, 1);
|
Summary
Теперь, когда вы закончили эту главу, вы научились как использовать проективное (проекционное) текстурирование чтобы проецировать двумерные изображения в ваши трехмерные сцены. Вы также научились как расширять/увеличивать (extend) эффект проективного текстурирования чтобы проецировать текстуры глубины на сцену. Затем вы узнали как использовать текстуру глубины чтобы вычислять тени с четкими и мягкими краями. В следующей главе мы рассмотрим несколько "шейдерных эффектов". Это эффекты, которые используют шейдеры, но не относятся строго к эффектам освещения - отражения, туман и т.д.