Математика в 3Д ► 15. Lens Flares. Проекция координат источника света на плоскость
Сегодня мы будем делать проекцию текстурных координат объекта источника света на плоскость. Вероятно это будет один из наиболее сложных уроков и к настоящему моменту самый объёмный. Ну то есть как сложный. Как вы убедитесь в процессе, здесь не будет ничего такого, чего бы мы все не проходили в школе, но почему-то когда доходит до применения на практике даже теорема Пифагора начинает казаться квантовой механикой. А объём такой получился потому, что я подробно задокументировал каждое действие.
Буду очень благодарен всем, кто решит стать платными подписчиками или разово задонатить на означенные цели. Это сильно мотивирует на продолжение.
О том, как настроен проект, я подробно рассказывал в первой части. А в прошлой части мы остановились на сборке эффекта бликов линзы в таком виде ↓
Добавлять новые элементы можно, насколько позволит воображение и ресурсы компьютера, но чтобы их использовать на практике, было бы здорово, если бы мы могли привязывать их координаты к другим объектам в сцене. Например, чтобы где-то в сцене был некий источник света, а на плэйне перед камерой отображалась его проекция - то есть чтобы из камеры это выглядело так, как будто эти лучи и свечение исходят из самого источника, где бы он ни находился. Напомню, что сама сцена выглядит у нас вот так ↓
То есть у нас есть плэйн, камера и источник света, оставшийся от дефолтной сцены и пока что расположенный где-то вне экрана. Давайте расположим источник света где-нибудь за плэйном, чтобы потом использовать его проекцию на плэйн ↓
И давайте теперь подумаем. У нас есть система из трёх объектов. Два из них - камера и плэйн - всегда перпендикулярны друг другу. Потому что так мы их расположили, и потому что плэйн припэренчен к камере. То есть если сейчас посмотреть на эту систему сверху, то мы увидим, что в ней присутствует прямоугольный треугольник со сторонами, положим, A, B и C ↓
Сейчас система координат находится на месте угла AB, а нам надо перенести её в угол AC. Обратите внимание, что нам не обязательно переносить именно систему координат объекта плэйна. Нод Texture Coordinate позволяет использовать в качестве объекта для системы координат Object любой объект в проекте, если указать его в окошке в самом низу.
Таким образом у нас есть выбор:
• взять систему координат объекта плэйн и сдвинуть её вдоль стороны А в угол СА
• взять систему координат объекта источника света и спроецировать её на плоскость (как и было заявлено, но мы так делать не будем и пойдём другим путём)
• взять систему координат объекта камеры и сдвигать её в направлении объекта источника света пока она не окажется на плоскости плэйна ↓
Вариант с использованием в качестве текстурных координат объекта камеры мне кажется наиболее рациональным, потому что нам уже известны координаты и источника света и камеры, а значит, мы можем очень просто вычислить вектор от камеры до источника света, и нам останется уменьшить его в нужное количество раз и использовать для сдвига текстурных координат камеры ↓
Если мы найдём способ найти длину стороны С, которая является гипотенузой прямоугольного треугольника АВС, то сможем разделить её на длину вектора от камеры до источника света, получить их отношение и использовать его в качестве множителя для координат этого вектора. Получив нужный вектор от камеры до проекции источника света на плэйне, мы вычтем его из текстурных координат камеры и таким образом сместим их на уровень плэйна.
Посмотрим, что у нас есть. У нас есть location всех трёх объектов по осям XYZ ↓
Соответственно, мы можем вычислить векторы а и b и их длины |a| и |b| ↓
🛈 Напомню, что вектор - это направленный отрезок. В трёхмерном пространстве он характеризуется тремя цифрами - значениями координат положения его конца по XYZ. Начало любого вектора считается от ( 0, 0, 0 ) и при расчётах не учитывается. Чтобы получить вектор из отрезка от одной точки пространства до другой, нужно из координат второй вычесть координаты первой. Таким образом, начало отрезка перемещается в ( 0, 0, 0 ), его длина и направление не меняются, мы получаем вектор в чистом виде, и определяется он значениями новых координат конца отрезка по XYZ.
Для вычисления длины вектора в Блендер есть готовые функции, которые мы будем использовать, поэтому с этим проблем не возникнет. Но для любознательных она равна квадратному корню из суммы квадратов его значений по XYZ
Напомню, нам нужно вычислить длину стороны C, которая является гипотенузой в прямоугольном прямоугольнике. У меня с математикой в школе было так себе, но я точно помнил, что если в прямоугольном треугольнике известна длина одной стороны и один угол, помимо прямого, то можно очень просто найти длины всех остальных сторон. Даже без Гугла я вспомнил, что косинус угла - это отношение прилежащего катета к гипотенузе - то есть в нашем случае стороны В к стороне С. Cos α = B / C. А значит, С = В / Cos α.
Длина стороны B у нас фактически уже есть, нам нужен только косинус угла BC, который я обозначил как α. А угол α в прямоугольном треугольнике полностью совпадает с углом α между векторами a и b, это он и есть. К сожалению, опции "найти угол между векторами" Блендер не предоставляет, поэтому придётся посчитать вручную. Для этого нам понадобится скалярное произведение векторов а и b. Для вычисления скалярного произведения векторов в Блендер есть функция Dot Product, а формула его вычисления выглядит так ↓
Поскольку фактически у нас есть всё необходимое, у нас есть и готовая формула для вычисления косинуса угла: скалярное произведение векторов разделить на произведение длин векторов ↓
Ещё раз, следите за руками ↓.
1. Косинус угла α - это отношение скалярного произведения векторов к произведению их длин.
2. В то же время косинус угла α - это отношение прилежащего к углу катета В к гипотенузе С.
3. Соответственно, длина гипотенузы С равна отношению прилежащего катета В к косинусу угла α. При этом под стороной В мы фактически понимаем длину вектора b.
4. Вместо стороны В подставляем длину вектора b, вместо косинуса угла подставляем формулу из пункта 1. Получаем, что сторона С равна отношению длины вектора b к отношению скалярного произведения векторов a и b и произведения их длин.
5. Перекомпоновываем формулу в удобочитаемый вид, алгебра вам в помощь
6. Чтобы сверху не писать длина вектора b умножить на длину вектора b пишем - длина вектора b в квадрате.
Всё у нас есть готовая формула нахождения длины стороны С. Не так уж сложно. Нет, ну правда, вспомните, мы же все, буквально каждый из нас, проходили всё это на уроках математики, классе в 7-м. Причём это считалось за простейшие примеры, там позже были и намного более сложные задачи. Как так получается, что когда дело доходит до применения на практике, это начинает казаться магией - вопрос к психологам.
Настало время применить то, что у нас получилось на практике.
Обратите внимание! В этой формуле мы использовали позиции объектов по XYZ - не их текстурные координаты, которые меняются на протяжении всего 3Д пространства, а "стационарные" параметры location, состоящие только из 3х числовых значений по XYZ. В редакторе материалов Блендер на данный момент нет нода, который позволял бы использовать отдельные параметры объектов - их позицию, вращение и масштаб. Поэтому нам придётся проявить изобретательность, а именно - использовать драйверы.
🛈 Драйверы в Блендер - это система привязок, которая позволяет использовать одни параметры для управления другими, как напрямую, так и с использованием в однострочных формулах, которые называются выражения, и используют простейшую математику и несколько вспомогательных модулей языка программирования Пайтон.
🛈 Для тех, кто знает Пайтон: фактически драйверы - это лямбда-функции, которые могут принимать в качестве аргументов переменные, которые определяются в редакторе драйвера. Функция должна возвращать значение соответствующего типа, чаще всего float или int, в зависимости от того, на какой параметр накладывается драйвер. В пространство имён драйверов импортированы классы из нескольких математических модулей. Полный словарь со всеми доступными функциями находится в bpy.app.driver_namespace. В него также можно дописывать и включать свои кастомные функции, таким образом существенно расширяя штатные возможности.
Процесс привязывания драйверами параметров объектов к нодам будет довольно долгим и нудным (наверное, минут пять, если знать, что делать). Лучше сразу постараться всё сделать внимательно, чтобы на этом этапе не было ошибок, потому что впоследствии их сложно будет отследить.
↑ Для начала нам нужен какой-то нод, на параметры которого мы сможем эти драйверы назначить. Мы будем использовать Convertor > Combine XYZ. Этот нод позволяет объединять 3 числовых параметра в вектор.
↑ Продублируем его 3 раза, и, чтобы не запутаться - а запутаться будет несложно - сразу же в N-панели во вкладке Node в графе Label переименуем их в Source, Camera и Plane
↑ Чтобы привязать с помощью драйвера числовые значения нодов к числовым значениям параметра location, для начала выберем объект с источником освещения и на его значении location по оси Х нажмём правой кнопкой мыши и выберем Copy as New Driver. Эта опция добавляет в буфер обмена выбранное значение в качестве драйвера
↑ Теперь в Combine XYZ, который мы назвали Source, щёлкнем правой кнопкой мыши на оси Х и выберем пункт Paste Driver
↑ После этого действия параметр должен окраситься в приятный (оценочное суждение) фиолетовый оттенок, а его значение, если всё было сделано правильно, должно стать равным значению location объекта источника освещения по оси Х.
Однако, по умолчанию добавляется просто локальное значение location по оси Х, которое отображается в N-панели вьюпорта при выделении объекта. Это означает, что если объект, например, припэренчен к другому объекту, то будут использоваться его локальные координаты относительно объекта-пэрента, а не глобальные координаты в 3Д пространстве сцены. Нам нужны именно глобальные координаты, поэтому нам нужно будет отредактировать добавленный драйвер, благо Блендер предоставляет доступ к ним, и нам не придётся производить сложные вычисления с использованием матриц трансформации и линейной алгебры.
↑ Нажимаем правой кнопкой мыши на параметре с драйвером и выбираем Edit Driver
↑ Меняем тип переменной, которая используется для определения значения, с Single Property на Transform Channel
↑ По умолчанию будет использована ось X и World Space - то есть ось Х в мировом (глобальном) пространстве координат. То есть как раз то, что нам нужно.
↑ Чтобы не проделывать все эти операции с остальными осями, щёлкаем правой кнопкой мыши на драйвере и выбираем Copy Driver, чтобы он скопировался в буфер обмена уже со всеми настройками
↑ С помощью правой кнопки мыши и Paste Driver добавляем скопированный драйвер на ось Y...
↑ И на ось Z.
↑ В результате все параметры оказываются привязаны к location источника освещения по оси Х. Нам остаётся только поменять для параметров Y и Z ось.
↑ ПКМ (правая кнопка мыши) на оси Y > Edit Driver
↑ Меняем X Location на Y Location
↑ ПКМ (правая кнопка мыши) на оси Z > Edit Driver
↑ Меняем X Location на Z Location
Теперь нам нужно по тому же принципу привязать к двум оставшимся нодам location камеры и плэйна
↑ Скопируем настроенный драйвер с оси Х первого нода: ПКМ > Copy Driver
↑ Вставим на ось Х нода, который мы назвали Camera: ПКМ > Paste Driver
↑ В настройках драйвера надо переназначить объект, использующийся для привязки параметра. ПКМ > Edit Driver
↑ Меняем объект на Camera - объект камеры.
↑ Копируем отредактированный драйвер: ПКМ > Copy Driver
↑ Вставляем на ось Y: ПКМ > Paste Driver
↑ Вставляем на ось Z: ПКМ > Paste Driver
↑ Чтобы заменить ось, на оси Y: ПКМ > Edit Driver
↑ Меняем канал трансформации в переменной на Y Location
↑ Чтобы заменить ось, на оси Z: ПКМ > Edit Driver
↑ Меняем канал трансформации в переменной на Z Location
↑ То же самое с объектом-плэйном. Вставляем на ось Х скопированный с предыдущего Combine XYZ драйвер. ПКМ > Paste Driver
↑ Нужно заменить объект-таргет в переменной, заходим внутрь: ПКМ > Edit Driver
↑ Меняем в переменной объект на Plane
↑ Копируем исправленный драйвер: ПКМ > Copy Driver
↑ Вставляем на ось Y: ПКМ > Paste Driver
↑ Вставляем на ось Z: ПКМ > Paste Driver
↑ ПКМ > Edit Driver на оси Y
↑ Меняем канал трансформации на Y Location
↑ ПКМ > Edit Driver на оси Z
↑ Меняем канал трансформации на Z Location
Медитативненько? Дзен ловится? Надеюсь, нигде не ошиблись, приступаем к реализации формул.
↑ Для начала нам нужно вычислить векторы от камеры до источника света и до плэйна. Я решил заменить обозначения, и теперь называть не стороны треугольника A, B и C, а точки в пространстве: A - позиция камеры, В - позиция плэйна, C - позиция проекции, которую мы вычисляем, D - позиция источника света. Чтобы найти векторы a и b, нам нужно из координат точек D и B вычесть координаты точки A.
↑ Делаем это с помощью Convertor > Vector Math в режиме Subtract (вычитание). Порядок подключения важен! В обоих случаях мы вычитаем координаты камеры из координат объектов, а не наоборот, иначе векторы окажутся направленными в обратную сторону и получится, что мы будем высчитывать какой-то совсем другой угол.
↑ Чтобы не путаться, в N-панели во вкладке Node, в графе Label переименуем ноды Vector Math в Vector a и Vector b
↑ Итак, смотрим на самую нижнюю формулу. Нам нужно их скалярное произведение. Добавляем Vector Math в режиме Dot Product и подключаем к нему Vector a и Vector b. При скалярном произведении порядок подключения неважен.
↑ Теперь нам нужны длины векторов. В Блендер их можно найти при помощи Vector Math в режиме Distance. Длина является абсолютной величиной и не зависит от направления вектора, поэтому значения location объектов можно подключать в любом порядке. Нам нужны расстояния от камеры до источника света и от камеры до плэйна.
↑ Чтобы не путаться, в N-панели во вкладке Node, в графе Label переименуем добавленные ноды в |a| и |b| - то есть длины векторов.
↑ Перекомпонуем ноды, чтобы они располагались как в формуле, чтобы не путаться. Нам нужно возвести |b| в квадрат - мы делаем это с помощью Convertor > Math в режиме Power (возведение в степень). Квадрат - это вторая степень, поэтому нижнее значение устанавливаем на 2. После этого перемножаем квадрат |b| и |a| с помощью Math в режиме Multiply (умножение) и делим результат на скалярное произведение (Dot Product) с помощью Math в режиме Divide (деление).
↑ Ура! Мы получили расстояние от камеры до проекции источника света на плэйн! Назовём последний нод |a| small - потому что это длина уменьшенного вектора |a|. В принципе, вы можете называть и как-то более удобочитаемо, но просто пока проходите урок, лучше, чтобы всё совпадало во избежание путаницы.
Теперь нам нужно высчитать сам уменьшенный вектор а
↑ Для этого сначала вычисляем отношение между длиной короткого вектора от камеры до проекции и длиной полного вектора от камеры до источника света, разделив |a| small на |a| с
помощью Math в режиме Divide (деление).
помощью Math в режиме Divide (деление).
↑ Умножаем Vector a на полученное отношение с помощью Vector Math в режиме Scale - и наш корректор для текстурных координат готов!
↑ Если до этого вы использовали какие-то другие, кастомные текстурные координаты и хотели бы их оставить про запас, можно объединить все исходящие линки, удерживая Shift, и проводя по ним, удерживая ПКМ - добавится точка Reroute
↑ После этого останется отделить один исходящий линк и потом при необходимости удобно подключать его к точке Reroute
↑ Добавим новые текстурные координаты Input > Texture Coordinate
↑ Выберем в нём в качестве используемого объекта объект Camera
↑ Теперь вычтем из текстурных координат корректирующий сдвиг, который мы делали, использую Vector Math в режиме Subtract (вычитание). Кто заметит здесь ошибку - молодец. Подсказка - она не в математике.
↑ Подключаем новые текстурные координаты... и ничего не работает. На экране чернота. Давайте разбираться, что не так.
🛈 Вообще вот здесь мне стоило сначала исправить ту ошибку, о которой я говорил выше. Но я сам допустил её по невнимательности и не заметил, и поэтому сначала начал показывать, как исправлять то, о чём знал сам, из предыдущего опыта. А именно:
↑ Если мы вспомним, камера у нас развёрнута на 90 градусов по оси Х. А значит, что её система координат развёрнута вместе с ней и получается, что наш коррекционный сдвиг, утаскивает её не в том направлении. Да, мне тоже не нравится формулировка "коррекционный сдвиг", но она отражает суть. Чтобы всё работало правильно, нам нужно развернуть систему координат. Для этого нам нужно довернуть её на тот же угол, на который развёрнута камера.
🛈 Я пришёл к этому решению чисто эмпирическим путём, и так и не понял до конца, почему если развернуть систему координат на угол прямо противоположный углу разворота камеры (умноженный на -1), чтобы она вроде бы как оставалась на месте, а корректирующий сдвиг не вычитать, а прибавлять, система будет работать неверно. Задачка для самостоятельного анализа.
↑ И вновь нам понадобятся драйверы. На этот раз мы будем копировать вращение камеры. Добавляем ещё один Combine XYZ и копируем вращение камеры по оси Х как драйвер с помощью: ПКМ > Copy as New Driver
↑ Вставляем в Combine XYZ с помощью: ПКМ > Paste Driver
↑ Точно так же нам нужно, чтобы учитывалось вращение в глобальных, мировых координатах. ПКМ > Edit Driver
↑ Меняем тип переменной с Single Property на Transform Channel
↑ На этот раз будем использовать X Rotation.
↑ Всё остальное оставляем по умолчанию: режим XYZ Euler (тот же который используется в объекте камеры для вращения), пространство - Мировые координаты
↑ Копируем драйвер
↑ Вставляем на оси Y и Z
↑ Редактируем ось Y
↑ Заменяем канал трансформации на Y Rotation
↑ Редактируем ось Z
↑ Заменяем канал трансформации на Z Rotation
↑ Для регулировки вращения системы координат нам понадобится нод Vector > Mapping
↑ Подключаем координаты в вектор, а значения вращения камеры в Rotation... и (спойлер - хотя мы всё сделали правильно) снова ничего не работает. И дело как раз в той ошибке, которую я упоминал раньше. Нашли?
↑ При подключении Texture Coordinate, поскольку мы использовали текстурные координаты камеры, я случайно использовал текстурные координаты типа Camera. А нужно-то было использовать текстурные координаты камеры как объекта, то есть выход Object, а не Camera. Если вспомнить, чем они отличаются, то всё становится понятным. Подключаем выход Object из текстурных координат с камерой, указанной в качестве объекта - и всё работает... но как-то странно.
↑ Это может быть не очевидным, но когда мы собирали элементы с лучами на плэйне, мы исходили из того, что его вращение по осям XYZ было равно (0, 0, 0), поэтому мы брали арктангенс осей X и Y, чтобы перенаправить ось Х по кругу. Но камера, как было сказано выше, расположена таким образом, чтобы быть всегда перпендикулярной плэйну. А поскольку мы используем теперь в качестве системы координат её, получается, что её ось X располагается по кругу в другой плоскости, перпендикулярной плоскости плэйна, поэтому мы видим только её плоский срез.
↑ Это можно было бы исправить, поменяв внутри группы с лучами оси, используемые в арктангенсе. Но если нам вдруг будет нужно подключить обратно плэйн в качестве системы координат, то всё придётся снова переделывать заново, а таких групп, как лучи, может со временем накопиться больше. Поэтому, чтобы сохранять всё полностью заменяемым, мы возьмём вращение камеры, которое уже использовали для исходного вращения системы координат, инвертируем это вращение, умножив на -1 с помощью Vector Math в режиме Scale и используем для финальной коррекции уже смещённой системы координат с помощью слота Rotation в ноде Mapping
↑ Теперь мы можем перемещать источник света, и его проекция на плэйне будет следовать за ним...
↑ ...куда бы он ни передвигался
↑ Компактизируем ноды, спрячем неиспользуемые входы с помощью Ctrl+H, скроем все ноды, кроме важных для понимания работы, с помощью H, объединим их в рамочку, выделив и нажав Ctrl+J, и в N-панели во вкладке Node в графе Label назовём рамочку Projection, чтобы знать, что делают в ней ноды, когда мы вернёмся к этому проекту на следующий день, или неделю или год спустя.
↑ Вот как выглядит проект на данном этапе. Не забудьте засейвиться!
Вы видите эти уроки, благодаря тем, кто забирает их к себе на стенки, рассказывает друзьям, знакомым, в сообществах, пабликах, чатах, коммьюнити. Без их поддержки развитие проекта было бы невозможным, и я благодарен каждому.
Со своей стороны выражаю большую благодарность спонсорам. Вы даёте мне силы продолжать.