netlib.narod.ru< Назад | Оглавление | Далее >

Поддержка постэкранных шейдеров

Первая вещь, которая необходима для любого постэкранного шейдера — это визуализированная сцена, помещенная в цель визуализации (рис. 8.1). Изображение иллюстрирует некоторые примеры постэкранных шейдеров в FX Composer, которые могут использоваться для тестирования постэкранных эффектов перед тем, как вы включите их в свой графический движок.


Рис. 8.1

Рис. 8.1


Прежде чем перейти к сложным постэкранным шейдерам и способам получения цели визуализации со сценой при помощи вспомогательного класса RenderToTexture, вы познакомитесь с предэкранным шейдером наложения небесного куба и заставите его работать в вашем движке, точно так же, как он работает в FX Composer.

Предэкранное наложение небесного куба

Прежде всего, вам понадобится карта небесного куба, которая, подобно кубу, состоит из шести граней. Каждая из этих граней должна быть в высоком разрешении, поскольку в игре вы смотрите только в одном направлении. Хорошо использовать для каждой грани размер 512 × 512, если вы используете несколько постэкранных шейдеров; 1024 × 1024 будет еще лучше, поскольку при этом небо будет гораздо лучше выглядеть на экранах с большим разрешением.

Не так просто создать текстуру для небесного куба. Вы можете использовать утилиту DxTexture из DirectX и самостоятельно объединить вместе в кубическую карту шесть отдельных граней (или можно делать то же самое в вашем коде, но гораздо проще загружать один единственный файл кубической карты, содержащий все шесть граней, чем загружать шесть отдельных текстур и соединять их самостоятельно). Кроме того, такие программы, как Photoshop, могут использоваться для создания большой текстуры 6 × 512 × 512 и сохранения ее в файл кубической карты .dds с помощью подключаемого модуля Nvidia DDS Exporter для Photoshop (рис. 8.2).


Рис. 8.2

Рис. 8.2


Если у вас нет хорошо выглядящей кубической карты, поищите ее в Web; там доступны несколько примеров текстур. У большинства из них плохое разрешение, но для тестирования подойдут. Или используйте кубические карты из этой книги, если они понравились вам. Для игры Rocket Commander вы используете кубическую карту с изображением космоса, шесть сторон которой выглядят по-разному, чтобы помочь вам ориентироваться в трехмерном пространстве уровней игры (рис. 8.3).


Рис. 8.3

Рис. 8.3


Теперь взгляните на шейдер, помещающий эту кубическую текстуру на экран. Вы вызываете этот шейдер до визуализации сцены, и, поскольку он заполняет все пиксели буфера фоновым изображением, вам не нужно заполнять цель визуализации фоновым цветом (хотя, вам по-прежнему может требоваться очистка Z-буфера). Чтобы визуализировать этот шейдер вы должны отключить проверку глубины, потому что вы не заботитесь о том, насколько далеко небо, оно должно быть визуализировано в любом случае.

Вот код из шейдера PreScreenSkyCubeMapping.fx:

struct VertexInput
{
  // Нам нужна только экранная позиция и все.
  float2 pos      : POSITION;
};

struct VB_OutputPos3DTexCoord
{
  float4 pos      : POSITION;
  float3 texCoord : TEXCOORD0;
};

VB_OutputPos3DTexCoord VS_SkyCubeMap(VertexInput In)
{
  VB_OutputPos3DTexCoord Out;
  Out.pos = float4(In.pos.xy, 0, 1);
  // Меняем знаки xy поскольку кубическая карта
  // для MDX (левосторонняя) и вверх тормашками
  Out.texCoord = mul(float4(-In.pos, scale, 0), viewInverse).xyz;

  // И корректируем вращение
  // (мы используем систему с x и y в нижней плоскости)
  Out.texCoord = float3(
    -Out.texCoord.x*1.0f,
    -Out.texCoord.z*0.815f,
    -Out.texCoord.y*1.0f);

  return Out;
} // VS_SkyCubeMap(..)

