пятница, 27 апреля 2012 г.

SSAO - Простой и практический подход.

Глобальное освещение (GI) — термин, используемый в компьютерной графике для вычисления освещения, вызванного неким взаимодействием между ближайшими поверхностями. Довольно часто термин GI используют только при окрашивании поверхности обьектов лучом отраженным от обьектов окружающей среды. Прямое освещение непосредственно от источника света – легко вычисляется в режиме реального времени на современном оборудовании, но мы не можем сказать то же самое о GI потому, что нам нужно обработать информацию о ближайших поверхностях для каждой точки в сцене, а управлять этим процессом еще довольно сложно. Однако есть определенные методы вычислений GI, которыми управлять не так сложно. Когда свет падает на сцену,или отражается от поверхности, то в сцене могут иметь место некоторые точки, которые имеют меньше шансов получить порцию света: уголки, трещиты между объектами, складки, и др. Это приводит к появлению тех областей, которые в результате будут темнее, чем окружающие их обьекты.

Предпосылки

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

Алгоритм, который я описываю здесь делает все расчеты в 2D, и не использует проекцию. Он использует попиксельную позицию и буфер нормалей, так что если ваш движок использует модель отложенного освещения, то считайте, что пол дела вы уже сделали. Если вы не можете восстановить позицию из буфера глубины, вы можете хранить позицию в буфере позиций с точностью числа с плавающей точкой. Я рекомендую второй способ, так как я не буду обсуждать способы восстановления позиции из буфера глубины в этом посте. В любом случае, для оставшейся части статьи предпологается, что буфер нормалей и буфер позиций у вас уже есть.
Все, что мы сделаем в этой статье - это получим буферы нормалей и позиций, и сгенерируем однокомпонентный попиксельный буффер заграждений. Вы сможете использовать эту информацию как угодно. Обычно она вычитается из окружающего освещения в сцене, но можно также использовать этот буфер в более замысловатых или странных применениях, например если захотите получить не фотореалистичные результаты.


Алгоритм

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

Дистанция "d" до окклюдера
Угол между нормалью "N" и векторм между occluder и occludee "V".

С учетом этих двух факторов выводится простая формула для вычисления затенения:

 Occlusion = max( 0.0, dot( N, V) ) * ( 1.0 / ( 1.0 + d ) )

Первое выражение max( 0.0, dot( N,V ) ), основывается на интуитивном предположении, что точки над вершиной вносят больший вклад в затенение, чем точки находящиеся левее или правее. Второе выражение  ( 1.0 / ( 1.0 + d ) )  смягчает  линейность эффекта зафисимую от расстояния. Можно использовать квадратичное затухания или воспользоваться любой другой функцией, это просто дело вкуса. Алгоритм очень прост: выбираем несколько соседей вокруг текущего пиксела и накапливаем их вклад в затенение с использованием формулы выше.

Чтобы выбрать окклюдеры, я использую 4 выборки (<1,0>, <-1,0>, <0,1>, <0,-1>) которые повернуты на 45 ° и 90 ° и отражаются в последствии случайным образом используя текстуру случайных нормалей.

Это код шейдеров HLSL для эффекта, который должен применяться к полноэкранному квадрату:

sampler g_buffer_norm; 
sampler g_buffer_pos; 
sampler g_random; 
float random_size; 
float g_sample_rad; 
float g_intensity; 
float g_scale; 
float g_bias; 
 
struct PS_INPUT 
     float2 uv : TEXCOORD0; 
}; 
 
struct PS_OUTPUT 
     float4 color : COLOR0; 
}; 
 
float3 getPosition(in float2 uv) 
    return tex2D(g_buffer_pos,uv).xyz; 
 
float3 getNormal(in float2 uv) 
    return normalize(tex2D(g_buffer_norm, uv).xyz * 2.0f - 1.0f); 
 
float2 getRandom(in float2 uv) 
     return normalize(tex2D(g_random, g_screen_size * uv / random_size).xy * 2.0f - 1.0f); 
 
float doAmbientOcclusion(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) 
   float3 diff = getPosition(tcoord + uv) - p; 
   const float3 v = normalize(diff); 
   const float d = length(diff)*g_scale; 
   return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d))*g_intensity; 
 
PS_OUTPUT main(PS_INPUT i) 
   PS_OUTPUT o = (PS_OUTPUT)0; 
 
   o.color.rgb = 1.0f; 
   const float2 vec[4] = {float2(1,0),float2(-1,0), 
                                     float2(0,1),float2(0,-1)}; 
 
float3 p = getPosition(i.uv); 
float3 n = getNormal(i.uv); 
float2 rand = getRandom(i.uv); 
 
float ao = 0.0f; 
float rad = g_sample_rad/p.z; 
 
//**SSAO Calculation**// 
int iterations = 4; 
for (int j = 0; j < iterations; ++j) 
  float2 coord1 = reflect(vec[j],rand)*rad; 
  float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, 
                          coord1.x*0.707 + coord1.y*0.707); 
   
  ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n); 
  ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n); 
  ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n); 
  ao += doAmbientOcclusion(i.uv,coord2, p, n); 
ao/=(float)iterations*4.0; 
//**END**// 
 
//Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc. 
return o; 
}

Эта техника очень напоминает технику описанную в "Hardware Accelerated Ambient Occlusion Techniques on GPUs". Основные различия в структуре выборки и функции AO. Он также может рассматриваться как Image-Space версия  "Hardware Accelerated Ambient Occlusion Techniques on GPUs". Некоторые пояснения по коду:

Радиус делится на p.z, для изменения масштаба в зависимости от расстояния до камеры. Если пропустить этот раздел, все точки на экране будут использовать же радиус выборки, и результат не будет учитывать перспективу.
В цикле for,
coord1 это первоначальная выборка координат на 90 °.
coord2 является те же координаты, повернутые на 45 °.

Результаты



Как видите, код не сложный и не большой. Результаты не самозатеняются и хало почти нет.
Это две основные проблемы всех алгоритмов SSAO, которые используют только буфер глубины в качестве исходных данных, вы можете увидеть их в этих изображениях:

Self-occlusion появляется, потому что традиционные алгоритмы формирования выборок внутри области вокруг каждого пикселя, поэтому на плоских поверхностях которые не «occluded», по меньшей мере, половина из выборок помечены как «occluded». Из за этого получается сероватый цвет для общей окклюзии. Белое хало вокруг объектов возникает, потому что в этих областях self-occlusion не выполняется. Таким образом избавление от self-occlusion на самом деле хорошо помогает избавиться от хало. При перемещении камеры результат окклюзии изменяется, если следовать этому методу. Если вы склоняетесь к качеству вместо скорости, можно использовать два или несколько проходов алгоритма (повторяющиеся цикл for в коде) с различными радиусами, один для захвата более глобальных AO, а другие для детализации небольших трещин.


Продолжаем

Я описал упрощенную реализацию SSAO, которая очень хорошо подходит для игр.
Однако ее легко улучшить если принимать во внимание скрытые от камеры поверхности, для получения более высокого качества. Это потребует три буфера: две позиции/глубина буферов (front/back faces) и один буфер нормалей. Но вы можете сделать это только с двумя буферами: хранить глубину front/back faces в красном и зеленом каналах буфера, соответственно, а затем восстановить позицию каждого из них. Таким образом у вас будет один буфер для позиций и второй буфер для нормалей. Таковы результаты при использовании 16 выборок для каждой позиции буфера:


left: front faces occlusion, right: back faces occlusion
Что бы применить это, нужно просто добавить вызова функции doAmbientOcclusion()
Это дополнительный код, который необходимо добавить:
внутри цикла for, добавьте эти вызовы:
ao += doAmbientOcclusionBack(i.uv,coord1*(0.25+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord2*(0.5+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord1*(0.75+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord2*1.125, p, n);

Добавьте эти две функции шейдер: 

 float3 getPositionBack(in float2 uv) 

    return tex2D(g_buffer_posb,uv).xyz; 

float doAmbientOcclusionBack(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) 

    float3 diff = getPositionBack(tcoord + uv) - p; 
   const float3 v = normalize(diff); 
   const float d = length(diff)*g_scale; 
   return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d)); 
}

Добавьте семлер с именем «g_buffer_posb», содержащий позицию backFaces (для его создания вам нужно рисовать сцену с передних граней).  Еще небольшое изменение, которое может помочь, на этот раз улучшить скорость, является добавление простой системы LOD (уровень детализации) в наш шейдер. Измените фиксированное количества итераций в соответствии с:

 int iterations = lerp(6.0,2.0,p.z/g_far_clip);

Переменная «g_far_clip» - это расстояние до дальней плоскости отсечения, которое должно быть передано в шейдер. Теперь количество итераций, применяемое к каждой точке зависит от расстояния до камеры. Таким образом для дальних точек выполнется меньше выборок, это позволяет повысить производительность без заметного ухучшения качества. Я не использовал это при измерении производительности (см. ниже), но все-же:




Так-же полезно рассмотреть, как этот метод сравнивается с трассировкой лучей AO. Цель этого сравнения заключается в том, чтобы увидеть, будет ли этот метод приравниваться к реальному AO при использовании достаточного количества выборок.


Left: the SSAO presented here, 48 samples per pixel (32 for front faces and 16 for back faces), no blur. Right: Ray traced AO in Mental Ray. 32 samples, spread = 2.0, maxdistance = 1.0; falloff = 1.0.

И совет на последок: не планируйте подключить шейдер в свой конвейер и автоматически получить реалистичный результат. Несмотря на эту реализацию, что-бы получить соотношение хорошей производительности и качества SSAO, вы должны настроить его как можно тщательнее для своих нужд и добиться максимальной производительности.
Добавление или удаление выборок и размытия, изменяя интенсивность и т.д. Вам следует также понять, подходит ли такой алгоритм SSAO для вас. Если у вас есть много динамических объектов в сцене, то возможно нет необходимости в SSAO вообще, может быть использование LightMap-ов достаточно для ваших целей, так- как они могут обеспечить лучшее качество для статических сцен. Я надеюсь, что вы извлекли пользу от этого метода. Весь код, представленный в этой статье доступен по mit-license.
Оригинал статьи можно найти здесь a-simple-and-practical-approach-to-ssao




четверг, 26 апреля 2012 г.

DBF TO ANY

DBF TO ANY - это удобная программа-конвертер данных формата (.dbf)  в данные MSSQL, PostgreSQL,  Oracle и  MySQL. Продукт ориентирован на профессионалов работающих с базами данных.




вторник, 24 апреля 2012 г.

[PARADIGMA] - Engine

Это графический движок, который сравнительно недавно был запущен в разработку. Его особенностью является то, что он использует Microsoft XNA. Имеется две системы рендерринга, Forward и Deffered. Так-же имеется собственный редактор уровней, набор вспомагательных утилит и конверторов, которые будут полезны любому игроделу.


Landscape + instancing + normal + specular


Поддержка бльшого числа источников света (Deferred Shading)


Редактор уровней


пятница, 20 апреля 2012 г.

SGS - Synthetic Engine.

SGS Project.


