суббота, 31 октября 2009 г.

Column-major vs Row-major

Теперь жалею, что когда-то решил использовать column-major матрицы в своей математической библиотеке. Во-первых, теперь приходится транспонировать их при передаче в D3D-шейдер, чтобы умножение вектора на матрицу можно было записывать как mul(v, M) (Direct3D), а не mul(M, v) (OpenGL). В первом случае это ложится на четыре красивые dp4 инcтрукции, во-втором - на комбинацию mul/mad. Во-вторых, для column-major матриц приходится записывать перемножение по правилу "post-multiplying":

WorldViewProj = Proj * View * World
HPos = WorldViewProj * Pos

Я не араб, и меня этот способ начал со временем напрягать. Придётся переписывать всю матричную библиотеку - ошибки молодости :( Думаю через шаблоны сделать возможность указать, какая именно матрица собирается использоваться - row-major или column-major, и если идёт присваивание матриц с разным порядком записи, чтобы осуществлялось автоматическое траспонирование:

Mat4< column_major > m1 = GetWorldViewProj();
Mat4< row_major > m2( m1 ); // transpose

среда, 28 октября 2009 г.

Ray-BV Intersection

Сегодня весь день обдумывал, в какую сторону двигаться дальше.

[Здесь была неверная инфа :)].
Полдня лазания по Гуглу и отладки кода, и в шейдеры легли максимально эффективные функции, тестирующие пересечения. Пересечение с боксом просто как две копейки. А люди выдумывают какие-то извраты через координаты Плюкера...

Тест на пересечение со сферой - 6 слотов инструкций:



Тест на пересечение с эллипсом - 14 инcтрукций:



Branchless тест на пересечение с AABB - 14 инcтрукций (включая одно деление):



При этом мне не нужно само пересечение, а только булево значение, а значит, нет необходимости в квадратном корне.

вторник, 27 октября 2009 г.

HD 2400 is slow

В общем провёл я предварительные тесты на производительность. Конкретно для Radeon HD 2400 результаты неутешительные - скорость падает приблизительно линейно с возрастанием кол-ва треугольников... Если для квада из двух треугольников получается ~100 fps, то для кубика из 12 треугольников - 20 fps. Для двух кубиков - 11... Скорость мало зависит от площади кадра, которую покрывают кубики, и от типа кушаемых данных - вещественные или байты, хотя при вещественных скорость снижается.

Понятно, откуда линейное падение скорости - от линейного увеличения длины цикла. Я не считал точно, но на глазок длина цикла ~60 инструкций (весь шейдер - 120). 24 треугольника требуют выполнить ~1440 + 60 = ~1500 инструкций. Боттлнек в недостатке вычислительной способности при тупом переборе циклом. Можно взять high-end видеокарту, но это только отодвинет верхнюю границу, после которой симптомы будут аналогичны.

В принципе результаты не так уж плохи (в расчёте на high-end), таким способом можно рассчитывать на возможность играться с несколькими десятками треугольников (мне нужно даже меньше). К тому же на HD 2400 динамический бранчинг нифига не фурычит, а только просаживает скорость :( Хотя очевидно, что он должен давать прирост, например, при cull-инге back-faced треугольников. На нормальном железе с нормальным бранчингом должен быть заметный выигрыш.

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

Вот как-то так. Но всё же я надеялся на чуть лучшие результаты.

PS. Ну, а что можно сделать с несколькими треугольниками? Поверьте, в умелых руках - очень многое :)

PPS. Вот перед сном думал, как можно ускорить.
1) Я делаю рэйкастинг для всех пикселей фреймбуфера, очевидно что это худший случай. Рэйкастинг - это потому что простейшая реализация. Для рэйтрейсинга легче. Если у нас есть рефрактор, не занимающий всю площадь экрана, то растеризатором сначала рисуем front-faced треугольники, а потом выполняем рэйтрейсинг.
2) Early stencil rejection. Делаем рэйтрейсинг в малю-ю-юсенький render target, разблуриваем получившуюся маску, "натягиваем" её на фреймбуфер и помечаем в стенсиле. Затем делаем нормальный рэйтрейсинг, стенсил тест отбрасывает ненужные фрагменты вне маски.
3) Почему-то подумалось об oct-tree со сферами, описывающими кубики-узлы. Тест на пересечение со сферой тривиален.

понедельник, 26 октября 2009 г.

Raycaster vs Rasterizer

