Войти
ФлеймФорумПроЭкты

Я пишу несмещной пат-трейсер

Страницы: 1 2 310 11 Следующая »
#0
19:59, 1 дек. 2019

Я целый год не писал ничего серьёзного, а ещё меня уже давно интересовала тема рейтрейсеров, в немалой степени благодаря хвастовствам Суслика. Но, разумеется, чтобы работать на вершинах, нужно сначала тщательно проработать фундамент.
Поэтому, в этой теме я собираюсь рассказать, как я делаю несмещённый трейсер путей.
_

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

    Изображение - точка пространства;

    Изображение - направление входящего луча;

    Изображение - направление исходящего луча;

    Изображение - отражательная функция;

    Изображение - функция сенсора;

    Изображение - функция излучателя.

Референсная модель:

0. Каждый пиксел - отдельный сенсор. Объектив камеры Изображение точечный и общий для всех, а вот сканируемый телесный угол - у каждого пиксела свой.

1. Для выбранного пиксела, выбираем случайную точку на плоскости изображения Изображение, при этом в качестве распределения берем ядро этого пиксела Изображение. Полученной точке на плоскости изображения будет в пространстве сцены соответствовать луч из центра камеры.
Если сцена задана в координатах камеры, то этому лучу будет соответствовать

    Изображение.

При этом предполагается, что (0,0) изображения соответствует центру кадра, оси u и xcam направлены вправо, оси v и ycam направлены вниз, а zcam - вперёд по оси камеры.

Продвигаем фотон по полученному лучу до первого столкновения.

2. При попадании в поверхность, расположенную в положении P:

Сначала регистрируем излучение, которое исходит от этой поверхности:

    Изображение,

где P - место события, o - направление, из которого фотон попал на материал.

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

    Изображение,

где i - направление, в котором фотон пойдёт после переотражения.

Продолжаем луч до следующего столкновения и повторяем шаг 2, пока фотон не будет поглощён.

3. Итоговое значение пиксела - это среднее арифметическое всех полученных измерений S.
_

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

    Изображение,

тогда как у источников света - напротив, нулевое отражение:

    Изображение


#1
(Правка: 20:38) 20:14, 1 дек. 2019

А теперь, собственно, к реализации.
_

Свои данные будем хранить в своём объекте, который привяжем к окну.
WinApi позволяет приклеивать к окну свой собственный буфер данных:

cbWndExtra

Type: int

The number of extra bytes to allocate following the window instance. The system initializes the bytes to zero. If an application uses WNDCLASS to register a dialog box created by using the CLASS directive in the resource file, it must set this member to DLGWINDOWEXTRA.


Но тут есть подвох - к этому буферу нельзя обратиться по адресу в памяти, можно только попросить винду записать/прочитать отдельные слова в этом буфере:
GetWindowLongPtrA function
LONG_PTR GetWindowLongPtrA(HWND hWnd, int  nIndex);

Retrieves information about the specified window. The function also retrieves the value at a specified offset into the extra window memory.


Однако, эта же функция вскрывает ещё один вариант - можно ещё дополнительно привязать к окну один пользовательский указатель:
nIndex

Type: int

The zero-based offset to the value to be retrieved. Valid values are in the range zero through the number of bytes of extra window memory, minus the size of a LONG_PTR. To retrieve any other value, specify one of the following values.
<...>
GWLP_USERDATA
-21
Retrieves the user data associated with the window. This data is intended for use by the application that created the window. Its value is initially zero.


Хм, а каким методом я пользовался в своём велосипедном двигле? SetClassLongPtr? Серьёзно?
Ну, значит, будет SetWindowLongPtr, так тому и быть:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
    {
        RendererThread* rtt = new RendererThread();
        SetWindowLongPtrW(hWnd, GWLP_USERDATA, (LONG_PTR)rtt);
        break;
    }
    case WM_PAINT:
    {
        RendererThread* rtt = (RendererThread*)GetWindowLongPtrW(hWnd, GWLP_USERDATA);
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        rtt->DrawFrame(hdc);
        EndPaint(hWnd, &ps);
        break;
    }
    case WM_DESTROY:
    {
        RendererThread* rtt = (RendererThread*)GetWindowLongPtrW(hWnd, GWLP_USERDATA);
        delete rtt;
        PostQuitMessage(0);
        break;
    }
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