С недавних пор началась разработка проекта SGS. Рабочее название проекта "Synthetic Engine". Вообще SGS - это аббревиатура расшифровующаяся как Synthetic Graphics System, или же Synthetic Game Studio. В основе программного комплекса лежит Microsoft DirectX11 API. В силу того, что проект создавался одним человеком, да еще на чистом энтузиазме, процесс разработки продвигается не очень быстро, однако кое - какие результаты уже получены. В этой статье я буду стараться периодически освещать состояние проекта, делиться получеными результатами. Приглашаются так-же все желающие поучавствовать в обсуждении вопросов связяных с творческим процессом создания графического движка.  И так, на сегодняшний день уже реализовано не мало. Архитектура программы изначально затачивается для реализации в дальнейшем трех систем рендерринга, Forward, Deffered, Raytracing. Физика - Nvidia PhysX библиотеки. В качестве неба было принято решение пока использовать SkyBox, ввиду его относительной простоты и понятности.


Реализована система материалов, позволяющая создавать базовые материалы такие как Lambert:

Blinn + Phong:


Normal + Specular + Masked


Metall


Metall + Normal



Self Shadowed Parallax



Рендерринг текста и Terrain generator.


Недавно были добавлены Parallel Split Shadow Maps, основаные на алгоритме описаном разработчиками компьютерной игры Battlefield3.



Так-же была разработана система постпроцессов. Пока реализовано следующее:

Bloom:


DOF:



SSAO:




Хотя SSAO пока еще требует хорошей доработки, это планируется в ближайшее время.
Так-же планируется написать свой редактор уровней, но он пока еще находится в зародыше.

 
 

Это заготовка, в которой пока доступны только элементарные функции по редактированию моделей. Как только ядро движка будет более-менее готово, работа над редактором возобновится. В качестве задатков на тулсеты уже поддерживаются некие функции по вспомагательной визуализации, такие как

визуализация тангенциального пространства:


Bounding Sphere & Bounding Box




Axis:



Cubemapper

Synthetic Cubemapper

Это абсолютно бесплатная программа, которая позволяет создавать кубические текстуры из шести (или менее) 2D текстур, и сохранять их в dds формате. Такие текстуры могут использоваться для создания неба в компьютерных играх, а так же для всевозможных "environment" эффектов, таких как отражение/преломление света и т.д.


Программа требует OS Windows7 и установленный XNA framework 3.1

Скачать бесплатно ...

четверг, 19 апреля 2012 г.

PDF Book Printer

PDF Book Printer  (beta)
Эта бесплатная утилита, двумя кликами мыши, позволяет распечатывать электронные книги (.pdf), из расчета - четыре страницы на один стандартный лист формата А4. Страницы книги группируются в буклеты по 10 листов (40 страниц) для дальнейшего скрепления между собой. Готовые буклеты пронумерованые и приготовленные для печати сохраняются в папке с программой, и могут быть переданы третьим лицам для самостоятельной печати или хранения. Для печати одного буклета вам потребуется положить в лоток принтера 10 листов, а после того как они отпечатаются, перевернуть всю пачку и положить ее снова в лоток вашего принтера и напечатать вторую сторону буклета. После второй процедуры печати, листы лягут в нужном порядке и вам останется только скрепить их посередине скрепками. Буклет готов. После этого можно приступать к печати следующего буклета. Если в распечатываемой книге число страниц не кратно 40, то оставшиеся страницы упаковываются в буклет с названием "Appendix", и на экран выводится сообщение о том, сколько листов потребуется положить в принтер для печати заключительного буклета.





вторник, 17 апреля 2012 г.

Практическое применение XNA. Часть 16.

Skybox

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




Шейдер для скайбокса:

TextureCube shaderTexture;
sampler SampleType;
cbuffer MatrixBuffer
{
   matrix worldMatrix;
   matrix viewMatrix;
   matrix projectionMatrix;
};

struct VS_INPUT
{
  float4 position : POSITION;
  float2 texcoord : TEXCOORD0;
};

struct PS_INPUT
{
  float4 position : SV_POSITION;
  float3 texcoord : TEXCOORD0;
};

PS_INPUT VS(VS_INPUT input)
{
  PS_INPUT output = (PS_INPUT)0;
   
  output.position = mul(mul(mul(input.position, worldMatrix), viewMatrix), projectionMatrix);
  output.texcoord = input.position;
  return output;
}

float4 PS(PS_INPUT input) : SV_TARGET
{
  return shaderTexture.Sample(SampleType, input.texcoord);
}

В результате у нас получится подобная картинка:

Вопросы?

четверг, 12 апреля 2012 г.

Практическое применение XNA. Часть 15.

Сериализация данных в игровых приложениях. (Serialization XNA)