It has begun... Первая реализация рэйкастера на DirectX 11!
Два текстурированных треугольника, диффуз + спекуляр, Radeon HD 2400, окно 1024x768, без мультисэмплинга.

Растеризатор, FPS ~= 390:


Рэйкастинг, FPS ~= 114:


Растеризатор, FPS ~= 490:


Рэйкастер, FPS ~= 123:



Два (нет, три) отличия этого метода от растеризатора - нельзя напрямую использовать MIP-фильтрацию и не работает мультисэмплинг. Ну и скорость :) Шейдер для рэйкастинга + шейдинга занимает сейчас около 100 инструкций. Я ещё пооптимизирую что можно, потом буду мерять, во что это выливается.

Дальше - сжатие данных и тесты. Посмотрим, сколько треугольников удастся отобразить, прежде чем FPS на моём лоу-энде упадёт до отметки 20.

How to pack normal into just 2 bytes

Необходимость в жёсткой оптимизации по чтению из памяти привели меня к упаковке предрасчитанных данных и вершин. Хотел было написать, как мне удалось реализовать упаковку нормали в unorm2 с потерей одного бита точности для хранения z sign. Но, порыскав по гуглу, нашёл великолепный анализ методов сжатия нормалей от Aras Pranckevičius (я и не думал, что их столько, видать deferred техники здорово включают изобреталку:):

Compact Normal Storage for small G-Buffers

Все представленные алгоритмы упаковывают нормаль в unorm2 и восстанавливают потерянную информацию, используя квадратный корень. Метод простой упаковки xy с последующей реконструкцией z оказался наименее качественным (зато наиболее быстрым). Видимо из-за dot(n.xy, n.xy), который усиливает ошибку квантования (а для world нормалей придётся пожертвовать ещё одним битом точности). Любопытый обзор, вероятно лучший в Сети - для поклонников deferred shading-a будет весьма ценнен. Я же собираюсь использовать наиболее быстрый вариант, т. к. у меня нормали будут интерполироваться по треугольнику, и каждую надо распаковать... Сойдёт и удовлетворительное качество, главное - минимум чтений из памяти.

четверг, 22 октября 2009 г.

Woop's unit triangle intersection test

После курения пейпров и вникания в кудовские кернелы реализовал и этот алгоритм. Математика - это вещь! Диву даёшься, как только получается реализовать полную проверку такого пересечения в несколько инструкций.

Кстати я не знаю точно, кто именно автор алгоритма. S. Woop указан в качестве соавтора пейпра, в котором рассматривается математика алгоритма, это правда. Но там же идут отсылки к более ранней работе J. Arenberg 1988 года, и говорится что это просто его расширенная версия. Но везде (презентации, на форумах) упоминается именно Woop.

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

Пока я фигачил тестовые реализации для этого тормоза CPU, в голове крутились мысли, как это должно быть реализовано на GPU класса DX10/11. Во-первых, зачем таскать текстурные координаты (а в перспективе - и нормали) вместе c precomputed данными? Ведь для поиска самого пересечения они не нужны, а требуются лишь тогда, когда найдено пересечение с треугольником и его индекс известен. Нужно отделить данные для интерполяции от precomputed данных для поиска пересечения, разнести их по двум буферам. Тогда буфер precomputed данных ужмётся до минимума, а значит, и фетчинг данных при линейном поиске по массиву пойдёт быстрее. А когда треугольник найден, то по индексу делаем выборку атрибутов вершин из второго буфера. Хотя тут и есть скачок по памяти, но он всего один на пиксель.

Тогда структура для алгоритма Мюллера должна быть такой:
struct prec_tri
{
float3 v0;
float3 e0;
float3 e1;
};

Тут к сожалению, в cbuffer пропадает впустую 3 вещественных, а это 12 байт из 48. Один из вариантов - использовать Buffer < float3 > и Load(). Для алгоритма Вупа достаточно матрицы 4x3 (четыре столбца по 3 элемента - поворот и перенос). Это те же 48 байт. А вот если мне нужна позиция в точке пересечения (для L, V), то не обязательно тащить вершины - можно попробовать найти по O + t * D, хотя неясно что будет с точностью и визуальным качеством. Но это так, мысли наперёд.

