пятница, 18 сентября 2009 г.

четверг, 10 сентября 2009 г.

С#+XNA. В погоне за fps. I

Краткое содержание:

- Введение;
- Часть 1-я. Новичкам;
- Часть 2-я. Та же песня с Microsoft'ом;
- Часть 3-я философская. Охотник или жертва;
- Заключение.


Введение.

Данная статья является попыткой посмотреть в корень проблемы. Проблемы достижения высоких показателей fps с точки зрения построения C# кода не касаясь при этом XNA раздела 3D, относящегося к непосредственным инструкциям видеокарте.


Часть 1-я. Новичкам.

Мои наблюдения основаны на постоянном присутствии в различных форумах и участии в различных проектах. Соль рассматриваемого мною вопроса заключается в использовании Fields и Properties членов классов и структур.
Для полноты ощущений, в моих изысканиях, меня интересовали следующие ссылочные типы:
- class;
- interface;
- object.
Сейчас мы просмотрим на время выполнения кода при различной организации доступа к членам класса.
Пример:


using System;
namespace TestObjectInterface
{
class Program
{
static Unit[] unit;
static object[] unitobj;
static iUnit[] uniti;

static int count;

static void Main(string[] args)
{
count = 10000000;
unit = new Unit[count];
unitobj = new object[count];
uniti = new iUnit[count];
Init();
double tf = TestFields();
double tp = TestProperties();
double tfo = TestFieldsObject();
double tpo = TestPropertiesObject();
double tpi = TestPropertiesInterface();
Console.WriteLine("testFields = "
+ tf.ToString() + " mc, k = 1");
Console.WriteLine("testProperties = "
+ tp.ToString() + " mc, k = "
+ (tp / tf).ToString());
Console.WriteLine("testFieldsObject = "
+ tfo.ToString() + " mc, k = "
+ (tfo / tf).ToString());
Console.WriteLine("testPropertiesObject = "
+ tpo.ToString() + " mc, k = "
+ (tpo / tf).ToString());
Console.WriteLine("testPropertiesInterface = "
+ tpi.ToString() + " mc, k = "
+ (tpi / tf).ToString());
//Console.ReadKey();
}

static void Init()
{
for (int i = 0; i < count; i++)
{
unit[i] = new Unit(i, i * 2);
uniti[i] = (iUnit)unit[i];
unitobj[i] = (object)unit[i];
}
}

static double TestFields()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = unit[i].x + unit[i].y; // 0
n = unit[i].x + unit[i].y; // 1
n = unit[i].x + unit[i].y; // 2
n = unit[i].x + unit[i].y; // 3
n = unit[i].x + unit[i].y; // 4
n = unit[i].x + unit[i].y; // 5
n = unit[i].x + unit[i].y; // 6
n = unit[i].x + unit[i].y; // 7
n = unit[i].x + unit[i].y; // 8
n = unit[i].x + unit[i].y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestProperties()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = unit[i].X + unit[i].Y; // 0
n = unit[i].X + unit[i].Y; // 1
n = unit[i].X + unit[i].Y; // 2
n = unit[i].X + unit[i].Y; // 3
n = unit[i].X + unit[i].Y; // 4
n = unit[i].X + unit[i].Y; // 5
n = unit[i].X + unit[i].Y; // 6
n = unit[i].X + unit[i].Y; // 7
n = unit[i].X + unit[i].Y; // 8
n = unit[i].X + unit[i].Y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestFieldsObject()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 0
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 1
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 2
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 3
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 4
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 5
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 6
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 7
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 8
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestPropertiesObject()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 0
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 1
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 2
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 3
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 4
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 5
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 6
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 7
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 8
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestPropertiesInterface()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = uniti[i].iX + uniti[i].iY; // 0
n = uniti[i].iX + uniti[i].iY; // 1
n = uniti[i].iX + uniti[i].iY; // 2
n = uniti[i].iX + uniti[i].iY; // 3
n = uniti[i].iX + uniti[i].iY; // 4
n = uniti[i].iX + uniti[i].iY; // 5
n = uniti[i].iX + uniti[i].iY; // 6
n = uniti[i].iX + uniti[i].iY; // 7
n = uniti[i].iX + uniti[i].iY; // 8
n = uniti[i].iX + uniti[i].iY; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}
}