По мере продвижения в изучении этой серии статей вы уже освоили некоторые азы создания игр. Теперь я хочу рассказать вам о сериализации данных. Что же это такое и для чего это нам нужно? Вообще пугающее новичков понятие сериализация используется не только в играх, а и во многих не игровых приложениях. Суть процесса сериализации заключается в следующем. Во время выполнения приложения, например игры, ваша программа использует некоторые переменные содержащие некоторые значения. Эти значения постоянно меняются, изменяются и поведения целых классов в зависимости от значений переменных. Например количество патронов, жизней, енергии или очков игрового персонажа. Понятное дело, чтобы насобирать максимум, скажем, жизней - игроку пришлось просидеть за игрой несколько часов. Игра в момент своего старта конечно же, присваевает игроку некоторое число жизней, которое вы укажите в своей программе, но в процессе тяжелых игровых событий персонаж это число ,допустим, удвоил. Если теперь он выйдет из игры, то потом начнет играть снова, он обнаружит что все что нажито честным и непосильным трудом сгорело, количество заветных жизней такое как было сначала. Все кто когда либо играл в компьютеоные игры, наверняка слышал термин сохранение игры. В любой момент времени, нажав на определенную клавишу, игра "сохраняется" в некий файл сохранения, имеющий как правило расширение .sav. После чего во время повторного старта игры, персонаж оказывается в том игровом месте и с такими же регалиями, которые он сохранял. Вопрос - откуда программа загрузила данные, которые разработчики нигде не определили? Ответ напрашивается сам собой. Конечно же из файла сохранения. Пока вроде все просто. Мы теоретически можем себе представить как из текстового файла подгрузить например имя игрока. Но как подгрузить n-мерный массив, или готовый класс? Несомненно найдутся люди, которые скажут, что способ такой есть, и написав специальную логику, которая на лету будет разбирать текст, конвертировать всевозможные значения и типы, в итоге все таки справится с этой задачей. Но мы пойдем другим путем. И путь этот назывется сериализация. Иными словами сериализация - это способ сохранения в бинарный файл целых классов, структур, перечислений, делегатов. Антипод сереиализации - десериализация. Это как вы наверное уже догадались обратный процесс, то есть извлечение из бинарного файла тех же классов, структур, делегатов и перечислений.
На практике все выше сказанное можно реализовать например вот так. Заведем новый пример с названием myTerrainSerialize. Это будет слегка модернизированый вариант примера myTerrain. Если помните в нем мы строили ландшафт на основе карты высот. Значения высоты вершины считывалось из нее же. В этом примере мы отделим код заполнения данными массива float[,] heightData в новый класс с названием Configurator. Выглядеть он будет так.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
namespace myTerrainSerialize
{
  [Serializable]
  public class Configurator
  {
    public int WIDTH;
    public int HEIGHT;
    public float[,] heightData;
    public Configurator(Game1 game)
    {
      LoadHeightData(game.Content.Load<Texture2D>("HeightMap"));
    }
    public void LoadHeightData(Texture2D heightMap)
    {
      // установили значение
      // сторон карты высот в
      // глобальные переменные
      WIDTH = heightMap.Width;
      HEIGHT = heightMap.Height;
      // создали массив цветов для хранения
      // данных карты высот с размерностью
      // высоты умноженной на ширину
      Color[] heightMapColors = new Color[WIDTH * HEIGHT];
      // загружаем данные из карты высот
      // в массив цветов
      heightMap.GetData(heightMapColors);
      // создали массив значений высот
      heightData = new float[WIDTH, HEIGHT];
      // в цикле
      for (int x = 0; x < WIDTH; x++)
      {
        for (int y = 0; y < HEIGHT; y++)
        {
          // если карта высот ч/б то не важно из какого канала
          // брать значение. Мы возьмем из красного
          heightData[x, y] = heightMapColors[x + y * WIDTH].R;
        }
      }
    }
  }
}

Перед тем как обьявить новый класс мы написали строчку [Serializable].
Это означает, что класс готов к сериализации. Он имеет массив

 public float[,] heightData;

который нам понадобится в дальнейшем при создании вершин. При первом запуске программы этот массив заполнится данными, после чего весь класс будет сохранен в файл savegame.sav ,который будет создан по пути
 C:\Users\имя пользователя\Documents\SavedGames\XNAGame\Player1.
Помимо этого массива, класс конфигуратора игры может хранить множество необходимых переменных, с их количеством и назначением вы можете поэксперриментировать самостоятельно. Продолжим далее. Теперь у нас есть то, что подлежит сохранению, но пока еще нет самого механизма сохранения и извлечения данных. Для этих целей создадим класс с названием Serializator.
  class Serializator
  {
    Game1 game;
    static StorageDevice HardDiskDevice;
    Configurator ourObj;
    public Serializator(Game1 game)
    {
      this.game = game;
      HardDiskDevice = Guide.EndShowStorageDeviceSelector(Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null));
      ourObj = new Configurator(game);
      //serialize();
    }
    //Процедура сериализации объекта
    void serialize()
    {
      //Контейнер для хранения данных
      StorageContainer container = HardDiskDevice.OpenContainer("XNAGame");
      //Полное имя файла - комбинация адреса контейнера и имени
      string filename = Path.Combine(container.Path, "savegame.sav");
      //Объект, предназначенный для сериализации и десериализации других объектов
      IFormatter formatter = new BinaryFormatter();
      //Создаем новый поток для записи файла
      Stream stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None);
      //Сериализуем объект ourObj в поток stream
      formatter.Serialize(stream, ourObj);
      //Закрываем поток
      stream.Close();
      //Уничтожаем контейнер
      container.Dispose();
    }
   
    //метод возвращающий массив высот из файла
    public static float[,] getdata()
    {
      StorageContainer container = HardDiskDevice.OpenContainer("XNAGame");
      IFormatter formatter = new BinaryFormatter();
      //Новый поток для чтения файла
      Stream stream = new FileStream(Path.Combine(container.Path, "savegame.sav"), FileMode.Open, FileAccess.Read, FileShare.Read);
      //Получаем данные из потока и приводим их к типу MyObj
      Configurator MyObj = (Configurator)formatter.Deserialize(stream);
      stream.Close();
      container.Dispose();
      return MyObj.heightData;
    }
  }
При первом запуске приложения мы не будем комментировать вызов метода serialize();
Иначе произойдет ошибка свидетельствующая о том, что в указанной директории еще нет искомого файла. Но в последующих запусках приложения, этот вызов комментируется. И когда класс Terrain потребует от Serializatora массив высот
heightData = Serializator.getdata();
то метод getdata() вернет ему необходимый массив взятый из класса Configurator, который был извлечен именно из файла savegame.sav.


