Использование Вершинных Текстур в играх на платформе XNA (перевод)
Catalin ZimaБонус: Освещение ландшафта
Как возможно Вы могли заметить, до сих пор мы рисовали ландшафт без освещения, сейчас мы будем это исправлять. Так как формирование формы поверхности ландшафта происходит в вершинном шейдере, то для расчета освещения данные о нормалях должны существовать до этапа формирования поверхности. Если бы у нас был статический ландшафт, то мы бы просто сформировали карту нормалей на этапе формирования карты высот и передавали бы их в шейдеры. Но при использовании динамически изменяемого ландшафта рельеф поверхности меняется практически непредсказуемым образом. Итак, для получения нормалей сетки ландшафта мы будет генерировать карту нормалей на основе интерполированной карты высот с использованием фильтра Собеля (Sobel Filter). Данная техника была позаимствована в одной из демосцен ATI. Откройте код шейдера TextureMorph.fx и добавьте следующий код:
// Значение Normal Strength подбирается наугад,
// пока не добьетесь приемлемого результат
// Чем выше значение, тем более яркое освещение будет получаться
float normalStrength = 8.0f;
float4 ComputeNormalsPS(in float2 uv : TEXCOORD0) : COLOR
{
// левый верхний - top left
float tl = abs(tex2D (textureSampler1,
uv + texelSize * float2(-1, -1) ).x);
// левый - left
float l = abs(tex2D (textureSampler1,
uv + texelSize * float2(-1, 0) ).x);
// левый нижний - bottom left
float bl = abs(tex2D (textureSampler1,
uv + texelSize * float2(-1, 1) ).x);
// верхний - top
float t = abs(tex2D (textureSampler1,
uv + texelSize * float2( 0, -1) ).x);
// нижний - bottom
float b = abs(tex2D (textureSampler1,
uv + texelSize * float2( 0, 1) ).x);
// верхний правый - top right
float tr = abs(tex2D (textureSampler1,
uv + texelSize * float2( 1, -1) ).x);
// правый - right
float r = abs(tex2D (textureSampler1,
uv + texelSize * float2( 1, 0) ).x);
// нижний правый - bottom right
float br = abs(tex2D (textureSampler1,
uv + texelSize * float2( 1, 1) ).x);
// расчёт вектора dx using Sobel:
//
// -1 0 1
// -2 0 2
// -1 0 1
float dX = -tl - 2.0f * l - bl + tr + 2.0f * r + br;
// расчёт вектора dy using Sobel:
//
// -1 -2 -1
// 0 0 0
// 1 2 1
float dY = -tl - 2.0f * t - tr + bl + 2.0f * b + br;
// расчёт нормализованного вектора нормали - Normal
float4 N = float4(normalize(float3(dX,
1.0f / normalStrength,
dY) ),
1.0f);
//преобразование из (-1.0 , 1.0) в (0.0 , 1.0);
return N * 0.5f + 0.5f;
}
technique ComputeNormals
{
pass P0
{
pixelShader = compile ps_3_0 ComputeNormalsPS();
}
}
Этот код нужен для генерирования нормализованной карты. Теперь мы должны добавить код реализующий освещение непосредственно в шейдер рисования ландшафта. Открываем код шейдера VTFDisplacement.fx и добавляем в него вектор направления света, текстуру и самплер для нее. Так как обращение к текстуре будет осуществляться из пиксельного шейдера, то мы можем использовать билинейную фильтрацию.
float4 lightDirection = {1, -0.7, 1, 0};
texture normalMap;
sampler normalSampler = sampler_state
{
Texture = <normalMap>;
MipFilter = Linear;
MinFilter = Linear;
MagFilter = Linear;
AddressU = clamp;
AddressV = clamp;
};
Нам нужно модифицировать пиксельный шейдер для реализации расчета освещения. Для расчета освещения мы будем использовать простую формулу L * N и немного подсвечивать сцену для эмуляции окружающего освещения на 0.2f.
float4 PixelShader(in float4 uv : TEXCOORD0,
in float4 weights : TEXCOORD2) : COLOR
{
[...]
float4 finalColor = sand * weights.x +
grass * weights.y +
rock * weights.z +
snow * weights.w;
// читаем данные нормали из карты и
// преобразовываем значения обратно в диапазон (-1, 1)
float4 normal = normalize( 2.0f * (tex2D(normalSampler, uv) - 0.5f));
float4 light = normalize(-lightDirection);
// dot product между векторами light and normal
float ldn = dot(light, normal);
ldn = max(0, ldn);
// добавляем коэффициент окружающего освещения и перемножаем с исходным цветом
return finalColor * (0.2f + ldn);
}
И последние изменения, которые надо произвести в классе Game1.cs. Нам нужна еще одна цель визуализации (RenderTarget2D) и код ее инициализации:
RenderTarget2D normalRenderTarget;
protected override void LoadGraphicsContent(bool loadAllContent)
{
[...]
normalRenderTarget = new RenderTarget2D(graphics.GraphicsDevice,256,256,1,SurfaceFormat.Color);
[...]
}
Метод расчета карты нормалей очень сильно похож на метод расчета карты высот (Render2TextureMorph). Его надо вызвать сразу же за методом расчета карты высот, , затем рассчитанную карту нормалей точно так же передаем в параметр шейдера отрисовки ландшафта.
protected void Render2TextureNormalCompute()
{
RenderTarget2D oldRT =
graphics.GraphicsDevice.GetRenderTarget(0) as RenderTarget2D;
DepthStencilBuffer oldDS = graphics.GraphicsDevice.DepthStencilBuffer;
graphics.GraphicsDevice.DepthStencilBuffer = morphDepthBuffer;
graphics.GraphicsDevice.SetRenderTarget(0, normalRenderTarget);
graphics.GraphicsDevice.Clear(Color.White);
morphSpriteBatch.Begin(SpriteBlendMode.None,
SpriteSortMode.Immediate,
SaveStateMode.None);
// передаём новую карту высот для расчёта карты нормалей
morphEffect.Parameters["textureMap1"].SetValue(
morphRenderTarget.GetTexture());
// выбираем технику для расчёта нормалей
morphEffect.CurrentTechnique = morphEffect.Techniques["ComputeNormals"];
//отрисовка
morphEffect.Begin();
morphEffect.CurrentTechnique.Passes[0].Begin();
morphSpriteBatch.Draw(morphRenderTarget.GetTexture(),
new Rectangle(0, 0, 256, 256),
Color.White);
morphEffect.CurrentTechnique.Passes[0].End();
morphEffect.End();
morphSpriteBatch.End();
graphics.GraphicsDevice.ResolveRenderTarget(0);
graphics.GraphicsDevice.SetRenderTarget(0, oldRT);
graphics.GraphicsDevice.DepthStencilBuffer = oldDS;
}
[...]
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
Render2TextureMorph((float)Math.Sin(gameTime.TotalGameTime.TotalSeconds)* 0.5f + 0.5f);
Render2TextureNormalCompute();
[...]
gridEffect.Parameters["normalMap"].SetValue(normalRenderTarget.GetTexture());
[...]
}
И, в конце концов, у Вас должно получиться, что-то вроде этого:
Для дальнейшего улучшения, карту нормалей можно “смешать” с каким-нибудь сгенерированным шумом для эффекта небольшого бампа (bump) на поверхности.
В завершении второго раздела подведём краткие итоги. Мы рассмотрели реализацию динамического изменения ландшафта. Выяснили, что использование отрисовки в текстуру (Render-To-Texture) может значительно поднять производительность и открывает дополнительные возможности по реализации специфичных эффектов. Так же мы рассмотрели, как можно рассчитать карту нормалей в реальном времени на основе карты высот. В последующих разделах мы оставим в покое отрисовку ландшафтов и рассмотрим другие интересные эффекты.
Полный код этого раздела Вы сможете скачать отсюда: Chapter2.zip (ссылка умерла)
I. Системы частиц
В данном разделе мы рассмотрим реализацию системы частиц, в которых обновление и анимация выполняется полностью на GPU, а точнее в пиксельном шейдере.
Реализацию кода данного раздела можно начать писать как в новом проекте, так и в проекте предыдущей главы. В бонусном разделе будет рассмотрена комбинация реализаций системы частиц и отрисовки ландшафта, но пока необходимости в предыдущем коде нет. Если вы решили начать с нового проекта, то добавьте в проект компонент камеры и создайте две папки Textures и Shaders.
Реализация основывается на двух целях визуализации определенного размера. В текстурах связанных с этими целями визуализации хранится информация для системы частиц. Каждый пиксель текстуры содержит информацию об одной частице: первая хранит информацию скорости частицы, другая местоположение в пространстве. Текстуры обновляются методом Render To Texture, затем, когда данные о частице будет записаны в текстуру, извлекается информация о позиции частицы через VTF. Кроме информации о координатах частицы, в четвёртой компоненте вектора (w) содержаться данные о времени жизни частицы. Когда время жизни частицы достигает определенного значения, она «умирает» и пиксель может использоваться для генерации новой частицы. Обновление данных координат и скорости частицы в текстурах происходит в пиксельном шейдере, примерно так же, как мы делали морфинг в предыдущем разделе.
Для начала, пожалуйста, добавьте файл flare.dds в папку Textures нашего проекта и установите процессор Texture(mipmapped) processor. Далее, открываем Game1.cs и добавляем следующие поля:
// используется для генерации рандомных значений,
// которые будут использоваться в различных шейдерах
Texture2D randomTexture;
// текстура дл рисования частиц
Texture2D particleTexture;
// Render target - сюда будут писаться данные координат частицы
RenderTarget2D positionRT;
// Render target - сюда будут писаться данные скорости частицы
RenderTarget2D velocityRT;
// временная render target,
// необходима, когда идёт запись в другие render targets
RenderTarget2D temporaryRT;
// буфер глубины
// используется при обновлении системы частиц
DepthStencilBuffer simulationDepthBuffer;
// вершинный буфер, который содержит вершины системы частиц
VertexBuffer particlesVB;
// Эффект для отрисовки частиц
Effect renderParticleEffect;
// Эффект для обновления физики (местоположение, скорость)
Effect physicsEffect;
// Sprite batch используется при 2D отрисовке
SpriteBatch spriteBatch;
// если True данные о физике будут сохраняться
// Если False текстуры скорости и координат
// будут установлены опять в начальное состояние
Boolean isPhysicsReset;
// разрешение render targets.
// Кол-во частиц particleCount * particleCount
int particleCount = 512;
Комментариев нет:
Отправить комментарий