А вот вопрос, как именно фетчить: через cbuffer, tbuffer или Load() - очень интересен. Судя по описаниям в SDK, tbuffer оптимизирован для случайного доступа, cbuffer - для последовательного. Может оказаться, что это "шило на мыло". Можно упереться не в память, а в расчёты, kd-tree то не планирую, цикл соответственно будет только расти с ростом кол-ва треугольников. Да и моя HD 2400 очень тормозная железяка, грех на ней рэйтресингом баловаться, а может заниматься оптимизациями - в самый раз? :). Думаю, не купить ли на последние деньги HD 4770 или 4850, в районе 100$ сейчас...

Кстати, привет от компайлера шейдеров! Что-то вроде такого он пережёвывает секунд 10:

#define MAX_TRIANGLES 200

[loop]
for (int i = 0; i less than MAX_TRIANGLES; ++i)
{
prec_tri t = g_buf[ i ];
// Дальше куча расчётов.
}

Если взять, скажем, 500 - думает секунд 40. Ассемблерный листинг показывает, что цикл не разворачивается. Может компайлер проверяет валидность i для всех итераций цикла? Пробовал разные флаги подсовывать, убирать оптимизацию - бесполезно. Единственное, что помогло - замена compile-time MAX_TRIANGLES на значение из буфера констант. Тормоза пропадают. Какой из этого вывод? Незнание - это блаженство (с) The Matrix.

вторник, 20 октября 2009 г.

GPU Ray-Triangle Intersection

Общепризнанным стандартом здесь является алгоритм Мюллера-Трумбора:

Fast, Minimum Storage Ray/Triangle Intersection.

Написан давно (1997 г), но хорош и подходит для GPU. Принцип работы - поиск барицентрических координат пересечения u и v, имея которые, можно легко найти точку пересечения или её текстурные координаты:
w = 1 - u - v
tc = tc0 * w + tc1 * u + tc2 * v
В CPU версии на вход подаются начало луча, его направление и три вершины треугольника. На выходе - скаляр t на луче (наименьший означает ближайший треугольник), и uv. После этого, проверив (t < min_t), надо найти собственно сами текстурные координаты точки пересечения, для этого нужны текстурные координаты вершин треугольника.

Одними из факторов, серьёзно ударяющих по производительности RTI, являются фетчинг данных для треугольника и загруженность регистров (регистрового файла GPU):

Основная трудность трассировки лучей на GPU

И хотя это становится проблемой при больших массивах трассируемых данных, это своего рода указатель и для ограниченного случая. Что касается фетчинга данных, то статические треугольники должны браться из буфера констант (идём тупым перебором по всему массиву, упаси господи юзать какие-то kd-tree). В базовом варианте нам нужно:
struct tri
{
float3 v0;
float3 v1
float3 v2;
float2 tc0;
float2 tc1;
float2 tc2;
};
Для буферов констант важен alignment, поэтому предъявляется требование к выравниванию вектора по границе, кратной 16 байт (float4) (NOTE: Более точно, данные вектора не должны пересекать границу, кратную 16 байтам). Поэтому размер структуры - sizeof(float4) * 5 = 80 байт (два float2 группируются в один float4). Много, хотя хорошо укладываемся в 16-byte alignment. Попробуем сжать.

Как видно из кода алгоритма (ссылку приводил в начале), из трёх вершин треугольника используется только один, остальные нужны для нахождения векторов рёбер треугольника:
edge1 = v1 - v0
edge2 = v2 - v0
В структуру вместо вершин! Заодно осводождаем два слота инструкций. Третьи текстурные координаты распихиваем по w компонентам векторов рёбер. Итого получаем:
struct prec_tri
{
float3 v0;
float4 e0;
float4 e1;
float4 tc0tc1;
};
Укладываемся в 64 байта.