Как вы сами убедились, сериализация - мощнейший инструмент который наверняка вам не раз пригодится.

Стратегия и тактика. (Как я это понимаю)

Сегодня я хотел бы поделиться размышлениями о таких понятиях, как стратегия и тактика, и о том, как эту информацию можно использовать для написания игрового искусственного интеллекта. Эти термины были придуманы людьми уже очень давно, и по сей день используются во многих отраслях человеческой деятельности, начиная от военных дисциплин, бизнеса и заканчивая жизненными позициями многих членов общества или играми. Вообще, понятие "Стратегия" можно приравнять к понятию "Пошаговое планирование". Это может быть некое долгосрочное, планирование строительства какого нибудь комбината, или-же наоборот, планирование заданий на один день у очень занятого человека. Так или иначе, стратегия - это, некое общее, абстрактное планирование, которое не учитывает многие факторы, которые может внести реальность. Предположим,  генерал некой армии решил захватить город N. Он составил пошаговый стратегический план захвата.

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

Стратегически все верно. Но каковы шансы на успех такой операции? Вполне возможно, что подойдя к мосту, солдаты обнаружат, что мост по каким-то причинам взорван, и дальше двигаться нельзя. План сорван, и битва проиграна? Но тут в дело должна вступить тактика.
Что же такое тактика? Тактика - это как-бы реализация (воплощение в жизнь) стратегии.
Иными словами обнаружив, что план не состоятелен по каким-либо причинам, его необходимо пересмотреть и скорректировать. Решить какие именно действия необходимо предпринять для составления нового плана и прожолжения битвы, можно только наткнувшись на конкретную проблему, которая появилась здесь и сейчас, и о возможном существовании которой составитель плана даже не подозревал. Таким образом, Стратегия ПРЕДПОЛАГАЕТ возможное развитие событий, а тактика РАСПОЛАГАЕТ конкретными фактами, и на их основании позволяет принимать по ситуации те или иные решения.

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


Цель игры - как можно быстрее дойти до зеленой фишки. Красный квадратик - это преграда о которой фишка пока не подозревает, потому что она находится дальше одной клетки от нее.
И вот синяя фишка строит стратегический план для достижения цели. Она начинает рекурсивно перебирать все возможные последовательности ходов. В результате этого перебора появится древовидная структура которая будет начинаться в точке H1W1 а заканчиваться в точке H1W4. Очевидно, что следует выбирать самую короткую ветку графа, для достижения цели в кратчайшие сроки. Ей будет являться такая последовательность: 

H1W1>>H1W2>>H1W3>>H1W4.

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



вторник, 10 апреля 2012 г.

Практическое применение XNA. Часть 14.

Теневые карты (Shadow Maps)

Суть теневых карт заключается в следующем. Сцену рендерят в несколько проходов как и впредыдущем примере. Количество проходов может быть различным. Это зависит от многих факторов, смотря какое качество теней вы желаете получить. Но сейчас это не главное, я постараюсь объяснить сам принцип этого подхода. Первым делом мы рендерим сцену с позиции источника освещения. И делаем это хитрым способом, так как будто это не просто координата источника света, а камера. Иными словами источник света может видеть сцену. То что видет источник можно сохранить в текстуру. Сперва увиденное записывается в цель визуализации или RenderTarget2D, а после из нее в текстуру. Более того, эта текстура должна в себе хранить не просто вид сцены как мы привыкли его видеть обычно, с цветными обьектами, а информацию о глубине сцены. Иначе говоря записываем в текстуру, своего рода, буфер глубины. Делается это таким образом, что бы цвет (значение) пикселя соответствовал расстоянию от источника света до каждого действительного пикселя сцены. Что бы вам легче было представить вышесказанное рисунок ниже показывает пример такой текстуры глубины.


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


