воскресенье, 21 июня 2009 г.

UV координаты вершины принадлежащей треугольнику

При сложном разбиении mesh’а (дополнительная генерация геометрии), треугольники дробятся таким образом, что текстурные координаты новых вершин бывает не так просто вычислить. Потому, что новая вершина может по разным причинам быть различно удаленной от вершин треугольника. Вспомнил, что как то приводил свой вывод расчета в форуме xnadev.ru, но как понадобилось самому, долго искал, где именно.

Делаю для себя заначку, чтоб добру не пропадать:



using System;
using Microsoft.Xna.Framework;

namespace Test
{
public class TestMath2
{
public TestMath2()
{
// вершины треугольника
Vector3 A = new Vector3(1f, -1f, 0f);
Vector3 B = new Vector3(3f, -4f, 0f);
Vector3 C = new Vector3(5f, -1f, 0f);
// текстурные координаты вершин
Vector2 tA = new Vector2(1f, 1f);
Vector2 tB = new Vector2(3f, 4f);
Vector2 tC = new Vector2(5f, 1f);
// точка в плоскости треугольника ограниченная его гранями
Vector3 D = new Vector3(3f, -2f, 0f);
// B
// .
// / \
// / \
// / \
// / .D \
// / \
// .-----------.
// A C
// расчет
Vector2 tD = CalculationDuvFromABC( A, tA,
B, tB,
C, tC,
D);
}
///
/// Нахождение текстурных координат точки D
///
/// Позиция точки A
/// Текстурные координаты точки A
/// Позиция точки B
/// Текстурные координаты точки B
/// Позиция точки C
/// Текстурные координаты точки C
/// Позиция точки D
/// Текстурные координаты точки D
private Vector2 CalculationDuvFromABC( Vector3 A, Vector2 tA,
Vector3 B, Vector2 tB,
Vector3 C, Vector2 tC,
Vector3 D)
{
Vector3 E = PointOfLinesCrossing(A, B, C, D);
float kAEC = (E - A).Length() / (C - A).Length();
Vector2 tE = tA + (tC - tA) * kAEC;
float kBDE = (D - B).Length() / (E - B).Length();
return (Vector2)(tB + (tE - tB) * kBDE);
}
///
/// Нахождение позиции точки пересечения двух прямых,
/// проходящих через точки AC и BD
///
/// Позиция точки A
/// Позиция точки B
/// Позиция точки C
/// Позиция точки D
/// Позиция точки E, полученной пересечением двух прямых AC и BD
private Vector3 PointOfLinesCrossing(Vector3 A, Vector3 B, Vector3 C, Vector3 D)
{
Vector3 vecAC = C - A;
Vector3 vecBD = D - B;
float Tac = Math.Abs( Matrix2x2Determinant( vecBD.X, A.X - B.X,
vecBD.Y, A.Y - B.Y)
/
Matrix2x2Determinant( vecBD.X, vecAC.X,
vecBD.Y, vecAC.Y));
return Vector3.Add(A, Vector3.Multiply(vecAC, Tac));
}
///
/// Определитель матрицы 2х2
///
/// a00
/// a01
/// a10
/// a11
/// Определитель
private float Matrix2x2Determinant( float a00, float a01,
float a10, float a11)
{
return (float)(a00 * a11 - a10 * a01);
}
}
}

Процессор Intel® Core™ i7 (Nehalem)

Вот уже чуть менее полу года такой кристалл (920) живет в моем доме. А чувство восторга не стало меньше. Свой вклад конечно же (подливает масла в огонь) вносит nVIDIA GeForce GTX 280. Вполне возможно это так же связанно с тем, что предыдущая смена железа была в далеком 2003 году. Кто знает.

Приведу список ссылок, где расписаны умения этого творения рук человеческих:

- i7 SDK (Software Development Key): ключ к разработке эффективных программ для новейшего процессора Intel Core i7 – подробный видео доклад доступен на www.techdays.ru. Докдладчик Victoria Zhislina работает в компании Intel с 1997 года и имеет экстремальный опыт создания графических библиотек и многопоточных приложений. В настоящее время занимается технической поддержкой компаний-разработчиков программного обеспечения, помогая им внедрить и эффективно использовать в своих продуктах возможности аппаратных платформ Intel (copy/paste).

- описание Intel’а

- википедия

P.S. если раньше не хватало процессоров, то сейчас самостоятельному Indie не хватает рук и времени …

Terrain Geomorphing in the Vertex Shader

Читал несколько статей на эту тему. Искал с рабочими примерами (желательно на XNA + HLSL), конкретно с пред расчетом и готовым шейдером. Но нашел только много теории и не для XNA и DirectX, а в общем. Написано умно, ничего не понял. Конкретно озадачив себя, просидел как всегда допоздна. На утро, проснулся с готовым представлением о содержимом шейдера. Мне повезло.



Демонстрация:



И так по порядку.
Рассмотрим вот такую картинку:



Здесь изображен процесс перехода между уровнями детализации ландшафта.
черным цветом - контур геометрии предыдущего слоя детализации;
синим цветом – его вершины;
красным цветом – вершины нового уровня детализации.
зеленым цветом – выделен контур новой геометрии с учетом достроенных вершин, в следствии разбиения;
фиолетовым цветом – вершины середин отрезков между вершинами предыдущего слоя детализации;
красные вектора в низ – delta вектора разности между серединами отрезков вершин предыдущего слоя и соответствующими им новыми вершинами. Дальше по тексту разъясню подробнее.

Что из себя представляет «Terrain Geomorphing in the Vertex Shader»?
Если посмотреть более пристально на видео пример к моей предыдущей статье "Procedural Landscape on XNA Game Studio 3.0", где нет геоморфинга,то можно увидеть как «выпрыгивает» (новый научный термин) новая геометрия, при наезде друг на друга слоев с различной детализацией.

Это не есть good!

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

Нам понадобится следующее описание формата вершин для хранения выше описанных данных:



namespace gEngine.gProgressiveLandscape
{
public struct gplVertexPositionNormalTexture
{

public Vector3 Position;
public Vector3 PositionGeomorphing;
public Vector3 Normal;
public Vector3 NormalGeomorphing;
public Vector2 TextureCoordinate;
public float IndexDetailedLayer;


public static readonly VertexElement[] VertexElements;

static gplVertexPositionNormalTexture()
{
VertexElements = new VertexElement[6];
short offset = 0;
// Position
VertexElements[0] = new VertexElement(0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Position, 0);
offset += (short)Marshal.SizeOf(new Vector3());
// PositionGeomorphing
VertexElements[1] = new VertexElement(0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Position, 1); // !
offset += (short)Marshal.SizeOf(new Vector3());
// Normal
VertexElements[2] = new VertexElement(0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Normal, 0);
offset += (short)Marshal.SizeOf(new Vector3());
// NormalGeomorphing
VertexElements[3] = new VertexElement(0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Normal, 1); // !
offset += (short)Marshal.SizeOf(new Vector3());
// TextureCoordinate
VertexElements[4] = new VertexElement(0, offset,
VertexElementFormat.Vector2,
VertexElementMethod.Default,
VertexElementUsage.TextureCoordinate, 0);
offset += (short)Marshal.SizeOf(new Vector2());
// NamberDetailedLayer
VertexElements[5] = new VertexElement(0, offset,
VertexElementFormat.Single,
VertexElementMethod.Default,
VertexElementUsage.TextureCoordinate, 1); // !
}

public gplVertexPositionNormalTexture( Vector3 position,
Vector3 positionGeomorphing,
Vector3 normal,
Vector3 normalGeomorphing,
Vector2 textureCoordinate,
float indexDetailedLayer)
{
this.Position = position;
this.PositionGeomorphing = positionGeomorphing;
this.Normal = normal;
this.NormalGeomorphing = normalGeomorphing;
this.TextureCoordinate = textureCoordinate;
this.IndexDetailedLayer = indexDetailedLayer;
}

public static int SizeInBytes
{
get
{
return (int)( Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector2()) +
sizeof(float));
}
}
}
}


Здесь PositionGeomorphing и есть то поле для хранения вектора смещения.

Нечто похожее делаем и с нормалями.

Все волшебство произойдет в шейдере!

Куда мы передадим дополительную информацию о:
float3 vEyePosition; - положении камеры
float layerRadius[7]; - реальные размеры уровней детализации

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

Сам шейдер выглядит следующим образом:



float4x4 matWorld : WORLD;
float4x4 matWVP : WORLDVIEWPROJECTION;

float3 vLightPosition;
float3 vEyePosition;

float layerRadius[7];

float fogBegin = 200.0f;
float fogEnd = 400.0f;

struct VS_INPUT
{
float3 Position : POSITION0;
float3 PositionGeomorphing : POSITION1;
float3 Normal : NORMAL0;
float3 NormalGeomorphing : NORMAL1;
float2 Texcoord : TEXCOORD0;
float IndexDetailedLayer : TEXCOORD1;
};

struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 Texcoord : TEXCOORD0;
float3 Light : TEXCOORD1;
float3 Normal : TEXCOORD2;
float Fog : TEXCOORD3;
};

VS_OUTPUT VS(VS_INPUT In)
{
VS_OUTPUT Out = ( VS_OUTPUT ) 0;
float Distance = length ( In.Position - vEyePosition );
float K = clamp ((( Distance / layerRadius[In.IndexDetailedLayer] - 0.5f ) * 2.0f ), 0.0f, 1.0f );
Out.Position = mul ( float4(In.Position + In.PositionGeomorphing * K, 1.0f ), matWVP );
Out.Fog = clamp ((( 1.0f - (Distance - fogBegin) / fogEnd) - 0.5f) * 2.0f, 0.0f, 1.0f);
//Out.Fog = 1.0f;
Out.Texcoord = In.Texcoord;
Out.Light = normalize ( mul(vLightPosition, matWorld) );
Out.Normal = normalize ( mul( In.Normal + In.NormalGeomorphing * K, matWorld ) );
return Out;
}

texture tex;
sampler2D sTexture = sampler_state
{
Texture = (tex);
};

float spec = 0.5f;

float4 PS(VS_OUTPUT In) : COLOR0
{
float4 specColor = tex2D( sTexture, In.Texcoord ) * spec;
float4 diffColor = clamp( tex2D( sTexture, In.Texcoord ) * dot( In.Normal, In.Light ), 0.0f, 1.0f );
float4 color = float4(1.0f, 1.0f, 1.0f, 1.0f) * (1.0f - In.Fog) + (diffColor + specColor) * In.Fog;
return color;
}

technique TerrainGeomorphing
{
pass Pass1
{
VertexShader = compile vs_3_0 VS();
PixelShader = compile ps_3_0 PS();
}
}


Да, в шейдере добавлен расчет тумана, его можно выбросить. Он влияет только на текстурирование.

Оригинальный блог

Procedural Landscape on XNA Game Studio 3.0

Динамический ландшафт



Демонстрация:



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

Закончил эту часть работы давно. Даже успели обсудить на xnadev.ru . Сейчас данный проект уже несколько раз преобразился. Возможно, в скором времени, дойдут руки сделать новое видео. Как раз будет повод рассказать о том, что из себя представляет «Terrain Geomorphing in the Vertex Shader» и с чем его едят. Пока расскажу об этом этапе.

Как это делалось?

Описал представление слоев детализации и данных о вершинах.
Карта логики представления слоев детализации выглядит так:



Карта логики связывания вершин слоев детализации в треугольники, с учетом связей между соседними слоями выглядит так:



На обеих картинках цвета слоев не совпадают. Лень переделывать. Просто остались наработанные картинки, которые изначально не предназначались для статьи.

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

private void backgroundWorker_DoWork
(object sender, DoWorkEventArgs e)
{
PartitionLevels();
geometry.InitIndex();
geometry.GenNormal();
}

- разбиение слоев детализации, добавление новых вершин (и корректировка их высот) в единый массив вершин;
- инициализация единого массива индексов, связывающих все треугольники (вершины), всех слоев детализации.
- генерация нормалей.

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

Блок схема обещанного метода:



Блок схема автоматически сгенерирована примочкой к Visual Studio под названием «Microsoft Visual Studio Learning Pack 2.0»

Код метода:



Код метода:
///
/// Рекурсивная сборка индексов отдельной ветки дерава уровней детализации ландшафта
///
private void InitNodeIndex (gplNode inNode, ref int inIndex)
{
// --- уровень детализации ---------------------------------------
int l = inNode.Level;
// --- индекс активной ветки --------------------------------------
int i = inNode.Index;
// --- размеры активного уровня детализации -----------------------
int w = levelInfo[l].W;
int h = levelInfo[l].H;
// --- признаки соединение со слоем более низкой детализации ------
bool bLeft = false;
bool bRight = false;
bool bUp = false;
bool bDown = false;
// --- индексные направления слоя для псевдодвухмерной карты ------
gxplTrends trends = new gxplTrends();
trends.NewTrends(i, w);
// --- если ветка не разбита ----------------------------------------------------------------------------------------
if (inNode.NodeChildren == null)
{
// --------------------------------------------------------------------------------------------------------------
#region --- собираем два треугольника ---------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
// - 1й треугольник -----------------------
indices[inIndex + 0] = trends.Center;
indices[inIndex + 1] = trends.Right;
indices[inIndex + 2] = trends.Down;
// - 2й треугольник -----------------------
indices[inIndex + 3] = trends.Down;
indices[inIndex + 4] = trends.Right;
indices[inIndex + 5] = trends.DownRight;
inIndex += 6;
// --------------------------------------------------------------------------------------------------------------
#endregion ------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
}
// --- если разбита -------------------------------------------------------------------------------------------------
else
{
// --------------------------------------------------------------------------------------------------------------
#region --- определение режима соединения с соседними ветками ---------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
// --- check left join --------------------------------------------
//if (!bNoLeft)
bLeft = (inNode.Coord.X > levelInfo[l].SX0) ?
map[trends.Left].NodeChildren == null : true;
// --- check right join -------------------------------------------
//if (!bNoRight)
bRight = (inNode.Coord.X < levelInfo[l].SX0 + levelInfo[l].W - 1) ?
map[trends.Right].NodeChildren == null : true;
// --- check up join ----------------------------------------------
//if (!bNoUp)
bUp = (inNode.Coord.Y > levelInfo[l].SY0) ?
map[trends.Up].NodeChildren == null : true;
// --- check down join --------------------------------------------
//if (!bNoDown)
bDown = (inNode.Coord.Y < levelInfo[l].SY0 + levelInfo[l].H - 1) ?
map[trends.Down].NodeChildren == null : true;
// --------------------------------------------------------------------------------------------------------------
#endregion ------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
#region --- сборка всех треугольников с учетом связей различных слоев детализации -------------------------------
// --------------------------------------------------------------------------------------------------------------
// --- индексные направления слоя +1 для псевдодвухмерной карты -------------------------
gxplTrends trends4x3 = new gxplTrends(inNode.NodeChildren[3].Index, levelInfo[l + 1].W);
// --------------------------------------------------------------------------------------------------------------
#region --- если с лева соединение с более высокой детализацией --------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
if (bLeft)
{
indices[inIndex + 0] = trends.Center;
indices[inIndex + 1] = trends4x3.Center;
indices[inIndex + 2] = trends.Down;
inIndex += 3;
// --- если с верху нет! соединения с более высокой детализацией (добавочный треугольник)
if (!bUp)
{
indices[inIndex + 0] = trends.Center;
indices[inIndex + 1] = trends4x3.Up;
indices[inIndex + 2] = trends4x3.Center;
inIndex += 3;
}
// --- если с низу нет! соединения с более высокой детализацией (добавочный треугольник)
if (!bDown)
{
indices[inIndex + 0] = trends.Down;
indices[inIndex + 1] = trends4x3.Center;
indices[inIndex + 2] = trends4x3.Down;
inIndex += 3;
}
}
else // если нет соединение с более высокой детализацией, то переходим к следующему уровню детализации
{
if (!bUp) // Х0
InitNodeIndex(inNode.NodeChildren[0], ref inIndex); // 00

if (!bDown) // 00
InitNodeIndex(inNode.NodeChildren[2], ref inIndex); // Х0
}
// --------------------------------------------------------------------------------------------------------------
#endregion ------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
#region --- если с права соединение с более высокой детализацией ------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
if (bRight)
{
indices[inIndex + 0] = trends.Right;
indices[inIndex + 1] = trends.DownRight;
indices[inIndex + 2] = trends4x3.Center;
inIndex += 3;
// --- если с верху нет! соединения с более высокой детализацией (добавочный треугольник)
if (!bUp)
{
indices[inIndex + 0] = trends4x3.Center;
indices[inIndex + 1] = trends4x3.Up;
indices[inIndex + 2] = trends.Right;
inIndex += 3;
}
// --- если с низу нет! соединения с более высокой детализацией (добавочный треугольник)
if (!bDown)
{
indices[inIndex + 0] = trends4x3.Center;
indices[inIndex + 1] = trends.DownRight;
indices[inIndex + 2] = trends4x3.Down;
inIndex += 3;
}
}
else // если нет соединение с более высокой детализацией, то переходим к следующему уровню детализации
{
if (!bUp) // 0Х
InitNodeIndex(inNode.NodeChildren[1], ref inIndex); // 00

if (!bDown) // 00
InitNodeIndex(inNode.NodeChildren[3], ref inIndex); // 0Х
}
// --------------------------------------------------------------------------------------------------------------
#endregion ------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
#region --- если с верху соединение с более высокой детализацией ------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
if (bUp)
{
indices[inIndex + 0] = trends.Center;
indices[inIndex + 1] = trends.Right;
indices[inIndex + 2] = trends4x3.Center;
inIndex += 3;
// --- если с лева нет! соединения с более высокой детализацией (добавочный треугольник)
if (!bLeft)
{
indices[inIndex + 0] = trends.Center;
indices[inIndex + 1] = trends4x3.Center;
indices[inIndex + 2] = trends4x3.Left;
inIndex += 3;
}
// --- если с права нет! соединения с более высокой детализацией (добавочный треугольник)
if (!bRight)
{
indices[inIndex + 0] = trends4x3.Center;
indices[inIndex + 1] = trends.Right;
indices[inIndex + 2] = trends4x3.Right;
inIndex += 3;
}
}
// --------------------------------------------------------------------------------------------------------------
#endregion ------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
#region --- если с низу соединение с более высокой детализацией -------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
if (bDown)
{
indices[inIndex + 0] = trends.Down;
indices[inIndex + 1] = trends4x3.Center;
indices[inIndex + 2] = trends.DownRight;
inIndex += 3;
// --- если с лева нет! соединения с более высокой детализацией (добавочный треугольник)
if (!bLeft)
{
indices[inIndex + 0] = trends4x3.Center;
indices[inIndex + 1] = trends.Down;
indices[inIndex + 2] = trends4x3.Left;
inIndex += 3;
}
// --- если с права нет! соединения с более высокой детализацией (добавочный треугольник)
if (!bRight)
{
indices[inIndex + 0] = trends4x3.Center;
indices[inIndex + 1] = trends4x3.Right;
indices[inIndex + 2] = trends.DownRight;
inIndex += 3;
}
}
//else
//{
//
//}
// --------------------------------------------------------------------------------------------------------------
#endregion ------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
#endregion ------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------
}
}


