EN
Andrey Sokolov
Andrey Sokolov
730 subscribers
goals
21 of 500 paid subscribers
Когда я наберу 500 платных подписчиков, смогу больше времени уделять записи видео и разработке аддонов для Blender.
359.48 of $ 1 131 money raised
Донаты || Donates
37.45 of $ 566 money raised
Cделать аддон True Time Remapping бесплатным для всех желающих навсегда. Make True Time Remapping add-on free for everyone forever.
113.08 of $ 114 money raised
На дисковый накопитель 4Tb для хранения бэкапов курса «Blender Избушка».

Волосы на геонодах ► 02. Длина волос

Первая задача - создание нод-группы для изменения длины волос без изменения их направления. Посмотреть, как настроен проект, можно здесь.
Буду очень благодарен всем, кто решит поддержать проект материально. В данный момент это можно сделать только здесь, на Boosty, сделав разовый платёж или оформив спонсорскую подписку приемлемого для вас уровня (от 200 р. в месяц). 
ОБЩАЯ ИДЕЯ
В нашем проекте каждый волос представляет из себя направленный отрезок в трёхмерном пространстве, состоящий из двух точек - начала и конца волоса. Каждая точка каждого волоса расположена на своих координатах в трёхмерном пространстве. Координаты точки - это три индивидуальные числовые значения, определяющие расстояние точки по осям X, Y и Z от центра объекта, а точнее - от его точки origin (оранжевая точка).
Идея в том, что для каждого волоса мы можем вычислить его вектор и, изменяя длину этого вектора, сможем вычислять значения, на которые нужно будет сдвигать точку конца волоса по X, Y и Z, чтобы изменялась длина волоса.
🛈 Вектор отрезка, как вы можете знать из моих уроков Математика в 3Д (или из школьной программы), определяется в трёхмерном пространстве всего тремя числами - значениями по X, Y и Z от центра объекта, на котором оказалась бы конечная точка этого отрезка, если бы начальная его точка оказалась в центре системы координат, на отметке (0, 0, 0) по X, Y и Z, но при этом длина и направление отрезка были бы сохранены.
Или простыми словами, если волос, передвинуть таким образом, чтобы его начало совместилось с точкой origin объекта, то координаты конца волоса и будут его вектором. Чтобы это сделать, нам нужно из координат его конца вычесть координаты его начала. А потом останется просто умножать этот вектор на нужные значения и использовать для сдвига концов волос. Всё. Вот так просто.
ПРАКТИЧЕСКАЯ РЕАЛИЗАЦИЯ
Итак, нам понадобятся несколько нодов:
• Нод, который будет двигать существующие точки
• Нод, который будет определять позиции точек
• Нод, который будет вычитать позиции точек, одни из других
• Несколько нодов для определения позиций начал волос. 
Сейчас в существующем блоке данных Геометрических Нодов, который добавился автоматически при добавлении объекта Empty Hair, всего три нода:
• Group Input - из него мы получаем исходную геометрию, в нашем случае - все волосы, которые мы наскульптили, со всеми их данными, к которым можно получать доступ при помощи других нодов.
• Deform Curve on Surface - этот нод привязывает волосы к деформированной с помощью модификаторов или Shape Keys поверхности объекта-эмиттера. Без него деформации, включая модификаторы, анимацию скелетной арматуры и Shape Keys, не учитываются.
• Group Output. - определяет, какая геометрия идёт в конечном итоге на выход. То есть Блендер рассчитывает результаты и принимает во внимание только те ноды, которые подключены в цепочку, которая приходит в Group Output.
Чтобы добавить новые ноды есть несколько способов.
• Через верхнее меню в Geometry Node Editor, пункт Add - выдаст список категорий, в каждой из которых можно выбрать нужный нод. Над всеми категориями есть графа Search, с помощью которой можно найти нужные ноды по имени
• Нажав на клавиатуре Shift + A - вызывает то же самое меню
• Зачастую наиболее удобный способ, недавно введённый в Блендер - нажать на нужный выход (кружочек, ромбик и т.д.) Левой Кнопкой Мыши и, удерживая её, вытянуть из выхода линк. Когда вы отпустите кнопку мыши, всплывёт меню поиска нодов по имени. Чаще всего ноды называются довольно предсказуемо.
Последний способ мы и используем. Нажимаем на выход Geometry в ноде Deform Curve on Surface и, удерживая мышку, вытягиваем из него линк ↓
Нам нужен нод, который будет изменять позиции точек. Во всплывающем меню начинаем вбивать "Position" и выбираем нод Set Position
/говорит голосом робота/: «Вы добавили нод Set Position» ↓ 
Только пока он не работает. Чтобы он начал воздействовать на геометрию, его надо подключить к Group Output
Повторим ещё раз, уже наглядно, что нам нужно сделать. Волосы состоят из двух точек, назовём их А (начало волоса) и В (конец волоса). Нам нужно вычислить вектор AB ↓
Полученный вектор мы сможем использовать для сдвига конечной точки, точки В.
• Если мы умножим вектор на 0, то точка В при сдвиге никуда не сдвинется, а вовсе даже останется на месте
• Если мы будем умножать вектор на положительные значения, точка В будет отдаляться от точки А, и длина волоса будет увеличиваться
• Если мы будем умножать вектор на отрицательные значения, вектор поменяет направление в обратную сторону, и точка В будет приближаться к точке А. При этом, при умножении на -1, вектор станет равен по длине длине волоса, и таким образом сократит длину волоса до нуля
Для начала нам нужно вычислить вектор АВ, или назовём его вектор а. Это делается путём вычитания координат точки А из координат точки В ↓
Позиции каких бы то ни было элементов геометрии являются входящими данными, поэтому их стоит искать в категории Shift + A > Input. Нам понадобится нод Position ↓
Если мы подключим нод Position ко входу Position нода Set Position, то... сюрприз! Ничего не поменяется! ↓
Можно было бы, конечно, просто пожать плечами и пройти мимо, типа, ну, значит так надо. Но именно здесь и сейчас, пока мы не нагородили огромное дерево нодов, стоит разобраться, что же именно происходит, когда мы подключаем входящие данные Position к другим нодам - потому что это ключевой момент для понимания, как работают Геометрические Ноды в принципе.
Итак, что же даёт на выходе нод Position? Сам по себе он не даёт на выходе ничего. Его содержимое напрямую зависит от того, к чему и в каком месте цепи он подключен. Дело в том, что в отличие от Shader Editor, Compositor и других нодовых редакторов, где обработка данных производится последовательно - то есть каждый последующий нод обрабатывает результаты предыдущего - в Геометрических Нодах сигнал подаётся в обе стороны. Position одновременно возвращает позиции всех элементов геометрии, которая приходит в тот нод, к которому Position в данный момент подключен (в том числе, через другие ноды, не имеющие входа Geometry).
Образующийся таким образом в Position (или любом другом ноде) массив данных называют Field (поле) - скоро нам этот термин понадобится, это ещё одна важная концепция в геонодах. Любой нод, обрабатывающий данные сразу многих элементов работает с полями: принимает в себя поля, безропотно обрабатывает поля, пока солнце ещё высоко, и на выходе также возвращает поля. Поле или Field - это просто массив данных одного типа, обрабатываемый внутри одного нода.
При подключении Position ко входу Position нода Set Position происходит следующее. Нод Position смотрит, к чему он подключен. Здесь это нод Set Position, у которого есть вход Geometry, через который он принимает в себя некую входящую геометрию - в данном случае, кривые, из которых состоят волосы - со всеми их параметрами - точками, позициями, нормалями, индексами и т.д., этих параметров там довольно много ↓
Нод Position из всего этого объёма данных, пришедших в Set Position, извлекает только данные о позициях точек в кривых, можно сказать, формирует из них поле, после чего по тому же "проводу", то есть линку, одновременно передающему данные в обоих направлениях, отправляет это поле обратно, чтобы использовать его в качестве значений для того слота, к которому он подключен ↓
То есть, каждый параметр нода Set Position - не какое-то одно значение, одна позиция одной точки одного волоса. Это одновременно все позиции всех точек всех волос. Подключив Position ко входу Position нода Set Position, мы сначала с помощью нода Position считываем данные о позициях всех элементов во входящей в Set Position геометрии, а потом эти же данные присваиваем с помощью слота Position в ноде Set Position этим элементам обратно в качестве их позиции. Поэтому ничего и не меняется.
Все ноды из категории Input зависят от того, к чему они подключены. Если поставить подряд два нода Set Position и к каждому из них подключить нод Position - даже один и тот же! - то для первого Set Position нод Position считает и вернёт значения позиций из геометрии на его входе, а для второго Set Position - уже изменённые с помощью первого Set Position.
Стоит с этим разобраться сразу, потому что без этого у вас не будет понимания, почему, в какой момент, какой нод, какие значения, для каких звеньев цепи воспроизводит.
Если эта концепция уложилась в голове, идём дальше. Если никак не укладывается - всё равно идём дальше, постепенно уложится.
Нам нужно из позиций концов волос вычесть позиции начал волос. Собственно, мы можем вычитать позиции начал волос из позиций вообще всех точек, включая сами начала волос. Нам нет необходимости каким-либо образом выделять в отдельное поле позиции концов волос: просто у векторов от начал до концов волос будет какая-то длина, определяющая, насколько будут сдвигаться конечные точки, а у векторов от начал до... начал волос будут высчитываться нулевые векторы, которые на что ни умножай, они нулевыми и останутся, и, таким образом, будут сдвигать позиции начал волос на ноль, то есть никуда не сдвигать. В итоге мы можем в качестве первых значений для вычисления векторов использовать Position в чистом виде.
Чтобы посчитать для волос их векторы, нам нужно вычесть из позиций всех (в данном случае - обеих) точек каждого волоса позицию начала этого волоса. Само вычитание производится при помощи Vector Math в режиме Subtract (вычитание). Этот нод точно так же может работать не с отдельными значениями отдельных элементов, а с полями, обрабатывая сразу множественные данные ↓
В дальнейшем мы будем использовать результат этого вычитания для параметра Offset, то есть для сдвига изначальных позиций точек, а не для определения самих позиций, как сейчас. Поэтому временно вообще отключим Subtract от Set Position, чтобы не путаться. 
Если отдельные позиции концов волос нам не нужны, то без отдельных позиций начал волос нам не обойтись. Здесь мы подходим к тому, что же такое волосы.
Волосы - это объект типа Curves (кривые), а каждый волос представляет из себя Spline (сплайн). Для дальнейшего понимания важно понимать разницу между кривыми и сплайнами, потому что у них разные параметры и разный функционал!
Curves - кривые - это тип объекта в целом. Ноды, в названии или в параметрах которых присутствует слово Curves, чаще всего работают со всеми данными объекта одновременно. Например Set Curve Radius задаёт радиус для всех сплайнов внутри объекта, а не только для сплайнов своей цепочки. Об этом речь пойдёт в следующих уроках.
Spline - сплайн - это группы соединённых элементов внутри объектов кривых. То есть каждый отдельный волос - две (или больше) точки, соединённые участком(-ами) кривой - это Spline
Все сплайны внутри объекта-кривых пронумерованы, у каждого есть свой индекс ↓
Мало того, все точки внутри объекта-кривых пронумерованы, и у каждой есть свой индекс. Причем индексы точек и индексы сплайнов растут параллельно: у сплайна с индексом 0 точки с индексами 0 и 1, у сплайна с индексом 1 точки с индексами 2 и 3, и т.д. ↓
Собственно, к чему это я вообще здесь завёл речь про индексы. Забегая совсем немного вперёд, есть нод, позволяющий выделить из поля значений новое поле значений по индексам элементов. Если мы как-то отделим индексы нужных точек и "скормим" ему в качестве ещё одного поля, то с помощью Position сможем получить их позиции в виде отдельного нового поля.
На случай, если вас всё ещё смущает слово поле - это просто набор однотипных данных, например - все позиции каких-то элементов, или все индексы каких-то элементов - которые в ноде обрабатываются одновременно.
Получить поле индексов мы можем при помощи Shift + A > Input > Index
Ради разнообразия, для поиска следующего нода воспользуемся Shift + A > Search ↓
Нам нужно найти нод Field at Index, который как раз и возвращает поле, состоящее из параметров только тех элементов, индексы которых будут ему переданы ↓
С индексами мы сейчас разберёмся, а пока что продублируем нод Position с помощью Shift + D, потому что в качестве самого параметра, нам нужны будут позиции начальных точек волос. Можно было бы и не дублировать Position, а использовать один и тот же, потому что он бы всё равно для Field at Index вернул своё поле значений, а для Subtract - своё. Но поначалу это может сбивать с толку, поэтому пока давайте всё же его для наглядности продублируем ↓
Тип данных, с которым работает Position - векторы. (Любую позицию можно рассматривать как вектор - ведь у неё есть те же три числовые значения). Поэтому, чтобы всё работало правильно, поле в Field at Index должно также создаваться с типом данных Vector
Нижний параметр в Field at Index, определяющий домен - позиции каких элементов мы высчитываем - мы оставляем на Points, потому что нам нужны позиции именно точек, пусть и только начальных, а не каких-либо других элементов. Подключим Position к Field at Index
Сейчас мы добавим все необходимые ноды, чтобы они у нас были перед глазами, а потом я объясню, что мы с ними будем делать. Нам понадобится нод Interpolate Domain (ищем через поиск) ↓
Он должен быть подключен к Field at Index, тип данных у него должен быть выставлен Vector, а домен - Spline
Interpolate Domain мы подключим в нижний слот Subtract и через поиск найдём нод Spline Length
А теперь давайте разбираться ↓
↑ Нам нужно, чтобы в Subtract (Vector Math в режиме вычитание) в нижний слот приходили позиции начальных точек, корней волос. Мы можем вытащить их позиции, используя Position, по индексам этих точек, используя Field at Index. Но чему, собственно, равны индексы этих точек, как нам передать их в Field at Index в качестве поля? Как нам отделить индексы начальных точек от индексов остальных точек.
Как мы уже выясняли, индексы сплайнов растут параллельно с индексами их точек. Только, поскольку точек больше, индексы сплайнов растут медленнее. Однако, если умножить индекс сплайна на количество точек внутри него, то таким образом мы получим индексы начальных точек в каждом сплайне. Сомневаетесь? Проверим:
• Сплайн с индексом 0. Точки с индексами 0, 1. Умножаем 0 на количество точек в сплайне (на 2) - получаем 0. Совпадает с индексом первой точки.
• Сплайн с индексом 1. Точки с индексами 2, 3. Умножаем 1 на количество точек в сплайне (на 2) - получаем 2. Совпадает с индексом первой точки.
• Сплайн с индексом 2. Точки с индексами 4, 5. Умножаем 2 на количество точек в сплайне (на 2) - получаем 4. Совпадает с индексом первой точки.
И так далее. Это работает.
Индекс элемента мы можем получить с помощью нода Index. Количество точек в каждом сплайне мы можем получить с помощью выхода Point Count в ноде Spline Length. Вот только как нам получить индекс именно сплайна, а не точки? Ведь по умолчанию все поля работают с параметрами элементов той геометрии, к которой они подключены. Position считывает позиции точек, с чего бы Index стал считывать индексы чего-то другого?
Здесь нам приходит на помощь нод Interpolate Domain, который переключает домен, в котором происходят все действия, на Spline, и теперь каждый подключенный слева от него по ходу движения нод будет пытаться работать с параметрами сплайнов, а не точек.
Interpolate Domain как бы радостно вбегает в офис и говорит всем нодам, которые идут до него: "Так, ребята, на точки мы больше не работаем! Все, у кого есть хоть какое-то представление о сплайнах, занимаются сплайнами". И часть нодов такие: "Ну мы же только всё для точек посчитали! Опять эти сплайны! Что там им нужно, опять индексы?!" - и начинают работать со сплайнами. А другая часть нодов такие: "Эээ, спла...? Что это такое? Как-как вы их назвали, ещё раз? Сплэ... Не, не слышали, не знаем, мы только с точками работаем," - и продолжают работать с точками. Никогда не работал в офисе. Это же как-то так происходит?
Подключаем все ноды: перемножаем друг с другом Index и выход Point Count из Spline Length с помощью Math в режиме Multiply (умножение), подключаем к Index в Field at Index. В нижний слот Field at Index подключаем Position. Field at Index через Interpolate Domain в режиме Spline подключаем в нижний слот Vector Math > Subtract, у которого к верхнему слоту подключен Position
↑ Рекомендую потратить некоторое время, чтобы досконально разобраться, какие параметры каких элементов в каких нодах этой системы будут обрабатываться при подключении к Set Position, и почему. Это пример не очень сложный в плане вычислений, но очень показательный в плане логики Геометрических нодов. Эту схему мне подсказали на stackexchange, и у меня пару дней ушло на то, чтобы полностью разобраться, каким образом получаются нужные индексы у начальных точек волос - пока я всё на бумажке сам себе не посчитал. Серьёзно, эти Interpolate Domain и Field at Index на несколько дней взорвали мозг, и именно после этой схемы я полез читать документацию, поняв, что самостоятельно не могу разобраться.
В этом уроке я попытался всё объяснить подробно, и надеюсь, вам понадобится гораздо меньше времени. Напишите в комментариях, как оно вообще - понятно, не понятно. Задавайте вопросы, не стесняйтесь. Мне очень важна обратная связь, чтобы понимать, понятно я объясняю или какие-то моменты лучше проговаривать как-то по-другому. Хотя, конечно, тут как не объясняй, а определённую самостоятельную умственную работу всё равно нужно провести.
Напомню, что всё это мы делали, чтобы вычислить вектор волоса - разницу между его началом и его концом. Теперь мы будем изменять размер этого вектора с помощью Vector Math в режиме Scale (масштаб), который по сути умножает все три числовые значения вектора (в случае с полями - все значения всех векторов) на одно и то же число ↓
Теперь, если мы будем использовать результат для параметра Offset (сдвиг, смещение) в Set Position, то получится, что мы сдвигаем начальные точки на (0, 0, 0), потому что вектор между начальными точками и теми же самыми начальными точками - нулевой, независимо от масштаба. Таким образом, начальные точки, корни волос, остаются на местах. А конечные точки сдвигаются с помощью полученного вектора по направлению волоса ближе или дальше от начальных - в зависимости от того умножаем мы вектор на положительные или отрицательные значения ↓
Подготовим ноды к объединению в нод-группу. С помощью Ctrl + H скроем в выделенных нодах неиспользуемые входы и выходы. С помощью H скроем выделенные ноды. Расположим ноды таким образом, чтобы они занимали как можно меньше места, но при этом легко прослеживалось, какой к какому подключен. Оставим открытыми те ноды, в которых выставлены какие-то важные нам для понимания их работы параметры. Это достаточно индивидуально, поэтому здесь сложно дать какие-то советы ↓
Теперь, когда уже всё собрано, мы можем убедиться, что вместо двух разных нодов Position для определения положения начальных точек и вообще всех точек, мы можем использовать один и тот же, и всё будет работать точно так же ↓
Потому что в зависимости от того, к чему Position подключен, он будет считывать и возвращать позиции разных элементов. Пожалуй, это один из ключевых для понимания моментов ↓
Для удобства управления размером вектора, определяющего сдвиг конечных точек, перед тем, как подавать в него входящее значение (пока что, временно, это Input > Value), вычтем с помощью Math в режиме Subtract (вычитание) из входящего значения единицу ↓
Теперь:
• Если значение равно 0, то, после вычитания 1, размер вектора становится -1, и длина волоса таким образом вычитается полностью. То есть при входящем значении 0 волосы будут нулевой длины, что более логично.
• Если значение равно 1, то, после вычитания 1, размер вектора становится 0, сдвига не происходит, и длина волос остаётся прежней
• При увеличении значения выше 1, волосы начинают расти.
Таким образом, входящее значение фактически будет работать как Scale, масштабирование, к которому мы все привыкли.
Выделим все нужные ноды, включая Set Position, но не включая Value, которое нужно только, чтобы при создании группы автоматически создался нужный вход в группу. Нажмём Ctrl + G - чтобы ноды объединились в группу ↓
↑ В N-панели справа, во вкладке Group выставим нужный порядок для входов, назовём вход, управляющий размером сдвига Scale и ограничим его минимальное значение нулём, чтобы волосы не начинали расти в обратную сторону при отрицательных значениях.
С помощью Tab выйдем из группы. Назовём её GN Hair Length и активируем рядом с названием значок щитка, чтобы, если группа окажется не использованной ни в одном блоке данных, Блендер не удалил её при закрытии проекта ↓
Поздравляю! Первый элемент управления готов! 
Вы видите эти уроки, благодаря тем, кто забирает их к себе на стенки, рассказывает друзьям, знакомым, в сообществах, пабликах, чатах, коммьюнити. Без такой поддержки развитие проекта было бы невозможным, и я благодарен каждому.
Выражаю большую благодарность спонсорам. Вы даёте мне возможность продолжать, а другим - бесплатно учиться.
avatar
"Сплайн с индексом 2. Точки с индексами 4, 5. Умножаем 2 на количество точек в сплайне (на 2) - получаем 2. Совпадает с индексом первой точки." Получаем 4. Небольшая описка.
Спасибо за уроки!
avatar
Денис Соколинский, спасибо, исправил!
avatar
Правильно ли я понял, что основная функция нода position "переприсылать" данные, с внесёнными изменениями, в подключенный нод?
Show more replies
avatar
Andrey Sokolov, сейчас буду тупить вслух. Если утрировать, то только формирует массив?
avatar
Ivan Borodulin, если утрировать, то да.
avatar
такой вариант тоже работает
Show more replies
avatar
Ivan Borodulin, 1: а какую если не первую)2: не первый индекс точки нужен или позиция?
На скрине 2 позиция точки вычисляется
avatar
олег лисов, до меня кажется дошло как это работает. По умолчанию SplineLength должен возвращать число соответствующее количеству точек на сплайне, но благодаря Accumulate Field, в частности смене домена на spline (вроде при работе с уже полученными данными о точках сплайна) данные преобразуются  и аккумулируются... и дальше по твоему тексту. За второй скрин тоже сяб.
avatar
Неужели я наконец-то прочитал как работает Position, спасибо огромное
avatar
Виктор Макинов, да, вроде бы несложно, но с непривычки то, что потоки данных идут в обе стороны, да ещё в одном и том же ноде могут разветвляться, в зависимости от того, к чему он подключен, может сбивать с толку.
avatar
Мне кажется, что тут совсем не помешает вот эта ссылка https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/fields.html#field-context
avatar
Аналогия с работой в офисе просто супер! Долго смеялся =D А знания от статьи бесценны, спасибо большое!))
avatar
Андрей, спасибо за замечательный материал! Мне кажется, что нода Position в данном случае дает на оба своих выхода одинаковые данные - информацию о позиции точек волос по соответствующему их индексу. Перестановка ноды Interpolate "в конец" (к информации от Index) ничего не меняет в длине волос, хотя, если бы она влияла на информацию от Position, это бы происходило. А в мануале Блендера нет информации, что у Spline есть параметр Position
avatar
Maria Shevchenko, спасибо! Вроде всё верно. Вообще там выше в комментах есть ответ от Олега Лисова, он предлагает более правильную и универсальную сборку, чем в этом уроке. 
avatar
Внимание! Вопрос-картинка:
Show more replies
avatar
Andrey Sokolov, спасибо за пояснения. Да, я тоже радовался 10 минут наличию данной таблицы и грустил над ней в поисках закономерности больше часа. Пока - черный ящик. Уважаемые подписчики, если, внезапно, кто-то однозначно понимает как работает Accumulate Field, поясните пожалуйста.
avatar
Andrey Sokolov, не понимаю почему поле из количества точек в сплайне оказывается корректным в качестве индекса позиций точек.
Вот, тут можно поискать смыслы на эту тему:
https://blender.stackexchange.com/questions/281822/problems-understanding-accumulate-field-with-edges

Subscription levels

1-й уровень

$ 2,27 per month
1-й уровень

2-й уровень

$ 5,7 per month
2-й уровень

3-й уровень

$ 11,4 per month
3-й уровень

4-й уровень

$ 22,7 per month
4-й уровень

5-й уровень

$ 57 per month
5-й уровень

Максимальная поддержка

$ 114 per month
Максимальная поддержка
Go up