На рисунке точкой A показана поверхность видимая источнику света. Точкой B поверхность не видимая. Теперь надеюсь, что вам общая теоретическая часть понятна и для более конкретного изучения можно переходить к практике. Давайте создадим новый проект и назовем его myShadowWorld. Сразу же добавим в него компонент Camera. Он нам уже знаком по предыдущим примерам. Добавим так же и класс Terrain. Но в этот раз это уже будет не компонент с внешним вызовом функций, а обычный класс. Это нужно для того, что бы мы имели возможность вызывать метод отрисовки в любой момент когда нам это будет нужно, ведь сцену придется рендерить дважды.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace myShadowWorld
{
public class Terrain
{
private int WIDTH;
private int HEIGHT;
private VertexBuffer vertexBuffer;
private IndexBuffer indexBuffer;
private VertexDeclaration vertexDecalaration;
private float[,] heightData;
Texture2D texture;
Game game;
public Terrain(Game game)
{
this.game = game;
}
public void Initialize()
{
texture = game.Content.Load<Texture2D>("Terrain/grass");
LoadHeightData(game.Content.Load<Texture2D>("Terrain/HeightMap"));
SetUpTerrainVertices(game.GraphicsDevice);
SetUpTerrainIndices(game.GraphicsDevice);
vertexDecalaration = new VertexDeclaration(game.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
}
private void LoadHeightData(Texture2D heightMap)
{
// установили высоту и
// ширину карты высот в
// глобальные переменные
WIDTH = heightMap.Width;
HEIGHT = heightMap.Height;
// создали массив цветов для хранения
// данных карты высот с размерностью
// высоты умноженной на ширину
Color[] heightMapColors = new Color[WIDTH * HEIGHT];
// загружаем данные из карты высот
// в массив цветов
heightMap.GetData(heightMapColors);
// создали массив значений высот
heightData = new float[WIDTH, HEIGHT];
// в цикле
for (int x = 0; x < WIDTH; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
// если карта высот ч/б то не важно из какого канала
// брать значение. Мы возьмем из красного
heightData[x, y] = heightMapColors[x + y * WIDTH].R;
}
}
}
public void SetUpTerrainVertices(GraphicsDevice device)
{
// создаем массив вершин с размерностью
// количества всех точек изображения
VertexPositionNormalTexture[] vertices =
new VertexPositionNormalTexture[WIDTH * HEIGHT];
for (int x = 0; x < WIDTH; x++)
for (int y = 0; y < HEIGHT; y++)
{
// устанавливаем значение высоты в Y
// каждого вектора позиции вершины
// делением на 5 регулируем амплитуду
// высоты
vertices[x + y * WIDTH].Position = new Vector3(x, heightData[x, y] / 5, y);
vertices[x + y * WIDTH].Normal = new Vector3(0, 0, 1);
// устанавливаем координаты текстуры делением
// на 50 определяем мозаичность (tiling) текстуры
vertices[x + y * WIDTH].TextureCoordinate.X = (float)x / 50.0f;
vertices[x + y * WIDTH].TextureCoordinate.Y = (float)y / 50.0f;
}
// создаем вершинный буффер
// на основе полученного массива
vertexBuffer =
new VertexBuffer(device, typeof(VertexPositionNormalTexture),
VertexPositionNormalTexture.SizeInBytes * WIDTH * HEIGHT, BufferUsage.Points);
// записываем в него данные
vertexBuffer.SetData(vertices);
}
private void SetUpTerrainIndices(GraphicsDevice device)
{
short[] indices = new short[(WIDTH - 1) * (HEIGHT - 1) * 6];
for (int x = 0; x < WIDTH - 1; x++)
{
for (int y = 0; y < HEIGHT - 1; y++)
{
indices[(x + y * (WIDTH - 1)) * 6] = (short)((x + 1) + (y + 1) * WIDTH);
indices[(x + y * (WIDTH - 1)) * 6 + 1] = (short)((x + 1) + y * WIDTH);
indices[(x + y * (WIDTH - 1)) * 6 + 2] = (short)(x + y * WIDTH);
indices[(x + y * (WIDTH - 1)) * 6 + 3] = (short)((x + 1) + (y + 1) * WIDTH);
indices[(x + y * (WIDTH - 1)) * 6 + 4] = (short)(x + y * WIDTH);
indices[(x + y * (WIDTH - 1)) * 6 + 5] = (short)(x + (y + 1) * WIDTH);
}
}
indexBuffer = new IndexBuffer(device, sizeof(short) * indices.Length, BufferUsage.WriteOnly, IndexElementSize.SixteenBits);
indexBuffer.SetData(indices);
}
public void Update()
{
}
public void Draw(Effect eff, string tech)
{
eff.Parameters["World"].SetValue(Matrix.CreateTranslation(0, 0, 0));
eff.Parameters["MeshTexture"].SetValue(game.Content.Load<Texture2D>("Terrain/grass"));
eff.CurrentTechnique = eff.Techniques[tech];
eff.Begin();
foreach (EffectPass pass in eff.CurrentTechnique.Passes)
{
pass.Begin();
game.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes);
game.GraphicsDevice.Indices = indexBuffer;
game.GraphicsDevice.VertexDeclaration = new VertexDeclaration(game.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, WIDTH * HEIGHT, 0, (WIDTH - 1) * (HEIGHT - 1) * 2);
pass.End();
}
eff.End();
}
}
}
 Так же добавим класс Scene. В нем нет ничего нового для нас. Этот класс нарисует нам несколько моделей но не спомощью BasicEffect а с нашим эффектом.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace myShadowWorld
{
  class Scene
  {
    Model tractor;
    Matrix tractorWorld;
    Model uaz;
    Matrix uazWorld;
    Model mi24;
    Matrix mi24World;
    Game game;
    Matrix[] transforms;
   
    public Scene(Game game)
    {
      this.game = game;
    }
    public void Load(Effect sEffect)
    {  
      tractor = game.Content.Load<Model>("Models/tractor");
      transforms = new Matrix[tractor.Bones.Count];
      tractor.CopyAbsoluteBoneTransformsTo(transforms);
      tractorWorld = transforms[0] *
        Matrix.CreateScale(2) *
        Matrix.CreateRotationY(0.5f) *
        Matrix.CreateTranslation(new Vector3(150, 0, 100));
      RemapModel(tractor, sEffect);
      mi24 = game.Content.Load<Model>("Models/mi24");
      transforms = new Matrix[mi24.Bones.Count];
      mi24.CopyAbsoluteBoneTransformsTo(transforms);
      mi24World = transforms[0] *
        Matrix.CreateScale(2) *
        Matrix.CreateRotationY(-0.3f) *
        Matrix.CreateTranslation(new Vector3(120, 0, 140));
      RemapModel(mi24, sEffect);
      uaz = game.Content.Load<Model>("Models/uaz");
      transforms = new Matrix[uaz.Bones.Count];
      uaz.CopyAbsoluteBoneTransformsTo(transforms);
      uazWorld = transforms[0] *
        Matrix.CreateScale(2) *
        Matrix.CreateRotationY(0.3f) *
        Matrix.CreateTranslation(new Vector3(140, 0, 170));
      RemapModel(uaz, sEffect);
    }
    public static void RemapModel(Model model, Effect effect)
    {
      foreach (ModelMesh mesh in model.Meshes)
      {
        foreach (ModelMeshPart part in mesh.MeshParts)
        {
          part.Effect = effect;
        }
      }
    }
    public void Update()
    {

    }
    public void Draw(string tech)
    {
      foreach (ModelMesh mesh in tractor.Meshes)
      {
        foreach (Effect effect in mesh.Effects)
        {
          effect.Parameters["World"].SetValue(tractorWorld);
          effect.Parameters["MeshTexture"].SetValue(game.Content.Load<Texture2D>("Models/traktor_color"));
          effect.CurrentTechnique = effect.Techniques[tech];
        }
        mesh.Draw();
      }
     
      foreach (ModelMesh mesh in mi24.Meshes)
      {
        foreach (Effect effect in mesh.Effects)
        {
          effect.Parameters["World"].SetValue(mi24World);
          effect.Parameters["MeshTexture"].SetValue(game.Content.Load<Texture2D>("Models/mi24_color"));
          effect.CurrentTechnique = effect.Techniques[tech];
        }
        mesh.Draw();
      }

      foreach (ModelMesh mesh in uaz.Meshes)
      {
        foreach (Effect effect in mesh.Effects)
        {
          effect.Parameters["World"].SetValue(uazWorld);
          effect.Parameters["MeshTexture"].SetValue(game.Content.Load<Texture2D>("Models/uazik_color"));
          effect.CurrentTechnique = effect.Techniques[tech];
        }
        mesh.Draw();
      }
   
    }
  }
}