В героической попытке придумать алгоритм покороче и избавиться от поиска барицентрических координат, я решил искать текстурные координаты через матрицу трансформации. Представим, что e0 и e1 формируют не ортонормированный базис в object space, тогда:
t0 = tc1 - tc0
t1 = tc2 - tc0
формируют базис в пространстве текстуры. Третий орт не нужен, т. к. текстурная плоскость совпадает с плоскостью треугольника. Тогда:
Muv = Mobj * Mx
откуда:
Mx = Mobj^-1 * Muv
(запись для row-major матриц). Из-за того, что наш треугольник представлен в object space, нужно переводить точку пересечения в локальные координаты треугольника, а потом переводить в локальные текстурные координаты:
tc = (p - v0).xy * Mx + tc0
Достаточно оперировать матрицами второго порядка, т. к. фактически из всех афинных преобразований используются поворот и масштабирование на плоскости. Но это не всё. Т. к. оси Oy и Ot в Direct3D направлены в разные стороны, нужна матрица преобразования координаты t:
t = t * (-1) + 1
Кроме того, можно не хранить текстурные tc0, а записать их как translation в матрицу 2x3.
Итого:
Muv * Mflip = Mobj * Mx
Mx = Mobj^-1 * Muv * Mflip
tc = ((p - v0).xy, 1) * Mx
Естественно, что матрица предрассчитывается и заливается в буфер констант. Матрица 2x3 займёт 2 * 4 * 4 = 32 байта (NOTE: Впрочем, матрица 2x2 укладывается во float4, и если tc0 дополнить какими-то полезными данными до float4, матрицу 2x3 использовать нет смысла). Я протестировал этот метод в случае, если известны координаты пересечения луча с треугольником - работает отлично. Единственное, что ускользнуло от меня - вместо матрицы флипа t = t * (-1) + 1 удаётся использовать t = t * (-1). Загадка :)

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

Одной из самых перспективных альтернатив алгоритму Мюллера является Woop’s unit triangle test. Вот что удалось накопать по этой теме:

Realtime Ray Tracing of Dynamic Scenes on an FPGA Chip
RPU: A Programmable Ray Processing Unit for Realtime Ray Tracing
Пост whiteambit на ompf.org

Сейчас копаю эту тему. По идее, в unit space отпадает необходимость в (p - v0). Не знаю, что получится, но поиск текстурных координат пересечения по двум матрицам - это жёстко :)

Hybrid approach for refractions

Я представляю себе рендеринг преломлений как комбинацию binary search/cubemap sample и ray-tri intersection. Пока остальные товарищи с ompf.org всеми правдами и неправдами выжимают из ПЕЧи все соки (вместо того чтобы подождать декаду, пока не появится соотв. железо), мы пойдём другим путём. Для сложной геометрии, которая должна искажаться, я планирую применять binary search z-буфера. Но желателен и вариант с пересечением луча и треугольника, там где треугольников мало. Например, дисплей in-game компьютера можно представить всего двумя текстурированными треугольниками (или чуть побольше, чтобы представить объём и т. д.). Или другую мелочь... Вообще, пока я не хочу раскрывать, какие сцены у меня крутятся в голове, это секрет :). Но уже сейчас понятно, что ограниченный RTI нужно реализовывать, z-буфер слишком негибок для качественных преломлений простой геометрии.

четверг, 15 октября 2009 г.

How antialiasing works. Part 4.

Ранее я писал про антиалиасинг на GPU:

Part 1
Part 2
Part 3

Но тема охвачена не полностью, ещё есть что изучать. По мере сил я и дальше буду выкладывать в блоге что-нибудь интересное по ней. Тогда я оставил невыясненным вопрос, что происходит, если фрагментный шейдер пишет в SV_Depth (gl_FragDepth в OpenGL, регистр oDepth в asm). У меня самого до сих пор не стояла задача изменять глубину вручную.