Свои задачи данная разработка уже выполнила. Для себя дальнейшие пути развития данной идеи я вижу два:
1) Растачивать дальше в этом направлении. Реализовать динамическую подгруздку данных о высотах с диска. Есть расчетные таблицы. Разработан алгоритм быстрого обновления кэшей высот к уровням детализации, без смещения (move) данных. Это позволит работать с ландшафтами огромного размера. Приведу пример расчета размеров LOD’ов для карты 1310.72^2 км с максимальной детализацией до 10^2 м:



2) Для игр. Придется переделать исходную коллекцию массивов высот в один. И реализовать новую выборку.

Вот как то так.

Оригинальный блог

суббота, 20 июня 2009 г.

Content Processor for Normal Map Texture

Пахомов Андрей (PAX), на основе моей статьи Calculation deep normal map, сделал (для XNA Game Studio приложений) свой Content Processor для загрузки, через Content Manager, и пересчета текстуры в карту нормалей.

Скачать можно тут (требуется регистрация xnadev.ru)

Оригинал блога

Effect “Bump mapping”

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



И так поехали.

Для чистоты эксперимента рассмотрим частный случай.

Описание условий эксперимента:
Объект – (Поверхность текстурирования) плоскость, квадрат.
Расположение объекта – (изначальное) параллельно плоскости экрана, лицом к камере.
Карта нормалей – (текстура) для ее построения используем расчет, приведенный в моей статье «Calculation deep normal map».

Нам потребуется новый тип описания формата вершин, назовем его VertexPositionTangentSpaceTexture.

(код его реализации)
public struct VertexPositionTangentSpaceTexture
{
public Vector3 Position;
public Vector3 Normal;
public Vector3 Binormal;
public Vector3 Tangent;
public Vector2 TextureCoordinate;
public static readonly VertexElement[] VertexElements;
static VertexPositionTangentSpaceTexture()
{
VertexElements = new VertexElement[5];
short offset = 0;
// Position
VertexElements[0] = new VertexElement(
0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Position,
0);
offset += (short)Marshal.SizeOf(new Vector3());
// Normal
VertexElements[1] = new VertexElement(
0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Normal,
0);
offset += (short)Marshal.SizeOf(new Vector3());
// Binormal
VertexElements[2] = new VertexElement(
0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Binormal,
0);
offset += (short)Marshal.SizeOf(new Vector3());
// Tangent
VertexElements[3] = new VertexElement(
0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Tangent,
0);
offset += (short)Marshal.SizeOf(new Vector3());
// TextureCoordinate
VertexElements[4] = new VertexElement(
0, offset,
VertexElementFormat.Vector2,
VertexElementMethod.Default,
VertexElementUsage.TextureCoordinate,
0);
}
public VertexPositionTangentSpaceTexture(
Vector3 position,
Vector3 normal,
Vector3 binormal,
Vector3 tangent,
Vector2 textureCoordinate)
{
this.Position = position;
this.Normal = normal;
this.Binormal = binormal;
this.Tangent = tangent;
this.TextureCoordinate = textureCoordinate;
}
public static int SizeInBytes
{
get
{
return (int)( Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector2()));
}
}
}
В его описании поля: Normal, Binormal и Tangent. Послужат нам описанием локальной системы координат для каждой вершины, так называемый tangent space – пространство косательной. В данном примере мы не будем ударятся в оптимизацию и экономию. Потому для наглядности примера используем все три вектора и тем самым заострим внимание на самой сути Bump Map’инга и возможно это помежет кому либо избежать подводных камней при изучении этого примера.

Генерируем поверхность с вершинами типа VertexPositionTangentSpaceTexture.

(пример)
public void Quad()
{
float y0 = 0.5f;
float y1 = -y0;
float x0 = -0.5f;
float x1 = -x0;
float z0 = 0.5f;
float z1 = -z0;
z0 = 0; z1 = 0;
verts = new VertexPositionTangentSpaceTexture[4];
indices = new int[6];
int iv, ii;
iv = 0;
ii = 0;
// front
verts[iv + 0].Position = new Vector3(x0, y0, z0);
verts[iv + 1].Position = new Vector3(x1, y0, z0);
verts[iv + 2].Position = new Vector3(x0, y1, z0);
verts[iv + 3].Position = new Vector3(x1, y1, z0);
verts[iv + 0].Normal = Vector3.Backward;
verts[iv + 1].Normal = Vector3.Backward;
verts[iv + 2].Normal = Vector3.Backward;
verts[iv + 3].Normal = Vector3.Backward;
verts[iv + 0].Binormal = Vector3.Up;
verts[iv + 1].Binormal = Vector3.Up;
verts[iv + 2].Binormal = Vector3.Up;
verts[iv + 3].Binormal = Vector3.Up;
verts[iv + 0].Tangent = Vector3.Right;
verts[iv + 1].Tangent = Vector3.Right;
verts[iv + 2].Tangent = Vector3.Right;
verts[iv + 3].Tangent = Vector3.Right;
verts[iv + 0].TextureCoordinate = Vector2.Zero;
verts[iv + 1].TextureCoordinate = Vector2.UnitX;
verts[iv + 2].TextureCoordinate = Vector2.UnitY;
verts[iv + 3].TextureCoordinate = Vector2.One;
indices[ii + 0] = iv + 0;
indices[ii + 1] = iv + 1;
indices[ii + 2] = iv + 2;
indices[ii + 3] = iv + 2;
indices[ii + 4] = iv + 1;
indices[ii + 5] = iv + 3;
iv += 4;
ii += 6;
// verts
vertexBuffer = new VertexBuffer(
graphics.GraphicsDevice,
VertexPositionTangentSpaceTexture.SizeInBytes * verts.Length,
BufferUsage.None);
vertexBuffer.SetData(verts);
// index
indexBuffer = new IndexBuffer(
graphics.GraphicsDevice,
sizeof(int) * indices.Length,
BufferUsage.None, IndexElementSize.ThirtyTwoBits);
indexBuffer.SetData(indices);
vertexDeclaration = new VertexDeclaration(graphics.GraphicsDevice, VertexPositionTangentSpaceTexture.VertexElements);
}
}