interface iUnit
{
int iX {get;}
int iY {get;}
}

public class Unit : iUnit
{
public int x;
public int X { get { return x; } }
int iUnit.iX { get { return x; } }

public int y;
public int Y { get { return y; } }
int iUnit.iY { get { return y; } }

public Unit(int inX, int inY)
{
x = inX;
y = inY;
}
}
}

Я не буду уделять внимание коду, пример достаточно детский. Скажу только то, что по 10 инструкций в каждом цикле сделано для контрастного выделения времени выполнения тела цикла, от самой инструкции цикла.
У меня программа отработала следующим образом:


На мой взгляд все достаточно наглядно. Самый скоростной способ это естественно обращение к полю класса на прямую.
А теперь поясню к чему рассказал и продемонстрировал прописную истину. Всем так же известно, что в разработке 3D, в отличие от бизнес, приложений особенно ярко выражена погоня за скоростью. Но тем не менее, специалисты начинающие(!) работать с XNA, уже как правило, достаточно уверенно себя ощущают в области C# разработки. Вот в этом то и заключается основной казус. Который объясняется тем фактом, что вся справочная и обучающая литература пестрит примерами описывающими доступ к полям классов через свойства. В следствии чего ребята начинают автоматически(!) применять данный подход во всех случаях. И там где это действительно необходимо, и где не очень. Да это хороший тон(!). Удобно перехватывать обращения к полю класса, обрабатывать возникающие при этом исключительные ситуации. Но не подходит для применения в 3D(!!!).
Не всегда можно быстро выдать столько информации по данному вопросу очередному собеседнику. Да и сейчас я описал вопрос не за пять минут, но в дальнейшем могу с легкостью ссылаться на себя :).


Часть 2-я. Та же песня с Microsoft'ом.

Не для кого не секрет, что я люблю поковырять рефлектором все, что меня хотя бы мало-мальски интересует. Добавлю то, что порой это приносит больше информации, чем например чтение непосредственно MSDN. Поэтому в довесок к первой части я решил привести один(!) пример из библиотеки XNA.
Рассмотрим XNA 3.1, виндовую библиотеку Microsoft.Xna.Framework.dll,
одноименный namespace, и пусть подопытным будет структура Vector3.
Вот часть кода данной структуры, в рамках которой мы сейчас и пообщаемся:


public struct Vector3
{
public float X;
public float Y;
public float Z;
...
private static Vector3 _one;
public static Vector3 One
{
get
{
return _one;
}
}
...
public Vector3(float x, float y, float z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
...
static Vector3()
{
...
_one = new Vector3(1f, 1f, 1f);
...
}
}

К полям X, Y и Z вопросов нет. Но обращаю ваше внимание на свойство One. По логике One возвращает единичный вектор постоянного значения. А вот тут по подробнее. Разум цепляется за формулировку "постоянное значение". Читаем MSDN и выясняем, что переменная или поле с постоянным значением может быть объявлена при помощи двух ключевых слов модификаторов доступа, которыми являются const и readonly. На всякий случай поясню формулировкой, переменная или поле постоянного значения бывает двух видов:
- const -> применяется при объявления полей и локальных переменных, постоянное значение присваивается на этапе компиляции, исключительно(!) при объявлении;
- readonly -> применяется при объявления полей, значение может присваивается как при объявлении, так и в конструкторе данного класса или структуры.
Но речь идет о поле постоянного значения которое не возможно рассчитать при компиляции, что накладывает ряд ограничений и в соответствии с рассматриваемым нами случаем, расширим формулировки:
- Модификатор static не допускается в объявлении константы;
- значение константы должно быть полностью вычислено во время компиляции;
- Единственными возможными значениями для констант ссылочных типов являются string и null;
Ясное дело на static ставим крест, он однозначно не подходит. А вот readonly наш размерчик! И как нельзя лучше вписывается в нашу ситуацию. Потому, заранее уже зная куда нас это приведет, предлагаю по тестировать и сравнить время чтения значения из свойства стандартной структуры с чтением значения из иначе построенной структуры.
Смотрим пример:


using System;
using Microsoft.Xna.Framework;
namespace TestFields
{
class Program
{

static int count;

static void Main(string[] args)
{
count = 10000000;
Console.WriteLine("testStandartVector3 = " +
TestStandartVector3().ToString() +
" mc");
Console.WriteLine("testMyVector3 = " +
TestMyVector3().ToString() +
" mc");
}

static double TestStandartVector3()
{
DateTime dtStart = DateTime.Now;
Vector3 vec;
for (int i = 0; i < count; i++)
{
vec = Vector3.One; // 0
vec = Vector3.One; // 1
vec = Vector3.One; // 2
vec = Vector3.One; // 3
vec = Vector3.One; // 4
vec = Vector3.One; // 5
vec = Vector3.One; // 6
vec = Vector3.One; // 7
vec = Vector3.One; // 8
vec = Vector3.One; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestMyVector3()
{
DateTime dtStart = DateTime.Now;
gVector3 vec;
for (int i = 0; i < count; i++)
{
vec = gVector3.One; // 0
vec = gVector3.One; // 1
vec = gVector3.One; // 2
vec = gVector3.One; // 3
vec = gVector3.One; // 4
vec = gVector3.One; // 5
vec = gVector3.One; // 6
vec = gVector3.One; // 7
vec = gVector3.One; // 8
vec = gVector3.One; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}
}

public struct gVector3
{
public float X;
public float Y;
public float Z;

public static readonly gVector3 One;

public gVector3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}

static gVector3()
{
One = new gVector3(1, 1, 1);
}
}
}

Получаем вот такой вот результат:


Внимание вопрос(!): какая была необходимость реализовывать "постоянное значение" через свойство возвращающее значение приватного поля, если в сравнительном отношении скорости, с реализацией через модификатор доступа readonly, составляет разницу практически в(!) два раза? Мне не понятно, а вам? Буду очень признателен тому человеку, который вежливо мне объяснит.
Товарищам, которые планируют достаточно часто применять постоянные вектора:
- Zero;
- One;
- UnitX;
- UnitY;
- UnitZ;
- Up;
- Down;
- Right;
- Left;
- Forward;
- Backward.
Рекомендую не использовать стандартные, а реализовать самостоятельно, как в выше приведенном примере.
Мысль которую я хотел донести, звучит следующим образом:
- изучайте все средства, которые собираетесь применять в своих проектах;
- в результате изучения берите только лучшее и делайте наконец то сказку былью;
- не знание законов не освобождает от ответственности. Это про специалистов, которые по своей беспечности, не разбираясь, применяют все подряд, а в последствии сетуют на Microsoft. Хотя это говорит с плохой стороны не(!) о Microsoft.
- допуская то, что не смотря на "много букв", у читателей данной статьи мнения разойдутся. Я добавил третью часть. Читать рекомендую всем, только одним принять к сведению, а иным просто улыбнуться.


Часть 3-я философская. Охотник или жертва?

Жертвам своей не компетентности, которые любят плохо отзываться о Microsoft, посвящается.
Что, из себя, представляет понятие мода? Мода - в широком смысле этого слова, определяет стремление, того или иного человека, быть похожим на основное большинство. Хм, так то оно так, когда в меру, без фанатизма и крайностей. А так же не идет в разрез с законами эволюции, которые не зависимо от нас хранят приоритетное право "сильных" и "особенно удачных" особей выживать. К примеру обратная крайность это не формальные движения в нашем обществе.
Применяем выше описанное отступление к нашей основной теме. И так у нас есть три основных типа людей. Систематизируем их и результат оформим в виде структуры:
- ярко выражено не(!) довольные Microsoft'ом -> модные "жертвы". Да, уже более десяти лет считается модно и наверное круто так себя вести;
- фанаты Microsoft'а -> назовем их "панки", так веселее смотрится не формальное определение. Обычно эти люди либо являются специалистами, но узкого профиля, либо совсем не являются таковыми. Если кого то обидел, сообщите мне. Я подберу более мягкое определение.
- специалисты -> "охотники". Ни кому не покланяются, ни кого не ругают. Им некогда отвлекаться на разную чушь. Эти люди берут от жизни только лучшее, прекрасно понимая при этом как и где это лучшее применять, и как и где не стоит. Они же становятся MCP, MVP, "черными поясами" Intel, … , и другими почетными товарищами.


Заключение.

Ну вот и все. Всем перца насыпал. Развеялся. Можно дальше "охотиться" ... оговорился :) ... работать и точить свое мастерство.

суббота, 29 августа 2009 г.

XNA: Water video

Рекомендую смотреть в режиме HD 1024x768



В данном исполнении нет каустики. Но сейчас я заинтересовался этой темой, нагреб кучу инфы, изучаю ... пробую.
Пока мои изыскания выглядят так:



Т.е. простое вычисление в один (сильно тяжелые вычисления для real time) проход на CPU.

Пока делал первое видео, в глаза бросилась вот какая штука. Изначально видео с водичкой весило 2,092 ГБ, конвертировал при помощи Camtasia Studio и как то сильно быстро изменялся прогресс конвертирования. Посмотрел загрузку процессоров ... и о чудо.



Camtasia Studio умеет распределять вычисления на все доступные процессоры.
Конвертация прошла в real time режиме :). Очень порадовало!

понедельник, 24 августа 2009 г.

понедельник, 10 августа 2009 г.

Windows 7 RTM Now Available for MSDN and TechNet Plus Subscribers

Windows 7 RTM стала доступна для подписчиков MSDN и TECHNET+





7 августа, для счастливых обладателей подписок MSDN Subscription и TECHNET Subscription стала доступна для скачивания Windows 7 RTM.

несколько строк из приглашения:
Dear ...
Как TechNet Плюс подписчик, Вы имеете преимущество того, чтобы быть среди первых в мире имеющих доступ к Windows 7 RTM.
Загрузите свою копию Windows 7 RTM и начните оценивать сегодня!
...
Windows Home Premium это - большой выбор для дома и для бизнеса.
...
Посетите Windows Springboard, полную последних ресурсов для Windows 7 включая ресурсы для прикладной совместимости, организации сети, работы и совместимости аппаратных средств, и много другого.


Мне крайне повезло иметь обе подписки. Очень рекомендую всем. Любая из этих подписок очень полезная штука.

Немного поясню для тех кто не знает что это.
-Во первых, платно подписаться может любой.
-Во вторых подписку можно получить в награду при определенных стечениях обстоятельств. Мне известно пока только об одной подобной возможности, т.к. сам получил обе подписки как бонус к награде MVP (Microsoft MVP, Кто такие MVP?, Кто такие RD и MVP?).
(пока писал, вспомнил о еще одной из без платных возможностей)
- программа для бизнеса BizSpark. Описание программы BizSpark, описание от Softline (партнера по сообществу BizSpark)


Что же такого интересного в этих подписках?
Коротко вкусности выглядят так:
-Обновленные продукты (6)
-Business Solutions (25)
-Библиотека MSDN (11)
-Операционные системы (29)
-Приложения (57)
-Серверы (65)
-Средства для разработчиков (42)
-Средства и ресурсы (105)
-Средства проектирования (4)

Там все, что вам нужно для счастья. Ко всему ПО (которое требует) там же можно получить ключи, скачать и оценить в полной мере. Под словами «в полной мере» я имею в виду установить, официально зарегистрировать, получить все обновления и в результате оценить продукт по достоинству в настоящем виде. Фишка не для дилетантов, а для людей ценящих свое время, силы и деньги. Переиначу известную поговорку - «Мы не на столько богаты, что бы тратить деньги на дешевые пиратские подделки».

воскресенье, 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'а