Если мы не пишем в depth из шейдера, то это значение рассчитывается fixed-function pipeline интерполяцией по треугольнику. В случае MSAA, каждый сэмпл фрагмента получает своё индивидуальное значение глубины, и тест глубины проводится отдельно для каждого сэмпла. Фрагментный шейдер исполняется для фрагмента только один раз, и одинаковые значения цвета записываются только в те сэмплы, который прошли индивидуальный depth test. Благодаря fine-grained depth test, в местах пересечения геометрии получается сглаженная лесенка:
Если шейдер пишет в SV_Depth, то посколько он исполняется только один раз, то и значение глубины (как и цвета) получается одинаковым для всех сэмплов. И хотя depth test продолжает выполняться на уровне сэмплов, отличия проявляются только на фрагментном уровне. Эффект сглаживания, конечно, пропадает:
Direct3D 10.1 вводит такую возможность как per-sample fragment shader evaluation, подобную функциональность предоставляет и расширение GL_ARB_sample_shading (если с расширением всё ясно, то в Direct3D пока не нашёл, как включать). Данная возможность может быть использована при рендеринге растительности с альфа-тестом. Вероятно, можно решить и приведённую выше проблему, т. к. теперь для каждого сэмпла можно записывать (или нет) depth, рассчитанный интерполятором до шейдера - но я не пробовал (железа под рукой нет :().

среда, 14 октября 2009 г.

Conservative Binary Search

Реализовал поиск пересечения с backfaced поверхностью через двоичный поиск.

Можно задавать два критерия глубины поиска: максимальное кол-во делений на луче и дельту расстояния до поверхности от текущей позиции поиска. В 3D-случае можно дополнительно запоминать предыдущие текстурные координаты сэмпла и сравнивать их с новыми, если они лежат в пределах одного пикселя - дальше делить нет смысла. Для более точных и быстрых результатов желательно, чтобы исходный диапазон поиска был как можно меньшим. Для простоты я брал луч длиной чуть больше диаметра линзы. Скриншоты:

Глубина поиска - 5 шагов:


Видно, что найденные точки "пляшут" вокруг поверхности. При движении линзы неточности поиска видны ещё более явно.

7 шагов:Уже лучше, но при движении заметно "дрожание" точек.

12 шагов:

Начиная с 10 шагов, поиск становится почти точным. Нужно понимать, что кол-во шагов для каждого луча - это кол-во выборок из depth-текстуры для каждого обрабатываемого фрагмента в 3D-случае. Большое кол-во выборок на пиксель создаёт большую нагрузку при увеличении разрешения, поэтому важно делать их как можно меньше. Можно применять комбинацию последовательного поиска с последующим уточнением двоичным, но при малых величинах (~10) выигрыш сомнителен. В случае с линзой проще за начало луча брать не его пересечение с лицевой поверхностью, а позицию на преломлённом луче, отодвинутую от начала на 1/2-1/3 диаметра линзы - можно сэкономить пару шагов при двоичном поиске.

Как бы то ни было, основная задача - попиксельная точность пересечения луча с backfaced геометрией. Вообще, как я считаю, в задаче real-time refraction главным является предельная точность и аккуратная обработка результатов, внимание к мелочам, что поможет создать правдоподобную иллюзию объёма и искажения, а не очередную поделку "на шейдерах".

People Can Fly

People Can Fly

Помню финальную серию 1998 года: Chicago Bulls - Utah Juzz. Был настоящий драйв, и Бог баскетбола выделывал иногда такое...

воскресенье, 11 октября 2009 г.

Fresnel Reflection

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

Кто занимается графикой, знает, что в эпоху Shader Model 2.0 были очень популярны различные аппроксимации уравнений Френеля, например, аппроксимация Шлика. Если поискать, можно легко их найти в "старых" шейдерах, реализующих там воду или стекло. Не знаю, как в Крайзисе считается, но в эпоху Shader Model 4.0/5.0 уже пора завязывать с этими аппроксимациями и считать подобные вещи честно. На самом деле вычислений здесь - кот наплакал. Если подразумевается использование преломлённого луча, то это значит что у нас уже есть косинусы углов падения и преломления. Придётся сделать всего семь умножений, два деления, плюс пять арифметических операций сложения/вычитания, чтобы получить коэффициент для неполяризованного света. Если преломления не считаются, а есть только угол падения, придётся использовать вариант с трансцендентными функциями. В этом случае формулу лучше запечь в текстуру и делать в шейдере выборку. Впрочем, первый вариант тоже является кандидатом на запекание в текстуру: cами по себе расчёты не сложны и не займут много инструкций (по нынешним меркам), но по моим предварительным оценкам, сложный шейдер, реализующий правдоподобный эффект преломления, выльется этак в 400-500 инструкций. В этих условиях целесообразнее сделать дополнительную выборку из текстуры, заодно и соотношение ALU/TEX будет выполняться получше.

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

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

Немного увеличим угол, эффект Френеля всё ещё не заметен:

При значительных углах видно, что отражённый луч получает все большую долю энергии падающего луча:

Почти предельный случай: почти вся энергия луча вкладывается в отражения (первое и второе).

А вот графики функций для стекла (по оси абсцисс - угол падения, по оси ординат - значение коэффициента). Для s-polarized (синий цвет) и p-polarized (красный):



А вот усреднённое значение для неполяризованного света:


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

Думаю, с Френелем всё понятно (для CG-случая, физическая сторона вопроса очень глубока).

P. S. Реализовал трассировку луча для линзы, наполненной водой. Вот как это выглядит:

Хорошо видны две "мертвые зоны" по бокам обзора. Также хорошо видно, как странно ведёт себя луч рядом с этими зонами. Поэкспериментируйте - налейте в стеклянную бутыль воды и посмотрите, как искажается изображение на мониторе. А потом сравните с картинкой. Я сравнивал - всё правильно :)

Math terms

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