Обращаю Ваше внимание на инициализацию опять же Normal, Binormal и Tangent. Фишка в том, что по условиям эксперимента я заранее знаю как будет расположен в пространстве текстурируемый объект. Потому так же знаю как будет расположенна нормаль к его лицевой стороне и соответствующие ей Binormal и Tangent.

В случае же со сложной, изогнутой поверхностью с большим количеством треугольников нам прийдется выполнять не простые вычисления для расчета tangent space к каждой вершине. Так же учитывать углы между плоскостями треугольников с общими вершинами и либо усреднять tangent space при малых углах, либо добовлять новые вершины для разделения значений tangent space соответствующие этим же треугольникам при углах близких к 90 градусам. Но это уже развитие идеи и большая тема для отдельной статьи.

Для чего нам это нужен tangent space?
Все просто. Как вы наверно знаете, в вершинном шейдере при обычном текстурировании, для правильного проецирования в плоскость экрана, мы выполняем следующие преобразования:

(пример)
Out.Position = mul( In.Position, matWVP );
Out.Light = normalize( vLight );
Out.Normal = normalize( mul( In.Normal, matWorld ) );
Out.Texcoord = In.Texcoord;

Т.е.
- преобразуем и проецируем координаты вершины в плоскость экрана матрицей matWVP (семантика :WORLDVIEWPROJECTION);
- преобразуем нормали матрицей matWorld (семантика :WORLD) – поворот нормалей для правильного расчета освещения в пиксельном шейдере;

Но в нашем случае нормали к вершинам поворачивать мы не будем. Потому, что это влечет за собой поворот всех нормалей, к каждому пикселю текстуры, принадлежащих выводимым треугольникам, а это лишние действия. Классический прием - поворот вектора освещения обратно повороту объекта, вот так:

(пример)
float3 vLight = mul(vLightPos, matWorldBack) - In.Position;
//float3 tangent = float3(1,0,0); \
//float3 binormal = float3(0,1,0); - в нашем частном случае
//float3 normal = float3(0,0,1); /
float3 normal = normalize(In.Normal);
float3 binormal = normalize(In.Binormal);
float3 tangent = normalize(In.Tangent);
float3x3 matTS = float3x3(tangent, binormal, normal);
Out.vLightTS = normalize(mul(vLight, matTS));

Мы поворачиваем вектор источника света vLightPos в соответствии с преобразованиями накопленными в матрице matWorldBack. Эта матрица является обратной матрицей для мировой матрицы объекта. Прернос и масштабирование в данном случае нас не интересуют, потому как vLightPos вектор, а не координата, и мы нормализуем итоговый результат.
Что значит обратная матрица? Матрица в которой собраны преобразования обратные оригинальной матрице. В данном случае с помощью обратной матрицы мы вращаем вектор освещения в сторону обратную вращению нашего объекта.
Получить обратную матрицу средствами XNA очень просто:
matWorldBack = Matrix.Invert(matWorld);
Далее переводим координаты источника света (в статичную!) локальную систему координат очередной вершины действиями:
float3 normal = normalize(In.Normal);
float3 binormal = normalize(In.Binormal);
float3 tangent = normalize(In.Tangent);
float3x3 matTS = float3x3(tangent, binormal, normal);
Out.vLightTS = normalize(mul(vLight, matTS));
Где, matTS – матрица описывающая tangent space вершины.