#2
20:41, 1 дек. 2019

Следующий шаг.
А каким образом, собственно, я буду рисовать картинки на окне? Не буду же я развёртывать целый D3D-контекст ради одной битмапы по одному разу за кадр? Должно же быть какое-то решение у самого GDI.
Так что, начинаем с того, о чём я слышал давно и никогда не пользовался:

BitBlt function
BOOL BitBlt(
    HDC hdc, int x, int y, int cx, int cy,
    HDC hdcSrc, int x1, int y1,
    DWORD rop);

The BitBlt function performs a bit-block transfer of the color data corresponding to a rectangle of pixels from the specified source device context into a destination device context.


Копирует пикселы из... "source device context"? Я, конечно, слышал, что можно делать HDC на HBITMAP; но городить такие леса ради одной картинки? Должен же быть способ попроще.
Просматривая секцию "See Also", видим это:
SetDIBits function
int SetDIBits(
    HDC hdc,
    HBITMAP hbm, UINT start, UINT cLines,
    const VOID* lpBits, const BITMAPINFO* lpbmi, UINT ColorUse);

The SetDIBits function sets the pixels in a compatible bitmap (DDB) using the color data found in the specified DIB.


Окей, эта функция пригодилась бы для того, чтобы перенести пикселы из моего буфера в HBITMAP.
Правда, не совсем понятно, зачем этой функции HDC, если весь эффект идёт внутрь одного HBITMAP...
Remarks
The device context identified by the hdc parameter is used only if the DIB_PAL_COLORS constant is set for the fuColorUse parameter; otherwise it is ignored.

А, палитра! Ну теперь всё ясно.

Ну ладно, на всякий случай, ещё раз глянем на список функций слева... Ово, что это? *прыгает по ссылке*
SetDIBitsToDevice function
int SetDIBitsToDevice(
    HDC hdc, int xDest, int yDest, DWORD w, DWORD h,
    int xSrc, int ySrc, UINT StartScan, UINT cLines,
    const VOID* lpvBits, const BITMAPINFO* lpbmi, UINT ColorUse);

The SetDIBitsToDevice function sets the pixels in the specified rectangle on the device that is associated with the destination device context using color data from a DIB, JPEG, or PNG image.


Ага! Именно то, что я искал. Осталось только разобраться с BITMAPINFO, и, собственно, можно пускать в работу.
RendererThread::RendererThread()
{
    // std::vector<uint8_t> bm;
    bm.resize(800 * 600 * 4);
    for (size_t i = 0; i < 800 * 600; ++i) {
        bm[4 * i] = (uint8_t)i;
        bm[4 * i + 1] = (uint8_t)(i >> 8);
        bm[4 * i + 2] = (uint8_t)(i >> 16);
        bm[4 * i + 3] = 0;
    }
}

void RendererThread::DrawFrame(HDC dc)
{
    BITMAPINFOHEADER bih;
    bih.biSize = sizeof(bih);
    bih.biWidth = 800;
    bih.biHeight = 600;
    bih.biPlanes = 1;
    bih.biBitCount = 32;
    bih.biCompression = BI_RGB;
    bih.biSizeImage = 0;
    bih.biXPelsPerMeter = 1;
    bih.biYPelsPerMeter = 1;
    bih.biClrUsed = 0;
    bih.biClrImportant = 0;
    SetDIBitsToDevice(
        dc,
        0, 0,
        800, 600,
        0, 0,
        0, 600,
        bm.data(),
        (BITMAPINFO*)&bih,
        DIB_RGB_COLORS);
}
Изображение
Неплохо!
Кстати, обратим внимание - скан-лайны отображаются снизу вверх.

#3
22:32, 1 дек. 2019

Еще немного и до DirectDraw дойдешь. )))
Хотя возможно тебе и Direct2D сойдет.