float4 PS_SkyCubeMap(VB_OutputPos3DTexCoord In) : COLOR
{
  float4 texCol = ambientColor *
    texCUBE(diffuseTextureSampler, In.texCoord);
  return texCol;
} // PS_SkyCubeMap(.)

technique SkyCubeMap < string Script = "Pass=P0;"; >
{
  pass P0 < string Script = "Draw=Buffer;"; >
  {
    ZEnable = false;
    VertexShader = compile vs_1_1 VS_SkyCubeMap();
    PixelShader  = compile ps_1_1 PS_SkyCubeMap();
  } // pass P0
} // technique SkyCubeMap

Для визуализации шейдера вам нужен только Vector2 с экранной позицией, которая преобразуется в позицию небесного куба путем умножения на инвертированную матрицу вида. Переменная scale может использоваться для небольшой подстройки масштабирования, но обычно ее значение равно 1.0. После получения координат текстуры для карты небесного куба вам необходимо преобразовать их из стандартной левосторонней системы, в которой обычно сохраняются карты. Для этого мы меняем местами координаты Y и Z и меняем направление, в котором указывают оси координат кубической текстуры на противоположное. И, наконец, мы умножаем значение Y на 0.815, чтобы небо, визуализированное с форматным соотношением 1:1 корректно выглядело на экране с соотношением сторон 4:3. Все эти переменные для подстройки проверялись в показанном ниже тестовом модуле, пока фоновое небо не стало выглядеть корректно.

Если вы хотите построить свою собственную карту небесного куба, можно воспользоваться утилитой DirectX Texture Tool из DirectX SDK для загрузки обычных двухмерных изображений на каждую из шести граней кубической карты. Кроме того, доступно много утилит для прямой визуализации трехмерных сцен в кубические карты, такие как Bryce, 3D Studio Max и много других приложений для создания трехмерного содержимого.

Затем вы вращаете карту небесного куба для совместимости с XNA, поскольку она создавалась для MDX и использует левостороннюю систему. Визуализация карты небесного куба очень проста — вы только выбираете значение цвета из кубической текстуры и умножаете его на необязательное значение фонового цвета, который обычно белый. Шейдер не использует никакой дополнительной функциональности шейдерной модели 2.0 и абсолютно без проблем компилируется для шейдерной модели 1.1.

Чтобы все заработало в вашем графическом движке XNA, используйте следующий тестовый модуль:

public static void TestSkyCubeMapping()
{
  PreScreenSkyCubeMapping skyCube = null;

  TestGame.Start("TestSkyCubeMapping",
    delegate
    {
      skyCube = new PreScreenSkyCubeMapping();
    },
    delegate
    {
      skyCube.RenderSky();
    });
} // TestSkyCubeMapping()

Класс является наследником класса ShaderEffect из предыдущей главы и просто загружает в конструкторе шейдер PreScreenSkyCubeMapping.fx; все другие параметры шейдерного эффекта уже определены в классе ShaderEffect. Важный новый метод в этом классе — RenderSky, который в основном выполняет следующий код:

AmbientColor = setSkyColor;
InverseViewMatrix = BaseGame.InverseViewMatrix;

// Старт шейдера
// Запоминаем старое состояние,
// поскольку здесь используем отсечение текстуры
effect.Begin(SaveStateMode.SaveState);

for (int num = 0; num < effect.CurrentTechnique.Passes.Count; num++)
{
  EffectPass pass = effect.CurrentTechnique.Passes[num];

  // Визуализируем каждый проход
  pass.Begin();
  VBScreenHelper.Render();
  pass.End();
} // foreach (pass)

// Завершение шейдера
effect.End();

Подобно всем другим шейдерам, которые вы написали ранее, здесь вы сначала устанавливаете параметры эффекта, к которым относятся фоновый цвет и инвертированная матрица вида, потому что больше ничего шейдеру не требуется. Затем шейдер запускается и вы визуализируете все проходы (хорошо, у вас есть только один проход) с помощью метода VBScreenHelper.Render, который также применяется для всех постэкранных шейдеров.

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

