Сегодня, когда всё популярнее становится трассировка лучей (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. С такой системой неудобно работать. Хуже того, если к значениям координат пикселей применяются различные операторы наподобие
Проще работать с интервалом от 0,0 до 1000,0, в котором центр каждого пикселя имеет дробную часть 0,5. Например, тогда целочисленный пиксель 43 будет иметь красивый интервал значений значений входящих в него субпикселей от 43,0 до 43,99999. Вот чертёж из статьи Пола:
В OpenGL центр пикселя всегда имел дробную часть (0,5; 0,5). Поначалу DirectX этого не придерживался, но в версии DirectX 10 взялся за ум.
Операции для преобразования из целочисленных координат в координаты пикселя с плавающей запятой заключаются в прибавлении 0,5; для преобразования float в integer достаточно использовать
Но это уже давняя история. Ведь сегодня все делают так, правда? Я вернулся к этой теме, потому что начал встречать в примерах (псевдо)кода генерации направления перспективной камеры для трассировки лучей такое:
Вектор 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 от каждого из краёв.
Вторая строка кода должна выглядеть так:
Тогда мы получим правильный интервал NDC для центров пикселей, от -0,999 до 0,999. Если мы вместо этого преобразуем угловые значения с плавающей запятой (0,0; 0,0) и (1000,0; 1000,0) этим способом (мы не прибавляем 0,5, потому что уже и так работаем с плавающей запятой), то получим полный интервал NDC, от -1,0 до 1,0, от края до края; это доказывает правильность кода.
Если 0,5 вас раздражает и вам не хватает симметрии, то для генерации случайных значений внутри пикселя, т.е. когда вы выполняете сглаживание испусканием случайных лучей через каждый пиксель, можно использовать такую изящную формулировку:
Мы просто прибавляем к каждому целочисленному значению местоположения пикселя случайное число из интервала [0.0,1.0). Средним этого случайного значения будет 0,5, то есть центр пикселя.
Так что скажу кратко: будьте внимательны. Реализуйте полупиксель правильно. По моему опыту, такие ошибки полупикселей всплывают во множестве разных мест (камеры, сэмплирование текстур и т.п.) на протяжении долгих лет моей работы над кодом растеризатора в Autodesk. Далее по конвейеру они не приносят ничего, кроме боли. Если мы не будем внимательны, они могут появиться и в трассировщиках лучей.
Смысл размещения центра пикселя в (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. Далее по конвейеру они не приносят ничего, кроме боли. Если мы не будем внимательны, они могут появиться и в трассировщиках лучей.
llia6an
Чертовски важное замечание. Код может быть непривычным, главное, чтобы по логике и смыслу всё было правильно. В играх бывают приколы, когда например справа и снизу полосы шириной в 1 пиксель рисуются одним цветом.