Часто можно встретить названия переменных вроде amb, что означает "a minus b". Между тем, немногие знают, что существует достаточно богатая, исторически сложившаяся терминология, применяемая к компонентам математических выражений. Простейший пример, известный всем: числитель (numerator) и знаменатель (denominator). Но есть и термины, известные только узкому кругу математиков.

Вот я нашёл прекрасный толковый словарь математических терминов, составленный Pat Ballew:

MATH WORDS, AND SOME OTHER WORDS, OF INTEREST

А вот список терминов, которые я "накопал" - они могут (теоретически) применяться в обозначении того или иного term'а формулы:

Addend
Subtrahend
Minuend
Residue
Multiplicand
Multiplier
Product
Mean
Magnitude
Divident (Numerator)
Divisor (Denominator)

В словаре толкуется историческое происхождение этих и многих других математических терминов, в основном это корни древних латыни и греческого языков. Думаю, многим будет интересно узнать происхождение слова "compute" :)

среда, 7 октября 2009 г.

Research In Refraction

Ранее я уже писал, что хочу заняться преломлениями и отражениями. Поэтому я начал собственный research в этой области, т. к. задача нетривиальная. В перспективе это должно вылиться в кульное DX11-demo.

Сначала разберёмся с физической стороной проблемы. Преломление - это изменение направления следования светового луча, возникающее на границе двух прозрачных сред с различной плотностью. В вакууме скорость света c составляет приблизительно 299 800 км в секунду. Когда луч путешествует в среде с ненулевой плотностью, его скорость замедляется. Замедление является функцией от длины волны и материала, из которого состоит среда. Чем выше плотность, тем сильнее замедляется луч. Это не проходит бесследно, видимый эффект заключается в изменение угла между нормалью преломляющей поверхности и направлением движения луча. Попав опять в вакуум, луч света приобретает прежнюю скорость с, а угол восстанавливается.

Преломление света описывается законом Снелла.

Для того, чтобы рассчитать, как луч будет преломляться на границе двух сред, используются индексы рефракции материалов, из которых эти среды состоят. Индекс рефракции n определяется по формуле:
n = c/v
где с - скорость распространения света в вакууме, v - скорость распространения в материале.

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

Раскопав таблицы индексов рефракции, я реализовал схематическое преломление луча для плоского стекла и для выпуклой линзы из различных материалов, чтобы разобраться в математике. Плоское стекло:

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

А вот я начал играться с линзой из различных материалов. К сожалению, линза не "отшлифована" как следует - вершины задавал на глазок, поэтому чёткий фокус получить не удалось. Тем не менее видно, что всё вычисляется корректно. Стекло:

Кварц:

Рубин:

Алмаз (диамант):

Проверка математики, если в линзе воздух:

А вот так свет будет преломляться, если в линзе будет воздух и она (линза) будет помещена в воду:

воскресенье, 4 октября 2009 г.

Shader Model 5.0 Dynamic Linking

Интересно стало, как же устроена данная фича. Во-первых: работает только в SM 5.0. Т. е. это не просто какая-то заумная идея замены #ifdef в шейдерах, а реализована посредством поддержки в железе. Во-вторых: ни в SDK, ни в MSDN нет описания ассемблера Shader Model 5.0. Четвёртого есть, пятого - нет, поэтому остаётся только догадываться, что означает та или иная запись/команда. Во всем Гугле есть только одна ссылка касательно ассемблера пятой модели - какой-то японец тоже активно копает эту тему. Думаю скоро Google проиндексирует и этот пост :)

Разобравшись с механизмом подачи инстансов реализаций в шейдер, я набросал простенький код и несколько вариаций на тему, чтобы понять, как всё работает. Исходный код:
interface IColor
{
float3 Get();
};

class Red : IColor
{
float x;
float3 Get() { return float3(1.0, 0.0, 0.0); }
};

class Green : IColor
{
float x;
float3 Get() { return float3(0.0, 1.0, 0.0); }
};

IColor g_Color;

cbuffer cbClassInstances
{
Red g_Red;
Green g_Green;
}

float4 VS(float4 vPos : POSITION) : SV_Position
{
return vPos;
}

float3 PS(float4 vPos : SV_Position) : SV_Target
{
return g_Color.Get();
}

