Pull to refresh

Почему центр пикселя должен быть в (0,5; 0,5)

Reading time3 min
Views6.3K
Original author: Eric Haines
Сегодня, когда всё популярнее становится трассировка лучей (ray tracing) выполняемая из «глаза» камеры, этот урок нужно усвоить заново: код становится лучше, а жизнь — проще, если центр пикселя находится в координате (0,5; 0.5). Если вы уверены, что делаете всё правильно, то продолжайте в том же духе, для вас в статье нет ничего нового. Прочитайте лучше вот это.

Смысл размещения центра пикселя в (0,5; 0,5) впервые объяснила (по крайней мере, мне) милая короткая статья Пола Хекберта «Что такое координаты пикселя?» из книги 1990 года Graphics Gems, стр. 246-248.

Сегодня эту статью найти трудновато, поэтому вкратце изложу её суть. Допустим, у нас есть экран с шириной и высотой 1000. Давайте рассмотрим только ось X. Может возникнуть искушение назначить 0,0 центром самого левого пикселя в строке, 1,0 — центром следующего, и так далее. Можно даже использовать округление, при котором координаты с плавающей запятой 73,6 и 74,4 переносятся в центр 74,0.

Однако над этим стоит поразмыслить. При таком сопоставлении левый край будет находиться в координате -0,5, а правый — в 999,5. С такой системой неудобно работать. Хуже того, если к значениям координат пикселей применяются различные операторы наподобие abs() или mod(), то такое сопоставление может привести к незначительным погрешностям на краях.

Проще работать с интервалом от 0,0 до 1000,0, в котором центр каждого пикселя имеет дробную часть 0,5. Например, тогда целочисленный пиксель 43 будет иметь красивый интервал значений значений входящих в него субпикселей от 43,0 до 43,99999. Вот чертёж из статьи Пола:


В OpenGL центр пикселя всегда имел дробную часть (0,5; 0,5). Поначалу DirectX этого не придерживался, но в версии DirectX 10 взялся за ум.

Операции для преобразования из целочисленных координат в координаты пикселя с плавающей запятой заключаются в прибавлении 0,5; для преобразования float в integer достаточно использовать floor().

Но это уже давняя история. Ведь сегодня все делают так, правда? Я вернулся к этой теме, потому что начал встречать в примерах (псевдо)кода генерации направления перспективной камеры для трассировки лучей такое:

 float3 ray_origin = camera->eye;
 float2 d = 2.0 * 
     ( float2(idx.x, idx.y) / 
       float2(width, height) ) - 1.0;
 float3 ray_direction =
     d.x*camera->U + d.y*camera->V + camera->W;

Вектор idx — это целочисленное местоположение пикселя, width и height — разрешение экрана. Вектор d вычисляется и используется для генерации вектора в мировом пространстве при помощи перемножения двух векторов, U и V. Затем прибавляется вектор W — направление камеры в мировом пространстве. U и V обозначают положительные направления осей X и Y плоскости отображения на расстоянии W от глаза. В представленном выше коде всё это выглядит красиво и симметрично; так оно по большей части и есть.


Вектор d должен обозначать пару значений от -1,0 до 1,0 в нормализованных координатах устройства (Normalized Device Coordinates, NDC) для точек на экране. Однако, здесь код даёт сбой. Продолжим наш пример: целочисленное местоположение пикселя (0; 0) переносится в (-1,0; -1,0). Кажется, это хорошо, правда? Но максимальное целочисленное местоположение пикселя равно (999; 999), что преобразуется в (0,998; 0,998). Суммарная разница 0,002 вызвана тем, что это неверное наложение сдвигает всю картинку на полпикселя. Эти центры пикселей должны находиться в 0,001 от каждого из краёв.

Вторая строка кода должна выглядеть так:

    float2 d = 2.0 *
        ( ( float2(idx.x, idx.y) + float2(0.5,0.5) ) / 
            float2(width, height) ) - 1.0;

Тогда мы получим правильный интервал NDC для центров пикселей, от -0,999 до 0,999. Если мы вместо этого преобразуем угловые значения с плавающей запятой (0,0; 0,0) и (1000,0; 1000,0) этим способом (мы не прибавляем 0,5, потому что уже и так работаем с плавающей запятой), то получим полный интервал NDC, от -1,0 до 1,0, от края до края; это доказывает правильность кода.

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

    float2 d = 2.0 *
        ( ( float2(idx.x, idx.y) + 
                float2( rand(seed), rand(seed) ) ) /
            float2(width, height) ) - 1.0;

Мы просто прибавляем к каждому целочисленному значению местоположения пикселя случайное число из интервала [0.0,1.0). Средним этого случайного значения будет 0,5, то есть центр пикселя.

Так что скажу кратко: будьте внимательны. Реализуйте полупиксель правильно. По моему опыту, такие ошибки полупикселей всплывают во множестве разных мест (камеры, сэмплирование текстур и т.п.) на протяжении долгих лет моей работы над кодом растеризатора в Autodesk. Далее по конвейеру они не приносят ничего, кроме боли. Если мы не будем внимательны, они могут появиться и в трассировщиках лучей.

См. также:

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+15
Comments4

Articles