#4
1:42, 2 дек. 2019

gamedevfor
> Еще немного и до DirectDraw дойдешь. )))
> Хотя возможно тебе и Direct2D сойдет.
А нафига?

#5
(Правка: 1:53) 1:53, 2 дек. 2019

по-моему, сейчас писать алгоритм на CPU имеет смысл только в том случае, если он и так слишком быстрый и нет смысл заморачиваться с его ускорением. едва ли с path tracing'ом — тот случай.

#6
3:41, 2 дек. 2019

Delfigamer
> Сначала регистрируем излучение, которое исходит от этой поверхности
Лучше сначала посчитать отражение, ибо светимость по физике домножается на коэффициент поглощения.

> В случае отражения, мы выбираем новое направление для фотона согласно распределению
Лучше переформулировать в терминах BRDF и вынести косинус.

> WinApi позволяет приклеивать к окну свой собственный буфер данных
Зачем привязывается к платформе на ровном месте?
Простенького окошка на SDL хватит за глаза, или, вообще, консольного приложения с выводом в png.

Suslik
> по-моему, сейчас писать алгоритм на CPU имеет смысл только в том случае
Референсный трейсер обязан быть на CPU, без рефов оптимизировать бесполезно.
Да и экспериментировать с каким-нибудь importance sampling тоже проще на нем.

#7
3:49, 2 дек. 2019

}:+()___ [Smile]
> Референсный трейсер обязан быть на CPU, без рефов оптимизировать бесполезно.
первый — возможно, да. но так как писатели path tracer'ов пишут по одному каждый год, то на определённом этапе проще уже референсный писать сразу на GPU, потому что можно просто его оптимизировать, а не писать для оптимизаций новый.

#8
4:01, 2 дек. 2019

Suslik
> по-моему, сейчас писать алгоритм на CPU имеет смысл только в том случае, если
> он и так слишком быстрый и нет смысл заморачиваться с его ускорением. едва ли с
> path tracing'ом — тот случай.
Меня скорее волнует математика процесса, а не убер-оптимизация, поэтому пока всё делается на ЦП.

}:+()___ [Smile]
> Лучше сначала посчитать отражение, ибо светимость по физике домножается на
> коэффициент поглощения.
Будем считать, что этот коэффициент уже сразу вмножен в "светимость".

}:+()___ [Smile]
> Лучше переформулировать в терминах BRDF и вынести косинус.
Так это же импортанс-самплинг!
Так-то там BRDF и выходит:

    Изображение,

только p гарантированно интегрируется к единице.

}:+()___ [Smile]
> Зачем привязывается к платформе на ровном месте?
> Простенького окошка на SDL хватит за глаза, или, вообще, консольного приложения
> с выводом в png.
Ой, придумал тоже, ради какого-то окошка качать целую библиотеку.

Suslik
> первый — возможно, да. но так как писатели path tracer'ов пишут по одному
> каждый год, то на определённом этапе проще уже референсный писать сразу на GPU,
> потому что можно просто его оптимизировать, а не писать для оптимизаций новый.
А я всегда думал, что я генерал-программист и каждый год пишу что-нибудь совершенно новое.

_

Рендерер будет работать в отдельном потоке.
Для передачи картинок между потоками воспользуемся тройным буфером, который я только что придумал и сейчас завелосипедю.
Принцип работы такой - есть три буфера, "задний", "средний" и "передний".
Принимающая сторона читает из "переднего" буфера, блокируя его на время чтения.
Передающая сторона пишет в "задний" буфер, так же блокируя его на время записи.
Когда запись завершается, "задний" и "средний" буферы меняются местами.
За счёт третьего буфера, оба потока могут работать одновременно и непрерывно, и при этом получатель всё равно будет получать свежие данные от источника.
Если взяться за код и начать продумывать основательно, можно доработать ещё детали.
В принципе, можно считать "задний" и "передний" буферы занятыми 100% времени - всё равно ими пользуются не более одного потока. Таким образом, главный вопрос алгоритма - в какие моменты времени производить перестановки.
Логично, что выдвигать "задний" следует сразу по окончании записи - это, своего рода, публикация данных. Даже если в "среднем" буфере были непрочитанные данные - они уже устарели, и их можно выбрасывать и писать очередной результат поверх.
"Передний" и "средний" буферы следует переставлять прямо перед началом процесса чтения - но только при условии, что "средний" буфер содержит именно новые данные, а не остатки от предыдущего чтения. Для этого будет дополнительно держать в буфере флаг "грязный", который будем поднимать вместе с перестановкой "зад"-"середина".