Есть переменная абcтрактного типа и два инстанса реализаций. Данный вариант назовём условно 1x2. Ассемблерный выхлоп:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_function_body fb0
dcl_function_body fb1
dcl_function_table ft0 = {fb0}
dcl_function_table ft1 = {fb1}
dcl_interface fp0[1][1] = {ft0, ft1}
dcl_output o0.xyz
dcl_temps 1
fcall fp0[0][0]
mov o0.xy, r0.xyxx
mov o0.z, l(0)
ret
label fb0
mov r0.xy, l(0,1.000000,0,0)
ret
label fb1
mov r0.xy, l(1.000000,0,0,0)
ret

Видно что объявляются прототипы функций (dcl_function_body). Далее мои фантазии. Ниже, объявляется таблица функций с "указателями" на адреса (метки). Каждая таблица содержит в фигурных скобках перечисление имён функций (меток), принадлежащих конкретной реализации интерфейса. Ещё строкой ниже - наш интерфейс, который связывается с одной из перечисленных в фигурных скобках таблицей функций. Какой именно - зависит от того, какой инстанс передать при вызове ID3D11DeviceContext::PSSetShader(). Функции ::Get() реализованы после основного кода шейдера как пары label/ret, легко проследить что кому принадлежит. Вызов нужного метода по установленному адресу (метке) производится новой командой fcall.

Вариант 1x3 отличается тем, что добавлена третья реализация IColor, Blue:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_function_body fb0
dcl_function_body fb1
dcl_function_body fb2
dcl_function_table ft0 = {fb0}
dcl_function_table ft1 = {fb1}
dcl_function_table ft2 = {fb2}
dcl_interface fp0[1][1] = {ft0, ft1, ft2}
dcl_output o0.xyz
dcl_temps 1
fcall fp0[0][0]
mov o0.xyz, r0.xyzx
ret
label fb0
mov r0.xyz, l(0,0,1.000000,0)
ret
label fb1
mov r0.xyz, l(0,1.000000,0,0)
ret
label fb2
mov r0.xyz, l(1.000000,0,0,0)
ret

Часть кода из варианта 2x2:
IColor g_Color0;
IColor g_Color1;

float3 PS(float4 vPos : SV_Position) : SV_Target
{
return g_Color0.Get() + g_Color1.Get();
}

Ассемблер 2x2:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_function_body fb0
dcl_function_body fb1
dcl_function_body fb2
dcl_function_body fb3
dcl_function_table ft0 = {fb0}
dcl_function_table ft1 = {fb1}
dcl_function_table ft2 = {fb2}
dcl_function_table ft3 = {fb3}
dcl_interface fp0[1][1] = {ft0, ft1}
dcl_interface fp1[1][1] = {ft2, ft3}
dcl_output o0.xyz
dcl_temps 2
fcall fp0[0][0]
fcall fp1[0][0]
mov r0.z, l(0)
mov r1.z, l(0)
add o0.xyz, r0.xyzx, r1.xyzx
ret
label fb0
mov r0.xy, l(0,1.000000,0,0)
ret
label fb1
mov r0.xy, l(1.000000,0,0,0)
ret
label fb2
mov r1.xy, l(0,1.000000,0,0)
ret
label fb3
mov r1.xy, l(1.000000,0,0,0)
ret

И ассемблер 2x3:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_function_body fb0
dcl_function_body fb1
dcl_function_body fb2
dcl_function_body fb3
dcl_function_body fb4
dcl_function_body fb5
dcl_function_table ft0 = {fb0}
dcl_function_table ft1 = {fb1}
dcl_function_table ft2 = {fb2}
dcl_function_table ft3 = {fb3}
dcl_function_table ft4 = {fb4}
dcl_function_table ft5 = {fb5}
dcl_interface fp0[1][1] = {ft0, ft1, ft2}
dcl_interface fp1[1][1] = {ft3, ft4, ft5}
dcl_output o0.xyz
dcl_temps 2
fcall fp0[0][0]
fcall fp1[0][0]
add o0.xyz, r0.xyzx, r1.xyzx
ret
label fb0
mov r0.xyz, l(0,0,1.000000,0)
ret
label fb1
mov r0.xyz, l(0,1.000000,0,0)
ret
label fb2
mov r0.xyz, l(1.000000,0,0,0)
ret
label fb3
mov r1.xyz, l(0,0,1.000000,0)
ret
label fb4
mov r1.xyz, l(0,1.000000,0,0)
ret
label fb5
mov r1.xyz, l(1.000000,0,0,0)
ret

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

И последнее. Пускай наш интерфейс продиктует необходимость реализации ещё нескольких незамысловатых функций:
interface IColor
{
float3 Get();
float Alpha();
float Intensity();
};

