Математика в 3Д ► 16. Lens Flares. Оптимизация: драйверы и Python
У нас было с собой несколько собранных масок самых разных форм, понимание, как работают текстурные координаты и какие математические и векторные операции можно с ними производить, три-четыре основные процедурные шума и несколько вспомогательных, некоторое количество собранных нод-групп, и мы даже использовали драйверы для привязок одних параметров к другим. Единственным, чего мы пока не касались, был пайтон. Но мы понимали, что рано или поздно перейдём и на эту дрянь.
Эх, догадываюсь, насколько мало людей будут это читать, но всё равно считаю, что такие уроки должны быть. Это уровень Про. Мне бы хотелось, чтобы русскоязычное Блендер-сообщество росло и развивалось, а без выхода из уютных и привычных рамок рост невозможен. Прочитайте хотя бы просто вступление, чтобы понимать, насколько существенной будет оптимизация, чтобы иметь в виду, что такое возможно. Да и не настолько уж сложно, если разобраться.
ВСТУПЛЕНИЕ
Общая идея. В прошлом уроке мы собрали систему, которая позволяет использовать сторонний объект для проекции текстурных координат на плоскость, перпендикулярную камере. Все расчёты мы делали с помощью нодов прямо в Shader Editor ↓
Но как работают ноды? Их цель - сформировать пиксели на экране, поэтому они не просчитывают магическим образом единовременно все параметры материалов для всего 3Д пространства в сцене. Вместо этого параметры рассчитываются только для тех точек поверхности объектов, которые на каждом конкретном кадре проецируются на сенсор камеры в виде пикселей - собственно, это то, что мы и видим на экране - пиксели. Будь они на финальном рендере или в 3Д вьюпорте.
Как это делается. Берём любой пиксель, смотрим, какой объект в сцене на его месте, какая у него поверхность, какие у неё значения цвета, прозрачности, Roughness, IOR и т.д., смотрим, в какую сторону развёрнута нормаль полигона, вычисляем, свет каких источников и с какой силой должен ею отражаться, рассеиваться, поглощаться, преломляться и т.д., смотрим, что может блокировать источники света и отбрасывать тень.
И так для каждого пикселя на экране.
🛈 В случаях, которые мы до этого момента рассматривали, для каждого пикселя бралось значение входящих текстурных координат, математически обрабатывалось и на выходе выдавалось уже изменённое значение. Эти преобразованные значения и образовывали формы и элементы, которые мы использовали.
На практике всё это означает, что если мы, например, с помощью одного нода Math в режиме Divide некое значение делим на 2, эта математическая операция производится в компьютере столько раз, сколько пикселей с этим материалом в данный момент отображается на экране. То есть для картинки с разрешением 1920 на 1080 пикселей, если материал занимает весь экран, операция деления должна быть выполнена 2 073 600 раз. А некоторые ноды внутри содержат не одно действие, а несколько. Например, на вычисление расстояния между векторами нужно порядка десяти простых математических операций - а это уже около 20 миллионов.
Разумеется, современные видеокарты спроектированы таким образом, чтобы быстро производить такие вычисления, и, например, в случае с текстурными координатами это действительно необходимо, потому что они меняются на протяжении 3Д пространства, и нам надо знать в какой точке пространства какое значение они принимают, чтобы делать дальнейшие расчёты. Но любые ресурсы не безграничны, и просто представьте, насколько мы теоретически можем разгрузить их, если подойти с умом.
Например, в прошлом уроке мы высчитывали векторы, их длины и угол между ними для отдельных точек пространства. Это статичные значения, их не требуется вычислять для каждого пикселя, потому что, они для всех пикселей остаются одинаковыми. Значение location объекта по XYZ - с какой стороны на него не смотри, даже если вообще отвернуться, не поменяется. Попытайтесь проанализировать и понять этот момент.
Теперь давайте посмотрим. Если выделить в отдельную рамку все расчёты, которые мы проводили со статичными значениями - то получится довольно приличное количество операций, которое компьютеру без всякой на то необходимости нужно совершить для каждого пикселя ↓
А теперь представьте, что можно посчитать всё это 3 (три) раза - по одному для каждой оси - и просто присвоить уже посчитанные значения каждому пикселю. Никакого читерства, никакого колдовства, всё абсолютно легально. Прирост производительности в несколько миллионов раз и растущий по экспоненте с увеличением разрешения. Неужели это не стоит того, чтобы разобраться?
🛈 Можно было бы отказаться от использования драйверов, перейти на чистый Пайтон и посчитать всё вообще только 1 раз, но драйверы всё же работают более стабильно, чем апишные обработчики смены кадра или содержимого сцены, которые позволяют такое делать, поэтому такая экономия уже будет неоправданной.
Кто решил, что готов попробовать, мы начинаем. Надеюсь, после этого урока вы увидите, насколько работа с Пайтон проще, логичнее и удобнее, насколько может расширять заложенный в Блендер функционал и устранять зависимость от наличия или отсутствия встроенных инструментов.
ПОДОГОТОВКА
Если у вас не работают драйверы или скрипты, в конце этого урока я собрал несколько полезных опций, на которые стоит обратить внимание.
↑ Для начала нам в Shader Editor нужен какой-нибудь нод, в котором мы сможем присваивать посчитанные значения. Это будет всё тот же Convertor > Combine XYZ
↑ Подключим его вместо предыдущих нодовых расчетов.
↑ Конечно, поскольку сейчас его значения равны (0, 0, 0), изображение пропадёт. Не страшно. Уберём рамочку с нодовыми расчётами в сторонку, чтобы не мешалась.
↑ Добавим на ось Х драйвер.
↑ Как и в прошлый раз для расчётов нам понадобятся входящие данные, а именно, location Источника света, Камеры и Плэйна в глобальных координатах. Мы можем добавлять входящие данные для драйверов с помощью переменных. Они добавляются нажатием кнопки +Add Input Variable. Мы добавим только 3.
🛈 Вообще, если нам нужно получить значение location в глобальном (мировом) пространстве, штатные возможности для переменных в Блендер позволяют использовать только один канал трансформации за раз. Например - значение X Location в World Space. То есть мы могли бы попытаться посчитать всё прямо в одну строчку в графе Expression (выражение), прямо из отдельных значений положения по XYZ всех трёх объектов, и тогда нам понадобилось бы 9 переменных - по одной для трёх осей у каждого из трёх объектов. Кстати вероятно у нас бы всё равно не получилось, потому что, как я однажды выяснил, длина строки Expression в драйверах Блендер ограничена, и нужное выражение туда может просто не поместиться
Поскольку мы уже вплотную подошли к программированию, нам нет нужды зависеть от штатных возможностей, поэтому мы добавим всего 3 переменные - по одной для объекта, и вместо того, чтобы передавать в драйвер отдельные значения location, мы передадим в качестве переменных их имена. Ниже покажу, как.
Имена объектов служат в Блендер уникальными идентификаторами объектов. Не может быть в проекте двух объектов с одним именем. Таким образом, по имени объекта позже в скрипте Пайтон мы сможем безошибочно найти сам объект и получить доступ ко всем его свойствам. Не говоря уже о том, что если нам понадобится заменить в системе какой-нибудь объект на другой, нам достаточно будет просто в драйвере указать новый объект.
↑ Пока что укажем для первой переменной объект Light
↑ Для второй - объект Camera
↑ Для третьей - объект Plane
↑ Убедимся, что у всех трёх переменных тип выставлен не Transform Channel, а Single Property - то есть отдельное свойство. Для понимания: и имя, и location, и многие другие параметры объекта могут выступать в качестве его Single Property.
↑ В графе Path у переменной вбиваем вручную слово name
Именно так, маленькими буквами, без пробелов, без каких-либо дополнительных знаков, точек, тире и т.п. Потому что это не просто перевод с русского на английский слова "имя", а название одного из свойств объекта в Блендер, заложенного в его API, и если к нему что-то добавить или как-то его исказить - даже если, например, написать Name, с большой буквы - Блендер будет расценивать это как попытку достучаться до какого-то другого свойства объекта, которое называется не name а Name, а поскольку свойства Name у объекта нет, он выдаст ошибку, и драйвер работать не будет. Программирование неточностей не прощает.
↑ Для всех остальных объектов также в графе Path должно быть name
↑ Переменным надо задать осмысленные и читаемые названия, которые впоследствии будут использованы в Expression. Назовём их source, camera и plane. Названия переменных могут состоять только из букв, нижних подчёркиваний и цифр. Использование других символов, в том числе, пробелов для разделения на части не допускается. Также не допускается, чтобы имя переменной начиналось с цифры. Переменные в Пайтон традиционно печатаются маленькими буквами, для читаемости кода. При использовании переменных в скрипте, который в драйверах представлен строкой Expression, их названия нужно писать в точности так, как они напечатаны в настройках переменных. Любые неточности приведут к ошибкам.
Теперь, когда мы будем впечатывать названия переменных в строку Expression, на "заднем фоне" вместо них будут напрямую подставляться те свойства, на которые переменные ссылаются. То есть, в нашем случае - свойства name соответствующих объектов, в их исходном, текстовом виде, а именно - "Light", "Camera" и "Plane".
↑ Можно попробовать вбить в строку Expression имя переменной source. Конечно, от этого числовое значение не превратится в слово "Light", а просто обозначится как ноль - но и ошибки "Error! Wrong Python Expression!" мы тоже не увидим. Блендер на "заднем фоне" увидит, что тип данных, который мы пытаемся ему подсунуть, не соответствует типу данных параметра, на который наложен драйвер, и отправит в качестве результирующего значения цифру 0. Но при этом, если мы будем в Expression использовать эту переменную для какой-либо функции, которая может работать с типом данных "строка" (str), она считается этой функцией в своём оригинальном виде, то есть как "Light".
↑ Теперь пришло время написать такую функцию. В Blender есть текстовый редактор, который может работать с кодом. Он очень примитивный, если сравнивать с профессиональными редакторами кода, такими как Visual Studio или PyCharm, но для несложных задач вполне пригоден. До этого года я даже как-то умудрялся писать в нём все свои аддоны. Чтобы его открыть, разделим окно и переведём его тип в Text Editor.
↑ Чтобы добавить новый текст, нажимаем кнопку +New
ИМПОРТ МОДУЛЯ BPY
↑ И с первой строке впечатываем import bpy
🛈 Дело в том, что по умолчанию в языке программирования Пайтон есть довольно немного (несколько десятков) ключевых слов, которые он воспринимает, как команды. Все остальные слова для Пайтон непонятны. О Блендере Пайтон, конечно, изначально тоже ничего не знает. Он о нём ничего не знает при каждом запуске скрипта. Поэтому, чтобы взаимодействовать с Блендер, нам нужно импортировать в тело скрипта специальный модуль. Этот модуль называется bpy. Он существует в виде набора файлов-скриптов, лежащих на диске, в которых полностью описываются все типы всех данных в Блендер и все возможные действия, которые с ними можно совершать - то есть полностью весь Блендер. Когда мы вводим команду import - одно из ключевых слов, которые понимает Пайтон - интерпретатор Пайтон (специальная программа-переводчик на машинный язык, которая считывает, что мы написали и переводит "железу" внутри компьютера) обращается к специальному списку, в котором прописаны все пути на диске, по которым могут находится модули, предназначенные для импорта, и ищет модуль по названию, которое мы указали. Найдя файл, который называется bpy.py, Пайтон полностью считывает его и выполняет всё его содержимое как программу, при этом в пространстве имён нашего скрипта становятся доступны все его части, и мы можем к ним обращаться. Модуль bpy прописан таким образом, что при его выполнении, как программы, не совершается никаких действий - кубики не удаляются и обезьянки не добавляются - но всё его содержимое становится доступным, и мы можем использовать его ниже в теле скрипта.
Модуль bpy нам пригодится позже. А пока что нам надо написать функцию, которая будет производить все необходимые нам расчёты.
ОПРЕДЕЛЕНИЕ ФУНКЦИИ
↑ Функции в Пайтон задаются с помощью конструкции:
def название_функции():
- и дальше, через отступы, в теле функции пишется, что должно происходить, когда она позже будет вызвана. Сейчас там стоит слово pass. Это называется определение функции.
Концепция функций такова. Сначала функцию нужно определить, то есть прописать, что она должна делать, а после этого её можно вызывать, чтобы она совершала все прописанные действия столько раз, сколько понадобится.
Подробнее по каждому элементу:
• На новой строке пишется слово def. Это ещё одно ключевое слово, которое Пайтон знает. Оно расшифровывается как define (определение), и даёт понять интерпретатору, что мы приступаем к определению функции.
🛈 Количество пустых строк перед этим может быть любым. По стандартным правилам PEP8, разработанным для стандартизации и читаемости кода, после блока импорта и между определениями функций и классов полагается отступать 2 строки, но я отступил одну, чтобы всё поместилось на одном экране.
• После def, через один пробел, пишется название функции, которое мы должны придумать самостоятельно. Для названий (имён) функций действуют те же правила, что и для названий (имён) переменных - используются только буквы, цифры и нижние подчёркивания, цифра не должна быть первым символом, предпочтительно не использовать заглавные буквы. Название должно быть осмысленным и отражать суть. Я назвал функцию offset. Это не совсем удачное название, потому что переводится как "сдвиг", а сдвиг может обозначать что угодно, но в целом остальным правилам оно соответствует, и поскольку, кроме этой функции, в скрипте у нас других не будет, таким я его и оставлю.
• После имени функции ставятся круглые скобки () - это для Пайтон всегда признак того, что объект является исполняемым. В том смысле, что при следующей встрече имени этой функции, уже без слова def, но с круглыми скобками, Пайтон должен будет исполнить всё, что заложено в её определении.
• После скобок идёт двоеточие, которое указывает, что со следующей строки начинается тело функции.
• Тело функции печатается с одинаковыми отступами от начала строки. Как только следующая строка печатается без отступа, она и всё последующее к телу функции уже не относится. Может использоваться несколько уровней отступов, каждый из которых будет составлять тело определённой части кода. Отступы традиционно делаются либо с помощью Tab либо с помощью 4-х пробелов. Если отступы расставлены неправильно, выполнение скрипта будет остановлено и Пайтон выдаст ошибку. Часто отступы расставляются в нужных местах автоматически. Например, если вся остальная часть строки с определением названия функции была записана правильно, то при переходе на новую строку после двоеточия автоматически будет добавлен нужный отступ.
Сейчас я временно поставил в теле функции только одно ключевое слово pass, которое обозначает "ничего не делать". Оно используется как заглушка, когда необходимо завершить некую конструкцию, чтобы она срабатывала без ошибок, но при этом не нужно выполнять никаких действий. Здесь слово pass нужно, потому что без тела функции скрипт будет выдавать ошибку и останавливать своё выполнение.
ПЕРЕДАЧА ФУНКЦИИ В ДРАЙВЕР
↑ Перед тем, как мы напишем большую и сложную функцию, давайте убедимся, что мы её сможем использовать в драйверах. Как вообще определяется, какие функции используются в драйверах? В модуле bpy, а точнее в его подмодуле app, отвечающем за работу приложения, есть специальный словарь, в котором содержатся имена функций и ссылки на сами функции. Этот словарь называется driver_namespace (пространство имён драйверов). Доступ к подмодулям, их элементам, свойствам и функциям в пайтон осуществляется через точку. Воспринимайте это как систему папок. Есть папка bpy, в ней есть папка app, в ней лежит файлик driver_namespace, и чтобы к нему обратиться, нам нужно вбить bpy.app.driver_namespace
↑ Давайте для наглядности будем смотреть, как это выглядит, с помощью Python Console - одного из вида окошек в Блендер, с помощью которого можно относительно удобно передвигаться по иерархии его питоновского API.
↑ Вбиваем в нём bpy.app.driver_namespace - убедитесь, что driver без s на конце, и что вообще вы всё впечатали буква в букву
↑ Нажимаем Tab - горячая клавиша для автоматического заполнения, с помощью которой можно посмотреть, какие могут быть варианты продолжения того, что мы вбили. И видим, что Пайтон предлагает множество вариантов. Все их объединяет то, что они должны быть впечатаны после bpy.app.driver_namespace в квадратных скобочках и одинарных кавычках. Это ключи словаря с функциями, которые можно использовать в драйверах. Кавычки указывают на то, что тип данных этих ключей - строка. То есть это просто обычный текст, которым прописаны имена функций. Именно эти имена мы и можем вызывать в поле Expression в драйверах.
↑ А к самой функции (или классу со множеством функций) здесь мы можем обратиться, если довобьём один из вариантов, который нам предлагают, например, bpy.app.driver_namespace['noise'] - и нажмём Tab. В этот момент "на заднем фоне" bpy.app.driver_namespace['noise'] заменяется на функцию или класс на которую ссылается ключ словаря и мы получаем доступ к её содержимому.
↑ Выберем функцию random (просто впечатываем название через точку), и если нажмём Tab ещё раз, то увидим её описание, на английском языке. "Возвращает рандомное число в диапазоне от 0 до 1". Можно закрыть скобочку и нажать Enter - тогда в консоли высветится рандомное число в диапазоне от 0 до 1. Можно нажать стрелочку вверх на клавиатуре, чтобы вернуть последнюю команду, ещё раз выполнить её, нажав Enter - и вы получите другое рандомное число.
Чтобы выполнить эту функцию в строке Expression драйвера, нам не нужно вбивать весь этот путь: драйвера Блендер автоматически подхватывают значения ключей из этого словаря. Поэтому в драйвере нам достаточно вбить сначала ключ словаря - noise, без кавычек, а потом, через точку, название функции - random - и поставить две круглые скобочки, которые обозначают, что функцию нужно выполнить. То есть:
noise.random()
Таким образом можно выяснить, какие функции можно использовать в драйверах. И похожим образом можно добавлять в драйверы новые функции.
↑ Возвращаемся к скрипту, и для начала после bpy.app.driver_namespace в квадратных скобочках и кавычках (двойных или одинарных - не принципиально) вбиваем ключ словаря. Название для него выбираем, придерживаясь тех же правил, что и для названия переменных и функций - маленькие буквы, нижние подчеркивания и цифры. Чтобы избежать путаницы, назовём ключ "tc_offset" (от texture coordinate offset). После этого указываем, чему будет равен этот ключ, то есть какую функцию будет вызывать драйвер при вызове этого ключа: ставим знак = и пишем название функции, которую мы определили раньше. Получилось:
bpy.app.driver_namespace["tc_offset"] = offset
Что означает, что при вызове в драйвере ключа tc_offset будет использоваться функция offset из этого скрипта.
↑ Чтобы скрипт сработал и добавил ключ tc_offset в пространство имён драйверов, нам нужно выполнить этот скрипт. Это делается в Text Editor по нажатию клавиши с треугольной иконкой Play в верхнем меню
↑ Теперь мы можем проверить, всё ли у нас получилось. Начинаем вбивать в консоли bpy.app.driver_namespace['t - нажимаем Tab, и видим, что одним из вариантов нам предлагают созданный нами ключ tc_offset.
↑ Мы даже можем сделать, чтобы при "сканировании" нашей функции через Tab высвечивалось её описание. Для этого сразу под строкой с определением имени функции надо поставить три двойные кавычки, впечатать описание и закрыть три двойные кавычки:
"""Texture Coordinate Projection Offset"""
Сдвиг проекции текстурных координат, в переводе. Это может быть полезно не только сторонним пользователям, которые будут использовать ваши проекты, но и вам самим, когда спустя некоторое время вы забудете, для чего вы писали эту функцию и что она должна делать. Вы удивитесь, насколько быстро забываются такие вещи.
↑ Если ещё раз выполнить скрипт, нажав плэй, то теперь, если вбить в Python Console полный адрес словаря с ключом нашей функции и нажать Tab, мы увидим, что там будет отображаться её описание.
↑ Чтобы использовать функцию в драйверах, она должна на выходе после всех расчетов вернуть некое числовое значение. Значение, которое возвращает функция, записывается в её теле после ключевого слова return. Пока никаких расчётов у нас нет, давайте просто вобьём конкретное число, например 1.25 - обратите внимание, что дробные числа записываются не через запятую, а через точку. Получается:
return 1.25
🛈 Имейте в виду, что после того, как в теле функции встречается слово return, функция немедленно прекращает дальнейшее выполнение и возвращает либо значение, которое прописано после return, либо, если там не прописано ничего - объект None - то есть ничего. Дело в том, что у функций может быть не только последовательное выполнение, они могут проверять параметры на соответствие заданным условиям, или перебирать массивы данных в цикле, поэтому return может встречаться и в середине тела функции, просто в каких-то случаях, например если не прошла проверка на соответствие условиям, он может оказываться в игнорируемой ветке выполнения. Но если вы с Пайтон или с любым другим языком программирования не знакомы, здесь об этом говорить ещё рано, мы это не будем использовать в этом уроке.
↑ Давайте же протестируем, работает наша функция или нет. Видит ли её драйвер. Нажимаем ПКМ > Edit Driver
↑ Вбиваем в строке Expression название ключа, по которому должна вызваться функция - tc_offset, и для того, чтобы сказать пайтону, что эту функцию надо выполнить, ставим круглые скобки () - это знак вызова функции. Получается:
tc_offset()
То, что вернет выполнившаяся функция, будет присвоено в качестве значения параметра, на котором установлен драйвер
🛈 Если не ставить скобок, то вместо того, чтобы выполнить функцию и вернуть (использовать) полученное из неё значение, Пайтон решит, что в качестве результата мы хотим вернуть саму функцию, как объект. При использовании в Expression это приведёт к ошибке. Но для программирования возможность указать существующую функцию, не выполняя её, является очень важной, и на этом принципе строятся многие более сложные концепции.
↑ Проверяем. Если всё верно, значение параметра, на котором используется драйвер, должно стать равным тому, которое мы указывали после return в теле функции, в этом случае - 1.25
ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ И ИХ СВОЙСТВ В ФУНКЦИИ
А как же нам так сделать - спросите вы (ведь спросите же, да? Ведь это кто-то читает?) - чтобы мы могли всё-таки как-то обратиться к свойствам объектов, к их location, провести нужные нам расчёты и сделать так, чтобы функция возвращала результат этих рассчётов. Для этого и существуют переменные. Помните, мы их настраивали в драйвере, ближе к началу урока? Они по замыслу возвращали имена объектов, которые мы указывали. Вот эти переменные мы и сможем провести в функцию в виде её параметров.
↑ Названия параметров функции, вместо которых при её вызове будут подставляться конкретные значения и которые будут использованы для расчётов в теле функции, указываются при определении функции между круглыми скобочками через запятую. Они могут не совпадать с теми названиями переменных, которые мы назначаем в драйверах, главное чтобы совпадали:
• названия параметров функции между круглых скобочек и эти же названия в теле функции
• названия переменных в драйверах и их же названия в строке Expression при вызове функции
Я назвал параметры source_name, camera_name и plane_name, поскольку ожидаю, что в функцию будут приходить имена объектов в виде строк. Теперь при вызове этой функции в драйвере мне обязательно нужно будет между скобочек указать через запятую имена соответствующих объектов, либо названия переменных, которые содержат ссылки на имена объектов - то есть то, что мы уже настраивали. Как удобно.
Хорошо. Допустим, имена объектов мы в функцию можем провести. Как нам теперь получить доступ к объектам, зная их имена?
↑ В
модуле bpy есть подмодуль data, в котором содержатся
все данные об объектах, сценах, материалах - вообще обо всех данных текущего
проекта. Словарь с объектами называется objects. Мы можем
посмотреть, как это выглядит в консоли, набрав bpy.data.objects и
нажав Tab - нам покажут в виде ключей список имён всех
объектов проекта. Я знаю это, потому что это знает Тайлер Дёрдан.
модуле bpy есть подмодуль data, в котором содержатся
все данные об объектах, сценах, материалах - вообще обо всех данных текущего
проекта. Словарь с объектами называется objects. Мы можем
посмотреть, как это выглядит в консоли, набрав bpy.data.objects и
нажав Tab - нам покажут в виде ключей список имён всех
объектов проекта. Я знаю это, потому что это знает Тайлер Дёрдан.
Как и в
случае с функциями в bpy.app.driver_namespace, где мы их находили по ключу, в случае с объектами, чтобы обратиться к конкретному объекту, нам нужно указать его имя в качестве
ключа после bpy.data.objects, сразу, без точки, в квадратных
скобках и в кавычках (одинарных или двойных).
случае с функциями в bpy.app.driver_namespace, где мы их находили по ключу, в случае с объектами, чтобы обратиться к конкретному объекту, нам нужно указать его имя в качестве
ключа после bpy.data.objects, сразу, без точки, в квадратных
скобках и в кавычках (одинарных или двойных).
↑ У нас имена объектов будет "проваливаться" в функцию в виде параметров, которые мы задаём при её определении. То есть то, что мы прописывали между круглых скобок в определении функции, будет ссылками на имена объектов. Поэтому чтобы достучаться до объекта по имени мы будем использовать не конкретные текстовые значения, а параметры функции. Их мы будем вписывать между квадратных скобок после bpy.data.objects в качестве ключей:
def offset(source_name, camera_name, plane_name):
bpy.data.objects[source_name]
Теперь если мы передадим при вызове функции в строке Expression драйвера в качестве параметров функции переменные с именами объекта, которые мы настраивали раньше, то пайтон "на заднем фоне" будет заменять [source_name] на ['Light'] - и таким образом мы получим доступ к самому объекту и всем его свойствам
↑ А свойств у объекта очень много, как мы сможем убедиться, если нажмём Tab в консоли после того, как введём имя объекта в словаре bpy.data.objects в виде ключа.
↑ Доступ к свойствам объектов осуществляется через точку. Можно начать вбивать в консоли название свойства, которое нас интересует, нажимая Tab, чтобы убедиться, что оно существует и называется именно так, как мы и ожидаем - location
↑ Чтобы достучаться до этого свойства в скрипте, точно также вбиваем его название через точку после словаря с ключом:
bpy.data.objects[source_name].location
Теперь при считывании этой строчки кода интерпретатор Пайтон будет вместо неё подставлять то значение, на которое она ссылается, а именно - значение location объекта Light, имя которого мы с помощью переменных в драйвере проведём во время вызова функции в качестве параметра source_name.
🛈 Для тех, кто знаком с типами данных Пайтон. Строго говоря, bpy.data.objects - это не совсем словарь. Ну, то есть не словарь, как тип данных Пайтон (dict). Это отдельный класс с большим функционалом, который, в том числе, работает как словарь.
ИСПОЛЬЗОВАНИЕ ПЕРЕМЕННЫХ ДЛЯ ЧИТАЕМОСТИ КОДА
↑ Но если, чтобы делать расчёты, мы будем использовать такие длинные пути до свойств объектов, код быстро станет нечитаемым, поэтому, для удобства мы сначала создадим новые переменные и поместим в них все объекты. Для этого придумываем имя переменной, например, source, и с помощью знака равно присваиваем ей значение bpy.data.objects[source_name]. То же самое сделаем для двух других объектов:
source = bpy.data.objects[source_name]
camera = bpy.data.objects[camera_name]
plane = bpy.data.objects[plane_name]
Теперь, чтобы обратиться к свойствам объекта, нам достаточно будет использовать имя переменных, которым они присвоены - source, camera и plane. Например, чтобы обратиться к свойству location объекта Light, нам достаточно будет ввести короткую строчку - source.location
↑ Если нам покажется, что и строчки вида source.location для расчётов слишком длинные и неудобные, то мы можем и их заменить на переменные:
source_loc = source.location
camera_loc = camera.location
plane_loc = plane.location
Это не обязательно, можно использовать и source.location и даже bpy.data.objects[source_name].location, но если это сделает дальнейший код читаемей, то почему бы и нет. Для читаемости переменные должны быть названы осмысленно, чтобы открывая код спустя какое-то время, можно было понять, на что они ссылаются.
РАСЧЁТЫ
Кто ещё не любит слово "расчёты"? Кому ещё мерещатся душные офисы с кипами бумаг или Циолковский, Королёв и Сахаров перед ватманскими листами с чертежами и формулами? У нас уже всё подсчитано в прошлом уроке, нам это надо просто записать. И с помощью Пайтон это будет намного проще и удобнее, чем в прошлом уроке с помощью нодов. Потому что возможностей в Пайтон намного больше, чем в нодах.
↑ В частности, нам не нужно придумывать, как высчитывать угол между векторами, потому что в нодах нет такой опции, а в Пайтон есть. Поэтому, чтобы высчитать длину вектора от Камеры до проекции Источника Света на Плэйн, нам достаточно просто разделить длину вектора от Камеры до Плэйна сразу на косинус угла между векторами
↑ Чтобы высчитать длину вектора от Камеры до Плэйна, нам сначала нужен сам вектор. Создаём переменную b, которая будет обозначать вектор b, и через знак равенства = говорим, что она будет равна разности между координатами позиции Плэйна plane_loc и координатами позиции Камеры camera_loc:
b = plane_loc - camera_loc
Всё читаемо, прозрачно и понятно. Если вы ещё не очень освоились с векторами и опасаетесь, что позже можете не понять, что это за b тут такое, можно назвать переменную не просто b, а, например, vector_b, и в дальнейшем использовать уже это название.
↑ Для нахождения угла между векторами нам понадобится также и вектор a. Находим его тем же способом - создав переменную а и с помощью = присвоив ей значение, равное разности координат позиции Источника Света source_loc и координат позиции Камеры camera_loc:
a = source_loc - camera_loc
Когда мы вычитаем из location Источника Света location Камеры, у результата остаётся тот же тип данных, что и у них обоих. В Блендер для параметра location объектов в качестве типа данных используется класс Vector из модуля mathutils. Мы этого не видим в коде, потому что это происходит при импорте модуля bpy. То есть location - это не просто 3 циферки для осей XYZ, а целый класс со множеством свойств и функций, позволяющий проводить с ним различные математические операции.
В частности, если мы обратимся к переменной a, которая, как и свойства location обоих объектов, из которых она получилась, относится к типу Vector, то через точку мы можем обратиться к свойствам, которые предоставляет класс Vector. В частности в нём прописана функция (или, правильнее назвать это метод, потому что функции внутри класса называются методы, чтобы нас ещё больше запутать), которая называется angle. Понять, что это функция, то есть исполняемый объект, который можно вызвать и он что-то сделает, а не просто укажет на значение, как тот же location, можно по тому, что в конце находятся круглые скобки.
Работает метод (давайте уже называть вещи правильно; ещё раз: метод - это функция внутри класса) - так вот, работает метод angle следующим образом. Он вызывается, как свойство одного объекта типа Vector, через точку, и между круглых скобок нужно указать другой объект типа Vector. Объект - здесь имеется в виду не объект в Блендер, а объект Пайтон, в котором вообще всё является объектами - и функции, и переменные, и классы.
То есть чтобы узнать угол между векторами a и b, нам нужно у вектора a через точку вызвать метод angle и в качестве параметра провести в него вектор b:
a.angle(b)
Результат присвоим в новую переменную angle (угол). Итого, получается:
a = source_loc - camera_loc
b = plane_loc - camera_loc
angle = a.angle(b)
↑ Теперь нам нужно разделить длину вектора b на косинус угла между векторами a и b, чтобы узнать длину вектора от Камеры до проекции Источника Света на Плэйн.
🛈 Когда я это пишу и смотрю на скриншот, то понимаю, что вероятно эту переменную не стоило называть c, потому что в ней, в отличие от a и b, будет содержаться не вектор (то есть 3 значения), а значение длины вектора. Имейте это в виду.
Длина вектора b содержится прямо в его свойствах и называется length. Причём обратите внимание, что в конце нет круглых скобок, то есть это не метод, а именно свойство, нам его не надо вызывать, чтобы вычислить - b.length
Знак деления в пайтон обозначается как /
И нам нужно посчитать косинус угла, для этого нужно вызвать функцию cos и в качестве параметра провести в неё ранее посчитанный angle - cos(angle). Получается:
с = b.length / cos(angle)
↑ Секунду! Но откуда взялась функция cos? Мы её нигде не определяли, в теле кода её нигде нет. Если оставить всё в таком виде, Пайтон не поймёт, что мы от него хотим, и выдаст ошибку. Поэтому, чтобы использовать функцию cos, её надо предварительно импортировать из модуля math. Этот модуль также в виде файлов лежит на диске в одной из папок, где Пайтон ищет модули. Нам не нужен весь модуль math, он очень большой, нам нужна только одна функция cos из него. Поэтому мы пишем в блоке импорта наверху:
from math import cos
То есть "из модуля math импортировать функцию cos". Всё, теперь мы можем её использовать.
↑ Вспомним, что нам нужен результат не в виде длины вектора, а в виде самого вектора от Камеры до проекции Источника Света на Плэйн. Для нахождения вектора создаём новую переменную a_small (то есть маленький вектор a) и делаем его равным произведению вектора а на отношение длины гипотенузы с на длину вектора а
a_small = a * (c / a.length)
🛈 Умножение в пайтон ообозначается знаком *
↑ Теперь мы можем использовать переменную a_small, содержащую результат вычислений, после return, как результат, который вернёт функция, и который будет использоваться в качестве параметра.
Но!
a_small - это вектор, то есть три значения по XYZ, а драйвер у нас накладывается на каждую ось индивидуально, и, соответственно, если мы попытаемся назначить на одну ось три значения, то получим ошибку. Поэтому нам нужно, чтобы функция возвращала только значение одной оси из полученного вектора. Для этого мы можем указать после a_small в квадратных скобках индекс оси, значение которой мы хотим вернуть. Индексы массивов, к которым относятся, в частности, и векторы, в Пайтон начинаются с 0. То есть первая ось идёт под индексом 0, вторая под индексом 1, третья под индексом 2.
Таким образом, когда мы пишем a_small[0], то получаем значение вектора a_small по оси Х.
ПРОВЕРКА ФУНКЦИИ В ДРАЙВЕРЕ
↑ Настало время проверить работу нашей функции. Включаем Shader Editor. Сейчас у нас ещё используется старый вариант функции, который мы запускали в самом начале. Чтобы обновить скрипт для драйверов, нам нужно его ещё раз выполнить. Нажимаем в Text Editor кнопку Play.
↑ И получаем ошибку - ERROR! Invalid Python Expression. Кто уже понял, что здесь не так? Прежде, чем идти дальше, посмотрите внимательно на вызов функции и подумайте сами, чего в нём не хватает.
↑ А не хватает в нём параметров между круглыми скобками. Нам же нужно провести в качестве параметров имена объектов, и сами себя они не проведут. Вписываем названия заданных ниже переменных source, camera и plane. Они подставляются в качестве параметров функции source_name, camera_name и plane_name - в том же порядке, в котором определено в функции.
Страшная надпись ERROR! Invalid Python Expression сменяется на Slow Python Expression - а это означает, что скрипт работает.
Но возвращает наша функция всегда только значение вектора сдвига по оси Х, мы не можем её использовать в таком виде для осей Y и Z. И что же нам теперь, дублировать скрипт 3 раза и делать 3 разные функции? Конечно нет.
ДИНАМИЧЕСКОЕ ИЗМЕНЕНИЕ ИНДЕКСОВ
↑ Мы просто добавим в параметры функции параметр index, который будем использовать при вызове функции в строке Expression, определяя для каждой оси свой индекс.
↑ И этот же параметр мы должны вписать вместо конкретной цифры в квадратных скобках после a_small в return
↑ Теперь можно исправить выражение в драйвере прямо в строке с параметром, не заходя в редактор драйвера. Добавляем через запятую индекс 1...
↑ И значение меняется на какое-то неправильное. Потому что - ещё раз - индексы в массивах начинаются с 0, а не с 1. Это важно запомнить, поэтому я обращаю на это ваше внимание.
↑ Меняем в выражении индекс с 1 на 0 - и всё работает правильно
↑ Теперь можно скопировать драйвер через ПКМ > Copy Driver
↑ Вставить его на ось Y
↑ Вставить его на ось Z
↑ Заменить в выражении для оси Y индекс на 1
↑ Заменить в выражении для оси Z индекс на 2
ЗАМЕНА ЛОКАЛЬНЫХ КООРДИНАТ НА ГЛОБАЛЬНЫЕ
↑ Смотрим. Не работает. Вернее работает - ошибок не выскакивает, какие-то значения присваиваются, но наших бликов линзы не видно. Где ошибка?
↑ Ошибка в том, что для расчётов мы использовали свойства location у всех объектов. Это то свойство, которое отображается в N-панели в качестве location. Посмотрите внимательно на Плэйн, где он расположен, и на его значение location в N-панели. Он, по идее, должен находиться на 5 метров ниже камеры по оси Z, а находится на 5 метров дальше по оси Y. Это происходит потому, что он припэренчен к камере, которая развёрнута по оси X на 90 градусов. Таким образом в локальных координатах Плэйна он остался на отметке -5 метров по оси Z, а в глобальной переместился на отметку 5 метров по оси Y. Его свойство location, которое мы использовали в расчётах, отображает его локальные координаты. А нам нужны глобальные.
↑ Чтобы получить доступ к глобальным координатам положения объекта, вместо location мы должны использовать matrix_world.translation - то есть матрицу трансформации в мировых координатах, а именно ту её часть, которая отвечает за перемещение объекта. Я знаю это, потому что... ну вы поняли.
↑ Для всех переменных, в которых мы использовали свойство location, мы заменяем его на matrix_world.translation
↑ Не забываем ещё раз выполнить скрипт, нажав Play в Text Editor
↑ Сразу после выполнения скрипта драйверы не обновятся сами, нам нужно зайти в выражение любого из них, делая вид, что мы что-то исправляем (исправлять ничего не нужно) - и когда мы нажмём Enter, применяя результат, все драйверы обновятся и пересчитают значения по-новому.
↑ Тадам! Всё работает! Супер быстро, сверх оптимизировано и мега-удобно.
ЗАМЕНА ОБЪЕКТА ИСТОЧНИКА СВЕТА
↑ И теперь мы можем очень легко заменить источник света на другой объект. Например продублируем источник света - на самом деле, это может быть вообще любой объект, любого типа.
↑ Входим в меню драйвера через ПКМ > Edit Driver
↑ Заменяем для переменной source один объект на другой
↑ Повторяем то же самое для оси Y
↑ И для оси Z
↑ Тадам! Готово! Изи!
↑ Ладно, два источника света нам пока не нужно, отменим всё, оставим один.
РЕГИСТРАЦИЯ СКРИПТА ДЛЯ АВТОМАТИЧЕСКОГО ВЫПОЛНЕНИЯ
↑ После выхода из программы Блендер сбросит все настройки и "забудет", что мы выполняли скрипт, поэтому нам нужно сделать, чтобы при открытии проекта скрипт выполнялся каждый раз автоматически. Для этого в Text Editor нужно зайти в верхнее меню > Text и рядом с параметром Register поставить галочку. Это даст понять Блендер, что скрипт зарегистрирован, и его нужно выполнить при открытие проекта.
ОТСЛЕЖИВАНИЕ ОШИБОК
Есть несколько пунктов в Edit > Preferences, которые могут быть очень полезны при работе с Python.
↑ Во первых если в Interface подключить Python Tooltips, при наведении курсора на любой параметр у вас будет отображаться Python-путь до него.
↑ Во-вторых, если у вас не работают драйверы или не выполняются скрипты, это может происходить если у вас в Save & Load отключена галочка Auto-Run Python Scripts. А по умолчанию она отключена, потому что, как мы можем видеть, скрипт может запускаться при открытии проекта, прописать в нём можно всё, что угодно, в том числе, и вообще к Блендер не относящееся, и чтобы обезопасить начинающих пользователей (а их всегда большинство), автоматический запуск скриптов отключен. Обезопасить себя от вредоносного кода в файлах проектов Блендер, который мог бы запускаться при их открытии, довольно просто - смотрите через Append, нет ли в скачанных файлах каких-либо лишних текстов в разделе Texts. Если есть, можете зааппендить их отдельно, не регистрируя и не запуская, и посмотреть, что в них прописано. Пайтон - не компилируемый, а интерпретируемый язык, это означает, что весь код, написанный на нём, не зашифрован, и его при должном навыке можно прочитать и понять. Если навыка не хватает, а происхождение, назначение или содержание текста в проекте Блендер вызывает сомнения, обратитесь за помощью к тем, кто лучше разбирается в программировании.
↑ Ещё одна полезная вещь, которая доступна пользователям Windows - это Верхнее меню > Window > Toggle Selected Console. Пользователи Linux и MacOS для использования системной консоли должны запустить Блендер из терминала, указав в нём полный путь до исполняемого файла Блендер. Консолью для вас будет служить, собственно сам терминал.
↑ В системную консоль Пайтон сбрасывает всю информацию об ошибках в скриптах и драйверах. Здесь мы можем отследить, что в какой момент у нас шло не так, с указаниями на то, какая строка кода в каком файле привела к ошибке. Без этой информации отладка кода практически невозможна.
Итак, мы почти в миллион раз оптимизировали нашу систему сдвига текстурных координат, с чем вас и поздравляю! Теперь мы сможем использовать её столько раз, сколько нам понадобится.
Вы видите эти уроки, благодаря тем, кто забирает их к себе на стенки, рассказывает друзьям, знакомым, в сообществах, пабликах, чатах, коммьюнити. Без их поддержки развитие проекта было бы невозможным, и я благодарен каждому.
Со своей стороны выражаю большую благодарность спонсорам. Вы даёте мне силы продолжать.