+ Показать

Выглядит прикольно, жаль, в сообщении просто так не покажешь.

#9
4:12, 2 дек. 2019

Delfigamer
> А я всегда думал, что я генерал-программист и каждый год пишу что-нибудь
> совершенно новое.
не бывает программистов, которые пишут только один path tracer. их либо не пишут вообще, либо пишут по одному в год.

#10
(Правка: 5:10) 4:57, 2 дек. 2019

Suslik
> не бывает программистов, которые пишут только один path tracer. их либо не
> пишут вообще, либо пишут по одному в год.
На самом деле, чем собственно трассирующие рендереры, меня больше интересуют альтернативные применения интеграла по путям; например, для вычисления импульсной переходной функции между источником звука и игроком или, там, для получения диффракционной картины (хоть для той же голограммы).
Но сначала, мне кажется, лучше всё-таки взять матан за рога и сделать для начала что-то, что можно проверить и количественно отладить.

_

Ну, по сути, вот подготовка и окончена.
Следующим шагом запилим геометрию.

Наложим на треугольник трёхмерный базис:
Изображение
Введём связанную с ним систему отсчёта, назовём её "треугольной". За начало координат возьмём вершину A, единицу абсцисс - сторону AB, единицу ординат - сторону AC. Для определения третьего направления возьмём векторное произведение AB и AC и нормализуем его. Полученный таким образом вектор AN определим как единицу аппликат.
Таким образом, для треугольника, заданного мировыми координатами точек {A, B, C}, можем тривиальным образом задать матрицу перехода из треугольных координат в мировые:

    Изображение

    Изображение

Обратив матрицу, получим оператор перехода в треугольные координаты:

    Изображение

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

1. Начало луча находится с передней стороны (принимаем CCW-порядок):

    Изображение

2. Луч направлен в сторону треугольника:

    Изображение

3. Точка пересечения H лежит в плоскости треугольника:

    Изображение

Отсюда, координаты точки пересечения вычисляются простыми соотношениями:

    Изображение

    Изображение

4. Точка пересечения лежит во внутренней области:

    Изображение

Мировые координаты H можно теперь получить двумя способами:
1. Преобразовать треугольные координаты оператором MWT.
2. Воспользоваться параметром луча Изображение.

Таким образом, трассировщик будет хранить геометрию в виде массива матриц MTW, и при собственно трассировке будет действовать на данный луч этими операторами и искать минимальный параметр Изображение среди допущенных пересечений.

#11
5:01, 2 дек. 2019

Delfigamer
брутфорсный трассировщик на треугольниках будет жутко медленный. ты просто заколебёшься ждать сходимости на CPU без хоть каких-то оптимизаций. я бы рекомендовал делать на сферах, так как за тот же объём вычислений можно рендерить гораздо более интересную геометрию (например, можно строить из сфер фракталы или делать csg).

#12
5:05, 2 дек. 2019

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

#13
10:24, 2 дек. 2019

Не, ну ребят, я все конечно понимаю, только может вам уже настоящей математикой заняться а не этой фигнёй ? Там миллионы долларов уже десятки лет ждут тех, кто решит последние 9 загадок или как там было в истории с Перельманом. А рейтрейсеры это как то совсем мимо и геймдева и науки.

#14
11:12, 2 дек. 2019

раб вакуумной лампы
а ты думаешь, graphics engineer'ам деньги за сидение на форуме платят? или за плюханье в construct'е?

Страницы: 1 2 310 11 Следующая »
ФлеймФорумПроЭкты