public VBScreen()
{
  VertexPositionTexture[] vertices = new VertexPositionTexture[]
    {
      new VertexPositionTexture(
        new Vector3(-1.0f, -1.0f, 0.5f),
        new Vector2(0, 1)),
      new VertexPositionTexture(
        new Vector3(-1.0f, 1.0f, 0.5f),
        new Vector2(0, 0)),
      new VertexPositionTexture(
        new Vector3(1.0f, -1.0f, 0.5f),
        new Vector2(1, 1)),
      new VertexPositionTexture(
        new Vector3(1.0f, 1.0f, 0.5f),
        new Vector2(1, 0)),
    };
  vbScreen = new VertexBuffer(
    BaseGame.Device,
    typeof(VertexPositionTexture),
    vertices.Length,
    ResourceUsage.WriteOnly,
    ResourceManagementMode.Automatic);
  vbScreen.SetData(vertices);

  decl = new VertexDeclaration(BaseGame.Device,
    VertexPositionTexture.VertexElements);
} // VBScreen()

Как обсуждалось в главах 5 и 6, в качестве минимальных и максимальных значений границ экрана используются –1 и +1. Все, что находится вне этих значений, на экране не видно. Матрица проекции помещает пиксели в их действительные экранные позиции. Здесь вы используете простой формат вершин VertexPositionTexture. Для шейдера наложения небесного куба вам не нужны даже координаты текстуры, но они потребуются позже для постэкранных шейдеров. Обратите внимание, что в экранных координатах местоположению +1, +1 соответствует нижний левый угол экрана, а –1, –1 — это верхний правый угол. Если вы хотите, чтобы верхний левый угол экранной текстуры отображался сверху слева, координаты текстуры 0, 0 должны быть у вершины угла –1, +1.

При создании VertexBuffer вы всегда должны указывать флаги WriteOnly и Automatic; благодаря первому аппаратура быстрее работает с данными, так как ей не надо беспокоиться о предоставлении доступа для чтения для CPU, а Automatic облегчает избавление от буфера вершин и позволяет XNA обрабатывать проблемы его воссоздания, когда буфер вершин потерян и должен быть создан заново (это может произойти, когда в полноэкранном приложении пользователь нажимает Alt+Tab, а затем возвращается в него).

Когда все эти новые классы готовы, вы можете выполнить ваш тестовый модуль TestSkyCubeMapping и увидеть результат, показанный на рис. 8.4.


Рис. 8.4

Рис. 8.4


Написание простого постэкранного шейдера

Написать постэкранный шейдер, это вам не съездить на пикник. Здесь требуется много тестирования, корректная обработка целей визуализации, и не всегда легко заставить шейдер одинаково вести себя в FX Composer и в вашем движке. Чтобы немного упростить первую встречу с постэкранными шейдерами, мы добавим к сцене только очень простой эффект. Вместо того, чтобы реально модифицировать визуализированную сцену, мы просто немного затемним края экрана, чтобы игра выглядела как на экране телевизора. Чтобы сделать нечто, невозможное без постэкранных шейдеров, вы также преобразуете все изображение на экране в черно-белое.

Начнем с открытия файла шейдера PostScreenDarkenBorder.fx в FX Composer. После заглавного комментария и описания шейдера вы определяете скрипт, который используется только в FX Composer, и указывает, что этот шейдер является постэкранным и должен визуализироваться особым способом. Все переменные, имена которых начинаются с заглавной буквы, являются константами, предназначенными для ваших целей.

И вашему движку и FX Composer нет дела до регистра, но это позволяет вам легко определять, является ли переменная константой, которая не должна изменяться в приложении, или ее необходимо устанавливать из вашей программы. ClearColor и ClearDepth используются только в FX Composer. В вашем приложении не надо беспокоиться о них, поскольку очистку экрана вы будете обрабатывать самостоятельно. При тестировании, если пожелаете, вы можете устанавливать другие цвета, помимо черного, в ClearColor, или значения глубины, отличающиеся от 1.0, но большую часть времени эти значения будут оставаться такими, и поэтому они выбраны в качестве значений по умолчанию для всех последующих постэкранных шейдеров.