После чего мы готовы к текстурированию с учетом освещения и псевдо рельефа.
В пиксельном шейде (пример):
float4 ps(PS_INPUT In) : COLOR0
{
float3 vNormalTS = tex2D( sTexture0, In.Texcoord ) * 2 - 1;
float4 color = float4(0.8f, 0.8f, 0, 1);
float3 vlightTS = In.vLightTS;

float light = clamp(dot(vNormalTS, vlightTS), 0, 1);
return mul(color, light);
}
Стоит обратить внимание на две строчки:
1) float3 vNormalTS = tex2D( sTexture0, In.Texcoord ) * 2 - 1;
где, мы берем нормаль к поверхности в данном пикселе текстуры и распаковываем ее в первоначальный вид, т.к. в normal map у нас хранятся данные о (естественно) нормалях, но упакованными по условной формуле:
color = normal * 0.5f + vector(0.5f, 0.5f, 0.5f);
Подробнее смотреть статью о расчете - «Calculation deep normal map».
2) float light = clamp(dot(vNormalTS, vlightTS, 0, 1);
сам расчет интенсивности освещения для данного пикселя текстуры.

(полный пример шейдера)
float4x4 matWorld : WORLD0;
float4x4 matWorldBack : WORLD1;
float4x4 matWVP : WORLDVIEWPROJECTION0;
float3 vLightPos;
float3 vViewPos;
struct VS_INPUT
{
float3 Position : POSITION0;
float3 Normal : NORMAL0;
float3 Binormal : BINORMAL0;
float3 Tangent : TANGENT0;
float2 Texcoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 Texcoord : TEXCOORD0;
float3 vLightTS : TEXCOORD3;
};
VS_OUTPUT vs( VS_INPUT In )
{
float3 vLight = mul(vLightPos, matWorldBack) - In.Position;
float3 normal = normalize(In.Normal);
float3 binormal = normalize(In.Binormal);
float3 tangent = normalize(In.Tangent);
float3x3 matTS = float3x3( tangent, binormal, normal );
VS_OUTPUT Out = ( VS_OUTPUT ) 0;
Out.Position = mul(float4( In.Position, 1 ), matWVP);
Out.Texcoord = In.Texcoord;
Out.vLightTS = normalize(mul(vLight, matTS));
return Out;
}
texture tex0;
sampler2D sTexture0 = sampler_state
{
Texture = (tex0);
MinFilter = LINEAR;
MagFilter = LINEAR;
MipFilter = LINEAR;
AddressU = CLAMP;
AddressV = CLAMP;
};
struct PS_INPUT
{
float2 Texcoord : TEXCOORD0;
float3 vLightTS : TEXCOORD3;
};
float4 ps(PS_INPUT In) : COLOR0
{
float3 vNormalTS = tex2D( sTexture0, In.Texcoord ) * 2 - 1;
float4 color = float4(0.8f, 0.8f, 0, 1);
float3 vlightTS = In.vLightTS;
float light = clamp(dot(vNormalTS, vlightTS), 0, 1);
return mul(color, light);
}
{
pass Pass_0
{
VertexShader = compile vs_2_0 vs();
PixelShader = compile ps_2_0 ps();
}
}

Результат:



Абсолютно прозрачные и простые примеры это хорошо. Но закончить статью хочется ярче. Потому поковыряв в приложении к RenderMonkey пример «Parallax Occlusion Mapping». Я добавил бликовую модель освещения и тут же картинка (ее можно увидеть в начале статьи) стала живее.
(пример шейдера)

float4x4 matWorld : WORLD0;
float4x4 matWorldBack : WORLD1;
float4x4 matWVP : WORLDVIEWPROJECTION0;
float3 vLightPos;
float3 vViewPos;
struct VS_INPUT
{
float3 Position : POSITION0;
float3 Normal : NORMAL0;
float3 Binormal : BINORMAL0;
float3 Tangent : TANGENT0;
float2 Texcoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 Texcoord : TEXCOORD0;
float3 vNormalWS : TEXCOORD1;
float3 vViewWS : TEXCOORD2;
float3 vLightTS : TEXCOORD3;
float3 vViewTS : TEXCOORD4;
};
VS_OUTPUT vs( VS_INPUT In )
{
float3 vLight = mul(vLightPos, matWorldBack) - In.Position;
float3 vViewDir = mul(vViewPos, matWorldBack) - In.Position;
float3 normal = normalize(In.Normal);
float3 binormal = normalize(In.Binormal);
float3 tangent = normalize(In.Tangent);
float3x3 matTS = float3x3( tangent, binormal, normal );
VS_OUTPUT Out = ( VS_OUTPUT ) 0;
Out.Position = mul(float4( In.Position, 1 ), matWVP);
Out.Texcoord = In.Texcoord;
Out.vNormalWS = In.Normal;
Out.vViewWS = vViewDir;
Out.vLightTS = normalize(mul(vLight, matTS));
Out.vViewTS = normalize(mul(vViewDir, matTS));
return Out;
}
texture tex0;
sampler2D sTexture0 = sampler_state
{
Texture = (tex0);
MinFilter = LINEAR;
MagFilter = LINEAR;
MipFilter = LINEAR;
AddressU = CLAMP;
AddressV = CLAMP;
};
struct PS_INPUT
{
float2 Texcoord : TEXCOORD0;
float3 vNormalWS : TEXCOORD1;
float3 vViewWS : TEXCOORD2;
float3 vLightTS : TEXCOORD3;
float3 vViewTS : TEXCOORD4;
};
float4 cSpecularColor = float4(1, 1, 1, 1);
float fSpecularExponent = 300;
float4 ps(PS_INPUT In) : COLOR0
{
float3 vNormalTS = tex2D( sTexture0, In.Texcoord ) * 2 - 1;
float4 color = float4(0.8f, 0.8f, 0, 1);
float3 vlightTS = In.vLightTS;
float3 vReflectionTS = normalize( 2 * dot( In.vViewTS, vNormalTS ) * vNormalTS - In.vViewTS );
float fRdotL = dot( vReflectionTS, In.vLightTS );
float4 cSpecular = saturate( pow( fRdotL, fSpecularExponent )) * cSpecularColor;
float light = clamp(dot(vNormalTS, vlightTS) * (color + cSpecular), 0, 1);
return mul(color, light);
}
technique BumpMapping
{
pass Pass_0
{
VertexShader = compile vs_2_0 vs();
PixelShader = compile ps_2_0 ps();
}
}

Скачать пример можно тут (требуется регистрация xnadev.ru) (чуть позже, в оригинальном блоге, ссылка станет доступна)

Оригинал блога

пятница, 19 июня 2009 г.

Calculation deep normal map

Calculation normal map for “Bump mapping” & “Parallax Mapping” II

Вот еще один, более сложный пример расчета.

Что тут добавлено:
I. При расчете переворачивается ось Y, для приведения соответствующего tangent space к нормальному виду.
Это связанно с тем, что в мировой системе координат ось Y смотрит в верх, а в текстуре в низ.
II. Сглаживание normal map
III. Масштабирование для получения более глубокого эффекта.

(код примера)
///
/// Расчет normal map к текстуре
///

/// Текстура для расчета (цветная, черно-белая - по барабану)
/// Во сколько целых раз нужно уменьшить (2^n)
/// Рассчитанная normal map текстура
private Texture2D GenNormalMamp(Texture2D inTex, int K)
{
int inX = inTex.Width;
int inY = inTex.Height;
int outX = inTex.Width / K;
int outY = inTex.Height / K;
float z = 1;
int l = inX * inY;
Texture2D outTex = new Texture2D(graphics.GraphicsDevice, outX, outY);
Color[] inColor = new Color[l];
Color[] outColor = new Color[l / (K * K)];
float[,] heightMap = new float[outX, outY];
Vector3[,] normalMap = new Vector3[outX, outY];
inTex.GetData(inColor);
// height map
for (int iy = 0; iy < outY; iy++)
{
for (int ix = 0; ix < outX; ix++)
{
Vector3 v3 = inColor[iy * inX * K + ix * K].ToVector3();
heightMap[ix, iy] = (v3.X + v3.Y + v3.Z) / 3;
}
}
// normal map
float[] delHeight = new float[8];
Vector3[] v38 = new Vector3[8];
Vector3 vAddPack = new Vector3(0.5f, 0.5f, 0.5f);
Vector3 vScale = new Vector3(2f, 2f, 1f);
for (int iy = 0; iy < outY; iy++)
{
for (int ix = 0; ix < outX; ix++)
{
if (iy == 0 || iy == outY - 1 || ix == 0 || ix == outX - 1)
{
normalMap[ix, iy] = Vector3.Backward;
continue;
}
delHeight[0] = heightMap[ix, iy] - heightMap[ix - 1, iy - 1];
delHeight[1] = heightMap[ix, iy] - heightMap[ix, iy - 1];
delHeight[2] = heightMap[ix, iy] - heightMap[ix + 1, iy - 1];
delHeight[3] = heightMap[ix, iy] - heightMap[ix - 1, iy];
delHeight[4] = heightMap[ix, iy] - heightMap[ix + 1, iy];
delHeight[5] = heightMap[ix, iy] - heightMap[ix - 1, iy + 1];
delHeight[6] = heightMap[ix, iy] - heightMap[ix, iy + 1];
delHeight[7] = heightMap[ix, iy] - heightMap[ix + 1, iy + 1];
v38[0] = new Vector3(-delHeight[0], delHeight[0], z - delHeight[0]);
v38[1] = new Vector3(0f, delHeight[1], z - delHeight[1]);
v38[2] = new Vector3(delHeight[2], delHeight[2], z - delHeight[2]);
v38[3] = new Vector3(-delHeight[3], 0f, z - delHeight[3]);
v38[4] = new Vector3(delHeight[4], 0f, z - delHeight[4]);
v38[5] = new Vector3(-delHeight[5], -delHeight[5], z - delHeight[5]);
v38[6] = new Vector3(0f, -delHeight[6], z - delHeight[6]);
v38[7] = new Vector3(delHeight[7], -delHeight[7], z - delHeight[7]);
normalMap[ix, iy] = Vector3.Zero;
for (int ii = 0; ii < 8; ii++) normalMap[ix, iy] += v38[ii] * vScale;
normalMap[ix, iy].Normalize();
}
}
// smooth
Vector3[] s = new Vector3[8];
int pas = 4;
for (int iS = 0; iS < pas; iS++)
{
for (int iy = 1; iy < outY - 2; iy++)
{
for (int ix = 1; ix < outY - 2; ix++)
{
if (normalMap[ix, iy] == Vector3.Backward) continue;
s[0] = normalMap[ix - 1, iy - 1];
s[1] = normalMap[ix, iy];
s[2] = normalMap[ix + 1, iy - 1];
s[3] = normalMap[ix - 1, iy];
s[4] = normalMap[ix + 1, iy];
s[5] = normalMap[ix - 1, iy + 1];
s[6] = normalMap[ix, iy + 1];
s[7] = normalMap[ix + 1, iy + 1];
normalMap[ix, iy] = Vector3.Zero;
for (int ii = 0; ii < s.Length; ii++)
normalMap[ix, iy] += s[ii];
normalMap[ix, iy].Normalize();
}
}
}
// color
int i = 0;
for (int iy = 0; iy < outY; iy++)
{
for (int ix = 0; ix < outX; ix++)
{
normalMap[ix, iy] *= new Vector3(16f, 16f, 1f);
normalMap[ix, iy].Normalize();
outColor[i] = new Color(new Color(normalMap[ix, iy] * 0.5f + vAddPack), heightMap[ix, iy]);
i++;
}
}
// texture
outTex.SetData(outColor);
outTex.Save(@"normalMap.tga", ImageFileFormat.Tga);
return outTex;
}

картинка normal map

результат Bump mapping'а

понедельник, 15 июня 2009 г.

Calculation normal map for “Bump mapping” & “Parallax Mapping”

Расчет normal map для“Bump mapping” и “Parallax Mapping”

Было время, когда разбирался с реализацией Bump и Parallax Mapping.
В статьях которые я находил, помнится очень раздражали высказывания типа:
"... Для расчета карт нормалей существуют специальные программы. На сайте nVidia для разработчиков есть плагин к PhotoShop'у. Все они действуют по одному и тому же принципу. В них задается карта высот, которая представляет собой черно-белое изображение, на котором более светлые пятна соответствует более высоким местам ..."(из оригинала).
Это не хорошо и не плохо. Просто нужна была конкретика.

Что нам может дать собственный расчет? Он избавит нас от необходимости включения в проект предварительно рассчитанных normal map текстур. И тем самым сократить размер дистрибутива. Так же во время расчета, проигравшись с коэффициентами, можно добиться различных результатов.

Потому, дабы сэкономить Вам время, привожу свой пример для XNA GS.

  private Texture2D GenNormalMamp(Texture2D inTex)
  {
  int x = inTex.Width;
  int y = inTex.Height;
  int l = x * y;
  Texture2D outTex = new Texture2D(graphics.GraphicsDevice, x, y);
  Color[] color = new Color[l];
  float[] height = new float[l];
  inTex.GetData(color);
  for (int i = 0; i < l; i++)
  {
  Vector3 v3 = color[i].ToVector3();
  height[i] = (v3.X + v3.Y + v3.Z) / 3;
  }
  float[] delHeight = new float[8];
  Vector3[] v38 = new Vector3[8];
  for (int iy = 0; iy < y; iy++)
  {
  for (int ix = 0; ix < x; ix++)
  {
  if (iy == 0 || iy == y - 1 || ix == 0 || ix == x - 1)
  {
  color[iy * x + ix] = new Color(0.5f, 0.5f, 1f, 1f);
  continue;
  }
  delHeight[0] = height[iy * x + ix] - height[(iy - 1) * x + ix - 1];
  delHeight[1] = height[iy * x + ix] - height[(iy - 1) * x + ix ];
  delHeight[2] = height[iy * x + ix] - height[(iy - 1) * x + ix + 1];
  delHeight[3] = height[iy * x + ix] - height[iy * x + ix - 1];
  delHeight[4] = height[iy * x + ix] - height[iy * x + ix + 1];
  delHeight[5] = height[iy * x + ix] - height[(iy + 1) * x + ix - 1];
  delHeight[6] = height[iy * x + ix] - height[(iy + 1) * x + ix ];
  delHeight[7] = height[iy * x + ix] - height[(iy + 1) * x + ix + 1];
  v38[0] = new Vector3(-delHeight[0], -delHeight[0], 1f);
  v38[1] = new Vector3(0f, -delHeight[1], 1f);
  v38[2] = new Vector3(delHeight[2], -delHeight[2], 1f);
  v38[3] = new Vector3(-delHeight[3], 0f, 1f);
  v38[4] = new Vector3(delHeight[4], 0f, 1f);
  v38[5] = new Vector3(-delHeight[5], delHeight[5], 1f);
  v38[6] = new Vector3(0f, delHeight[6], 1f);
  v38[7] = new Vector3(delHeight[7], delHeight[7], 1f);
  Vector3 vS = new Vector3();
  for (int s = 0; s < 8; s++) vS += v38[s];
  Vector3 v = vS;
  v.X = v.X * 0.5f + 0.5f;
  v.Y = v.Y * 0.5f + 0.5f;
  color[iy * x + ix] = new Color(v);
  }
  }
  outTex.SetData(color);
  return outTex;
  }


Берем стандартную текстуру из приложения к Microsoft DirectX SDK:


И получаем:

Если не поленюсь, чуть позже, приведу примеры реализации “Bump mapping” & “Parallax Mapping” в XNA.


Effect “Edge Detect”

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

Суть в следующем. Рендеринг выполняется в два прохода, потому для этого нам понадобятся два файла эффектов PositionNormal.fx и EdgeDetect.fx с различными техниками.

I. Как вы уже догадались первой техникой мы будем рисовать normal map.

Т.е. в эффекте PositionNormal.fx вместо цвета будем сохранять данные о нормали к данной точке. Но в следствии того, что каждый компонент цвета может хранить число из интервала {0, 1}, а нормали могут иметь отрицательные компоненты, то перед сохранением нормаль упакуем старым добрым способом: 
Out.Normal = mul(normal, 0.5f) + 0.5f;

1) Очищаем экран в цвет Color(0.5f, 0.5f, 0f, 1f), на самом деле это не цвет а так же упокованная нормаль (0f, 0f, -1f, 1f).

