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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Сегодня, когда всё популярнее становится трассировка лучей (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. Далее по конвейеру они не приносят ничего, кроме боли. Если мы не будем внимательны, они могут появиться и в трассировщиках лучей.
Источник: https://habr.com/ru/post/506742/


Интересные статьи

Интересные статьи

Федеративная система Matrix поддерживает связь с другими сетями через мосты. Это пример инфраструктуры, к которой нужно стремиться Signal 4 января 2021 года WhatsApp внёс изменения в...
SWAP (своп) — это механизм виртуальной памяти, при котором часть данных из оперативной памяти (ОЗУ) перемещается на хранение на HDD (жёсткий диск), SSD (твёрдотельный накоп...
Мы недавно писали, как затеяли конференцию, полностью посвященную инженерным процессам и практикам. Наша цель — собрать в одном месте профессионалов, которые развивают техническое лидерство у ком...
В предыдущих статьях, опубликованных мной на Хабре («Автоматический кошачий туалет» и «Туалет для Мэйн-Кунов») представлялась модель туалета, реализованного на ином, от существующих, принципе смы...
Привет, Хабр! Представляю Вашему вниманию перевод статьи «Why can’t I set the font size of a visited link?» автора Jim Fisher. Посещенные ссылки отображаются фиолетовым; не посещенные — го...