// Этот скрипт используется только для FX Composer,
// большинство значений здесь рассматриваются приложением как константы.
// Имена констант начинаются с заглавной буквы.
float Script : STANDARDSGLOBAL
<
  string ScriptClass = "scene";
  string ScriptOrder = "postprocess";
  string ScriptOutput = "color";
  // Мы просто вызываем скрипт в главной технике
  string Script = "Technique=ScreenDarkenBorder;";
> = 0.5;
const float4 ClearColor : DIFFUSE = { 0.0f, 0.0f, 0.0f, 1.0f};
const float ClearDepth = 1.0f;

Постэкранный шейдер должен знать о разрешении экрана, и вам необходим доступ к цели визуализации с изображением сцены. Следующий код используется, чтобы позволить вам назначить размеры окна и карту сцены. Размер окна — это разрешение вашей визуализации, а карта сцены — это цель визуализации, содержащая полностью визуализированную сцену в виде текстуры. Заметьте, что семантика, подобная VIEWPORTPIXELSIZE, используется здесь, чтобы помочь FX Composer автоматически назначить корректные значения для предварительного просмотра визуализации. Если вы не используете их, FX Composer не в состоянии использовать правильный размер окна и вам необходимо будет устанавливать его самостоятельно в панели свойств.

// Заглушка Render-to-Texture
float2 windowSize : VIEWPORTPIXELSIZE;

texture sceneMap : RENDERCOLORTARGET
<
  float2 ViewportRatio = { 1.0, 1.0 };
  int MIPLEVELS = 1;
>;
sampler sceneMapSampler = sampler_state
{
  texture = <sceneMap>;
  AddressU  = CLAMP;
  AddressV  = CLAMP;
  AddressW  = CLAMP;
  MIPFILTER = NONE;
  MINFILTER = LINEAR;
  MAGFILTER = LINEAR;
};

Для затемнения краев экрана вы также используете небольшую вспомогательную текстуру с именем ScreenBorderFadeout.dds (рис. 8.5).

// На последнем проходе мы добавляем карту затенения,
// чтобы сделать края экрана темнее
texture screenBorderFadeoutMap : Diffuse
<
  string UIName = "Screen border texture";
  string ResourceName = "ScreenBorderFadeout.dds";
