Реализация строгой трехточечной перспективы

Реализациястрогойтрехточечнойперспективы

Эта статья может показаться странной: каждый учебник в Интернете учит, что трехточечная перспектива – это просто художественный термин для «обычного 3D», когда вы настраиваете камеру, настраиваете ее расстояние, FOV , и масштабирование, и все готово. Точки схода, которые вы используете при использовании ручки и бумаги, соответствуют точкам пересечения осей X, Y и Z вашей плоскости отсечения, и это все, что она написала … За исключением того, что это не «истинная» трехточечная перспектива. Это упрощенная для компьютерной графики версия трехточечной перспективы: строгая версия немного сложнее.

Сложность состоит в том, что в строгой трехточечной перспективе ваша точки схода буквально точки схода: они не представляют собой пересечения осей, уходящих в бесконечность, и плоскости отсечения где-то на расстоянии относительно вашей камеры, исчезающая точки равны бесконечности. Это проблема, потому что это означает, что мы не имеем дело с линейным пространством, а это значит, что мы не можем использовать линейную алгебру для вычисления хороших «трехмерных мировых координат в двухмерные экранные координаты» с помощью матричных операций. Это небольшая проблема, учитывая, что это фундаментальный подход, который позволяет создавать эффективную трехмерную компьютерную графику практически на любом современном оборудовании.

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

Заметка о том, почему вы хотите сделать это

Реально? Вы этого не сделаете.

Прежде чем мы продолжим, я хочу прояснить, что вы почти никогда нужна строгая трехточечная перспектива: она не столько «полезна», сколько довольно странная трехмерная эстетика. Но это проблема программирования, и пока я пишу этот текст, ни у кого нет страницы об этом в Интернете, так что это, безусловно, достаточно сложная задача, чтобы понять, что черт возьми, с этой сумасшедшей проекцией и объясни код, который нам нужен для ее достижения.

Работа в экспоненциальном пространстве

Давайте посмотрим на двухточечную перспективу, чтобы понять, с чем мы имеем дело:

image-20210603162927284

У нас есть две точки схода, обозначенные здесь Z и X, и некоторая произвольная «нулевая» точка, где мы просто скажите «это (0,0,0)» (используя три координаты, потому что прямо сейчас есть подразумеваемая координата Y, но мы скоро сделаем это явным). Нам также нужна некоторая точка, которая будет служить нашим «нулем»: когда вы рисуете на листе бумаги, вы можете просто решить, где он находится, но при работе с компьютерами нам нужно четко указывать, где находятся фиксированные точки. Это фактически делает «двухточечную перспективу» трехточечной перспективой для компьютеров, потому что вам нужно указать три точки, но это ни здесь, ни там. Затем нам также нужно указать, какова высота от нашего нуля до горизонта, потому что (опять же), пока вы рисуете, вы можете просто крыть его, но компьютеры должны знать, что это за значение, чтобы произвольные координаты могли быть сопоставлены экран правильно. Сделайте «двухточечную перспективу» на самом деле перспективой «три точки и число». Допустим, высота горизонта для нашей перспективы равна единице, тогда это даст нам следующую перспективу:

image-20210603175127056

И с этим у нас есть все необходимое для создания красивой графики в двухточечной перспективе. За исключением одной вещи, которую вы почти наверняка уже заметили, но, возможно, не задумали: приведенный выше «график» не является нормальным графиком. Мы видим, что сетка становится все более и более тонкой по мере приближения к X, Z, а также к горизонту. Вместо стандартной сетки, точнее называемой декартовой , и конкретно Евклидова , система координат такая:

image-20210603163818568

У нас есть что-то вроде этого:

image-20210604174011025

То есть вместо бесконечной сетки, которую мы никогда не поместим на какой-либо лист бумаги конечного размера, у нас есть ограниченная сетка, в которой расстояние между a мировая координата в нуле и бесконечности фиксированное расстояние в единицах экранных координат . Пока мы понимаем разницу между мировыми и экранными координатами: координаты экрана похожи на камеру, это то, что мы «видим», учитывая проекцию, которую мы сказали камере использовать, тогда как мировые координаты – это «реальные вещи». . Даже несмотря на то, что наша проекция может превратить бесконечность в фиксированную точку, что касается мировой системы координат, все еще нет способа, чтобы что-то на самом деле было на бесконечности. не говоря уже о том, чтобы быть за бесконечностью, даже если наша проекция позволяет нам поместить курсор мыши точно на или даже за пределы того места, где бесконечность проецируется на экран.

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

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

image-20210603165407601

Наша сетка не только не линейна, она даже не декартова: хотя это может выглядеть как будто этот треугольник такой же, как и вышеупомянутая прямоугольная сетка, но с верхним правым углом, перемещенным внутрь, так что он образует диагональ с двумя другими точками, на самом деле это не так. Если вы посмотрите на верхнюю левую и нижнюю правую точки, вы увидите, что все линии нашей сетки сходятся. Используя маркировку Z и X для двух точек, вы можете видеть, что все значения x =…, z = ∞ лежат в одной точке, Z , чего не было бы, если бы все, что мы делали, было перемещать нашу угловую точку внутрь. Если бы это было так, все эти значения лежали бы где-то между Z и середину диагонали (с аналогичным случаем для X ), и наши линии сетки не сходятся к единичным точкам. Вместо этого вся диагональная линия представляет собой «точку» (∞, ∞). Даже если на самом деле ничего не может туда попасть, как только точка достигнет бесконечности либо в X, Z, либо в обоих измерениях, она растянется по всей диагонали, а линии (n, ∞) и (∞, n) станут точки Z и X .

Как не декартова система координат, двухточечная перспектива является примером своего рода проекции, которую мы ‘ нам придется оценивать старомодным способом: математически не может достичь , используя матрицу преобразования, чтобы превратить мировые координаты в координаты экрана. То, на чем основана вся современная 3D-графика, от программного обеспечения до математических расчетов, выполняемых физическими чипами на вашем графическом процессоре.

Итак, вот над чем мы работаем. с, нам лучше всего выяснить, какой код нам нужно написать для работы с ним.

image-20210603165407601. строгая двухточечная перспектива

Итак, давайте реализуем двухточечную перспективу, учитывая наши четыре точки Z , X и наша нулевая точка, которую мы назовем C .

Сначала давайте определим наше преобразование, чтобы преобразовать линейное значение в экспоненциальное отношение. Нам нужно что-то, что при заданном значении x = 1, например, дает значение «0,5», означающее, что x = 1 можно найти на полпути по оси X, x = 2 дает «0». 90 »(т.е. три четверти расстояния от (0,0,0) по оси X) и т. Д.

Вы можете заметили шаблон, по которому мы просто движемся, уменьшая вдвое оставшийся интервал для каждого целого шага s , который мы делаем, поэтому это просто экспоненциальный спад:

{E6F60D04-8E90-4DCC-B2E8-166A04D42BB3}

Когда s = 0, это дает нам f (s) = 1, а если s = ∞ (игнорируя, что практически говоря это, конечно, невозможно), получаем f (s) = 0. Это несколько противоположно тому, что мы на самом деле хотим, а именно иметь f (0) равным 0 и f (∞) равным 1, поэтому мы можем заставить это:

{87566E73-CC17-45C0-9D34-4E7B3DB63C64}

Теперь s = 0 дает нам f (s) = 0, а s = ∞ дает нам f (s) = 1. Идеально. И, конечно, реализовать это по сути тривиально:

  двойной  distanceToRatio   (двойной  s  )   { возвращаться  1.0   -   1.0   /   pow   (  2.0  ,   s  );  }   

Теперь мы можем определить функцию, которая превращает трехмерный мир. координату (с y = 0 на данный момент) в координату 2D экрана, выполнив то же самое, что и на бумаге: найдите расстояние до координаты по оси X, сделайте то же самое для оси Z, и тогда наша координата может быть изменена................................................................................................................................................................................................................................................... нарисованный как пересечение линии от Z до точки на оси X и от X до точки на оси Z. Например, рисунок x = 0,5, z = 1 дает:

image-20210604085352991

С функцией отображения координат, имеющей вид:

   Vec2  получать ( двойной Икс ,  двойной  z  )   { если  (Икс  ==   0   &&   z   ==   0  )  возвращаться  C  ;   Vec2   px  знак равно   лерп   (  C  ,  ИКС ,   DistanceToRatio   (Икс ));   Vec2   pz  знак равно   lerp   (  C  ,   Z  ,   distanceToRatio   (  z  ));  возвращаться  lli   ( ИКС,   pz  ,   Z  ,   px  );  }   

В этом коде lerp - это функция линейной интерполяции (в данном случае для векторов, а не скаляров) и lli вычисляет точку пересечения двух линий:

   Vec2   lli   (  Vec2   l1p1  ,   Vec2   l1p2  ,   Vec2   l2p1  ,   Vec2   l2p2  )   { возвращаться  lli   (  l1p1  .  Икс ,   l1p1  .   y  ,   l1p2  .  Икс,   l1p2  .   y  ,   l2p1  . Икс  ,   l2p1  .   y  ,   l2p2  .  Икс,   l2p2  .   y  );  }   Vec2   lli   (двойной  x1  ,  двойной  y1  ,  двойной  x2  ,  двойной  y2  ,  двойной   x3  ,  двойной  y3  ,   двойной  x4  ,  двойной  y4  )   { двойной  d   знак равно )  (  x1   -   x2  )     (  y3   -   y4  )   -   (  y1   -   y2  )     (  x3   -    x4  ;  если  (  d   ==   0  )  возвращаться ноль ;  двойной  f 15   знак равно   (  x1     y2   -   y1     x2  );  двойной  f 45   знак равно   (  x3     y4   -   y3     x4  );  двойной  nx  знак равно   f 20     (  x3   -   x4  )   -   f 45     (  x1   -   x2  );  двойной  ny  знак равно   f 15     (  y3   -   y4  )   -   f 42     (  y1   -   y2  );  возвращаться новый  Vec2   (  nx   /   d  ,   ny   /   d  );  }    

С Код выше, теперь мы можем рисовать объекты на плоскости y = 0:

  пустота  drawSomeGeometry   ()   {  beginShape   ();   вершина   (  0  ,   0  );   вершина   (  3  ,   0  );   вершина   (  3  ,   1  );   вершина   (  1  ,   1  );   вершина   (  1  ,   3  ;   вершина   (  o  ,   3  );   endShape   ( ЗАКРЫТЬ );   beginShape   ();   вершина   (  2  ,   2  );   вершина   (  2  ,   3  );   ве  rtex   (  3  ,   3  );   вершина   (  3  ,   2  );   endShape   ( ЗАКРЫТЬ );  }  пустота  вершина   ( двойной Икс ,  двойной  z  )   {  addShapeVertex   ( получать  (Икс ,   z  ));  }   

Это дает нам следующий рисунок:

image-20210603172921646

Конечно, все дело в двух и трех пунктах перспектива, заключается в рисовании перспективы , а не плоских проекций, поэтому давайте расширим нашу get () , чтобы она принимала высота во внимание. Это требует, чтобы несколько значений были указаны / вычислены заранее как часть определения наших точек схода, поэтому мы можем использовать их в нашем обновленном по высоте get () функция:

   Vec2   HC   знак равно )  lli   (  C  ,   C  .   плюс   (  0  ,     ),   Z  ,  ИКС );   // C проецируется на горизонт Z - X  двойной  dyC  знак равно   C  .   y   -   HC  .   y  ;   // расстояние по оси Y в пиксели экрана между C и его проекцией  двойной  yScale  знак равно   5.0  ;   // это определяет, какая высота отображается как «уровень» для зрителя  двойной  yFactor   знак равно  dyC   /   yScale  ;   // насколько нам нужно масштабировать мир-y, чтобы получить screen-y    

Мы можно установить все это одновременно, мы устанавливаем X , Z и C координаты экрана, что означает, что все это уже будет доступно. к тому времени, когда мы начнем рисовать.

Затем мы обновляем наш get () функции на, чтобы учесть высоту:

   Vec2  получать  ( двойной Икс ,  двойной  y  ,  двойной  z  )   { если  (  Икс  ==   0   &&   y   ==   0   &&   z   ==   0  )  возвращаться  C  ;   // начинаем так же, как и раньше, которое покрывает y == 0   Vec2   px   знак равно  лерп   (  C  ,  ИКС ,   stepToDistanceRatio   ( Икс ));   Vec2   pz  знак равно  лерп   (  C  ,   Z  ,   stepToDistanceRatio   (  z  ));   Vec2  земля  знак равно )  lli   (ИКС ,   pz  ,   Z  ,   px  );  если  (  y   ==   0  )  возвращаться земля ;   // если это не так, наша отметка - это вертикальное смещение от плоскости земли,  // с участием высота, масштабируемая на основе того, насколько близко находится наша точка плоскости земли   // к горизонту (X - Z), а также насколько близко находятся наши точки схода.   // мы слева или справа от нашей центральной линии?   логическое   inZ   знак равно  (земля . Икс  <  C  . Икс );   // определяем наш первый коэффициент масштабирования высоты, основываясь на том, как   // приближаемся к Z или X, в зависимости от того, с какой стороны   // центральная линия находится координата нашей базовой плоскости.  двойной  RX   знак равно  inZ  ?   ( земля.  Икс  -   Z  .  Икс )   /   (  C  . Икс  -   Z  .  Икс)  :   ( ИКС.  Икс  -   земля.  Икс )   /   (ИКС .  Икс  -   C  .  Икс);   // затем определите второй коэффициент масштабирования высоты на основе   // насколько близко координата нашей базовой плоскости к горизонту.   Vec2   на оси   знак равно  lli   (  inZ  ?   Z  :  ИКС  ,   C  ,  земля ,  земля .   плюс   (  0  ,   12  ));  двойной  ry   знак равно  ( земля .   y   -   HC  .   y  )   /   (  на оси  .   y   -   HC  .   y  );   // наша конечная высота экрана - это мировая высота, умноженная на   // yFactor (который является высотой, если координаты x / z   // были равны нулю), умноженному на два масштабных коэффициента.  возвращаться земля. минус  (  0  ,   rx     ry     y     yFactor  );  }    

А теперь мы можем нарисовать правильный двухточечный рисунок в перспективе:

  пустота  drawSomeGeometry   ()   {  Vec2    п  знак равно   { получать  (  0  ,   0  ,   0  ),  получать  (  1  ,   0  ,   0  ),   получать (  1  ,   0  ,   2  ),  получать  (  0  ,   0  ,   2  ),   получать (  0  ,   7  ,   2  ),  получать  (  1  ,   7  ,   2  ),   получать (  1  ,   7  ,   0  ),  получать  (  0  ,   7  ,   0  ),   }   // ... а затем несколько линий между этими угловыми точками ...  }   

Дает нам следующий «рисунок» в трехмерной перспективе:

image-20210604105357855

Выглядит неплохо! Но это только двухточечная перспектива. Трехточечная перспектива усиливает неевклидовость, также делая высоту осью «фиксированного расстояния до бесконечности». Если вы зашли так далеко: все может стать действительно странным!

Трехточечная перспектива

Превращение нашей высоты в экспоненциальное измерение дает нам этот восхитительный мир космоса:

image-20210604155438583

Теперь у нас есть три точки схода, которые никогда не могут быть достигнуты (кроме округления пикселей ) и там, где в двухточечной перспективе, по крайней мере, у нас есть параллельные линии для наших вертикалей, этого больше нет: все вертикали теперь сходятся в Y. Итак, давайте напишем get3 () функция для вычисления экрана координаты с использованием трехточечной перспективы.

Прежде всего, давайте набросаем, как получить нашу трехмерную точку в таком пространстве:

image-20210604155438583

По сути, мы делаем то же самое, что и для двухточечной перспективы, дважды: строим точку XY из ее X и Координата оси Y, затем мы делаем то же самое для точки YZ, а затем находим пересечение между линией XY – Z и линией X – YZ. Вот и все, мы закончили. В коде:

   Vec2   get3   (двойной Икс,  двойной  y   ,  двойной  z  )   { если  ( Икс  ==   0   &&   y   ==   0   &&   z   ==   0  )  возвращаться  C  ;   Vec2   px  знак равно   lerp   (  C  ,  ИКС,   stepToDistanceRatio   ( Икс));   Vec2   pz   знак равно    lerp   (  C  ,   Z  ,   stepToDistanceRatio   (  z  ));  если  (  y   ==   0  )  возвращаться  lli   ( ИКС,   pz  ,   Z  ,   px  );   Vec2   py  ( знак равно   лерп   (  C  ,   Y  ,   stepToDistanceRatio   (  y  ));   Vec2   YZ   знак равно  lli   (  Y  ,   pz  ,   Z  ,   py  );   Vec2   XY   знак равно  lli   (  Y  ,   px  ,  ИКС ,   py  );  возвращаться  lli   (  XY  ,   Z  ,  ИКС ,   YZ  );  }   

Итак, как это выглядит для луча 1x7x2, который мы нарисовали ранее с использованием двухточечной перспективы?

image-20210604163158179

Это не здорово ... это правильно, но также довольно ужасно ... Причина этого в том, что, используя экспоненциальное затухание с основанием 2, мы подойти к Y действительно быстро. Однако мы можем контролировать это, изменив значение, которое мы используем в качестве экспоненциального основания.

  двойной  yBase  знак равно   1. 34  ;  двойной  stepToDistanceRatio   (двойной шаг)   { возвращаться  stepToDistanceRatio   (  2.0  ,  шаг);  }   двойной  stepToDistanceRatio   (двойной база ,  двойной шаг)   { возвращаться  1.0   -   1.0   /   pow   ( база ,   шаг);  }   Vec2   get3   ( двойной Икс,  двойной  y  ,  двойной  z   )   { если  (Икс  ==   0   &&   y   ==   0   &&   z   ==   0  )  возвращаться  C   ;   Vec2   px   знак равно  лерп   (  C  ,  ИКС,   stepToDistanceRatio   ( Икс);   Vec2   pz   знак равно  lerp   (  C  ,   Z  ,   stepToDistanceRatio   (  z  ));  если  (  y   ==   0  )  возвращаться  lli   ( ИКС ,   pz  ,   Z  ,   px  );   Vec2   py  знак равно   lerp   (  C  ,   Y  ,   stepToDistanceRatio   (  yBase  ,   y  ));   Vec2   YZ  знак равно   lli   (  Y  ,   pz   ,   Z  ,   py  );   Vec2   XY   знак равно  lli   (  Y  ,   px  ,  ИКС ,   py  );  возвращаться  lli   (  XY  ,   Z  ,  ИКС,   YZ  );   }    

Это немного улучшает внешний вид:

image-20210604160406689

Вот и все, мы успешно реализовали строгую трехточечную перспективу!

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

image-20210604163834688 Нет (почти) нет прямые линии в экспоненциальном пространстве

До сих пор мы рассматривали возможность рисования прямых, выровненных по осям линий, и это может обмануть вас, заставив думать, что экспоненциальное пространство очень похоже на Евклидово пространство, что было бы огромной ошибкой. На самом деле существует только два вида линий, которые выглядят прямыми в строгой двух- или трехточечной перспективе: линии, выровненные по осям, и перпендикуляры к осям. Все остальное, по сути, представляет собой кривую, которую мы можем увидеть, если попытаемся соединить некоторые точки, которые «должны» лежать на прямой линии, если это было евклидово пространство. Например, построим линию y = x / 2 на плоскости XY и посмотрим, что произойдет:

  пустота  drawCurveIllustration   ()   { для  (двойной я знак равно )  0  ;  я <  25  ;  я  + =   1.0   /   3.0  )   { круг (получать  ( я, я  /   2  ,   0  ));  }  }   

Мы ожидаем, что это будет прямая линия, возможно, указывающая в неожиданном направлении, но ...

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

Итак, давайте выберем путь наименьшего сопротивления:

  пустота изгиб  (  Vec3   p1  ,   Vec3   p2  )   {  двойной  шаги   знак равно  Макс  (  8.0  ,   p2  .  минус (  p1  ).   mag   ());   // т.е. «8 или более шагов»   beginShape   ();  для (двойной я знак равно  0  ;  я <=   1  ;   я + =   1.0   /   шаги  )   {  вершина   ( получать  (  лерп   (  p1  ,   p2  ,  я )));  }   endShape   ();  }   

А теперь давайте еще лучше рассмотрим это экспоненциальное поведение:

 пустота  drawCurveIllustration   ()   { изгиб  ( новый  Vec3   (  0  ,   0  ,   0  ),  новый  Vec3   (  25  ,   5  ,   0  ));   // y = 0. 34Икс изгиб  (  новый  Vec3   (  0  ,   0  ,   0  ),  новый  Vec3   (  33  ,   12  ,   0  ));   // y = 0,5 Икс изгиб ( новый  Vec3   (  0  ,   0  ,   0   ),  новый  Vec3   (  33  ,   17  ,   0  ));   // y = 0. 90Икс изгиб  ( новый  Vec3   (  0  ,   0    ,   0  ),  новый  Vec3   (  25  ,   33  ,   0  ));   // y = x  изгиб  ( новый  Vec3   (  0  ,   0  ,   0  ),  новый  Vec3   (  17  ,   33  ,   0  ));   // y = 1. 33Икс изгиб (новый  Vec3   (  0  ,   0  ,   0  ),  новый  Vec3   (  17  ,   25  ,   0  ));   // y = 2x   изгиб (новый  Vec3   (  0  ,   0  ,   0  ),  новый  Vec3   (  5  ,   33  ,   0  ));   // y = 4x}   

Это показывает нам следующие кривые:

image-20210604164835460

Теперь мы можем точно увидеть, насколько неевклидово это экспоненциальное пространство: функции, которые являются расходящимися прямыми линиями в евклидовом пространстве, становятся сходящиеся кривые (сходящиеся в точках схода), за исключением линий, выровненных по оси и перпендикулярных осям, которые удерживают n их прямолинейное поведение.

Это означает, что даже простой куб большую часть времени будет выглядеть «совсем не как куб». Конечно, если мы создадим простой куб с длиной ребра 2, используя координаты, выровненные по оси, он будет выглядеть ничем не иначе по сравнению со стандартными трехмерными проекциями:

image-20210604171151334

Но если мы немного повернем тот же куб по трем осям ... ну ...

 пустота рисовать  ()   {  Vec3   центр   знак равно новый  Vec3   (  1  ,   1  ,   1  );   drawRotatedCube   (  2  ,   центр  ,   0. 17  );  }  пустота  drawRotatedCube   (двойной край,   Vec3   центр  ,  двойной угол )   {  Vec3    баллы   знак равно   getCubePoints   ( край);  для  (  Vec3  п :   баллы  )   { п .   rotateX   ( центр ,  угол );  п .   rotateY   ( центр ,  угол );   п.   rotateZ   (центр ,  угол );  }   drawCube   (  баллов  );  }    

… все становится очень весело, очень быстро!

{F50C2277-E240-420A-ADBD-9CC788AF0B1F}.png

И помните: в мировых координатах ни одно из этих ребер на самом деле не искривлено, это все прямые линии с совершенно прямыми углами между ними. Экспоненциальное пространство просто полностью игнорирует это.

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

Leave a comment

Your email address will not be published. Required fields are marked *