2) Рисуем сцену первый раз при помощи PositionNormal.fx
(Текст эффекта)
float4x4 matWorld : WORLD;
float4x4 matWVP : WORLDVIEWPROJECTION; 
struct VS_INPUT 
{
  float4 Position : POSITION0;
  float3 Normal : NORMAL0;
};
struct VS_OUTPUT 
{
  float4 Position : POSITION0;
  float3 Normal : COLOR0;
};
VS_OUTPUT vs_main( VS_INPUT In )
{
  VS_OUTPUT Out = ( VS_OUTPUT ) 0;
  Out.Position = mul( In.Position, matWVP );
  float3 normal = normalize(mul(In.Normal, matWorld)); 
  Out.Normal = mul(normal, 0.5f) + 0.5f;
  return Out;
}
struct PS_INPUT 
{
  float3 Normal : COLOR0;
};

float4 ps_main(PS_INPUT In) : COLOR0
{  
 return float4(In.Normal, 1.0f);
}
technique PositionNormal
{
  pass Pass_0
  {
  VertexShader = compile vs_3_0 vs_main();
  PixelShader = compile ps_3_0 ps_main();
  }
}
все просто, стоит только заострить внимание на двух строчках:
...
Out.Normal = mul(normal, 0.5f) + 0.5f;
...
return float4(In.Normal, 1.0f);
...