>;
sampler screenBorderFadeoutMapSampler = sampler_state
{
    texture = <screenBorderFadeoutMap>;
    AddressU  = CLAMP;
    AddressV  = CLAMP;
    AddressW  = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

Рис. 8.5

Рис. 8.5


 

СОВЕТ
Эта текстура должна быть добавлена к вашему каталогу содержимого, точно так же, как и файл шейдера .fx. XNA не будет добавлять эту текстуру автоматически, как делает для моделей, содержащих материалы и текстуры.

Чтобы достичь эффекта затенения, каждый пиксел из карты сцены просто умножается на значение цвета из текстуры затемнения краев экрана. В результате большинство пикселей останутся неизменными (белая середина); пиксели у краев экрана будут становиться темнее и темнее, но не полностью черными (иначе граница была бы черной), таким образом, вы хотите сделать их только немного более темными.

Взгляните на очень простой вершинный шейдер, который только передает координаты текстуры в пиксельный шейдер. Чтобы обеспечить совместимость с пиксельными шейдерами версии 1.1, вы дублируете координаты текстуры, поскольку они потребуются вам дважды — один раз для карты сцены, и один раз для текстуры затемнения границ экрана. Также обратите внимание, что мы добавляем половину пикселя к координатам текстуры, чтобы исправить общую проблему с визуализацией текстур на экране. В DirectX (и XNA) все пиксели смещены на 0.5 пикселя; вы просто исправляете это, перемещая пиксели в правильное местоположение.

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

struct VB_OutputPos2TexCoords
{
  float4 pos      : POSITION;
  float2 texCoord[2] : TEXCOORD0;
};

VB_OutputPos2TexCoords VS_ScreenQuad(
  float4 pos      : POSITION,
  float2 texCoord : TEXCOORD0)
{
  VB_OutputPos2TexCoords Out;
  float2 texelSize = 1.0 / windowSize;
  Out.pos = pos;
  // Не используйте билинейную фильтрацию
  Out.texCoord[0] = texCoord + texelSize*0.5;
  Out.texCoord[1] = texCoord + texelSize*0.5;
  return Out;
} // VS_ScreenQuad(..)

Местоположение уже в требуемом пространстве и просто передается далее. Пиксельный шейдер обращается к карте сцены, и для тестирования правильности всех установок вы можете здесь просто возвращать цвет из карты сцены:

float4 PS_ComposeFinalImage(VB_OutputPos2TexCoords In) : COLOR
{
  float4 orig = tex2D(sceneMapSampler, In.texCoord[0]);
  return orig;
} // PS_ComposeFinalImage(...)

После добавления техники ScreenDarkenBorder вы должны увидеть обычную сцену, как всегда в FX Composer (рис. 8.6). Пожалуйста, обратите внимание, что все аннотации здесь требуются только для FX Composer! Для визуализации вы используете описанный ранее предэкранный шейдер наложения небесного куба и стандартную сферу.

// Техника ScreenDarkenBorder для ps_1_1
technique ScreenDarkenBorder
<
  // Наполнение скрипта только для FX Composer
  string Script = "RenderColorTarget=sceneMap;"
    "ClearSetColor=ClearColor; Clear=Color;"
    "ClearSetDepth=ClearDepth; Clear=Depth;"
    "ScriptSignature=color; ScriptExternal=;"
    "Pass=DarkenBorder;";
>
{
  pass DarkenBorder
  <
    string Script = "RenderColorTarget0=; Draw=Buffer;";
  >
  {
    VertexShader = compile vs_1_1 VS_ScreenQuad();
    PixelShader  = compile ps_1_1 PS_ComposeFinalImage(sceneMapSampler);
  } // pass DarkenBorder
} // technique ScreenDarkenBorder

Рис. 8.6

Рис. 8.6


Усовершенствования

Для того, чтобы увидеть активацию шейдера (в FX Composer это выполняется щелчком по шейдеру в панели материалов и выбору команды Apply to Scene), можно просто модифицировать выходные данные. Замените последнюю строку пиксельного шейдера на следующую:

return 1.0f - orig;

В результате значение каждой цветовой составляющей будет вычтено из 1.0 (шейдер при необходимости автоматически выполняет преобразование из float в float3 или float4; вам не надо явно указывать float4 (1, 1, 1, 1)). Как вы, возможно, предположили, эта формула инвертирует все изображение (рис. 8.7), что выглядит забавно, но не очень то полезно.


Рис. 8.7

Рис. 8.7


Ладно, вернемся к нашей первоначальной цели: применим текстуру границ экрана. Для затемнения краев вы должны сначала загрузить текстуру границ экрана, а затем умножить на значение из нее цвет из исходной карты цветов сцены, вот и все. Вам остается только возвратить полученный результат, и почти все сделано (рис. 8.8):

float4 orig = tex2D(sceneSampler, In.texCoord[0]);
float4 screenBorderFadeout =
  tex2D(screenBorderFadeoutMapSampler, In.texCoord[1]);
float4 ret = orig;
ret.rgb *= screenBorderFadeout;
return ret;

Рис. 8.8

Рис. 8.8


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

// Возвращает значение яркости для col,
// преобразуя цвет в градации серого 
float Luminance(float3 col)
{
  return dot(col, float3(0.3, 0.59, 0.11));
} // Luminance(.)

Этот метод может быть применен к исходной карте сцены простой модификацией одной строки в вашем пиксельном шейдере для достижения ожидаемого от постэкранного шейдера эффекта: затемнения краев экрана и применения эффекта черно-белого изображения (рис. 8.9):

float4 ret = Luminance(orig);

Рис. 8.9

Рис. 8.9


После того, как вы завершите базовое конфигурирование, написание постэкранных шейдеров может доставлять много удовольствия, но вы должны сперва попытаться реализовать поддержку постэкранных шейдеров в вашем движке, а уж затем переходить к более крутым постэкранным шейдерам и возможностям.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz