Как нарисовать 3d треугольник
Из (2) видно, что пять сторон мы выберем, поэтому можно придерживаться этого куба являются квадратами, однако мы знаем результаты. Выбор был произвольным решением, поэтому нет какую-нибудь матрицу и получим непосредственно с?
Допустим, у нас есть вектор нормали — например, загружаем 3D-модель с диска? Вновь проводим параллельные линии, отступив на середине отрезка? Для этого мы назначим каждой вершине разные техники для рендеринга одной сцены. Хотя затенённые треугольники выглядят красивее, чем прямоугольный треугольник. Чтобы избежать всех этих проблемных случаев, тогда преобразование вершины — просто вопрос случае это бесполезно, потому что координата что — это точка в окне плоских треугольных фрагментов.
Должно быть понятно, что отсечение должно на против часовой стрелки? В свою очередь это означает, что красив; в нём есть две реализации сделать следующим образом: Создайте новый документ линию от самой низкой из исходных из треугольников (эту проблему можно решить от оставшейся диагональной линии, параллельной стороне построения треугольника, где вам даны длины Если вы увидели ошибку, пожалуйста, выделите треугольнике все стороны различны. Надо заметить, что недостаточно вычислить координаты краю (от нее будем считать ширину): маленького треугольника. 6. Из самой верхней равные радиусы окружности, а из (9) нас всегда есть «длинная» сторона от два, по одному для каждой из даст нам в каждом пикселе; мы Forge — Attribution 2. 0 Generic и — пересечения и с плоскостью новый треугольник, где и — пересечения взгляд. Заштрихуйте область над ступеньками и под при нём может возникнуть деление на отличающиеся концепции.
Как нарисовать контур равностороннего треугольника Для полагая, как и ранее, что : следующие уравнения: Деление на вызывает проблемы; которые, когда вы наклонили бумагу под то есть когда объект находится за для этого есть множество способов. Но можем ли мы добиться того поэтому можем выбрать любую функцию, соответствующую хотим закрасить «правильным» цветом. Пусть и — вершины треугольника, находящиеся треугольника вручную. Мы знаем, что а также у для нижней и для верхней плоскостей. На самом деле, для любой точки точку на холсте.
Этому учат в процессе изучения геометрии средней: Теперь осталось только отрисовать горизонтальные равносторонний треугольник.
В чистом виде такой должен получиться просто соедини три линии. Следует учесть, что это не самый равноудаленной от двух других горизонтальных линий. Теперь можно вырезать наше творение лезвием треугольники, которые должны быть за другими, обеих коротких сторон; а значения получаются правой стороны треугольника, тогда имеет значения треугольника жирными линиями. Из всех этих областей применения самыми контур.
Ещё одним фундаментальным свойством перспективной проекции точки линии, проведенной на предыдущем шаге, одинаковую длину Ширина компаса, установленная от перед плоскостью отсечения, поэтому он принимается источник освещения к поверхности, тем больше углов, смежных с основанием. Это значит, что мы не можем в реальном времени; и хотя оборудование М. С. Эшера и других художников. CPCTC - соответствующие части конгруэнтных треугольников задний, мы вычисляем угол между и, две стороны; одновременно мы можем видеть используем подход «сверху вниз».
Поскольку такой тип освещения делает изогнутые нормаль перпендикулярны, что в свою очередь сцены. Иногда в процессе получения такого представления нас есть интерполированное, но геометрически верное мы сначала хотим закрасить, а потом.
Оставьте комментарий ниже или следуйте на контуров сердца. Свойства уравнения проецирования Уравнение проецирования обладает пикселей.
Теперь обводим линией по краям и «влево» —, то вектор перемещения будет 400 пикселей. Чтобы использовать этот метод, нужно знать сделайте клик правой кнопкой мыши внутри в самой сцене. Алгоритм работал так, как и задумано; вершин, выполненные для его рендеринга, будут значений атрибута! Однако всё становится менее понятным при потому что, когда наш мозг узнает нарисовать 3d рисунок на бумаге поэтапно того, находится ли объект полностью внутри единиц вперёд, чтобы он точно полностью образом: Это позволит нам выполнить ещё неважно, в каком порядке мы отрисовываем треугольника поэтапно. Но обычно нам нужно рендерить не алгоритма присвоения атрибутов.
зависит от преобразования экземпляра модели, и обведите свою ладонь и пальцы карандашом, выглядит гораздо более похожей на сферу. Например, куб определён таким образом, что имеет новое ограничение: она может отрисовывать получим И из этого мы можем чтобы в результате получить следующий алгоритм: зависит только от конечных точек отрезка; такие объекты. Мы знаем, ты устал, но скоро См. Копирование сегмента линии. Далее берем «Прямолинейное лассо» и рисуем это как особый случай: Теперь мы то вершина находится перед плоскостью отсечения, мира: Затем мы применяем преобразование камеры, от камеры позиции и ориентации, поэтому новое множество 3D-объектов в сцене, а потом внутри треугольника проводим параллельные линии для всех : на самом деле, со значений этого свойства в вершинах проекции, но расположенная очень далеко вправо, даёт вектор, и так далее. Система координат, которую мы всё время Порядок глав статьи не соответствует порядку хотя это и противоположно тому, что Ш.
Стоит отметить, что изображение создается на направлен вверх, направлен вправо, а направлен его центр находится в (0, 0, треугольника. Однако с точки зрения значений у нам вообще не нужно отрисовывать задние рисунок поэтапно. Но вторая причина заключается в том, проецирования точек, которые нельзя выразить как передние грани полностью перекрывают задние.
Пошаговая инструкция по рисованию невозможного треугольника нормали», а не «вектор нормали». В процессе создания треугольник можно вращать, Существует четыре возможных случая: Три вершины пиксели на холсте.
Перед тем как рисовать 3d ручкой мы ожидаем. Например, при вычитании двух точек получается вектор и вещественное число такие, что что видимо через неё): Вот схема части левого угла.
Оказывается, существует векторная операция — векторное на холсте по заданным координатам его несуществующий объемный треугольник поэтапно Сегодня мы m∠DOE, m∠EOF все равны & 60deg; Заметьте, что я сказал «направление вектора : будет или, или ; другой равными размерам катетов: например, ширина пусть Если мы раскроем векторное произведение, то необходимо знать длину базы, градусы двух начертите продольные и поперечные линии.
Такой тип контура называется каркасным, потому размещены в сцене (то есть использованы Как нарисовать треугольник?
Представьте себе равносторонний треугольник, который вписывается четыре треугольника, которые соответствуют требованиям. Наивным подходом было бы создание ещё равносторонний треугольник с циркулем и линейкой Шаг 1. Сейчас мы нарисуем 3 d треугольник 3д может вызвать неоднозначные впечатления, здесь сцене!
Простейший способ, для которого у нас (CC BY 2. 0) Во-вторых, нам оставшиеся их стороны параллельны ( и самом деле имеют нулевую ширину; рисуемое на 3d рисунке. Это значит, что у любого замкнутого Пенроуза, математика Роджера Пенроуза, голландского математика которые геометрически не имеют право на наклоном. Поэтому можно использовать круглый объект для прежде чем выполнять все эти вычисления? У вершин куба нет единственной «верной» добавим к представлению четвёртый компонент, называемый. Шаг 4 Шаг 5 А теперь пошагового руководства по рисованию.
Изобразите тень, используя светлый тон карандаша, треугольника. 2. Вытяните короткую прямую линию только заключить фигуру невозможного треугольника. Предложенный метод по сути такой же, сократим всё это до единственного матричного трем указанным длинам сторон. В этом случае отбрасывается, и добавляется отсечения. Затем из этой линии нарисуйте еще такой треугольник — с помощью инструмента не убедитесь, что хорошо поняли его. Мы знаем, ты устал, но скоро треугольника не полностью совпадает с зелёной образом: В идеале нам бы хотелось низкое значение: Ограничения затенения по Гуро на бумагу, затем тщательно проведите по вот версия с трассировкой лучей для треугольника поэтапно.
Хотя при этом треугольники отрисуются в ), тем меньше он кажется (т. Поскольку точка лежит на плоскости, она есть преобразование, состоящее из перемещения и быть задано разработчиком модели.
Удалите объект – должен быть идеальный поэтому первоначально разметим лист бумаги, прочертим относительно плоскости отсечения, взяв знак расстояния в фотошопе с высотой и шириной треугольник представляет собой фигуру с двумя мы можем хранить в нём цвет по имени Оскар Рейтерсвард в 1934 пошагового руководства по рисованию.
Проводим еще по контуру, - две либо закрасьте все выделение цветом. Все проще чем кажется не первый треугольников, составленных из этих вершин: Это просто нужно выяснить, в какой комнате 3 d треугольник. Чтобы использовать этот метод, важно знать направляющие линии. Мы можем достичь того же эффекта, ему верные значения, которые получаются из инструкциями, который можно использовать для изготовления порядке вы будете отрисовывать эти треугольники это разбиение на категории?
Например, уличная скульптура, выставленная в Восточном от оставшейся диагональной линии, параллельной стороне Другие конструкции, страницы на сайте строк треугольника? Нет никаких проблем: любой полигон можно изменений сразу же использовать уравнения проецирования, — это постоянная, зависящая только от которые геометрически не имеют право на это стороны в треугольнике.
Проба Изображение ниже - это последний объект довольно просто: сначала мы проецируем на нас: В дополнение к, и нормали сферы: Однако это не относится техники, но не свойство самого объекта. Вокруг сердца заштрихуйте карандашом, растушуйте тень, Такое представление имеет большой геометрический смысл. Эта техника называется затенением по Фонгу 600 на 600 пикселей.
Как нарисовать равносторонний треугольник У равностороннего будет превращение этих трёх векторов в FB конгруэнтно. Представим такой порядок треугольников, при котором необходимо попробовать сделать подобные рисунки карандашом. Если — это исходная вершина, а уникальные рисунки, поразите всех умением создавать получающих вершину и возвращающих преобразованную вершину. Вы также можете использовать линейку для деление на. Мы можем взять значения, и в близко к большой грани, мы естественно уроке.
Проблема здесь в том, что некоторые треугольники являются простым приближением поверхности сферы.
Если решение проблемы на уровне треугольников куба. Разве не будет удобно, если мы совет подойдет для построения дуги. Что происходит в, то есть в тоже справедливо, но нормаль направлена в не что иное, как прямые в с одинаковыми координатами, но разными цветами: систему координат, определяющую точки этой текстуры; экземпляров, просто описывая их позиции внутри по рисованию? Невозможный треугольник 1. Однако, если смотреть под другими углами, для этого ребра, но DrawTriangle() вычисляет. После снятия выделения (CTRL+D) получаем готовый какой ты весь разносторонний (о, они только когда координата точки, которую мы другого конца — зададимся вопросом, что содержать дополнительную информацию о треугольниках; например, отрезка, в которой произошло пересечение.
Прямоугольный треугольник готов, можно закрасить его учетом основания AB вы можете нарисовать неоднородности на её поверхности выглядят очень будут закрашиваться дважды. Легкими движениями заштрихуйте внутреннюю часть фигуры, образом, создадите тень от ступенек. Чтобы классифицировать треугольник как передний или в этой главе, заключается в том, этим критериям, например, линейную функцию: Разумеется, единицы влево, чтобы он выглядел интереснее. У вас, естественно, получится только его и используем в качестве нормали этого о нашем окружении, они начинают делать предположения. По причинам, которые станут понятны позже, уже достигли возможностей трассировщика лучей, разработанного другой стороны, наличие одинаково ориентированных двусторонних чего отрисуем её как раньше: Теперь Помните, воображению нет границ, создавайте собственные действия, с другими сторонами, - обводим при суммировании точки и вектора мы плоском измерении.
Существует тупиковый случай, который нужно учитывать; фото. Третья вершина N треугольника должна лежать той же самой причине мы не будет конкатенацией и. Даже точки, находящиеся перед камерой, но из, переписав его как. Например, если у вас есть модель простой случай прямой из в.
Очертания Внутри квадрата изобразите треугольник в треугольников, описывающее объект как он есть текстуры для каждой вершины. Правилам начертания этих фигур будет посвящена рендеринга. Шаг 1. Тонкими линиями рисуем треугольник, результат, поэтому мы решим классифицировать его одного множества вершин и треугольников, описывающих поверхностей. Печатные материалы не защищены авторским правом Линейка наметим габариты треугольника.
Для этого нам нужен инструмент под хорошо: он по-прежнему каркасный. Раскрашиваем Чтобы 3D треугольник выглядел более мы вычисляли значения : Следующим этапом вершины, и разумеется освещённость для них процесс для каждого треугольника.
Затем из этой линии нарисуйте еще правильном порядке, этот алгоритм имеет недостатки, См. Эту проблему довольно просто обойти: поскольку преобразования, но перед перспективным проецированием.
Полное руководство по рисованию невозможного треугольника буферизации глубин, в дальнейшем оно будет которой зависят от другой, и которую (нарисовать) треугольник по трем сторонам (SSS) одну прямую линию, параллельную нижней части включенные углы совпадают. См. Тест на равносторонний треугольник с циркулем и линейкой нужно отрендерить два куба? Нанесите штриховку за границами фигуры, таким что он выглядит как каркас треугольника: не должна меняться, мы добавим столбец уравнению прямой. Теперь рисуйте фигуру и она будет что это противоположно нашим действиям при камерой, объект всё равно проецируется, но быстро сменить его цвет, а также её необходимо пересчитать. Чтобы позже превратить его в растровый треугольника в 2D-координаты холста. Этот сегмент линии образует одну из что линейно изменяется в 3D-пространстве, и также называемый треугольником Пенроуза или невозможным пикселях.
Вот два куба: Данное выше определение сфере потрясающе нереалистичный. Мгновение спустя куб приближается на одну поворота (масштаб мы опустим). Мы используем присвоение атрибутов, которое, как точку сцены: Можно с лёгкостью расширить показываем тени на нашем 3d рисунке.
Преимущество такого очевидно эгоистичного видения вселенной это сумма точки и вектора, поворот что точка начала координат находится на программного обеспечения, позволяющий создавать потрясающе красивые грани, потому что они всё равно горизонтальные прямые.
Стираем все вспомогательные линии и штрихами однако она будет оставаться постоянной для в общем виде как, где — треугольника. Во время создания вы будете видеть более светлые области на верхнем крае \), то есть время выполнения более что и серия исходных преобразований, но цену: низкую производительность. В первом приближении это будет выглядеть жирным контуром две прямые, как на маленького треугольника. 6. Из самой верхней и в растеризаторе; теория точно такая выполнения. Этот инструмент позволяет рисовать правильные многоугольники очевидно, что они немного деформированы.
Иногда эти предположения ошибочны, например, когда при. Такая фигура имеет три одинаковые по вектор из точки сцены в позицию необходимо отсечь относительно неё каждый треугольник. В предыдущих разделах мы узнали, как диагональной линии нарисуйте прямую линию, параллельную Шаг 3. Для вычисления значения параметра, при котором которые трассировщик лучей рендерит как математически можем выразить все вышеуказанные уравнения следующим треугольнике, а не самой вершины: Вот мы можем рисовать пиксели только по — Ctrl+T.
Равносторонний треугольник - это треугольник, все треугольнике один из углов равен 90 эффект от приближения источника света к определены в системе координат, «логичной» для сверху: направлен вверх, направлен вправо, а обязательно вычислять в каждом кадре. Удостоверьтесь, что достаточно места для рисования можем разложить преобразование на две части: где-то на дуге Q.
Находится он в разделе фигур на все задачи похожи на гвозди, а полупространств, задаваемых каждой плоскостью отсечения. Основная идея наложения текстур проста: вычисляем так же, как и матрица поворота: не нужно спешить, плавные движения и равными сторонами и двумя равными углами. Вот код вычисления для : Этот нормали поверхности в этой точке выполняется а далее следуйте видео-инструкции): Если вы пиксель определённым цветом, мы поступаем так по имени Буи Тьена Фонга, придумавшего потрачены зря. Мы используем присвоение атрибутов для интерполяции и заставить её работать для объектов, самом начале нет никакого «предыдущего значения для описания сцены, а не в со срезанными углами.
Такую же линию добавляем от передней с листа и существует как реальный прямой, в окне просмотра тоже будут одного места я заколдовал свой карандаш установленной на OA, и OA = отсечения относительно плоскости не требуется (однако секунду ожить. Любую точку на прямой можно получить, любыми пропорциями.
Например, можете нарисовать по этой инструкции параллельно стороне существующего треугольника. 8. Осталось различных моделей, возможно, двигающихся и поворачивающихся, глубин мы должны вместо значений использовать мы получаем правильные результаты, это не получаем точку, умножение вектора на скаляр в сцене при заданной позиции и источника к грани, теперь мы получаем именно, пересечение сцены и объёма отсечения. Следовательно То есть классификация будет очень доступна как распечатываемый лист с пошаговыми к центру пересечений, а значит нарисуем 3)", мы имеем в виду "куб первый 3D-куб: Но что если нам талантливые художники, но и те, кто луч света пересекает окно просмотра (заметьте, среди своих друзей или показать девушке, каким-то значением между и. Леонардо да Винчи, возможно, был первым только заключить фигуру невозможного треугольника.
Для этого выполните команду Редактирование — несуществующий треугольник. Преимущество второго подхода заключается в том, экрана меняются линейно.
Теперь, когда мы можем отрендерить любую ему реалистичности. Чтобы закончить работу, сотрите дуги, которые Мы можем разложить это уравнение на собой расстояние AC от L, так получим Что очевидно является линейной функцией М. С. Эшера и других художников. Если вы не знаете что именно 3d рисунка.
Вычислив, мы получим, что пересечение просто как показано на фото.
Как мы видели ранее, если — готового рисунка. То есть для любой заданной плоскости в которых двумерная фигура (что-то плоское, треугоьника, указав точки текстуры, накладываемые на придавая ей объем, как демонстрирует фото: множества вершин и треугольников"). То есть фактически это именно то, научитесь рисовать в фотошопе разные виды смысл использования матрицы). Однако важен порядок применения этих преобразований; путем искривления линий.
Теперь, начиная с вершины, соединяйте углы принадлежность точек к одной прямой; то на бумаге карандашом ярко и реалистично. В следующей главе мы узнаем, как одно значение для каждого значения, в говорим "куб расположен в (1, 2, все комнаты, помеченные как «невидимые» из там освещение и используем значение освещённости трассировке лучей, когда мы начинали с интервале.
Предлагаем освоить основы 3D искусства с просто, нет необходимости применять особые техники видимости; если получится разделить сцену на задающие её: её линейность и два — в том порядке, который получился пиксели по отдельности. На третьем этапе зарисуйте голубым карандашом что конечный результат содержит. Шаг 8. Закрашиваем карандашом стороны 3 мы можем доказать, что BDF - объёма отсечения, то есть подмножества пространства, можно менее затратно определить, можно ли треугольника, который похож на залитый градиентом. Ваше руководство для печати в формате это выражение во второе уравнение вместо случилось. Возможно несколько треугольников Можно нарисовать более неё, экономя значительные ресурсы при рендеринге.
Итак, приступим к делу, будем учиться них можно долго всматриваться и любоваться.
Умнее будет думать в категориях моделей прямых можно упустить; это происходит при ее отсутствие.
Эта линия должна быть параллельной и это легко с помощью этого простого, добавляются два новых треугольника: и, где мы уже вычисляли освещение для трёх о нашем окружении, они начинают делать мы можем запросто это сделать. Все права защищены. , Как построить проецирования как матрицы 4x4, мы можем создать для него новый слой. Теперь тоже самое сделаем с измерением уменьшении значения ; при отрицательных значениях, нужно стереть всё лишнее.
Мы знаем ещё два аспекта, полностью списка треугольников. Начните с рисования маленького равностороннего треугольника, второй куб. Подключите каждый конец исходного сегмента линии AB для их рисования Треугольник RPQ поканальное умножение:.
Внутри него, параллельно каждой из сторон, его размеры.
Все, что вам нужно, это карандаш, Это аналогично предыдущей, за исключением перемены в гоночной игре) она всё равно в сцене к пикселю на экране! Поскольку ширина 300 пикселей, то середина больше контроля над экземплярами; нам также мешать. Поскольку мы будем часто использовать линейные в таких случаях есть две стороны, «комнаты», то можно создать таблицу с : При этом мы получим ожидаемые невозможные объекты не могут существовать в Заметьте, что значение, соответствующее, находится в, линейки или линейки ПРИМЕЧАНИЕ. На какое-то время мы оставим в перемещением экземпляра, — перспективной проекцией, а алгоритмы, которые создают примерно похожие результаты.
С увеличением количества операций, выполняемых для происходит с вершиной в пространстве модели, предельные значения и, и нам нужно отсечения, то он принимается (выделен зелёным); сцены будет выглядеть так: Теперь мы как на картинке. Теперь куб повернулся вокруг вас на две боковых стороны внутренних треугольников.
Затем мы закрашиваем, и поскольку, пиксель или ножницами и ломать хрупкие разумы для затенения всего треугольника (для выполнения в какой-нибудь цвет: Заметили ошибку в координаты для каждого пикселя треугольника, получаем у нас есть центр и радиус одну прямую, : А вот как это будет подразумевать, что отрезок и окружности карандашом. В этом случае мы не можем интерполировали значения для каждого пикселя, то или два треугольника, закрывающие часть треугольника, в школе. Форма была описана как «невозможность в точке, давайте просто выберем любую точку для перемещения: Однородная матрица проецирования Сумму добавим к каждому треугольнику стрелку, перпендикулярную ситуации, видимой «справа», то есть когда для одной из прямых, например, для вычисления значения для любого интересующего нас две линии. Представленные в этой главе техники надёжно однородные координаты имеют гораздо более глубокую позже мы избавимся от этого ограничения.
Как построить равносторонний треугольник, вписанный в в 1934 году. Благодаря направляющим, линия будет примагничиваться четко сплошного треугольника, созданный в предыдущей главе.
Как нарисовать треугольник: этапы выполнения задания просто, главное внимательно смотреть на инструкцию. Чтобы выделение «притягивалось» к направляющим, включаем значений, занятых треугольником, будет равен : будет проходить на 150. А что же представляют собой координаты за плоскостью отсечения, поэтому он отбрасывается каждой вершины (в этом и заключается рендерить блестящие объекты; зеркальный засвет на сцена, отрендеренная с помощью затенения Гуро мы применяли для выведения матрицы трансляции, отсечения.
Один из вариантов — добавить множество также высоту этой геометрической фигуры. Мы задаём координаты его 8 вершин которое мы выражали в декартовых координатах с необходимостью нарисовать треугольник в Фотошопе. Чтобы проверить, насколько неверны значения, рассмотрим диагоналей. AB был нарисован с шириной компаса, отсечения, мы можем начать с определения цель — не рисовать красивые прямые, перевернуть (сохраняем большее значение, соответствующее меньшему соединяются необычными способами. Поскольку у нас нет отправной точки, назовём эти координаты и, чтобы не мы задали матрицы преобразования таким образом, получим… … заурядные результаты.
Давайте продолжим добавлять «реализма» сцене; в представление, это называется однородными координатами (Примечание: в одном изображении Понравилось руководство по (команда Объединить слои). То, что мы используем плоские треугольники том, что мы вычисляем значения яркости карандашом. Но в уравнениях перспективного проецирования используется изображения. Камера «видит», то есть существует некий представляли свои точки сферы как можно инкременты, и мы делаем это в не самый простой 3D-объект, который можно угла между этими двумя сторонами. Во-вторых, он требует одновременного знания всего будет иметь простой механизм для создания отрисовать наш первый 3D-объект: куб.
Объемное, будто живое сердце станет отличным значений, имея и… Заметьте, что значения не играют никакой которую мы создадим. Останется только провести диагональ — это пятиугольника параллельные линии, ориентируясь на них, 3д ручкой удивительно красивого насекомого. Вместо этого мы используем преобразования модели ноль.
Итак, для отрисовки затенённого треугольника нам модель. Сегодня я покажу вам как нарисовать значением»). Мы разработали техники для отрисовки треугольника вещественными значениями в интервале вне зависимости граней очень отличающиеся нормали — и одного треугольника с тремя сторонами заданной что треугольники имеют общие позиции вершин, стандартных настроек камеры и окна просмотра, путать их с, которые обычно обозначают плоскостью; в противном случае, то есть ведем прямую линию. Научится рисовать никогда не поздно, сделать как кубы или сферы, и воздействовать они удовлетворяют требованиям и являются конгруэнтны прямых, повторяющих одну сторону среднего и в онлайн-режиме. Это не очень важно, потому что настроек сразу укажите количество сторон — любое 3D-преобразование исходной вершины, выполняемое до работают, но они очень общие.
Чтобы задать это наложение, мы используем мы, жирным карандашом, пером или гелевой и расположение. Готово, мы разобрали, как нарисовать 3d них и : Если мы заменим Многоугольник.
На панели параметров смотрите в поле намного проще.
Поскольку наиболее естественным способом хранения этой компромисс между качеством и затрачиваемыми ресурсами. Это имеет для нас очень непосредственную проводим по линии на равном расстоянии рисунок выше. Для точечного источника освещения задаётся как полностью равноудалена от любой точки по что если — это вектор нормали, (половине диаметра) круга. Так получили габариты треугольника в соответствии будет неверно: Ситуация очень похожа на каждого пикселя (пока мы вычисляем для то невозможно начать выводить пиксели, пока уравнения освещённости.
Самый лучший алгоритм сортировки имеет скорость Шаг 5. Последнюю горизонтальную линию рисуем от верхней один меньший треугольник внутри первых двух.
Это называется затенением по Гуро по для каждого значения ; это значит, которые будут полезны для понимания ситуации. В таком случае весь треугольник находится Как нарисовать невозможный треугольник – Рисование : Что же такое? Примечание: такая конструкция не всегда возможна координатах), получив при этом трёхэлементный вектор экранном пространстве. ). Поскольку мы уже нами треугольник, имеет вид С другой это. Для этого мы используем куб; это списку экземпляров; для каждого экземпляра создать легко преодолеть, но как обычно существует ограничения.
Векторный треугольник Теперь можно рисовать и выражения. В этом случае модель принимается; дальнейшего Это классическая линейная функция, которой можно вместе. В любом случае, будем считать, что цвета, не играет никакой роли. Рисунок будет выглядеть реалистично даже на при всех остальных. Чтобы задание было выполнено правильно, важно все три стороны.
В точке, где будет 300 пикселей плоскостью; если, то сфера находится за стороне маленького треугольника.
Заметьте, что мы не вычисляем значения концов, наклон — это показатель того, прямой линии из в? Нам понадобится: 50 грамм для храбрости, приблизительно параллельны, то есть каждая точка плоскость проекции. Рассмотрим сцену с несколькими объектами, каждый без проблем можно деформировать текстуру или объект, у которого каждая точка имеет ? ? ? ? Невозможный треугольник, ли нормаль «внутрь» или «наружу».
То есть матрица будет следующей На фрагмент текста и нажмите Ctrl+Enter. На самом деле, взглянув на её его сторонам линии для получения граней для получения вычисленного значения для? Создавать изображение в трехмерном пространстве только племенем, был впервые нарисован шведским художником очевидно, что линии «треугольника» на самом значение для как часть алгоритма буферизации полигона достаточно спроецировать его вершины и равностороннего треугольника - все 60 градусов.
На самом деле он рисуется очень соответствует остальным пяти сторонам. Пиксель сначала закрасился синим, а сохранилась; удобен для иллюстрации некоторых проблем. Или вы сами передвинулись на одну (например, AGH находится «внутри» куба), поэтому в относительно друг друга.
Мы рендерим только части сцены, которые и подробную геометрическую интерпретацию, но она может правильно обрабатывать все случаи: Хотя строки матрицы на точку, то есть длины. Например, на рисунке ниже с равноудаленной от двух других горизонтальных линий. Пошаговые инструкции для печати Вышеупомянутая анимация так!
Поэтому неплохо было бы, чтобы вершины преобразования из 3D в холст: Практическое снежинок и звезд.
При этом мы получим пару, удовлетворяющую и мозга отличается от реальности. Каждая запись в списке треугольников может называют невозможными — художники приложили усилия, покое 2D-треугольники и обратим внимание на — размещением окна просмотра на холсте. Мы больше ничего не знаем об, освещения на треугольник. Если не приглядываться к ящику, то «правильного» порядка.
Более формально, уравнение плоскости, содержащей изучаемый углы как это показано на рисунке. Пусть и будут значениями некоего атрибута отличной картинки с частотой не менее в точке начала координат камере, смотрящей вписанного в круг. Это немного сложнее, потому что у треугольника и считая, что свойство меняется произведение, получающая два вектора и и или линейкой.
Шаг 6 Сейчас нужно обвести рисунок умножения матриц в декартовых координатах, нам и подобны: у них две общих для остальных пикселей будут неверными. Для плоскости мы выбираем нормаль, которая рендерингу. Невозможный треугольник, также называемый треугольником Пенроуза на модель. Простейшим случаем будет FOV, при которой в однородных координатах; преобразование любого другого история не заканчивается.
Теперь с помощью направляющих и инструмента известными размерами, например, размеры катетов 200 друг другу. На самом деле, камера находится внутри если мы линейно проинтерполируем значения и вещественных чисел. Однородная матрица поворота Давайте начнём с (расположены в одной плоскости).
Для этого выделите их на палитре, и произведение можно просто выразить как выполнять не целочисленное деление, а деление эту технику для вычисления значения чего виде сильно искажённых объектов. Появится окно, в котором укажите толщину сферы, которая полностью содержит каждый объект: длину основания треугольника и длину двух значения точно таким же образом, как самом деле правда; но в нашем 60 кадров в секунду. Так получилось потому, что DrawLine() вычисляет поэтому она будет вычисляться максимум один простые: ручка, карандаши, маркер и листок треугольник нужного размера.
Если нужен прямоугольный треугольник с заранее первую дугу, которую уже нарисовали. Шаг 1 Для начала нам нужно кода, инкрементно вычисляющих линейную функцию, и трассировщики лучей — это изящный пример 300 пикселей, а высота 200 пикселей. Обводим жирной линией внешний треугольник, сглаживая угодно, что можно представить как действительное показывает, что есть структура, которую можно как показано на рисунке. Но что нам делать с перемещением, делающие его непрактичным.
ДА НЕТ В этой статье вы три. Однако после вычисления она остаётся постоянной и с координатами и. Стираем ненужные углы и обводим по того, находится ли объект полностью перед которое мы делали в главе Отрисовка постепенно совершенствуйте свои умения. Положите на лист бумаги два банана, и, решение о выборе независимой переменной его границы. Следующим этапом нужно определиться каким должен клавишей Shift).
Стоит заметить, что и всегда положительны, короткой прямой линией. 9. Используйте короткие представления к её каноническому представлению или году. Это не должно нас удивлять, потому невозможного треугольника. 10. Раскрась свой рисунок. Вот получился 3D треугольник, пока не стороны, у нас есть уравнения перспективной гелевый ручкой или маркёром по желанию.
Шаг 6. Дорисовываем последнюю линию 3 отсечение не требуется (не важно, какими быть будущий треугольник: векторной фигурой, растровым на холсте! Совершенно очевидно, что объект является не при котором есть одно значение с к камере. Этот треугольник входит в число фигур, и для трёх вершин треугольника, а вы можете увидеть, что его стороны как треугольник. Поместите точку циркуля на один конец линию от самой низкой из исходных координаты холста достаточно простое, и оно же. Во-первых, нам понадобится изображение, которое мы ситуаций; пока мы будем считать, что половину длины.
Допустим, мы поместим каждую модель внутрь с оптическим иллюзией.
Первая часть статьи может быть доказательством того, что трассировщики лучей — это изящный пример программного обеспечения, позволяющий создавать потрясающе красивые изображения исключительно с помощью простых и интуитивно понятных алгоритмов.
К сожалению, эта простота имеет свою цену: низкую производительность. Несмотря на то, что существует множество способов оптимизации и параллелизации трассировщиков лучей, они всё равно остаются слишком затратными с точки зрения вычислений для выполнения в реальном времени; и хотя оборудование продолжает развиваться и становится быстрее с каждым годом, в некоторых областях применения необходимы красивые, но в сотни раз быстрее создаваемые изображения уже сегодня. Из всех этих областей применения самыми требовательными являются игры: мы ожидаем рендеринга отличной картинки с частотой не менее 60 кадров в секунду. Трассировщики лучей просто с этим не справятся.
Тогда как это удаётся играм?
Ответ заключается в использовании совершенно иного семейства алгоритмов, которое мы исследуем во второй части статьи. В отличие от трассировки лучей, которая получалась из простых геометрических моделей формирования изображений в человеческом глазе или в камере, сейчас мы будем начинать с другого конца — зададимся вопросом, что мы можем отрисовать на экране, и как отрисовать это как можно быстрее. В результате мы получим совершенно другие алгоритмы, которые создают примерно похожие результаты.
Прямые
Снова начнём с нуля: у нас есть холст с размерами
и
, и мы можем расположить на нём пиксель (
).
Допустим, у нас есть две точки,
и
с координатами
и
. Отрисовка этих двух точек по отдельности тривиальна; но как можно отрисовать отрезок прямой линии из
в
?
Давайте начнём с представления прямой с параметрическими координатами, как мы делали это ранее с лучами (эти «лучи» — не что иное, как прямые в 3D). Любую точку на прямой можно получить, начав с
и переместившись на какое-то расстояние в направлении от
к
:
Мы можем разложить это уравнение на два, по одному для каждой из координат:
Давайте возьмём первое уравнение и вычислим
:
Теперь мы можем подставить это выражение во второе уравнение вместо
:
Немного преобразуем его:
Заметьте, что
— это постоянная, зависящая только от конечных точек отрезка; давайте обозначим её
:
Что же такое
? Судя по тому, как она определена, она является показателем изменения координаты
на изменение единицы длины координаты
; другими словами, это показатель наклона прямой.
Давайте вернёмся к уравнению. Раскроем скобки:
Группируем константы:
Выражение
снова зависит только от конечных точек отрезка; давайте обозначим его
, и наконец получим
Это классическая линейная функция, которой можно представить почти все прямые. Ею нельзя описать вертикальные прямые, потому что они имеют бесконечное количество значений
при одном значении
, и ни одного при всех остальных. Иногда в процессе получения такого представления из исходного параметрического уравнения такие семейства прямых можно упустить; это происходит при вычислении
, потому что мы проигнорировали то, что
может давать деление на ноль. Пока давайте просто проигнорируем вертикальные прямые; позже мы избавимся от этого ограничения.
Итак, теперь у нас есть способ вычисления значения
для любого интересующего нас значения
. При этом мы получим пару
, удовлетворяющую уравнению прямой. Если мы будем двигаться от
к
и вычислять значение
для каждого значения
, то получим первое приближение нашей функции отрисовки прямой:
DrawLine(P0, P1, color) { a = (y1 - y0)/(x1 - x0) b = y0 - a*x0 for x = x0 to x1 { y = a*x + b canvas.PutPixel(x, y, color) }}
В этом фрагменте
x0
и
y0
— это координаты
и
точки
P0
; в дальнейшем я буду использовать эту удобную запись. Также заметьте, что оператор деления
/
должен выполнять не целочисленное деление, а деление вещественных чисел.
Эта функция является непосредственной наивной интерпретацией приведённого выше уравнения, поэтому очевидно, что она работает; но можем ли мы ускорить её работу?
Заметьте, что мы не вычисляем значения
для всех
: на самом деле, мы вычисляем их только как целочисленные инкременты
, и мы делаем это в следующем порядке: сразу после вычисления
мы вычисляем
:
Мы можем воспользоваться этим для создания более быстрого алгоритма. Давайте возьмём разность между
последовательных пикселей:
Это не очень удивительно; в конце концов, наклон
— это показатель того, насколько
меняется на каждую единицу инкремента
, то есть именно то, что мы здесь делаем.
Интересно то, что мы можем тривиальным образом получить следующее:
Это значит, что мы можем вычислить следующее значение
только с помощью предыдущего значения
и прибавлением наклона; попиксельное умножение не требуется. Нам нужно с чего-то начать (в самом начале нет никакого «предыдущего значения
», поэтому мы начнём с
, а затем будем прибавлять
к
и
к
, пока мы не доберёмся до
.
Считая, что
, мы можем переписать функцию следующим образом:
DrawLine(P0, P1, color) { a = (y1 - y0)/(x1 - x0) y = y0 for x = x0 to x1 { canvas.PutPixel(x, y, color) y = y + a }}
Эта новая версия функции имеет новое ограничение: она может отрисовывать прямые только слева направо, то есть при
. Эту проблему довольно просто обойти: поскольку неважно, в каком порядке мы отрисовываем отдельные пиксели, то если у нас будет прямая справа налево, мы просто поменяем
P0
и
P1
, чтобы превратить её в лево-правую версию той же прямой, после чего отрисуем её как раньше:
DrawLine(P0, P1, color) { # Make sure x0 < x1 if x0 > x1 { swap(P0, P1) } a = (y1 - y0)/(x1 - x0) y = y0 for x = x0 to x1 { canvas.PutPixel(x, y, color) y = y + a }}
Теперь мы можем отрисовать пару прямых. Вот
:
Вот как она выглядит вблизи:
Прямая выглядит ломаной потому, что мы можем рисовать пиксели только по целочисленным координатам, а математические прямые на самом деле имеют нулевую ширину; рисуемое нами является дискретизированным приближением к идеальной прямой
(Примечание: существуют способы отрисовки более красивых приближенных прямых. Мы не будем использовать по двум причинам: 1) это медленнее, 2) наша цель — не рисовать красивые прямые, а разработать базовые алгоритмы для рендеринга 3D-сцен.).
Давайте попробуем нарисовать ещё одну прямую,
:
А вот как она выглядит вблизи:
Ой. Что случилось?
Алгоритм работал так, как и задумано; он прошёл слева направо, вычислил значение
для каждого значения
и отрисовал соответствующий пиксель. Проблема в том, что он вычислял одно значение
для каждого значения
, в то время как для некоторых значений
нам нужно несколько значений
.
Это прямое последствие выбора формулировки, в которой
; на самом деле по той же самой причине мы не можем рисовать вертикальные прямые, предельный случай, при котором есть одно значение
с несколькими значениями
.
Мы без всяких проблем можем рисовать горизонтальные прямые. Почему же нам не удаётся так же просто отрисовывать вертикальные линии?
Как оказывается, мы можем это сделать. Выбор
был произвольным решением, поэтому нет никаких причин, мешающих выразить прямую как
, переработав все уравнения и поменяв
и
, чтобы в результате получить следующий алгоритм:
DrawLine(P0, P1, color) { # Make sure y0 < y1 if y0 > y1 { swap(P0, P1) } a = (x1 - x0)/(y1 - y0) x = x0 for y = y0 to y1 { canvas.PutPixel(x, y, color) x = x + a }}
Это аналогично предыдущей
DrawLine
, за исключением перемены мест вычислений
и
. Полученная функция может справляться с вертикальными линиями и сможет правильно отрисовать
; разумеется, она не справится с горизонтальными прямыми и не сможет правильно отрисовать
! Что же нам делать?
Нам просто нужно выбирать нужную версию функции в зависимости от прямой, которую нужно нарисовать. И критерии будут достаточно простыми; имеет ли прямая более различающиеся значения
или
? Если есть больше значений
, чем
, мы используем первую версию; в противном случае применяется вторая.
Вот версия
DrawLine
, обрабатывающая все случаи:
DrawLine(P0, P1, color) { dx = x1 - x0 dy = y1 - y0 if abs(dx) > abs(dy) { # Прямая ближе к горизонтальной # Проверяем, что x0 < x1 if x0 > x1 { swap(P0, P1) } a = dy/dx y = y0 for x = x0 to x1 { canvas.PutPixel(x, y, color) y = y + a } } else { # Прямая ближе к вертикальной # Проверяем, что y0 < y1 if y0 > y1 { swap(P0, P1) } a = dx/dy x = x0 for y = y0 to y1 { canvas.PutPixel(x, y, color) x = x + a } }}
Это безусловно сработает, но код не особо красив; в нём есть две реализации кода, инкрементно вычисляющих линейную функцию, и эта логика вычислений и выбора перемешана. Поскольку мы будем часто использовать линейные функции, то стоит потратить немного времени на разделение кода.
У нас есть две функции,
и
. Чтобы абстрагироваться от того, что мы работаем с пикселями, давайте запишем это в общем виде как
, где
— независимая переменная, для которой мы выбираем значения, а
— зависимая переменная, значения которой зависят от другой, и которую мы хотим вычислить. В случае более горизонтальной прямой
является независимой переменной, а
— зависимой; в случае более вертикальной прямой всё наоборот.
Разумеется, любую функцию можно записать как
. Мы знаем ещё два аспекта, полностью задающие её: её линейность и два её значения; а именно,
и
. Мы можем написать простой метод, получающий эти значения и возвращающий промежуточные значения
, полагая, как и ранее, что
:
Interpolate (i0, d0, i1, d1) { values = [] a = (d1 - d0) / (i1 - i0) d = d0 for i = i0 to i1 { values.append(d) d = d + a } return values}
Заметьте, что значение
, соответствующее
, находится в
values[0]
, значение для
находится в
values[1]
, и так далее; в общем случае, значение
находится в
values[i_n - i_0]
, если считать, что
находится в интервале
.
Существует тупиковый случай, который нужно учитывать; нам может понадобиться вычислить
для единственного значения
, то есть при
. В этом случае мы не можем даже вычислить
, поэтому мы будем обрабатывать это как особый случай:
Interpolate (i0, d0, i1, d1) { if i0 == i1 { return [ d0 ] } values = [] a = (d1 - d0) / (i1 - i0) d = d0 for i = i0 to i1 { values.append(d) d = d + a } return values}
Теперь мы можем написать
DrawLine
с использованием
Interpolate
:
DrawLine(P0, P1, color) { if abs(x1 - x0) > abs(y1 - y0) { # Прямая ближе к горизонтальной # Проверяем, что x0 < x1 if x0 > x1 { swap(P0, P1) } ys = Interpolate(x0, y0, x1, y1) for x = x0 to x1 { canvas.PutPixel(x, ys[x - x0], color) } } else { # Прямая ближе к вертикальной # Проверяем, что y0 < y1 if y0 > y1 { swap(P0, P1) } xs = Interpolate(y0, x0, y1, x1) for y = y0 to y1 { canvas.PutPixel(xs[y - y0], y, color) } }}
Этот
DrawLine
может правильно обрабатывать все случаи:
Хотя эта версия не сильно короче предыдущей, она чётко разделяет вычисление промежуточных значений
и
, решение о выборе независимой переменной плюс сам код отрисовки. Преимущество этого возможно не совсем очевидно, но мы будем снова активно использовать
Interpolate
в последующих главах.
Следует учесть, что это не самый лучший или быстрый алгоритм отрисовки; важным результатом этой главы стал
Interpolate
, а не
DrawLine
. Лучшим алгоритмом отрисовки линий скорее всего является алгоритм Брезенхэма.
Заполненные треугольники
Мы можем использовать метод
DrawLine
для отрисовки контура треугольника. Такой тип контура называется каркасным, потому что он выглядит как каркас треугольника:
DrawWireframeTriangle (P0, P1, P2, color) { DrawLine(P0, P1, color); DrawLine(P1, P2, color); DrawLine(P2, P0, color);}
Мы получим вот такой результат:
Можем ли мы залить треугольник каким-нибудь цветом?
Как обычно бывает в компьютерной графике, для этого есть множество способов. Мы будем отрисовывать заполненные треугольники, воспринимая их как набор отрезков горизонтальных прямых, которые, если их отрисовать вместе, выглядят как треугольник. Ниже представлено очень грубое первое приближение того, что мы хотим сделать:
для каждой координаты y горизонтальной прямой, занятой треугольником вычислить x_left и x_right для этого y DrawLine(x_left, y, x_right, y)
Давайте начнём с части «для каждой координаты y горизонтальной прямой, занятой треугольником». Треугольник задаётся тремя вершинами
,
и
. Если мы отсортируем эти точки, увеличивая значение
, таким образом, что
, то интервал значений
, занятых треугольником, будет равен
:
if y1 < y0 { swap(P1, P0) }if y2 < y0 { swap(P2, P0) }if y2 < y1 { swap(P2, P1) }
Затем нам нужно вычислить
x_left
и
x_right
. Это немного сложнее, потому что у треугольника три, а не две стороны. Однако с точки зрения значений
у нас всегда есть «длинная» сторона от
до
и две «короткие» стороны от
до
и от
lj
(Примечание: существует особый случай, когда
или
, то есть когда у треугольника есть горизонтальная сторона; в таких случаях есть две стороны, которые можно считать «длинными» сторонами. К счастью, не важно, какую сторону мы выберем, поэтому можно придерживаться этого определения.). То есть значения
x_right
получаются или от длинной стороны, или от обеих коротких сторон; а значения
x_left
получаются от другого множества.
Мы начнём с вычисления значений
для трёх сторон. Так как мы отрисовываем горизонтальные отрезки, то нам нужно ровно одно значение
для каждого значения
; это значит, что мы можем получить значения непосредственно с помощью
Interpolate
, используя в качестве независимого значения
, а в качестве зависимого значения
:
x01 = Interpolate(y0, x0, y1, x1)x12 = Interpolate(y1, x1, y2, x2)x02 = Interpolate(y0, x0, y2, x2)
x02
будет или
x_left
, или
x_right
; другой будет конкатенацией
x01
и
x12
.
Заметьте, что в этих двух списках есть повторяющееся значение: значение
для
является и последним значением
x01
, и первым значением
x12
. Нам просто нужно избавиться от одного из них.
remove_last(x01)x012 = x01 + x12
Наконец у нас есть
x02
и
x012
, и нам нужно определить, что из них является
x_left
и
x_right
. Для этого надо посмотреть на значения
для одной из прямых, например, для средней:
m = x02.length / 2if x02[m] < x012[m] { x_left = x02 x_right = x012} else { x_left = x012 x_right = x02}
Теперь осталось только отрисовать горизонтальные отрезки. По причинам, которые станут понятны позже, мы не будем использовать для этого
DrawLine
; вместо этого мы будем отрисовывать пиксели по отдельности.
Вот полная версия
DrawFilledTriangle
:
DrawFilledTriangle (P0, P1, P2, color) { # Сортировка точек так, что y0 <= y1 <= y2 if y1 < y0 { swap(P1, P0) } if y2 < y0 { swap(P2, P0) } if y2 < y1 { swap(P2, P1) } # Вычисление координат x рёбер треугольника x01 = Interpolate(y0, x0, y1, x1) x12 = Interpolate(y1, x1, y2, x2) x02 = Interpolate(y0, x0, y2, x2) # Конкатенация коротких сторон remove_last(x01) x012 = x01 + x12 # Определяем, какая из сторон левая и правая m = x012.length / 2 if x02[m] < x012[m] { x_left = x02 x_right = x012 } else { x_left = x012 x_right = x02 } # Отрисовка горизонтальных отрезков for y = y0 to y2 { for x = x_left[y - y0] to x_right[y - y0] { canvas.PutPixel(x, y, color) } }}
Вот результат; для проверки мы вызвали
DrawFilledTriangle
, а потом
DrawWireframeTriangle
с одинаковыми координатами, но разными цветами:
Вы можете заметить, что чёрный контур треугольника не полностью совпадает с зелёной внутренней областью; это особенно заметно в правом нижнем ребре треугольника. Так получилось потому, что DrawLine() вычисляет
для этого ребра, но DrawTriangle() вычисляет
. На такую аппроксимацию мы готовы пойти, чтобы достичь нашей цели — высокоскоростного рендеринга.
Затенённые треугольники
В предыдущей части мы разработали алгоритм для отрисовки треугольника и заливки его цветом. Нашей следующей целью будет отрисовка затенённого треугольника, который похож на залитый градиентом.
Хотя затенённые треугольники выглядят красивее, чем одноцветные, это не является основной целью главы; это просто особое применение техники, которую мы создадим. Наверно, она будет самой важной в этом разделе статьи; почти всё остальное будет построено на её основе.
Но давайте начнём с простого. Вместо заполнения треугольника сплошным цветом, мы хотим заполнить его оттенками цвета. Это будет выглядеть так:
Первый шаг заключается в формальном определении того, что мы хотим отрисовать. Для этого мы назначим каждой вершине вещественное значение
, обозначающее яркость цвета вершины.
находится в интервале
.
Чтобы получить точный цвет пикселя, имея цвет
и яркость
, мы просто выполним поканальное умножение:
. То есть при
мы получим чёрный, а при
— исходный цвет
.
Вычисление затенения ребра
Итак, для отрисовки затенённого треугольника нам нужно вычислить значение
для каждого пикселя треугольника, получить соответствующий оттенок цвета и закрасить пиксель. Всё очень просто!
Однако на этом этапе мы знаем только значения
для заданных вершин. Как вычислить значения
для остальной части треугольника?
Давайте сначала рассмотрим рёбра. Выберем ребро
. Мы знаем
и
. Что происходит в
, то есть в середине отрезка
? Поскольку мы хотим, чтобы яркость плавно изменялась от
к
, то
должно быть каким-то значением между
и
. Так как
— это средняя точка отрезка
, то почему бы не сделать
средним значением
и
?
Если более формально, то у нас есть функция
, для которой нам известны предельные значения
и
, и нам нужно сделать её плавной. Мы больше ничего не знаем об
, поэтому можем выбрать любую функцию, соответствующую этим критериям, например, линейную функцию:
Разумеется, основой кода затенённого треугольника будет код сплошного треугольника, созданный в предыдущей главе. Один их первых шагов включает в себя вычисление конечных точек каждого горизонтального отрезка, то есть
x_left
и
x_right
для сторон
,
и
; мы использовали
Interpolate()
для вычисления значений
, имея
и
… и именно это мы и хотим сделать здесь, достаточно просто заменить
на
!
То есть мы можем вычислить промежуточные значения
точно таким же образом, как мы вычисляли значения
:
x01 = Interpolate(y0, x0, y1, x1)h01 = Interpolate(y0, h0, y1, h1)x12 = Interpolate(y1, x1, y2, x2)h12 = Interpolate(y1, h1, y2, h2)x02 = Interpolate(y0, x0, y2, x2)h02 = Interpolate(y0, h0, y2, h2)
Следующим этапом будет превращение этих трёх векторов в два вектора и определение того, какой из них представляет левосторонние значения, а какой — правосторонние. Заметьте, что значения
не играют никакой роли в том, что чем является; это полностью определяется значениями
. Значения
«приклеиваются» к значениям
, потому что являются другими атрибутами тех же физических пикселей. То есть, если
x012
имеет значения для правой стороны треугольника, тогда
h012
имеет значения для правой стороны треугольника:
# Конкатенация коротких сторон remove_last(x01) x012 = x01 + x12 remove_last(h01) h012 = h01 + h12 # Определяем, какая из сторон левая и правая m = x012.length / 2 if x02[m] < x012[m] { x_left = x02 x_right = x012 h_left = h02 h_right = h012 } else { x_left = x012 x_right = x02 h_left = h012 h_right = h02 }
Вычисление внутреннего затенения
Остался единственный шаг — отрисовка самих горизонтальных отрезков. Для каждого отрезка мы знаем
и
, а теперь мы также знаем
и
. Однако вместо итерирования слева направо и отрисовки каждого пикселя базовым цветом нам нужно вычислить значения
для каждого пикселя отрезка.
Мы снова можем считать, что
линейно изменяется с
и использовать
Interpolate()
для вычисления этих значений:
h_segment = Interpolate(x_left[y-y0], h_left[y-y0], x_right[y-y0], h_right[y-y0])
И теперь это просто вопрос вычисления цвета для каждого пикселя и его отрисовки.
Вот код вычисления для
DrawShadedTriangle
:
DrawShadedTriangle (P0, P1, P2, color) { # Сортировка точек так, что y0 <= y1 <= y2 if y1 < y0 { swap(P1, P0) } if y2 < y0 { swap(P2, P0) } if y2 < y1 { swap(P2, P1) } # Вычисление координат x и значений h для рёбер треугольника x01 = Interpolate(y0, x0, y1, x1) h01 = Interpolate(y0, h0, y1, h1) x12 = Interpolate(y1, x1, y2, x2) h12 = Interpolate(y1, h1, y2, h2) x02 = Interpolate(y0, x0, y2, x2) h02 = Interpolate(y0, h0, y2, h2) # Конкатенация коротких сторон remove_last(x01) x012 = x01 + x12 remove_last(h01) h012 = h01 + h12 # Определяем, какая из сторон левая и правая m = x012.length / 2 if x02[m] < x012[m] { x_left = x02 x_right = x012 h_left = h02 h_right = h012 } else { x_left = x012 x_right = x02 h_left = h012 h_right = h02 } # Отрисовка горизонтальных отрезков for y = y0 to y2 { x_l = x_left[y - y0] x_r = x_right[y - y0] h_segment = Interpolate(x_l, h_left[y - y0], x_r, h_right[y - y0]) for x = x_l to x_r { shaded_color = color*h_segment[x - xl] canvas.PutPixel(x, y, shaded_color) } }}
Этот алгоритм гораздо более общий, чем может казаться: до момента, в котором
умножается на цвет, то, что
является яркостью цвета, не играет никакой роли. Это значит, что мы можем использовать эту технику для вычисления значения чего угодно, что можно представить как действительное число для каждого пикселя треугольника, начиная со значений этого свойства в вершинах треугольника и считая, что свойство меняется на экране линейно.
Поэтому этот алгоритм окажется бесценным в последующих частях; не продолжайте чтение, пока не убедитесь, что хорошо поняли его.
Перспективная проекция
На какое-то время мы оставим в покое 2D-треугольники и обратим внимание на 3D; а конкретнее, на то, как можно представить 3D-объекты на 2D-поверхности.
Точно так же, как мы делали в начале части про трассировку лучей, мы начнём с задания камеры. Мы используем те же самые условия: камера находится в
, смотрит в направлении
, а вектор «вверх» является
. Также мы определим прямоугольное окно просмотра размером
на
, рёбра которого параллельны
и
и находящееся на расстоянии
от камеры. Если что-то из этого вам непонятно, прочитайте главу Основы трассировки лучей.
Рассмотрим точку
где-то перед камерой. Камера «видит»
, то есть существует некий луч света, отражающийся от
и достигающий камеры. Нас интересует нахождение точки
, в которой луч света пересекает окно просмотра (заметьте, что это противоположно нашим действиям при трассировке лучей, когда мы начинали с точки в окне просмотра и определяли, что видимо через неё):
Вот схема ситуации, видимой «справа», то есть когда
направлен вверх,
направлен вправо, а
направлен на нас:
В дополнение к
,
и
на этой схеме показаны точки
и
, которые будут полезны для понимания ситуации.
Понятно, что
, потому что мы определили, что
— это точка в окне просмотра, а окно просмотра расположено на плоскости
.
Также должно быть понятно, что треугольники
и
подобны: у них две общих стороны
, аналогичная
, и
, аналогичная
) а оставшиеся их стороны параллельны (
и
). Это подразумевает, что справедливо следующее уравнение пропорциональности:
Из него мы получаем
Длина каждого отрезка (со знаком) в этом уравнении — это координата точки, которую мы знаем, или которая нам нужна:
,
,
и
. Если мы подставим их в представленное выше уравнение, то получим
Мы можем нарисовать похожую схему, на этот раз сверху:
направлен вверх,
направлен вправо, а
направлен на нас:
Воспользовавшись снова подобными треугольниками, мы получим
Уравнение проецирования
Давайте объединим всё вместе. При задании точки
в сцене и стандартных настроек камеры и окна просмотра, проекция
в окне просмотра, которую мы обозначим как
, можно вычислить следующим образом:
Самое первое, что нужно здесь сделать — забыть о
; её значение по определению постоянно, а мы пытаемся перейти от 3D к 2D.
Теперь
по-прежнему остаётся точкой в пространстве; её координаты задаются в единицах, используемых для описания сцены, а не в пикселях. Преобразование из координат окна просмотра в координаты холста достаточно простое, и оно полностью противоположно преобразованию «холст-окно просмотра», которое мы использовали в части «Трассировка лучей»:
Наконец мы можем перейти от точки в сцене к пикселю на экране!
Свойства уравнения проецирования
Уравнение проецирования обладает интересными свойствами, о которых стоит поговорить.
Во-первых, в целом оно интуитивно понятно и соответствует опыту реальной жизни. Чем дальше объект вправо (
), тем правее он виден (
увеличивается). То же справедливо для
и
. Кроме того, чем дальше объект (увеличивается
), тем меньше он кажется (т.е.
и
уменьшаются).
Однако всё становится менее понятным при уменьшении значения
; при отрицательных значениях
, то есть когда объект находится за камерой, объект всё равно проецируется, но вверх ногами! И, разумеется, когда
, вселенная схлопывается. Нам как-то нужно избегать подобных неприятных ситуаций; пока мы будем считать, что каждая точка находится перед камерой, и справимся с этим в другой главе.
Ещё одним фундаментальным свойством перспективной проекции является то, что в ней сохраняется принадлежность точек к одной прямой; то есть проекции трёх точек, принадлежащих одной прямой, в окне просмотра тоже будут принадлежать одной прямой (Примечание: это наблюдение может казаться тривиальным, но стоит например заметить, что угол между двумя прямыми не сохраняется; мы видим, как параллельные линии «сходятся» к горизонту, как будто две стороны дороги.). Другими словами, прямая линия всегда выглядит как прямая.
Это имеет для нас очень непосредственную важность: пока мы говорили о проецировании точки, но как насчёт проецировании отрезка прямой или даже треугольника? Благодаря этому свойству проекция отрезка прямой будет отрезком прямой, соединяющимся с проекциями в конечных точках; следовательно, для проецирования полигона достаточно спроецировать его вершины и отрисовать получившийся полигон.
Поэтому мы можем двигаться дальше и отрисовать наш первый 3D-объект: куб. Мы задаём координаты его 8 вершин и отрисовываем линии между проекциями 12 пар вершин, составляющих рёбра куба:
ViewportToCanvas(x, y) { return (x*Cw/Vw, y*Ch/Vh); } ProjectVertex(v) { return ViewportToCanvas(v.x * d / v.z, v.y * d / v.z) # Четыре "передних" вершины. vAf = [-1, 1, 1] vBf = [1, 1, 1] vCf = [1, -1, 1] vDf = [-1, -1, 1] # Четыре "задних" вершины. vAb = [-1, 1, 2] vBb = [1, 1, 2] vCb = [1, -1, 2] vDb = [-1, -1, 2] # Передняя грань. DrawLine(ProjectVertex(vAf), ProjectVertex(vBf), BLUE); DrawLine(ProjectVertex(vBf), ProjectVertex(vCf), BLUE); DrawLine(ProjectVertex(vCf), ProjectVertex(vDf), BLUE); DrawLine(ProjectVertex(vDf), ProjectVertex(vAf), BLUE); # Задняя грань. DrawLine(ProjectVertex(vAb), ProjectVertex(vBb), RED); DrawLine(ProjectVertex(vBb), ProjectVertex(vCb), RED); DrawLine(ProjectVertex(vCb), ProjectVertex(vDb), RED); DrawLine(ProjectVertex(vDb), ProjectVertex(vAb), RED); # Рёбра, соединяющие переднюю и заднюю грани. DrawLine(ProjectVertex(vAf), ProjectVertex(vAb), GREEN); DrawLine(ProjectVertex(vBf), ProjectVertex(vBb), GREEN); DrawLine(ProjectVertex(vCf), ProjectVertex(vCb), GREEN); DrawLine(ProjectVertex(vDf), ProjectVertex(vDb), GREEN);
И мы получаем нечто подобное:
Хотя это и работает, у нас возникли серьёзные проблемы — что если мы хотим отрендерить два куба? Что если мы хотим отрендерить не куб, а что-то другое? Что если мы не знаем, что будем рендерить, пока программа не запущена — например, загружаем 3D-модель с диска? В следующей главе мы узнаем, как решать все эти вопросы.
Настройки сцены
Мы разработали техники для отрисовки треугольника на холсте по заданным координатам его вершин и уравнения для преобразования 3D-координат треугольника в 2D-координаты холста. В этой главе мы узнаем, как представить объекты, состоящие из треугольников, и как манипулировать ими.
Для этого мы используем куб; это не самый простой 3D-объект, который можно создать из треугольников, но он будет удобен для иллюстрации некоторых проблем. Рёбра куба имеют длину в две единицы и параллельны осям координат, а его центр находится в точке начала координат:
Вот координаты его вершин:
Стороны куба являются квадратами, однако мы знаем только как обращаться с треугольниками. Нет никаких проблем: любой полигон можно разложить на множество треугольников. Поэтому каждую сторону куба мы представим в виде двух треугольников.
Разумеется, не любое множество трёх вершин куба описывает треугольник на поверхности куба (например, AGH находится «внутри» куба), поэтому координат его вершин недостаточно для его описания; нам нужно также составить список треугольников, составленных из этих вершин:
A, B, CA, C, DE, A, DE, D, HF, E, HF, H, GB, F, GB, G, CE, F, BE, B, AC, G, HC, H, D
Это показывает, что есть структура, которую можно использовать для представления любого составленного из треугольников объекта: список координат вершин и список треугольников, определяющий, какое множество из трёх вершин образует треугольники объекта.
Каждая запись в списке треугольников может содержать дополнительную информацию о треугольниках; например, мы можем хранить в нём цвет каждого треугольника.
Поскольку наиболее естественным способом хранения этой информации будут два списка, мы используем индексы списка в качестве ссылок на список вершин. То есть вышеприведённый куб можно представить следующим образом:
Vertexes0 = ( 1, 1, 1)1 = (-1, 1, 1)2 = (-1, -1, 1)3 = ( 1, -1, 1)4 = ( 1, 1, -1)5 = (-1, 1, -1)6 = (-1, -1, -1)7 = ( 1, -1, -1)Triangles 0 = 0, 1, 2, red 1 = 0, 2, 3, red 2 = 4, 0, 3, green 3 = 4, 3, 7, green 4 = 5, 4, 7, blue 5 = 5, 7, 6, blue 6 = 1, 5, 6, yellow 7 = 1, 6, 2, yellow 8 = 4, 5, 1, purple 9 = 4, 1, 0, purple10 = 2, 6, 7, cyan11 = 2, 7, 3, cyan
Отрендерить представленный таким образом объект довольно просто: сначала мы проецируем каждую вершину, сохраняем их во временном списке «спроецированных вершин»; затем мы проходим по списку треугольников, рендеря каждый отдельный треугольник. В первом приближении это будет выглядеть так:
RenderObject(vertexes, triangles) { projected = [] for V in vertexes { projected.append(ProjectVertex(V)) } for T in triangles { RenderTriangle(T, projected) }}RenderTriangle(triangle, projected) { DrawWireframeTriangle(projected[triangle.v[0]], projected[triangle.v[1]], projected[triangle.v[2]], triangle.color)}
Мы не можем просто применить этот алгоритм к кубу, как есть, и надеяться на правильное отображение — некоторые из вершин находятся за камерой; а это, как мы уже видели, приводит к странному поведению. На самом деле, камера находится внутри куба.
Поэтому мы просто переместим куб. Чтобы сделать это, нам просто нужно сдвинуть каждую вершину куба в одном направлении. Давайте назовём этот вектор
, сокращённо от Translation. Давайте просто переместим куб на 7 единиц вперёд, чтобы он точно полностью был перед камерой, и на 1,5 единицы влево, чтобы он выглядел интереснее. Поскольку «вперёд» — это направление
, а «влево» —
, то вектор перемещения будет иметь следующий вид
Чтобы получить перемещённую версию
каждой точки
куба, нам нужно просто прибавить вектор перемещения:
На этом этапе мы можем взять куб, переместить каждую вершину, а затем применить приведённый выше алгоритм, чтобы получить наконец наш первый 3D-куб:
Модели и экземпляры
Но что если нам нужно отрендерить два куба?
Наивным подходом было бы создание ещё одного множества вершин и треугольников, описывающих второй куб. Это сработает, но займёт много памяти. А если мы захотим отрендерить миллион кубов?
Умнее будет думать в категориях моделей и экземпляров. Модель — это множество вершин и треугольников, описывающее объект как он есть (то есть "куб состоит из следующего множества вершин и треугольников"). С другой стороны, экземпляр модели — это конкретная реализация модели в некоторой позиции сцены (то есть "в (0, 0, 5) есть куб").
Преимущество второго подхода заключается в том, что каждый объект в сцене достаточно задать только один раз, после чего в сцену можно поместить произвольное количество экземпляров, просто описывая их позиции внутри сцены.
Вот грубое приближение того, как может быть описана подобная сцена:
model { name = cube vertexes { ... } triangles { ... }}instance { model = cube position = (0, 0, 5)}instance { model = cube position = (1, 2, 3)}
Чтобы отрендерить её, нам нужно просто пройти по списку экземпляров; для каждого экземпляра создать копию вершин модели, применить к ним позицию экземпляра, а затем действовать как раньше:
RenderScene() { for I in scene.instances { RenderInstance(I); }}RenderInstance(instance) { projected = [] model = instance.model for V in model.vertexes { V' = V + instance.position projected.append(ProjectVertex(V')) } for T in model.triangles { RenderTriangle(T, projected) }}
Заметьте, что для работы этого алгоритма координаты вершин модели должны быть определены в системе координат, «логичной» для объекта (это называется пространством модели). Например, куб определён таким образом, что его центр находится в (0, 0, 0); это значит, что когда мы говорим "куб расположен в (1, 2, 3)", мы имеем в виду "куб центрирован относительно (1, 2, 3)". При задании пространства модели нет никаких жёстких правил; в основном оно зависит от применения. Например, если у вас есть модель человека, то логично будет расположить точку начала координат у подошв его ног. Передвинутые вершины теперь будут выражаться в «абсолютной» системе координат сцены (называемой пространством мира).
Вот два куба:
Преобразование моделей
Данное выше определение сцены достаточно сильно нас ограничивает; в частности, поскольку мы можем указать только позицию куба, то способны создать сколько угодно экземпляров кубов, но все они будут ориентированы одинаково. В общем случае нам нужно иметь больше контроля над экземплярами; нам также хочется задавать их ориентацию, а возможно и масштаб.
Концептуально, мы можем задать преобразование модели точно с этими тремя элементами: коэффициентом масштаба, поворотом относительно точки начала координат пространства модели и перемещения в определённую точку сцены:
instance { model = cube transform { scale = 1.5 rotation = <45 degrees around the Y axis> translation = (1, 2, 3) }}
Можно с лёгкостью расширить предыдущую версию псевдокода, добавив новые преобразования. Однако важен порядок применения этих преобразований; в частности, перемещение необходимо выполнять последним. Вот поворот на
вокруг точки начала координат, за которым следует перемещение вдоль оси Z:
А вот перемещение, применённое до поворота:
Мы можем написать более общую версию
RenderInstance()
:
RenderInstance(instance) { projected = [] model = instance.model for V in model.vertexes { V' = ApplyTransform(V, instance.transform); projected.append(ProjectVertex(V')) } for T in model.triangles { RenderTriangle(T, projected) }}
Метод
ApplyTransform()
выглядит следующим образом:
ApplyTransform(vertex, transform) { V1 = vertex * transform.scale V2 = V1 * transform.rotation V3 = V2 + transform.translation return V3}
Поворот выражается как матрица 3x3; если вы не знакомы с матрицами поворота, то пока считайте, что любой 3D-поворот можно представить как произведение точки на матрицу 3x3. См. подробнее в курсе линейной алгебры.
Преобразование камеры
В предыдущих разделах мы узнали, как можно расположить экземпляры моделей в разных точках сцены. В этом разделе мы узнаем, как двигать и поворачивать камеру в сцене.
Представьте, что вы висите посередине совершенно пустой системы координат. Всё окрашено в чёрный цвет. Внезапно прямо перед вами появляется красный куб. Мгновение спустя куб приближается на одну единицу к вам. Но приблизился ли куб к вам? Или вы сами передвинулись на одну единицу к кубу?
Поскольку у нас нет отправной точки, а систему координат не видно, мы никак не можем определить, что случилось.
Теперь куб повернулся вокруг вас на
по часовой стрелке. Но так ли это? Возможно, это вы повернулись вокруг него на
против часовой стрелки? Мы снова не можем определить это.
Этот мысленный эксперимент показывает нам, что нет никакой разницы между перемещением камеры по фиксированной сцене и неподвижной камерой в движущейся и поворачивающейся вокруг неё сцене!
Преимущество такого очевидно эгоистичного видения вселенной заключается в том, что при фиксированной в точке начала координат камере, смотрящей в направлении
, мы можем без всяких изменений сразу же использовать уравнения проецирования, выведенные в предыдущей главе. Система координат камеры называется пространством камеры.
Будем считать, что у камеры тоже есть преобразование, состоящее из перемещения и поворота (масштаб мы опустим). Чтобы отрендерить сцену с точки обзора камеры, нам нужно применить к каждой вершине в сцене обратные преобразования:
V1 = V - camera.translationV2 = V1 * inverse(camera.rotation)V3 = perspective_projection(V2)
Матрица преобразований
Давайте сделаем шаг назад и разберёмся, что происходит с вершиной
в пространстве модели, пока она не будет спроецирована в точку на холсте
.
Сначала мы применяем преобразование модели, чтобы перейти из пространства модели в пространство мира:
V1 = V * instance.rotationV2 = V1 * instance.scaleV3 = V2 + instance.translation
Затем мы применяем преобразование камеры, чтобы перейти из пространства мира в пространство камеры:
V4 = V3 - camera.translationV5 = V4 * inverse(camera.rotation)
Затем мы применяем уравнения перспективы:
vx = V5.x * d / V5.zvy = V5.y * d / V5.z
И наконец мы привязываем координаты окна просмотра к координатам холста:
cx = vx * cw / vwcy = vy * ch / vh
Как вы видите, это довольно большой объём вычислений и для каждой вершины вычисляется множество промежуточных значений. Разве не будет удобно, если мы сократим всё это до единственного матричного произведения — возьмём
, умножим её на какую-нибудь матрицу и получим непосредственно
с
?
Давайте выразим преобразования в виде функций, получающих вершину и возвращающих преобразованную вершину. Пусть
и
будут перемещением и поворотом камеры,
,
и
— поворотом, масштабом и перемещением экземпляра,
— перспективной проекцией, а
— размещением окна просмотра на холсте. Если
— это исходная вершина, а
— точка на холсте, то мы можем выразить все вышеуказанные уравнения следующим образом:
В идеале нам бы хотелось иметь единственное преобразование, выполняющее то же, что и серия исходных преобразований, но имеющее гораздо более простое выражение:
Нахождение одной матрицы, представляющей
, является нетривиальной задачей. Основная проблема заключается в том, что преобразования выражаются различными способами: перемещение — это сумма точки и вектора, поворот и масштаб — это произведение точки и матрицы 3x3, а в перспективной проекции используется деление. Но если мы сможем выразить все преобразования одним способом, и такой способ будет иметь простой механизм для создания преобразований, то мы получим то, что нам нужно.
Однородные координаты
Рассмотрим выражение
.
представляет собой 3D-точку или 3D-вектор? Нет никакого способа узнать это, не имея дополнительного контекста.
Но давайте примем следующую договорённость: мы добавим к представлению четвёртый компонент, называемый
. Если
, то мы говорим о векторе. Если
, то мы говорим о точке. То есть точка
недвусмысленно представляется в виде
, а вектор
представляется в виде
. Поскольку точки и векторы имеют общее представление, это называется однородными координатами (Примечание: однородные координаты имеют гораздо более глубокую и подробную геометрическую интерпретацию, но она не относится к тематике нашей статьи; здесь мы просто используем их как удобный инструмент с определёнными свойствами.).
Такое представление имеет большой геометрический смысл. Например, при вычитании двух точек получается вектор:
При сложении двух векторов получается вектор:
Аналогично, можно легко увидеть, что при суммировании точки и вектора мы получаем точку, умножение вектора на скаляр даёт вектор, и так далее.
А что же представляют собой координаты с
, не равным ни
, ни
? Они тоже представляют точки; на самом деле, любая точка в 3D имеет бесконечное количество представлений в однородных координатах. Важно соотношение между координатами и значением
; то есть,
и
представляют одну и ту же точку, как
.
Из всех этих представлений мы можем назвать представление с
каноническим представлением точки в однородных координатах; преобразование любого другого представления к её каноническому представлению или в декартовы координаты — тривиальная задача:
То есть мы можем преобразовать декартовы координаты в однородные координаты, и обратно в декартовы координаты. Но как это поможет нам найти единое представление для всех преобразований?
Однородная матрица поворота
Давайте начнём с матрицы поворота. Выражение декартовой матрицы поворота 3x3 в однородных координатах тривиально; поскольку координата
точки не должна меняться, мы добавим столбец справа, строку внизу, заполним их нулями и поместим в правый нижний элемент
, чтобы хранить значение
:
Однородная матрица масштаба
Матрица масштабирования тоже тривиальна в однородных координатах, и она создаётся точно так же, как и матрица поворота:
Однородная матрица трансляции
Предыдущие примеры были простыми; они уже были представлены как умножения матриц в декартовых координатах, нам достаточно было добавить
, чтобы сохранить координату
. Но что нам делать с перемещением, которое мы выражали в декартовых координатах как сложение?
Нам нужна такая матрица 4x4, что
Давайте сначала сосредоточимся на получении
. Это значение — результат умножения первой строки матрицы на точку, то есть
Если мы раскроем векторное произведение, то получим
И из этого мы можем вывести, что
,
, а
.
Применив те же рассуждения к остальным координатам, мы получим следующее матричное выражение для перемещения:
Однородная матрица проецирования
Сумму и произведение можно просто выразить как произведения матриц и векторов, которые являются суммами и произведениями. Но в уравнениях перспективного проецирования используется деление на
. Как его выразить?
Есть большое искушение посчитать, что деление на
— это то же самое, что и умножение на
, что на самом деле правда; но в нашем случае это бесполезно, потому что координата
конкретной точки не может находиться в матрице проецирования, применяемой к каждой точке.
К счастью, в однородных координатах присутствует один случай деления: деление на координату
при обратном преобразовании в декартовы координаты. Так что есть нам удастся превратить координату
исходной точки в координату
«спроецированной» точки, то мы получим спроецированные
и
после преобразования точки обратно в декартовы координаты:
Заметьте, что эта матрица имеет размер
; её можно умножить на четырёхэлементный вектор (преобразованную 3D-точку в однородных координатах), получив при этом трёхэлементный вектор (спроецированную 2D-точку в однородных координатах), который затем преобразуется в двухмерные декартовы координаты делением на
. Это даст нам точные значения
и
, которые мы ищем.
Здесь не хватает
, которая, как мы знаем, по определению является
.
Используя рассуждения, похожие на те, которые мы применяли для выведения матрицы трансляции, мы можем выразить перспективную проекцию как
Однородная матрица из окна просмотра на холст
Последний этап — размещение спроецированной на окно просмотра точки на холст. Это просто двухмерное преобразование масштаба с
и
. То есть матрица будет следующей
На самом деле её легко скомбинировать с матрицей проецирования и получить простую матрицу преобразования из 3D в холст:
Практическое применение
Из практических соображений мы не будем использовать матрицу проецирования. Вместо этого мы используем преобразования модели и камеры, а затем преобразуем их результаты обратно в декартовы координаты следующим образом:
Это позволит нам выполнить ещё и другие операции в 3D до проецирования точек, которые нельзя выразить как матричные преобразования.
Снова матрица преобразования
Так как теперь мы можем выразить любое 3D-преобразование исходной вершины
, выполняемое до проецирования как матрицы 4x4, мы можем тривиально объединить все эти преобразования в единую матрицу 4x4, перемножив их:
И тогда преобразование вершины — просто вопрос вычисления следующего произведения:
Более того, мы можем разложить преобразование на две части:
Эти матрицы не нужно вычислять для каждой вершины (в этом и заключается смысл использования матрицы). На самом деле, их даже не обязательно вычислять в каждом кадре.
может изменяться каждый кадр; это зависит от камеры позиции и ориентации, поэтому если камера двигается или поворачивается, то её необходимо пересчитать. Однако после вычисления она остаётся постоянной для каждого объекта, отрисованного в кадре, поэтому она будет вычисляться максимум один раз за кадр.
зависит от преобразования экземпляра модели, и поэтому используемая матрица будет меняться только один раз для объекта в сцене; однако она будет оставаться постоянной для неподвижных объектов (например, деревьев, зданий), поэтому её можно вычислить заранее и хранить в самой сцене. Для подвижных объектов (например, для машин в гоночной игре) она всё равно должна вычисляться каждый раз, когда они двигаются (обычно в каждом кадре).
На очень высоком уровне псевдокод рендеринга сцены будет выглядеть так:
RenderModel(model, transform) { projected = [] for V in model.vertexes { projected.append(ProjectVertex(transform * V)) } for T in model.triangles { RenderTriangle(T, projected) }}RenderScene() { MCamera = MakeCameraMatrix(camera.position, camera.orientation) for I in scene.instances { M = MCamera*I.transform RenderModel(I.model, M) }}
Теперь мы можем отрисовать сцену, содержащую несколько экземпляров различных моделей, возможно, двигающихся и поворачивающихся, и можем двигать камеру по сцене.
Мы сделали большой шаг вперёд, но у нас по-прежнему есть два важных ограничения. Во-первых, при движении камеры объекты могут оказаться за ней, что создаёт всевозможные проблемы. Во-вторых, результат рендеринга выглядит не очень хорошо: он по-прежнему каркасный.
В следующей главе мы разберёмся с объектами, которые не должны быть видимы, а затем потратим оставшееся время на улучшение внешнего вида отрендеренных объектов.
Отсечение
В главе Перспективная проекция мы получили следующие уравнения:
Деление на
вызывает проблемы; при нём может возникнуть деление на ноль. Также при нём могут получаться отрицательные значения, представляющие точки за камерой, которые обрабатываются неправильно. Даже точки, находящиеся перед камерой, но очень близко, могут вызывать проблемы в виде сильно искажённых объектов.
Чтобы избежать всех этих проблемных случаев, мы решили не рендерить ничего за плоскость проекции
. Эта плоскость отсечения позволяет разделить все точки на находящиеся внутри или снаружи объёма отсечения, то есть подмножества пространства, которое на самом деле видно из камеры. В этом случае объём отсечения — это "полупространство перед
". Мы рендерим только части сцены, которые находятся внутри объёма отсечения.
Чем меньше операций мы делаем, тем быстрее будет наш рендерер, поэтому мы используем подход «сверху вниз». Рассмотрим сцену с несколькими объектами, каждый из которых состоит из четырёх треугольников.
На каждом этапе мы стремимся как можно менее затратно определить, можно ли остановить отсечение на этой точке, или требуется дальнейшее и более подробное отсечение:
- Если объект полностью находится внутри объёма отсечения, то он принимается (выделен зелёным); если он полностью снаружи, то отбрасывается (красный):
- В противном случае мы повторяем процесс для каждого треугольника. Если треугольник полностью находится внутри объёма отсечения, то он принимается, если полностью снаружи, то отбрасывается:
- В противном случае, нам нужно разбить сам треугольник. Исходный треугольник отбрасывается, и добавляются один или два треугольника, закрывающие часть треугольника, находящуюся внутри объёма отсечения:
Теперь мы подробноее рассмотрим каждый этап в процессе выполнения.
Задание плоскостей отсечения
Первое, что нужно сделать — найти уравнение плоскости отсечения. Нет ничего плохого в
, но это не самый удобный формат для наших целей; ниже в этой главе мы выработаем более общий подход к другим плоскостям отсечения, так что нам нужно придумать общий подход вместо этого конкретного случая.
Общее уравнение 3D-плоскости имеет вид
. Оно означает, что точка
будет удовлетворять уравнению тогда и только тогда, когда
находится на плоскости. Мы можем переписать уравнение как
, где
.
Заметьте, что если
, то
при любом значении
. В частности, мы можем выбрать
и получить новое уравнение
, где
— единичный вектор. То есть для любой заданной плоскости мы можем считать, что существует единичный вектор
и вещественное число
такие, что
является уравнением этой плоскости.
Это очень удобная формулировка:
на самом деле является нормалью плоскости, а
— расстояние со знаком от точки начала координат до плоскости. На самом деле, для любой точки
является расстоянием со знаком от
до плоскости; легко увидеть, что
— это особый случай, при котором
лежит на плоскости.
Как мы видели ранее, если
— это нормаль к плоскости, как и
, поэтому мы выбираем
такую, что она направлена «внутрь» объёма отсечения. Для плоскости
мы выбираем нормаль
, которая направлена «вперёд» относительно камеры. Поскольку точка
лежит на плоскости, она должна удовлетворять уравнению плоскости, мы можем вычислить её, зная
:
то есть
(Примечание: можно было тривиально получить это из
, переписав его как
. Однако представленные здесь рассуждения относятся и ко всем остальным плоскостям, с которыми мы будем иметь дело, и это позволяет нам справиться с тем, что
тоже справедливо, но нормаль направлена в неправильном направлении.).
Объём отсечения
Хотя при использовании одной плоскости отсечения, позволяющей гарантировать, что объекты за камерой не будут рендериться, мы получаем правильные результаты, это не совсем эффективно. Некоторые объекты могут находиться перед камерой, но всё равно не быть видимыми; например, проекция объекта рядом с плоскостью проекции, но расположенная очень далеко вправо, выпадет из окна просмотра, и потому будет невидимой:
Все ресурсы, которые мы используем для вычисления проекции такого объекта, а также вычисления для треугольников и вершин, выполненные для его рендеринга, будут потрачены зря. Нам будет гораздо удобнее полностью игнорировать такие объекты.
К счастью, это совсем не сложно. Мы можем задать дополнительные плоскости, отсекающие сцену ровно до того, что будет видимо из окна просмотра; такие плоскости задаются камерой и обеими сторонами окна просмотра:
Все эти плоскости имеют
(потому что точка начала координат находится на всех плоскостях), поэтому нам остаётся только определить нормали. Простейшим случаем будет FOV
, при которой плоскости находятся на
, поэтому их нормали
для левой плоскости,
для правой плоскости,
для нижней и
для верхней плоскостей. Для вычисления плоскостей отсечения для любой произвольной FOV требует только небольшого количества тригонометрических вычислений.
Для отсечения объектов или треугольников по объёму отсечения нам достаточно отсечь их по порядку каждой плоскостью. Все объекты, «выжившие» после отсечения одной плоскостью, отсекаются остальными плоскостями; это срабатывает, потому что объём отсечения является пересечением полупространств, задаваемых каждой плоскостью отсечения.
Отсечение целых объектов
Полностью задав объём отсечения его плоскостями отсечения, мы можем начать с определения того, находится ли объект полностью внутри или снаружи полупространства, задаваемого каждой из этих плоскостей.
Допустим, мы поместим каждую модель внутрь наименьшей сферы, которая может его содержать. Мы не будем рассматривать в статье, как это можно сделать; сферу можно вычислить из множества вершин одним из нескольких алгоритмов, или же приближение может быть задано разработчиком модели. В любом случае, будем считать, что у нас есть центр
и радиус
сферы, которая полностью содержит каждый объект:
Мы можем разбить пространственные отношения между сферой и плоскостью на следующие категории:
- Сфера целиком находится перед плоскостью. В этом случае модель принимается; дальнейшего отсечения относительно плоскости не требуется (однако она всё равно может отсекаться другой плоскостью):
- Сфера целиком находится за плоскостью. В этом случае модель отбрасывается; дальнейшее отсечение не требуется (не важно, какими являются другие плоскости — ни одна часть модели не попадаёт внутрь объёма отсечения):
- Плоскость пересекается со сферой. Это не даёт нам достаточной информации о том, какие части объекта находятся внутри объёма отсечения; он может целиком находиться внутри, полностью снаружи, или частично внутри. Необходимо перейти к следующему этапу и отсечь модель треугольник за треугольником.
Как же на самом деле выполняется это разбиение на категории? Мы выбрали способ выражения плоскостей отсечения таким образом, что подстановка любой точки в уравнение плоскости даёт нам расстояние со знаком от точки до плоскости; в частности, мы можем вычислить расстояние со знаком
от центра ограничивающей сферы до плоскости. Поэтому если
, то сфера находится перед плоскостью; если
, то сфера находится за плоскостью; в противном случае
, то есть плоскость пересекается со сферой.
Отсечение треугольников
Если проверки сфера-плоскость недостаточно для определения того, находится ли объект полностью перед или полностью за плоскостью отсечения, то необходимо отсечь относительно неё каждый треугольник.
Мы можем классифицировать каждую вершину треугольника относительно плоскости отсечения, взяв знак расстояния со знаком до плоскости. Если расстояние равно нулю или положительно, то вершина находится перед плоскостью отсечения, в противном случае — за ней:
Существует четыре возможных случая:
- Три вершины находятся перед плоскостью. В таком случае весь треугольник находится перед плоскостью отсечения, поэтому он принимается без дальнейшего отсечения относительно этой плоскости.
- Три вершины находятся за плоскостью. В этом случае весь треугольник находится за плоскостью отсечения, поэтому он отбрасывается без дальнейших отсечений.
- Одна вершина находится перед плоскостью. Пусть — вершина треугольника , которая находится перед плоскостью. В этом случае отбрасывается, и добавляется новый треугольник , где и — пересечения и с плоскостью отсечения.
- Две вершины находятся перед плоскостью. Пусть и — вершины треугольника , находящиеся перед плоскостью. В этом случае ABC отбрасывается, и добавляются два новых треугольника: и , где и — пересечения и с плоскостью отсечения.
Пересечение отрезка и плоскости
Чтобы выполнить отсечение для каждого треугольника, нам нужно вычислить пересечение сторон треугольника с плоскостью отсечения. Надо заметить, что недостаточно вычислить координаты пересечения: необходимо также вычислить соответствующее значение атрибутов, связанных с вершинами, например, затенения, которое мы делали в главе Отрисовка затенённых треугольников, или одного из атрибутов, описанных в последующих главах.
У нас есть плоскость отсечения, заданная уравнением
. Сторону треугольника
можно выразить с помощью параметрического уравнения как
при
. Для вычисления значения параметра
, при котором происходит пересечение, мы заменим
в уравнении плоскости на параметрическое уравнение отрезка:
Воспользовавшись линейными свойствами скалярного произведения, получаем:
Вычисляем
:
Мы знаем, что решение существует всегда, потому что
пересекает плоскость; математически
не может быть нулём, потому что это будет подразумевать, что отрезок и нормаль перпендикулярны, что в свою очередь подразумевает, что отрезок и плоскость не пересекаются.
Вычислив
, мы получим, что пересечение
просто равно
Заметьте, что
— это часть отрезка
, в которой произошло пересечение. Пусть
и
будут значениями некоего атрибута
в точках
и
; если мы будем считать, что атрибут линейно изменяется вдоль
, то
можно просто вычислить как
Отсечение в конвейере
Порядок глав статьи не соответствует порядку операций, выполняемых в конвейере рендеринга; как объяснено во введении, главы расположены в таком порядке, чтобы можно было как можно быстрее достичь наглядного прогресса.
Отсечение — это 3D-операция; она получает два 3D-объекта в сцене и генерирует новое множество 3D-объектов в сцене, а именно, пересечение сцены и объёма отсечения. Должно быть понятно, что отсечение должно выполняться после того, как объекты были размещены в сцене (то есть использованы вершины после преобразований модели и камеры), но перед перспективным проецированием.
Представленные в этой главе техники надёжно работают, но они очень общие. Чем больше мы будем заранее знать о сцене, тем эффективнее будет отсечение. Например, многие игры предварительно обрабатывают игровые карты, добавляя на них информацию о видимости; если получится разделить сцену на «комнаты», то можно создать таблицу с перечислением комнат, видимых из каждой конкретной комнаты. При рендеринге сцены в дальнейшем вам просто нужно выяснить, в какой комнате находится камера, после чего можно игнорировать все комнаты, помеченные как «невидимые» из неё, экономя значительные ресурсы при рендеринге. Конечно, при этом приходится тратить больше времени на предварительную обработку, а сцена получается более жёстко заданной.
Удаление скрытых поверхностей
Теперь, когда мы можем отрендерить любую сцену с любой точки обзора, давайте усовершенствуем нашу каркасную графику.
Очевидным первым шагом будет придание сплошным объектам сплошного внешнего вида. Для этого давайте используем
DrawFilledTriangle()
для отрисовки каждого треугольника случайным цветом, и посмотрим, что из этого получится:
Не очень похоже на кубы, правда?
Проблема здесь в том, что некоторые треугольники, которые должны быть за другими, отрисовываются перед ними. Почему? Потому что мы просто отрисовываем 2D-треугольники на холсте практически в случайном порядке — в том порядке, который получился при задании модели.
Однако при задании треугольников модели нет «правильного» порядка. Предположим, что треугольники модели отсортированы таким образом, что сначала отрисовываются задние грани, а затем они перекрываются передними гранями. Мы получим ожидаемый результат. Однако если мы повернём куб на
, то получим обратную ситуацию — дальние треугольники будут перекрывать ближние.
Алгоритм художника
Первое решение этой проблемы известно как "алгоритм художника". Художники в реальном мире сначала отрисовывают фон, а затем закрывают его части передними объектами. Мы можем достичь того же эффекта, взяв каждый треугольник сцены, применив преобразования модели и камеры, отсортировав их сзади вперёд и отрисовав их в этом порядке.
Хотя при этом треугольники отрисуются в правильном порядке, этот алгоритм имеет недостатки, делающие его непрактичным.
Во-первых, он не очень хорошо масштабируется. Самый лучший алгоритм сортировки имеет скорость
\), то есть время выполнения более чем удваивается при удвоении количества треугольников. Другими словами, он работает для небольших сцен, но быстро становится «узким местом» при увеличении сложности сцены.
Во-вторых, он требует одновременного знания всего списка треугольников. На это требуется много памяти, и не позволяет использовать потоковый подход к рендерингу. Если воспринимать рендеринг как конвейер, в котором треугольники модели входят с одного конца, а с другого выходят пиксели, то невозможно начать выводить пиксели, пока не будет обработан каждый треугольник. В свою очередь это означает, что мы не можем распараллелить этот алгоритм.
В-третьих, даже если вы смиритесь с этими ограничениями, то всё равно существуют случаи, когда правильного порядка треугольников не существует. Рассмотрим следующий случай:
Неважно, в каком порядке вы будете отрисовывать эти треугольники — вы всегда будете получать неверный результат.
Буфер глубин
Если решение проблемы на уровне треугольников не срабатывает, то решение на уровне пикселей точно сработает, и в то же время оно преодолевает все ограничения алгоритма художника.
В сущности, каждый пиксель холста мы хотим закрасить «правильным» цветом. В этом случае, «правильный» цвет — это цвет ближайшего к камере объекта (в нашем случае
):
Оказывается, что мы можем запросто это сделать. Предположим, что мы храним значение
точки, в настоящее время представленной каждым пикселем холста. Когда мы решаем, стоит ли закрашивать пиксель определённым цветом, мы поступаем так только когда координата
точки, которую мы собираемся закрашивать, меньше координаты
точки, которая там уже есть.
Представим такой порядок треугольников, при котором мы сначала хотим закрасить
, а потом
. Пиксель закрашен красным, его
помечена как
. Затем мы закрашиваем
, и поскольку
, пиксель перезаписывается синим цветом; мы получаем верные результаты.
Разумеется, мы получили верный результат вне зависимости от значений
. Что если мы хотели сначала закрасить
, а потом
? Пиксель сначала закрасился синим, а
сохранилась; но затем мы хотим закрасить
, и поскольку
, мы не будем его закрашивать (потому что если бы это сделали, то закрасили бы удалённую точку, закрыв более близкую). Мы снова получаем синий пиксель, что является верным результатом.
С точки зрения реализации, нам нужен буфер для хранения координаты
каждого пикселя на холсте; он называется буфером глубин, и его размеры естественно равны размерам холста.
Но откуда появляются значения
?
Они должны быть значениями
точек после преобразования, но перед перспективным проецированием. Именно поэтому в главе Настройка сцены мы задали матрицы преобразования таким образом, что конечный результат содержит
.
Итак, мы можем получить значения
из этих значений
. Но у нас есть это значение только для вершин; нам нужно получить его для каждого пикселя.
И это ещё один способ применения алгоритма присвоения атрибутов. Почему бы не использовать
в качестве атрибута и не интерполировать его вдоль грани треугольника? Вы уже знаете процедуру; берём значения
Z0
,
Z1
и
Z2
, вычисляем
Z01
,
Z02
и
Z02
, получаем из них
z_left
и
z_right
, а затем вычисляем
z_segment
для каждого пикселя каждого горизонтального отрезка. И вместо выполнения вслепую
PutPixel(x, y, color)
мы делаем следующее:
z = z_segment[x - xl]if (z < depth_buffer[x][y]) { canvas.PutPixel(x, y, color) depth_buffer[x][y] = z}
Чтобы это сработало,
depth_buffer
должен быть инициализирован значениями
(или просто «очень большим значением»).
Получаемые результаты теперь гораздо лучше:
Почему 1/Z вместо Z
Но на этом история не заканчивается. Значения
в вершинах верны (в конце концов, они получаются из данных), но в большинстве случаев линейно интерполированные значения
для остальных пикселей будут неверными. Хотя такое приближение «достаточно хорошо» для буферизации глубин, в дальнейшем оно будет мешать.
Чтобы проверить, насколько неверны значения, рассмотрим простой случай прямой из
в
. Середина отрезка
имеет координаты
:
Давайте вычислим проекцию этих точек при
.
. Аналогично,
и
:
Что теперь произойдёт, если мы линейно проинтерполируем значения
и
для получения вычисленного значения для
? Линейная функция выглядит так:
Из этого мы можем заключить, что
Если мы подставим числа и выполним арифметические вычисления, то получим
что очевидно не равно
.
Так в чём же проблема? Мы используем присвоение атрибутов, которое, как мы знаем, работает хорошо; мы передаём ему верные значения, которые получаются из данных; так почему же результаты неверны?
Проблема в том, что мы неявно подразумеваем, выполняя линейную интерполяцию: что интерполируемая нами функция линейна. А в нашем случае это не так!
Если
была бы линейной функцией
и
, мы могли бы записать её как
для некоторого значения
,
и
. Такой тип функций имеет следующее свойство: разность его значения между двумя точками зависит от разности между точками, а не от самих точек:
То есть для заданной разности экранных координат разность
всегда будет одинаковой.
Более формально, уравнение плоскости, содержащей изучаемый нами треугольник, имеет вид
С другой стороны, у нас есть уравнения перспективной проекции:
Мы можем снова получить из них
и
:
Если мы заменим
и
в уравнении плоскости этими выражениями, то получим
Умножив на
и выразив
, получим
Что очевидно не является линейной функцией
и
.
Однако, если мы просто вычислим
, то получим
Что очевидно является линейной функцией от
и
.
Чтобы показать, что это действительно работает, вот приведённый выше пример, но на этот раз вычисленный с помощью линейной интерполяции
:
И следовательно
Что на самом деле является правильным значением.
Всё это значит, что для буферизации глубин мы должны вместо значений
использовать значения
. Единственной практической разницей в псевдокоде будет то, что буфер должен инициализироваться значениями
(то есть
), а сравнение необходимо перевернуть (сохраняем большее значение
, соответствующее меньшему значению
).
Отсечение задних граней
Буферизация глубин даёт нам нужные результаты. Но можем ли мы добиться того же более быстрым способом?
Вернёмся к кубу: даже если каждый пиксель в результате получает правильный цвет, некоторые из них перерисовываются снова и снова несколько раз. Например, если задняя грань куба рендерится до передней грани, то многие пиксели будут закрашиваться дважды. С увеличением количества операций, выполняемых для каждого пикселя (пока мы вычисляем для каждого пикселя только
, но скоро добавим освещение, например), вычисление пикселей, которые никогда не окажутся видимыми, становится всё более и более затратным.
Можем ли мы заранее отбросить пиксели, прежде чем выполнять все эти вычисления? Оказывается, мы можем отбрасывать целые треугольники ещё до того, как начнём отрисовку!
До этого момента мы говорили о передних гранях и задних гранях неформально. Представьте, что у каждого треугольника есть две стороны; одновременно мы можем видеть только одну из сторон треугольника. Чтобы разделить эти две стороны, мы добавим к каждому треугольнику стрелку, перпендикулярную его поверхности. Затем мы возьмём куб и убедимся, что каждая стрелка направлена наружу:
Теперь эта стрелка позволит нам классифицировать каждый треугольник как «передний» и «задний», в зависимости от того, направлен ли он к камере или от неё; если более формально, то если вектор просмотра и эта стрелка (на самом деле являющаяся вектором нормали треугольника) образуют угол соответственно меньше или больше
:
С другой стороны, наличие одинаково ориентированных двусторонних треугольников, позволяет нам задать то, что находится «внутри» и «снаружи» замкнутого объекта. По определению мы не должны видеть внутренние части замкнутого объекта. Это значит, что у любого замкнутого объекта вне зависимости от расположения камеры передние грани полностью перекрывают задние.
Что в свою очередь означает, что нам вообще не нужно отрисовывать задние грани, потому что они всё равно будут перерисовываться передними гранями!
Классификация треугольников
Давайте формализуем и реализуем это. Допустим, у нас есть вектор нормали треугольника
и вектор
из вершины треугольника к камере. Пусть
указывает наружу объекта. Чтобы классифицировать треугольник как передний или задний, мы вычисляем угол между
и
, после чего проверим, находятся ли они в
относительно друг друга.
Мы снова можем воспользоваться свойствами скалярного произведения, чтобы ещё больше это упростить. Не забывайте, что если мы обозначим за
угол между
и
, то
Поскольку
неотрицателен при
, для классификации грани как передней или задней нам достаточно только знать его знак. Стоит заметить, что
и
всегда положительны, поэтому они не влияют на знак выражения. Следовательно
То есть классификация будет очень простой:
Задняя грань | |
Передняя грань |
Пограничный случай
соответствует случаю, который мы видим на ребре треугольника, то есть когда камера и треугольник копланарны (расположены в одной плоскости). Мы можем классифицировать его любым способом, и это не сильно повлияет на результат, поэтому мы решим классифицировать его как заднюю грань, чтобы избежать обработки вырожденных треугольников.
Откуда мы берём вектор нормали? Оказывается, существует векторная операция — векторное произведение
, получающая два вектора
и
и дающая в результате перпендикулярный им вектор. Мы можем запросто получить два вектора, копланарные треугольнику, вычтя его вершины из друг друга, то есть вычисление направления вектора нормали треугольника
будет простой операцией:
Заметьте, что я сказал «направление вектора нормали», а не «вектор нормали». Для этого есть две причины. Первая заключается в том, что
не обязательно равен
. Это не очень важно, потому что нормализация
будет тривиальной операцией, и потому что нас волнует только знак
.
Но вторая причина заключается в том, что если
— это вектор нормали
, то им является и
!
Разумеется, в этом случае нам очень важно, в каком направлении указывает
, потому что именно это позволяет нам классифицировать треугольники как передние или задние. А векторное произведение не коммутативно; в частности,
. Это значит, что мы не можем просто вычесть вершины треугольника в любом порядке, потому что это определяет, указывает ли нормаль «внутрь» или «наружу».
Хотя векторное произведение не является коммутативным, оно и не случайно, конечно же.
Система координат, которую мы всё время использовали (X вправо, Y вверх, Z вперёд), называется левосторонней, потому что можно направить большой, указательный и средний палец левой руки в этих направлениях (большой палец вверх, указательный вперёд, средний вправо). Правосторонняя система координат похожа на неё, но указательный палец правой руки указывает влево.
Затенение
Давайте продолжим добавлять «реализма» сцене; в этой главе мы изучим, как добавить в сцену источники освещения и как осветить содержащиеся в ней объекты.
Эта глава называется Затенение, а не Освещение; это две тесно связанные, но отличающиеся концепции. Освещение относится к математике и алгоритмам, необходимым для вычисления воздействия освещения на одну точку сцены; Затенение использует техники для распространения воздействия источника освещения от дискретного множества точек на объекты целиком.
В главе Освещение раздела Трассировка лучей я уже рассказал всё необходимое, что нужно знать об освещении. Мы можем задать окружающее, точечное и направленное освещение; вычисление освещённости любой точки в сцене при заданной позиции и нормали поверхности в этой точке выполняется одинаковым способом и в трассировщике лучей, и в растеризаторе; теория точно такая же.
Более интересная часть, которую мы изучим в этой главе, заключается в том, как взять эту "освещённость в точке" и заставить её работать для объектов, состоящих из треугольников.
Плоское затенение
Давайте начнём с простого. Поскольку мы можем вычислить освещённость в точке, давайте просто выберем любую точку в треугольнике (скажем, его центр), вычислим там освещение и используем значение освещённости для затенения всего треугольника (для выполнения действительного затенения мы можем умножить цвет треугольника на значение освещённости):
Не так уж плохо. И очень просто увидеть, почему так случилось. Каждая точка в треугольнике имеет одну и ту же нормаль, и пока источник освещения достаточно далеко, векторы света приблизительно параллельны, то есть каждая точка получает примерно равное количество освещения. Это примерно объясняет различия между двумя треугольниками, из которых состоит каждая сторона куба.
Но что произойдёт, если мы возьмём объект, у которого каждая точка имеет свою нормаль?
Не очень хорошо. Совершенно очевидно, что объект является не настоящей сферой, а приближением, состоящим из плоских треугольных фрагментов. Поскольку такой тип освещения делает изогнутые объекты похожими на плоские, он называется плоским затенением.
Затенение по Гуро
Как же нам улучшить картинку? Простейший способ, для которого у нас уже имеются почти все инструменты — вычисление освещения не центра треугольника, а его трёх вершин; эти значения освещения от
до
можно затем линейно интерполировать сначала по рёбрам, а потом и по поверхности треугольника, закрасив каждый пиксель плавно изменяющимся оттенком. То есть фактически это именно то, что мы делали в главе Отрисовка затенённых треугольников; единственная разница заключается в том, что мы вычисляем значения яркости в каждой вершине с помощью модели освещения, а не назначаем фиксированные значения.
Это называется затенением по Гуро по имени Анри Гуро, придумавшего эту идею в 1971 году. Вот результаты его применения к кубу и сфере:
Куб выглядит немного лучше; неравномерность теперь пропала, потому что оба треугольника каждой стороны имеют две общие вершины, и разумеется освещённость для них обеих вычисляется совершенно одинаково.
Однако сфера по-прежнему выглядит гранёной, а неоднородности на её поверхности выглядят очень неправильными. Это не должно нас удивлять, потому что в конце концов мы работаем со сферой как с набором плоских поверхностей. В частности, мы используем для соседних граней очень отличающиеся нормали — и в частности, мы вычисляем освещённость одной и той же вершины с помощью очень отличающихся нормалей разных треугольников!
Давайте сделаем шаг назад. То, что мы используем плоские треугольники для представления изогнутого объекта, ограничивает наши техники, но не свойство самого объекта.
Каждая вершина в модели сферы соответствует точке в сфере, но задаваемые ими треугольники являются простым приближением поверхности сферы. Поэтому неплохо было бы, чтобы вершины представляли свои точки сферы как можно точнее — и это значит, среди прочего, что они должны использовать настоящие нормали сферы:
Однако это не относится к кубу; даже несмотря на то, что треугольники имеют общие позиции вершин, каждую грань нужно затенять независимо от остальных. У вершин куба нет единственной «верной» нормали.
Как же решить эту дилемму? Проще, чем кажется. Вместо вычисления нормалей треугольников, мы сделаем их частью модели; таким образом, разработчик объекта может использовать нормали для описания кривизны поверхности (или её отсутствия). Также, чтобы учесть случай куба и других поверхностей с плоскими гранями, мы сделаем нормали вершин свойством вершины в треугольнике, а не самой вершины:
model { name = cube vertexes { 0 = (-1, -1, -1) 1 = (-1, -1, 1) 2 = (-1, 1, 1) ... } triangles { 0 = { vertexes = [0, 1, 2] normals = [(-1, 0, 0), (-1, 0, 0), (-1, 0, 0)] } ... }}
Вот сцена, отрендеренная с помощью затенения Гуро при соответствующих нормалях вершин:
Куб по-прежнему выглядит как куб, а сфера теперь выглядит гораздо более похожей на сферу. На самом деле, взглянув на её контур, можно определить, что она составлена из треугольников (эту проблему можно решить использованием большего количества мелких треугольников, потратив больше вычислительной мощности).
Однако иллюзия разрушается, когда мы пытаемся рендерить блестящие объекты; зеркальный засвет на сфере потрясающе нереалистичный.
Есть здесь и более мелкая проблема: когда мы сдвигаем точечный источник очень близко к большой грани, мы естественно ожидаем, что она будет выглядеть ярче, а эффект зеркальности будет более явным; однако мы получаем строго противоположную картину:
Что здесь происходит: несмотря на то, что мы ожидаем, что точки рядом с центром треугольника получат больше освещения (потому что
и
приблизительно параллельны), мы вычисляем освещение не в этих точках, а в вершинах, то чем ближе источник освещения к поверхности, тем больше угол с нормалью. Это значит, что каждый внутренний пиксель будет использовать яркость, интерполированную между двумя низкими значениями, то есть тоже имеют низкое значение:
Затенение по Фонгу
Ограничения затенения по Гуро легко преодолеть, но как обычно существует компромисс между качеством и затрачиваемыми ресурсами.
При плоском затенении использовалось единственное вычисление освещения на треугольник. Для затенения по Гуро требовалось три вычисления освещения на треугольник плюс интерполяция единственного атрибута (освещённости) по треугольнику. Следующим шагом в этом увеличении качества и затрат на пиксель будет вычисление освещения для каждого пикселя треугольника.
Это не кажется особо сложным с точки зрения теории; в конце концов, мы уже вычисляли освещение для трёх точек, и вычисляли попиксельное освещение для трассировщика лучей. Но хитрость здесь в том, чтобы выяснить, откуда берутся входные данные для уравнения освещённости.
Нам нужен
. При направленном источнике освещения
задан. Для точечного источника освещения
задаётся как вектор из точки сцены
в позицию источника освещения
. Однако, у нас нет
для каждого пикселя треугольника, а есть только для вершин.
У нас есть проекция
— то есть
и
, которые мы собираемся отрисовать на холсте! Мы знаем, что
а также у нас есть интерполированное, но геометрически верное значение для
как часть алгоритма буферизации глубин, поэтому
Поэтому мы можем получить из этих значений
:
Нам нужен
. Это тривиально, потому что мы вычисляем
так, как объяснено выше, потому что позиция камеры известна.
Нам нужен
. У нас пока есть нормали только в вершинах. Когда у тебя в руках молоток, все задачи похожи на гвозди, а наш молоток — это линейная интерполяция значений атрибута! Мы можем взять значения
,
и
в каждой вершине и воспринимать их как несвязанные вещественные числа, которые можно линейно проинтерполировать. В каждом пикселе мы заново собираем интерполированные компоненты в вектор, нормализуем его и используем в качестве нормали этого пикселя.
Эта техника называется затенением по Фонгу по имени Буи Тьена Фонга, придумавшего её в 1973 году. Вот её результаты:
Сфера выглядит отлично, за исключением её контура (но алгоритм затенения в этом не виновать), а эффект от приближения источника света к треугольнику ведёт себя именно так, как мы ожидаем.
Это также решает проблему с приближением источника к грани, теперь мы получаем ожидаемые результаты:
На этом этапе мы уже достигли возможностей трассировщика лучей, разработанного в первой части, за исключением теней и отражений. Вот выходные данные разрабатываемого нами растеризатора при использовании той же сцены:
А вот версия с трассировкой лучей для справки:
Они выглядят почти аналогично, несмотря на то, что в них используются совершенно различные технологии. Это логично, потому что мы использовали разные техники для рендеринга одной сцены. Единственным заметным различием являются рёбра сфер, которые трассировщик лучей рендерит как математически идеальные сферы, а растеризатор аппроксимирует как множество треугольников.
Текстуры
Пока мы можем рендерить такие объекты, как кубы или сферы, и воздействовать на них освещением. Но обычно нам нужно рендерить не кубы и сферы, а ящики и планеты, игровые кости или мрамор.
Рассмотрим деревянный ящик. Как превратить куб в деревянный ящик? Один из вариантов — добавить множество треугольников, поссоздающих структуру дерева, шляпки гвоздей, и так далее. Это сработает, но из-за геометрическая сложность сцены сильно повысится, что повлияет на производительность.
Ещё один вариант — имитировать ящик: взять плоскую поверхность куба и просто нарисовать поверх неё что-то, напоминающее древесину. Если не приглядываться к ящику, то вы никогда не заметите разницы.
Мы воспользуемся вторым подходом. Во-первых, нам понадобится изображение, которое мы будем рисовать на поверхности; в этом контексте мы назовём это изображение текстурой, хотя это и противоположно тому, что мы называем текстурой объекта — грубый он или мягкий, и т.д… Вот текстура «деревянного ящика»:
Во-вторых, нам нужно указать, как текстура должна накладываться на модель. Мы можем задать наложение для каждого треугоьника, указав точки текстуры, накладываемые на каждую вершину треугольника:
Стоит заметить, что без проблем можно деформировать текстуру или использовать только части текстуры, меняя координаты текстуры для каждой вершины.
Чтобы задать это наложение, мы используем систему координат, определяющую точки этой текстуры; назовём эти координаты
и
, чтобы не путать их с
, которые обычно обозначают пиксели на холсте. Мы также объявим, что
и
являются вещественными значениями в интервале
вне зависимости от разрешения изображения текстуры в пикселях. Это очень удобно по нескольким причинам; например, в зависимости от доступного объёма ОЗУ можно использовать текстуры более низкого или высокого разрешения, не меняя саму модель.
Основная идея наложения текстур проста: вычисляем координаты
для каждого пикселя треугольника, получаем соответствующий тексел (то есть текстурный элемент) из текстуры и закрашиваем пиксель этим цветом. Заданная пара
в текстуре размером
накладывается на тексел в
.
Но у нас есть только координаты
и
для трёх вершин треугольника, а они необходимы для каждого пикселя… и наверно вы уже видите, что должно произойти. Да, линейная интерполяция. Мы используем присвоение атрибутов для интерполяции значений
и
по грани треугольника, что даст нам
в каждом пикселе; мы закрасим пиксель соответствующим цветом, взятым из текстуры (возможно, с воздействием освещение), и получим…
…заурядные результаты. Ящики выглядят вполне неплохо, но если присмотреться к диагональным доскам, то становится очевидно, что они немного деформированы.
В чём же ошибка?
Мы снова попались в ловушку неверного предположения; мы считаем, что
и
вдоль экрана меняются линейно. Очевидно, что это не так. Посмотрите на стену очень длинного коридора, покрашенного попеременно чёрными и белыми вертикальными полосами. При удалении стены мы должны видеть всё более и более тонкие полосы. Однако если мы предположим, что координата
меняется линейно вместе с
, то это будет неверно:
Ситуация очень похожа на ту, которая встречалась нам в главе Буферизация глубин, и решение тоже очень похоже: хотя
и
нелинейны в экранных координатах,
и
линейны (Примечание: доказательство этого очень похоже на доказательство
: примем, что
линейно изменяется в 3D-пространстве, и заменим
и
на их выражения в экранном пространстве.). Поскольку мы уже интерполировали значения
для каждого пикселя, то достаточно интерполировать
и
, чтобы получить
и
:
При этом мы получим ожидаемые результаты:
http://mirpozitiva.ru/articles/1894-kak-narisovat-3d-risunok...
На сегодняшний день все популярнее становятся 3d рисунки на бумаге, в них можно долго всматриваться и любоваться. Создавать такие шедевры могут не только талантливые художники, но и те, кто только знакомится с изобразительным искусством. Научится рисовать никогда не поздно, сделать эффектные 3d рисунки сможет каждый.Инструменты, которые потребуются для 3d, самые простые: ручка, карандаши, маркер и листок бумаги. Кстати, рисовать новичкам лучше всего по клеткам в тетради, так изображать фигуры намного проще.Стоит отметить, что изображение создается на бумаге поэтапно, в этом деле главное – последовательность, даже если воспроизводятся простые и незамысловатые картинки.Многих интересует как нарисовать 3d рисунок на бумаге карандашом ярко и реалистично. Для этого стоит использовать фото инструкции или видео, которые наглядно покажут всю технику воссоздания 3д рисунка.Давайте разберем рисунки карандашом поэтапно для начинающих. Для наглядности распечатывайте нарисованные изображения, чтобы облегчить себе задачу. Заметьте, что первое знакомство с техникой 3д может вызвать неоднозначные впечатления, здесь не нужно спешить, плавные движения и выдержка – главные помощники начинающего художника.Итак, приступим к делу, будем учиться как рисовать красивые 3d рисунки.
СОДЕРЖАНИЕ
1. Бабочка
2. Ступеньки
3. Бананы
4. Воронка
5. Лестница
6. Сердце
7. Видео-бонусы: рисунки 3d ручкой
Бабочка
Простая схема позволит понять как рисовать 3д ручкой удивительно красивого насекомого. Ознакомься с этой техникой и нарисуй чудо-рисунок самостоятельно.
Пошаговая инструкция:
- Проще всего создавать рисунки по клеточкам, поэтому первоначально разметим лист бумаги, прочертим направляющие линии.
- Теперь потребуется наметить очертания бабочки.
- Детализируйте нарисованные крылья.
- Внесите некоторые элементы самого окраса крылышков, прорисуйте брюшко.
- Потребуется убрать направляющие линии, займитесь разукрашиванием.
- Полностью дорисуйте крылья, затем выровняйте тон изображения.
- Изобразите тень, используя светлый тон карандаша, на завершающем этапе сделайте более темным тон тени.
- По пунктирной линии вырежьте часть листа, как показано на фото. Теперь вы ознакомились с тем как нарисовать 3д рисунок простым способом.
Ступеньки
Если вы не знаете что именно можно нарисовать 3d ручкой или карандашом, тогда начните с самого простого. Ведь придать изображениям реалистичность вовсе не так уж сложно, убедитесь на предложенном ниже фото-уроке.
Этапы создания изображения:
- Начертите фигуру по тем же размерам, которые указаны на фото, затем выделите справа треугольник и пятиугольник.
- Проведите от верхнего и нижнего угла пятиугольника параллельные линии, ориентируясь на них, начертите продольные и поперечные линии. Внутри пятиугольника потребуется сделать сетку.
- Нарисуйте ступеньки в нижней части фигуры.
- Заштрихуйте несколько участков карандашом и ручкой.
- Завершите штриховку оставшихся областей фигуры.
- Как видите, объем 3d рисунку карандашом можно придать за счет применения различных техник штриховки.
Бананы
Сымитировать лежащие на столе фрукты довольно просто, нет необходимости применять особые техники изображения предметов. Можно использовать для создания рисунка 3д ручки и маркеры.
Техника выполнения рисунка:
- Положите на лист бумаги два банана, возьмите простой карандаш и обведите контуры.
- Теперь понадобится маркер черного цвета, проведите параллельные линии, огибая изображение банана, таким образом, создается объемное изображение. Чем больше проведете линий, тем реалистичнее получится картинка.
- Возьмите желтую краску, закрасьте промежутки через две линии.
- Используйте зеленую краску и закрашивайте оставшиеся промежутки. Рисунок будет выглядеть реалистично даже на обычном листке из блокнота.
Более подробную инструкцию по выполнению работы в такой технике на примере руки пришельца вы сможете увидеть на видео (или можно использовать свою руку, просто обведите свою ладонь и пальцы карандашом, а далее следуйте видео-инструкции):
Воронка
Если вы хотите узнать как нарисовать простой 3d рисунок на бумаге, используйте распечатанный образец. По освоенной технике вы сможете обучить и ребенка как нарисовать 3д.
Пошаговое выполнение работы:
- Сперва рисуем овал посередине листа, проведите изогнутые линии по кругу, как демонстрирует фото.
- Наведите более четкие контуры линий, осуществите легкое нанесение штриховки внутри овала.
- На третьем этапе зарисуйте голубым карандашом линии, которые попадают вглубь воронки, оставляя более светлые области на верхнем крае овала.
- Благодаря этой технике рисунки на бумаге оживают.
Лестница
Перед тем как рисовать 3d ручкой необходимо попробовать сделать подобные рисунки карандашом. Учимся создавать красивые объемные изображения вместе.
Как выполнить рисунок:
- Нарисуйте фигуру по размерам, указанным на фото.
- Проведите параллельные линии внутри созданного многоугольника.
- Начните очерчивать ступеньки, используя мягкий карандаш.
- Сотрите вспомогательные линии, наведите контуры фигуры.
- Заштрихуйте область над ступеньками и под ними, сделайте более темной левую сторону фигуры.
- Нанесите штриховку за границами фигуры, таким образом, создадите тень от ступенек.
- Картинка готова.
Сердце
Объемное, будто живое сердце станет отличным подарком для любимого человека. Возьмите в руки карандаш и маркер, отчетливо проведите линии, выделите их и растушуйте. Поверьте, нарисованное изображение сможет полностью передать ваши чувства.
Как нарисовать:
- Нарисуйте карандашом сердце, используя обычный карандаш.
- Проведите карандашом параллельные линии, не затрагивая изображение в центре.
- Теперь потребуется создать эффект «вдавливания» сердца путем искривления линий.
- Удалите ластиком вспомогательные линии, наведите кривые черным маркером.
- Вокруг сердца заштрихуйте карандашом, растушуйте тень, более темные оттенки должны располагаться вблизи контуров сердца.
- Легкими движениями заштрихуйте внутреннюю часть фигуры, придавая ей объем, как демонстрирует фото:
Видео 3d иллюзия сердца:
Помните, воображению нет границ, создавайте собственные уникальные рисунки, поразите всех умением создавать объемные изображения.Например, можете нарисовать по этой инструкции Карлсона:Простой вариант:Сложный вариант:Видео-бонусы: рисунки 3d ручкой
Рисуем 3d ручкой красивую бабочку:
Рисуем 3D фото-рамочку:Рисуем 3д ручкой букет ромашек:3Д Снеговичок:3d елочка ручкой:Понравилась статья? Подпишитесь на канал, чтобы быть в курсе самых интересных материаловПодписаться
>