3) Нарисованную сцену сохраняем в текстуру

II. Анализируя полученную normal map, выводим контуры объектов сцены.

У меня есть несколько вариаций техник для этого эффекта.
Различаются они сложностью геометрии сцены и подходами сравнения нормалей соседних пикселей.
Своем пример я не стал усложнять и рассмотрел случай, когда фигура на сцене имеет гладкие грани (по всей плоскости грани, нормали имеют одинаковое значение).
Рисуем при помощи EdgeDetect.fx
(Текст эффекта)

float dx;
float dy;
texture ScreenTexture; 
sampler ScreenS = sampler_state
{
 Texture =
};
float4 PS(float2 texCoord: TEXCOORD0) : COLOR
{
 float4 normal = tex2D(ScreenS, texCoord); 
 float4 normalLeft = tex2D(ScreenS, float2(texCoord.x - dx, texCoord.y));
 float4 normalRight = tex2D(ScreenS, float2(texCoord.x + dx, texCoord.y));
 float4 normalUp = tex2D(ScreenS, float2(texCoord.x, texCoord.y - dy));
 float4 normalDown = tex2D(ScreenS, float2(texCoord.x, texCoord.y + dy));
 
 float4 normalSum = mul(normalLeft + normalRight + normalUp + normalDown, 0.25f);
 
 if(normal.x == normalSum.x && normal.y == normalSum.y && normal.z == normalSum.z)
  return normal;
 else
  return float4(1.0f, 1.0f, 1.0f ,1.0f); // цвет контура
}
technique EdgeDetect
{
 pass P0
 {
  PixelShader = compile ps_3_0 PS();
 }
}

где:
dx, dy - задают толщину линии контура.

В этом примере я не стал распаковывать сравниваемые нормали normal и normalSum.
Тут этого не требовалось, и мы не будем перегружать пиксельный шейдер.
Распаковка типа:
normal = (normal - 0.5f) * 2.0f;
Может потребоваться тогда, когда Вам нужно будет анализировать normal map с отображенными объектами, поверхности которых изогнуты.

В примере кроме контура объекта выводятся цветные грани, не пугайтесь, это для наглядности я отобразил нормали.

Скачать пример можно тут (для скачивания требуется регистрация на xnadev.ru). 

На реализацию эффекта вдохновило демо, группы ASD.