Напомню что функция RemapModel служит для замены эффекта который присвоен модели по умолчанию импортером среды разработки, на наш эффект. Ну и наконец наш главный класс.
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    Camera CAM;
    Terrain terrain;
    Scene scene;
    Effect sEffect;
    Vector3 LightPos;

Эти переменные вам уже знакомы.
    RenderTarget2D shadowRT;
    DepthStencilBuffer shadowDB;
Цель визуализации для хранения данных о глубине сцены с позиции источника света. И буфер глубины который мы использовали в предыдущем примере.
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
shadowRT = new RenderTarget2D(GraphicsDevice, 1024 * 4, 1024 * 4, 0, SurfaceFormat.Rgb32);
shadowDB = new DepthStencilBuffer(GraphicsDevice, 1024 * 4, 1024 * 4, DepthFormat.Depth24);
LightPos = new Vector3(128, 100, 0);
sEffect = Content.Load<Effect>("Effects/ShadowMap");
terrain.Initialize();
scene.Load(sEffect);
}
Здесь наиболее важным является создоние shadowRT и shadowDB. Чем большие величины будут установлены, тем точнее будут результаты ваших вычислений. Однако тем сильнее станет нагрузка на видеопроцессор. В зависимости от конфигурации вашего компьютера вам, возможно, придется искать компромис между производительностью и качеством (это нормально).
protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState().IsKeyDown(Keys.Escape))
this.Exit();
sEffect.Parameters["CameraPos"].SetValue(Camera.Position);
sEffect.Parameters["CameraView"].SetValue(Camera.viewMatrix);
sEffect.Parameters["CameraProj"].SetValue(Camera.projectionMatrix);
sEffect.Parameters["LightPos"].SetValue(LightPos);
sEffect.Parameters["LightView"].SetValue((Matrix.CreateLookAt(LightPos, new Vector3(128, 0, 128), Vector3.Up)));
sEffect.Parameters["LightProj"].SetValue(Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 1.45f, 90, 300));

terrain.Update();
scene.Update();
base.Update(gameTime);
}
В методе Update() мы присваеваем нашему эффекту те значения параметров которые будут использоваться и в первом и во втором проходах рендерринга. Далее  метод Draw()
protected override void Draw(GameTime gameTime)
{
//========= первый проход =============
GraphicsDevice.RenderState.DepthBufferFunction = CompareFunction.LessEqual;
// Устанавливаем Render Target для shadow map
GraphicsDevice.SetRenderTarget(0, shadowRT);
// Сохраняем текущий буфер глубины
DepthStencilBuffer old = GraphicsDevice.DepthStencilBuffer;
// Устанавливаем наш самодельный буфер глубины
GraphicsDevice.DepthStencilBuffer = shadowDB;
GraphicsDevice.Clear(Color.Black);
terrain.Draw(sEffect, "ShadowMapRender");
scene.Draw("ShadowMapRender");
GraphicsDevice.SetRenderTarget(0, null);
// Возвращаем предыдущий буфер глубины
GraphicsDevice.DepthStencilBuffer = old;
В первом проходе мы подготавливаем графическое устройство таким образом, что бы рендерринг сцены с позиции источника освещения происходил в shadowRT. если из него взять текстуру и визуализировать ее с помощью SpriteBatch то мы увидим глубину сцены.
spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState);
spriteBatch.Draw(shadowRT.GetTexture(), new Rectangle(0, 0, 300, 200), Color.White);
spriteBatch.End();



Во втором проходе рисуется сама сцена с позиции камеры
//========== второй проход ============
sEffect.Parameters["ShadowMapTexture"].SetValue(shadowRT.GetTexture());
GraphicsDevice.RenderState.CullMode = CullMode.None;
GraphicsDevice.RenderState.AlphaBlendEnable = false;
GraphicsDevice.Clear(Color.CornflowerBlue);
terrain.Draw(sEffect, "ShadowRender");
scene.Draw("ShadowRender");
При этом в эффект передается текстура глубины. Зачем она нужна я говорил в первой части этой главы. И в результате сравнения необходимых данных получаем такое изображение