Остальной код я не привожу, думаю от понятен. Ассемблерный выхлоп для 1x2:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_function_body fb0
dcl_function_body fb1
dcl_function_body fb2
dcl_function_body fb3
dcl_function_body fb4
dcl_function_body fb5
dcl_function_table ft0 = {fb0, fb2, fb4}
dcl_function_table ft1 = {fb1, fb3, fb5}
dcl_interface fp0[1][3] = {ft0, ft1}
dcl_output o0.xyzw
dcl_temps 2
fcall fp0[0][0]
fcall fp0[0][1]
fcall fp0[0][2]
mov r0.z, l(0)
mul o0.xyzw, r0.xyzw, r1.xxxx
ret
...
...

пятница, 2 октября 2009 г.

GT300, игры и борьба с пиратством

NVIDIA пошла на досрочное разглашение подробностей нового поколения своих GPU... Видимо пытаются подогреть интерес из-за проблем с выпуском годных чипов. Если всё, что они понаписывали - правда, и новый чип может исполнять хоть в каком-то виде код C++ - то Intel пора хвататься за голову. Можно представить себе, как в 2012 выйдёт новый универсальный потоковый процессор от NVIDIA на её же материнской плате, выполняющий через эмулятор код старых x86 программ в 10 раз быстрее какого-нибудь 8-ядерного Intel Core i10... Помните, что случилось с Silicon Graphics?

PS. А пока что NVIDIA показывает фейковые прототипы с отпиленной (ножовкой) PCB. Интересно, был ли хоть чип под системой охлаждения? Совсем обнаглели.

Ещё одна интересная тема - а зачем, собственно, делать новые видеокарты, если рынок PC-игр, похоже, верно загибается? Всякая шняга, конечно, на нём всегда будет присутствовать, но на новые крупные многомиллионные тайтлы (способные показать "красу" и утилизировать мощь топовых видеокарт) придётся закатать губу. Crytek фичекатят енджин под дряхлый PS3 - "больше никаких эксклюзивов на PC". Есть правда Rage... Думаю, что чем больше распиарят новые DirectX 11 игры, тем быстрее их повзламывают и повыкладывают на торрентах. Замкнутный круг. Пока отсутствие софта не скажется на спросах на железо, которое этот самый софт должно запускать, HW-вендоры, похоже, проблему нелицензионного копирования решать не намерены.

Вот пришла интересная идея, как ограничить запуск одной лицензионной копии игры одной аппаратной конфигурацией. Основана, как и прошлая идея с "remote function execution", на том, что пользователь получает в своё распоряжение не весь код игры, а только часть - на этот раз, без шейдеров :). Для реализации необходимо, чтобы в GPU видеокарты был встроен механизм криптования бинарных шейдеров (та часть, что отвечает за дешифровку) и зашит уникальный ключ для дешифровки. Механизм защиты следующий:

Шейдеры в виде исходных кодов хранятся только на сервере издателя. В процессе первого запуска игры юзер подключается к серверу издателя игры (нужен интернет), вводит серийный номер с диска и задаёт свои уникальные имя/пароль, игра отсылает их на сервер. Если сервер подтверждает уникальность серийного номера копии игры, то дальше игра отсылает модель GPU, уникальный GPUID, версию установленного видеодрайвера и т. д. Затем на сервере хранящиеся там шейдеры компилируется в бинарные блобы, подходящие под GPUID и видеодрайвер пользователя и шифруются ключом (на сервере хранится таблица пар GPUID/ключ). Получается такой shader cache из зашифрованных бинарных шейдеров, который единожды передаётся и инсталлируется на машине игрока. В таком виде шейдеры загружаются в видеокарту, а там GPU использует свой ключ для их дешифровки перед загрузкой кода в видеопамять. Предполагается, что прочитать из видеопамяти расшифрованный бинарник невозможно.

Всё. Полученный shader cache будет работать только с тем GPU, ключ которого соответствует ключу, использованного при его шифровании. Если пользователь сменил аппаратную конфигурацию, то процедуру получения нового кэша придётся повторить, при этом введя свои имя/пароль и серийный номер диска. При этом "аккаунт" для старой железки становится невалидным.

четверг, 1 октября 2009 г.

Луна 2112

Посмотрел камрип. Зачётное кино! Люблю такую фантастику.
PS. Если что - камрип это потому, что у нас его не показывают в кинотеатрах. А так бы сходил.