Как видите теперь наши тени от моделей отбрасываются не только на землю но и на самих себя, и на другие модели.  Само собой разумеется, что такие результаты можно было получить только с помощью специального эффекта. Мы в начале книги касались темы шейдеров. Тема эта очень не простая и требует глубокого разбирательства. Вторая часть этой книги, как раз будет посвящена высокоуровневому языку шейдеров. В заключении этой ститьи приведу код этого эффекта, попутно его коментируя. Глобальные переменные которые будут использоваться в эффекте это:
float4 MaterialAmbientColor;    
float4 MaterialDiffuseColor;
две переменные хранящие параметры цвета.
float3 LightPos;
float3 LightDir;
float4x4 LightView;
float4x4 LightProj;
переменные хранящие все данные о источнике света, а именно его позиция, направление, и матрицы 4х4 вида и проекции.
texture MeshTexture;      
texture ShadowMapTexture;
sampler MeshTextureSampler = sampler_state
{
    Texture = <MeshTexture>;
    MipFilter = LINEAR;
    MinFilter = LINEAR;
    MagFilter = LINEAR;
};
sampler ShadowMapSampler = sampler_state
{
 Texture = <ShadowMapTexture>;
    MinFilter = POINT;
    MagFilter = POINT;
    MipFilter = POINT;
    AddressU = Clamp;
    AddressV = Clamp;
};
две текстуры , одна для текстурирования моделей, другая для хранения дпнных глубины.
float4x4 World;            
float3 CameraPos;
float4x4 CameraView; 
float4x4 CameraProj;
переменные хранящие данные о камере
float4 GetPositionLight(float4 position)
{
  float4x4 WorldViewProjection = mul(mul(World, LightView), LightProj);
  return mul(position, WorldViewProjection);
}
Функция помогающая получить позицию света.
struct VS_SHADOW_OUTPUT
{
 float4 Position : POSITION;
 float Depth : TEXCOORD0;
};

VS_SHADOW_OUTPUT RenderShadowMapVS(float4 vPos: POSITION)
{
 VS_SHADOW_OUTPUT Out;
 Out.Position = GetPositionLight(vPos);
 // Глубина это Z/W. Это значение будет возвращено пиксельному шейдеру.
 // Вычитание из еденицы даёт нам большую точность в floating-point данных
 Out.Depth.x = 1-(Out.Position.z/Out.Position.w);
 return Out;
}
float4 RenderShadowMapPS( VS_SHADOW_OUTPUT In ) : COLOR
{
 // Глубина это Z деленный на W. Мы возвращаем
 // это значение полностью в 32 битном красном канале
 // используя SurfaceFormat.Single. Это сохраняет данные
 // floating-point с наибольшнй точностью.
    return float4(In.Depth.x, 0, 0, 1);
}
Это код шейдера получающего значение глубины для каждого пикселя.
struct VS_OUTPUT
{
    float4 Position   : POSITION0;
    float2 TextureUV  : TEXCOORD0;
    float3 vNormal   : TEXCOORD1;
    float4 vPos       : TEXCOORD2; 
};

VS_OUTPUT RenderSceneVS( float3 position : POSITION, 
                           float3 normal : NORMAL, 
                           float2 vTexCoord0 : TEXCOORD0 )
{
     VS_OUTPUT Output;
     float4x4 wvp = mul(mul(World, CameraView), CameraProj);    
     Output.Position = mul(float4(position, 1.0), wvp);
     Output.vNormal =  mul(normal, World);    
     Output.vPos = float4(position,1.0);    
  Output.TextureUV = vTexCoord0;
   
     return Output;
}
struct PS_INPUT
{
    float2 TextureUV  : TEXCOORD0; 
    float3 vNormal   : TEXCOORD1;
    float4 vPos       : TEXCOORD2; 
};
struct PS_OUTPUT
{
    float4 RGBColor : COLOR0;
};
PS_OUTPUT RenderScenePS( PS_INPUT In )
{
    PS_OUTPUT Output;   
   
    // Стандартное выравнивание освещения
    float4 vTotalLightDiffuse = float4(1, 1, 1, 1);
    float3 lightDir = normalize(LightPos-In.vPos);  // направление света
    vTotalLightDiffuse += LightDiffuse * max(0,dot(In.vNormal, lightDir));
    vTotalLightDiffuse.a = 1.0f;
 // Сейчас, принимая во внимание ShadowMap смотрим если мы в тени
    float4 lightingPosition = GetPositionLight(In.vPos);// Полючаем нашу позицию на shadow map   
    // Получаем значение глубины ShadowMap для этого пикселя
    float2 ShadowTexC = 0.5 * lightingPosition.xy / lightingPosition.w + float2(0.5, 0.5);
    ShadowTexC.y = 1.0f - ShadowTexC.y;
    float shadowdepth = tex2D(ShadowMapSampler, ShadowTexC).r;       
    // Проверяем наше значение по отношению к значению глубины
    float ourdepth = 0.98 - (lightingPosition.z / lightingPosition.w);   
    // Проверяем shadowdepth по отношению к глубине этого пикселя (фактор неточности добавлен из расчета ошибки плавающей точки)
 if (shadowdepth - 0.03 > ourdepth)
 {    
     // если мы в тени, выключаем свет
  vTotalLightDiffuse = float4(0.3, 0.3, 0.3, 1);
 };
    Output.RGBColor = tex2D(MeshTextureSampler, In.TextureUV) * (vTotalLightDiffuse + LightAmbient);       
    return Output;   
}
Финальный шейдер в котором происходит сравнение данных глубины с данными камеры и на основании результатов сравнения выполняется затенение. Как я уже говорил ранее, для новичков этот код достаточно сложен и именно по этому моя следующая книга будет посвящена языку шейдеров, в которой мы шаг за шагом изучим все особенности и способы работы с HLSL.