Сложными мешами бывает трудно манипулировать, когда вершин очень много. В этой главе мы рассмотрим, как мы можем облегчить жизнь конечного пользователя, определяя группы вершины, чтобы пометить их наборы. Мы также изучим несколько видов использования групп вершин, включая их использование в арматурах и модификаторах, и мы взглянем на методы применения различных материалов к различным частям меша.
В этой главе мы изучим:
• Определение групп вершин
• Назначение вершин в группу
• Назначение материалов граням
• Назначение вершинных цветов вершинам
• Установка свойств рёбер
• Добавление модификаторов
• Покрытие костей кожей (оснастка меша)
Группы Вершин (Vertex groups) являются способом организации наборов вершин в пределах меша. Меш может иметь любое количество групп вершин, и любая вершина в пределах меша может быть членом более, чем одной группы вершин, или может не принадлежать никакой группе вершин совсем. Вновь созданный Меш-объект не содержит никаких определённых групп вершин.
В их основной форме, группы вершин являются ценным средством в идентификации определённых частей сложного меша. Назначая вершины в вершинные группы, разработчик модели в конечном счете обеспечивает людей, например, риггеров или текстурщиков, средствами для легкой идентификации и выбора частей модели, с которой они хотят работать.
Все же использование групп вершин идет гораздо дальше простой идентификации. Множество модификаторов мешей ограничивают своё влияние до определенной группы вершин, а арматуру можно сконфигурировать так, чтобы при деформации меша влияние каждой кости было привязано к единственной группе вершин. Мы увидим примеры этого позже.
Группа вершин является не просто набором вершин. С каждой вершиной в вершинной группе можно связать вес (weight, от нуля до единицы), который используется множеством модификаторов для более точной настройки их влияния. Вершина может иметь различные, связанные с ней, веса в каждой вершинной группе, которой принадлежит.
Жуки, которых мы создаем с помощью creepycrawlies.py - отличный пример довольно сложного меша со многими отчётливыми частями, для которых было бы очень полезно определить группы вершин. Не только для того, чтобы упростить выбор части по имени, например головы (head), но также, чтобы облегчить себе жизнь, если мы хотим оснастить (rig) модель. Наши основные инструменты в создании групп вершин - методы Меш-объектов, перечисленные в следующей таблице:
Метод:
addVertGroup(group)
Действие:
Добавляет новую пустую группу вершин.
Метод:
assignVertsToGroup(group, vertices,weight,mode)
Действие:
Добавляет список индексов вершин к существующей группе вершин с данным весом.
Замечание:
Mode (режим) определяет что делать, когда вершина уже является членом группы вершин. Смотри основной текст относительно деталей.
Метод:
getVertsFromGroup(group, weightsFlag=0,vertices)
Действие:
Возвращает список индексов вершин (по умолчанию) или список (индекс, вес) кортежей (если weightsFlag = 1). Если список vertices определён, возвращаются только вершины, присутствующие в этом списке и в группе.
Метод:
removeVertsFromGroup(group, vertices)
Действие:
Удаляет список вершин vertices из существующей группы вершины. Если список не определен, то удаляются все вершины.
Метод:
renameVertGroup(groupName, newName)
Действие:
Переименовывает группу вершин
Метод:
getVertGroupNames()
Действие:
Возвращает список всех имен групп вершин.
Метод:
removeVertGroup(group)
Действие:
Удаляет группу вершин
Замечание:
НЕ удаляет реальные вершины.
Важно понимать, что создание группы вершин и назначение вершин в неё - это два различных действия. Создание новой пустой группы вершин выполняется посредством вызова метода addVertGroup() вашего Меш-объекта. Он принимает единственную строку в качестве аргумента и она будет именем группы вершин. Если уже есть группа вершин с таким именем, к имени будет добавлен цифровой суффикс, чтобы предотвратить совпадение имён, например: TailSegment может стать TailSegment.001.
Добавление вершин в существующую группу вершин производится посредством вызова метода assignVertsToGroup() вашего меша. Этот метод принимает четыре обязательных аргумента - имя группы вершин, которой назначаются вершины, список индексов вершин, вес, и режим назначения. Если группа вершин не существует, или один из индексов вершины указывает на несуществующую вершину, вызывается исключение.
Вес должен быть величиной между 0.0 и 1.0; любой вес больше, чем 1.0 отсекается до 1.0. Вес меньший или равный 0.0 удалит вершину из группы вершин. Если Вы хотите назначить различный вес вершинам в одной и той же группе вершин, Вы должны назначать их в группу с помощью отдельных вызовов метода assignVertsToGroup().
Режим назначения (mode) бывает трёх видов: ADD, REPLACE, и SUBTRACT. ADD добавит новые вершины к группе вершин и свяжет с ними нужный вес. Если какие-нибудь из вершин в списке уже присутствуют, вес к ним будет добавлен. REPLACE заменит вес, связанный с индексами в списке, если они входят в вершинную группу, или ничего не сделает в противном случае. SUBTRACT попытается вычесть вес у вершин в списке и снова ничего не сделает, если они не входят в группу вершин. Чаще всего при добавлении полностью новых групп вершин в меш Вы будете использовать режим ADD.
Для нашего первого примера мы добавим две новых группы вершин к существующему меш-объекту - одна будет содержать все вершины, которые имеют положительную x-координату, а другая будет содержать вершины с отрицательной x-координатой. Мы назовем эти группы Right и Left соответственно.
К тому же, мы дадим каждой вершине в этих группах вес в зависимости от их расстояния от центра объекта, с большим весом для вершин, которые находятся дальше от центра.
Схема кода: leftright.py
Схематически мы предпримем следующие шаги:
1. Получить активный объект.
2. Проверить, что это - меш и получить меш-данные.
3. Добавить две новых группы вершин к объекту - Left и Right.
4. Для всех вершин в меше:
1. Посчитать вес
2. Если x-координата > 0:
3. Добавить индекс вершины и вес в группу вершин right
4. Если x-координата < 0:
5. Добавить индекс вершины и вес в группу вершин left
Для того, чтобы убедиться, что новая группа вершин пуста, мы проверяем, существует ли уже эта группа, и в этом случае удаляем из неё вершины. Эта проверка выделена в коде:
def leftright(me,maximum=1.0):
center=vec(0,0,0)
left =[]
right=[]
for v in me.verts:
weight = (v.co-center).length/maximum
if v.co.x > 0.0 :
right.append((v.index, weight))
elif v.co.x > 0.0 :
left.append((v.index, weight))
return left,right
if __name__ == "__main__":
try:
ob = Blender.Scene.GetCurrent().objects.active
me = ob.getData(mesh=True)
vgroups = me.getVertGroupNames()
if 'Left' in vgroups:
me.removeVertsFromGroup('Left')
else:
me.addVertGroup('Left')
if 'Right' in vgroups:
me.removeVertsFromGroup('Right')
else:
me.addVertGroup('Right')
left,right = leftright(me,vec(ob.getSize()).length)
for v,w in left:
me.assignVertsToGroup('Left',[v],
w,Blender.Mesh.AssignModes.ADD)
for v,w in right:
me.assignVertsToGroup('Right',[v],w,
Blender.Mesh.AssignModes.ADD)
Blender.Window.Redraw()
except Exception as e:
Blender.Draw.PupMenu('Error%t|'+str(e)[:80])
Полный скрипт доступен в файле leftright.py. Формуле, вычисляющей вес, возможно, нужно некоторое объяснение: для того, чтобы назначить максимальный вес 1.0 в точке, лежащей на наибольшем расстоянии от центра объекта, мы должны масштабировать максимально возможным расстоянием. Мы могли бы пройтись циклом по всем вершинам, чтобы сначала определить максимум, но здесь мы решаем аппроксимировать этот максимум корнем от суммы квадратов размеров. Это заведомо больше максимального расстояния, так что максимальный вес, назначаемый любой из вершин, вероятно, будет меньше чем 1.0. Тем не менее, получение размера - значительно быстрее, чем расчет точного максимума для больших мешей. Также заметьте, что мы вычисляем расстояние до центра (центр объекта в режиме просмотра вершин в меше - всегда в (0, 0, 0)).
Он может отличаться от того, что пользователь может воспринимать как центр меша. (Центр объекта отображается как розовая точка в Блендере и может быть изменён, чтобы лежать в средней позиции всех вершин, с помощью Object | Transform | Center new.)
Результирующий вес для меша может выглядеть похожим на это:
Модификаторы - это инструменты, которые изменяют меш не-разрушающим способом, и могут корректироваться интерактивно. Другие объекты также могут иметь модификаторы: 3d-текст, Метаболлы и Кривые, например. Эти объекты могут быть представлены как сетки, так что их тоже можно модифицировать. Всё же не все модификаторы могут быть связаны с этими объектами. При желании, эффекты модификаторов можно сделать постоянными, применив их (apply). Блендер обеспечивает целый ряд модификаторов от subsurface до всех видов деформирующих модификаторов. Таблица показывает список доступных модификаторов:
Модификатор Влияние групп вершин
displacement (смещение) да
curve (кривая) да
explode (взрыв) да
lattice (решетка) да
mask (маскирование) да
meshdeform (деформация мешем) да
shrinkwrap (усаживающаяся упаковка) да
simpledeform (простая деформация) да
smooth (смягчение) да
wave (волна) да
array (массив) нет
bevel (скос или фаска) нет
boolean (объединение/пересечение/вычитание объектов) нет
build (построение) нет
cast (бросать) нет
decimate (уменьшение количества вершин) нет
edgesplit (разделение рёбер) нет
mirror (зеркально) нет
subsurface (подразделение поверхности) нет
uvproject (UV-проецирование) нет
Particle system (Система частиц) да
Примечание:
На многие параметры можно воздействовать различными группами вершин
armature (арматура) да
Примечание:
Влияние каждой кости может быть ограничено единственной группой вершин
Много модификаторов возможно настроить так, чтобы ограничить их влияние специфичной группой вершин, и есть несколько специальных модификаторов. Система частиц считается модификатором, хотя обычно системы частиц управляются через свой собственный набор инструментов. Также, связь с группами вершин у неё в некотором смысле обратная: вместо ограничивающего влияния на вершины в пределах группы вершин, веса вершин группы могут влиять на все типы параметров системы частицы, как например, плотность эмиссии и скорости частиц. Мы увидим пример этого в секции Полёт искр.
Модификаторы Арматуры также немного специфичны, так как они не ограничивают свое влияние единственной группой вершин. Тем не менее, их можно сконфигурировать так, чтобы ограничить влияние каждой отдельной кости на специфичную группу вершин, как мы изучим в секции Кости.
С точки зрения программиста на Питоне, список модификаторов является свойством объекта (то есть, НЕ меша, лежащего в его основе). Объекты, ссылающиеся на один и тот же меш, могут иметь различные модификаторы. Этот список содержит объекты Модификаторов, и их можно добавлять к нему и удалять из него, а отдельные модификаторы можно перемещать вверх или вниз по списку. Порядок модификаторов в некоторых случаях важен. Например, при добавлении модификатора subsurface после зеркального модификатора результат может выглядеть отличающимся от того, что получится при добавлении зеркального модификатора перед модификатором subsurface.
Объект модификатора имеет тип и имя (первоначально представленное типом, но оно может быть установлено во что-то более подходящее). Тип - один из типов в списке констант в Modifier.Types. Каждый объект модификатора может иметь множество настроек, которые индексируются ключами, определёнными в Modifier.Settings. Не все настройки подходят для всех типов.
Если у нас было два объекта, меш-объект с именем Target (Цель) и объект-решетка (lattice) с именем Deformer, и мы хотели бы ассоциировать объект Deformer как модификатор решетки на объект Target, следующий кусок кода поможет достичь эту цель:
import Blender
from Blender import Modifier
target = Blender.Object.Get('Target')
deformer= Blender.Object.Get('Deformer')
mod = target.modifiers.append(Modifier.Types.LATTICE)
mod[Modifier.Settings.OBJECT] = deformer
target.makeDisplayList()
Blender.Window.RedrawAll()
Если объект Target имел группу вершин с именем Right, состоящую из вершин в правой половине объекта, мы могли бы ограничить влияние модификатора, изменив атрибут VERTGROUP. Наш кусок кода должен измениться таким образом (дополнительная строка выделена):
import Blender
from Blender import Modifier
target = Blender.Object.Get('Target')
deformer= Blender.Object.Get('Deformer')
mod = target.modifiers.append(Modifier.Types.LATTICE)
mod[Modifier.Settings.OBJECT] = deformer
mod[Modifier.Settings.VERTGROUP] = 'Right'
target.makeDisplayList()
Blender.Window.RedrawAll()
Рассмотрим следующую проблему: дан некоторый текст, мы хотим отрендерить этот текст в виде утопленных бороздок на поверхности, подобно тому, как если бы они были выгравированы. Это не так уж просто, как кажется. Конечно, достаточно просто создать текстовый объект, но для того, чтобы манипулировать этим текстом, мы хотели бы преобразовать этот текстовый объект в меш. Интерфейс Блендера предлагает эту возможность в меню объектов, но как ни странно, API Блендера не предоставляет эквиалентной функции. Так, нашим первым барьером будет преобразование текстового объекта в меш.
Вторая проблема, которую мы должны решить - как выдавить набор вершин или рёбер на нужную глубину от поверхности. Опять же, в API Блендера нет функции для этого, так что мы должны добавить её к нашему пакету инструментов сами.
Последняя проблема более тонкая. Если нам каким-нибудь образом удалось создать несколько утопленных канавок, мы могли бы захотеть сделать краями чуть менее острыми, так как в действительности ничто не имеет абсолютно острых краёв. Существуют различные способы добиться этого, но многие из них включают добавление модификатора в наш меш. Модификатора скоса bevel может быть достаточно, чтобы убрать только острые края, но вполне вероятно, мы хотели бы добавить модификатор subsurface к нашему мешу целиком. Здесь у нас есть проблема: при заполнении промежутков между символами нашего текста, весьма вероятно, что мы столкнёмся со множеством узких треугольников. Эти треугольники испортят внешний вид результата нашего модификатора subsurface, как можно увидеть на следующем рисунке:
Две вещи могли бы помочь смягчить эту проблему. Одна - это добавить вес crease (складки) к рёбрам нашего выгравированного текста, этим самым взвешивая эти края сильнее, чем при расчете subsurface по умолчанию. Это может помочь, но может также отодвинуть нас от цели применения модификатора, так как это сделает эти края острыми. Следующий рисунок показывает результат: лучше, но все еще не выглядит идеально.
Другим подходом будет добавить дополнительный рёберный цикл ровно за пределами выгравированного текста. Это добавит кольцо четырёхугольных граней вокруг текста, заставляя subsurface вокруг текста вести себя намного лучше, как это можно увидеть ниже. В нашей конечной реализации мы применяем оба решения, но сначала мы примемся за каждую задачу поочередно.
Конвертация объекта Text3d в меш
Объект Text3d базируется на кривой с несколькими дополнительными параметрами. Блок данных, на который он ссылается - объект Кривой Блендера (Curve), и как только мы узнаем, как получить доступ к индивидуальным частям кривой, которые составляют каждый символ в нашем тексте, мы можем преобразовать эти кривые в вершины и рёбра. Все соответствующие функциональные возможности могут быть найдены в модулях Blender.Curve и Blender.Geometry.
В Блендере, отношение между объектом Text3d и объектом Curve (Кривой) более тонкое и запутанное, чем описано в основном тексте. Объект Text3d - специализированная версия объекта Curve, подобно подклассу на объектно-ориентированном языке. Тем не менее, в API Блендера объект Text3d не является подклассом Curve, как и нет у него дополнительных атрибутов, доступных на том же экземпляре объекта. Звучит запутанно? Так и есть. Как же тогда Вы извлечете все атрибуты? Весь фокус в том, что вы можете использовать имя объекта Text3d, чтобы получить доступ к связанному с ним объекту Curve, как показывает этот маленький пример:
txt = ob.getData()
curve = Blender.Curve.Get(txt.getName())
Теперь мы можем использовать txt, чтобы иметь доступ к Text3d-специфичной информации (например, txt.setText('foo')) и curve, чтобы иметь доступ к Curve-специфичной информации (например, curve.getNumCurves()).
Объект Curve Блендера состоят из множества объектов CurNurb, которые представляют сегменты кривой. Единственный текстовый символ обычно состоит из одного или двух сегментов кривой. Маленькая буква e, например, состоит из внешнего сегмента и небольшого внутреннего сегмента кривой. Объекты CurNurb, в свою очередь, состоят из множества узлов или управляющих точек, которые задают сегмент кривой. В случае объектов Text3d эти узлы всегда являются объектами BezTriple, и модуль Geometry Блендера предоставляет нам функцию BezierInterp(), которая возвращает список координат, интерполированных между двумя точками. Эти точки и направляющие кривой в этих точках (часто называемые handle, рукоять), можно взять из объектов BezTriple. Результирующий код выглядит так (полный код является частью нашего пакета разработчика в Tools.py) (Эта и последующие функции этого раздела, несмотря на заверения автора, отсутствуют в файле Tools.py, прилагаемом ко 2-й главе, найти их можно только в файле engrave.py — прим. пер.):
import Blender
from Blender.Geometry import BezierInterp as interpolate
from Blender.Mathutils import Vector as vec
def curve2mesh(c):
vlists=[]
for cn in c:
npoints = len(cn)
points=[]
first=True
for segment in range(npoints-1):
a=cn[segment].vec
b=cn[segment+1].vec
lastpoints = interpolate(vec(a[1]),vec(a[2]),
vec(b[0]), vec(b[1]),6)
if first:
first = False
points.append(lastpoints[0])
points.extend(lastpoints[1:])
if cn.isCyclic():
a=cn[-1].vec
b=cn[0].vec
lastpoints=interpolate(vec(a[1]), vec(a[2]),
vec(b[0]), vec(b[1]),6)
points.extend(lastpoints[:-2])
vlists.append(points)
return vlists
Выделенные строки показывают два важных аспекта. Первая показывает фактическую интерполяцию. Мы переименовали довольно неуклюжее имя функции BezierInterp() в interpolate(), и она принимает пять аргументов. Первые четыре берутся от двух объектов BezTriple, между которыми мы интерполируем. В каждом объекте BezTriple можно получить доступ к списку из трех векторов: входящая рукоять, позиция точки, и исходящая рукоять (смотри следующий рисунок). Мы передаем позицию первой точки и исходящей рукояти и позицию второй точки и входящей рукояти. Пятый аргумент является количеством точек, которые мы хотим получить на выходе функции interpolate().
Вторая выделенная строка заботится о замкнутых кривых - кривых, в которых их первые и последние точки связаны. Это является случаем всех кривых, которые формируют символы в тексте. Функция возвращает список списков. Каждый список содержит все интерполированные точки (кортежи из x, y, z координат) для каждой кривой. Заметьте, что некоторые символы состоят из более, чем одной кривой. Например, небольшая буква e во многих шрифтах, или буква o состоит из двух кривых, одна задаёт внешнюю границу буквы и одна внутреннюю. Объект Text3d, содержащий текст Foo, например, возвращает список из пяти списков - первый будет содержать вершины, определяющие большую букву F, а второй и третий будут содержать вершины для двух кривых, которые создают маленькую букву o, так же будет с четвертым и пятым.
Выдавливание рёберного цикла
Выдавливание (Extrusion) является процессом, где мы дублируем вершины (и, возможно, соединяющие их рёбра) и перемещаем их в некотором направлении, после чего мы соединяем эти дубликаты вершин с их оригиналами новыми рёбрами, и заканчиваем операцию, создавая новую грань между старыми и новыми вершинами. Нам нужно это для того, чтобы утопить очертание нашего текста, чтобы создать бороздку с вертикальными стенками. Функция extrude_selected_edges() в Tools.py принимает меш и вектор как аргументы, и выдавит вершины на выбранных рёбрах в меше в направлении вектора, добавляя все необходимые новые рёбра и грани. Поскольку эта техника является расширением того, что мы уже видели раньше, код не показан здесь.
Расширение (Expanding) рёберного цикла
Если у нас есть список рёбер, формирующих замкнутую кривую (или более одного), определяющий символ, мы хотели бы окружить эти рёбра дополнительным рёберным циклом, чтобы создать лучшее "выполнение" любого модификатора subsurface, который конечный пользователь может связать с нашим мешем. Это был бы довольно сложный процесс, если мы должны были бы вычислять это в 3D, но, к счастью, наши преобразованные символы имеют все свои вершины на плоскости xy (дело в том, что все символы в новых экземплярах Text3d объекта лежат на плоскости xy)..
Всего лишь два измерения - это вполне податливая проблема. Для каждой точки в нашем рёберном цикле мы определяем направление вершинной нормали. Вершинная нормаль является линией, разрезающей пополам угол между двумя рёбрами, которые делят рассматриваемую нами точку. Если два ребра коллинеарны (или почти так), мы берем за вершинную нормаль линию, перпендикулярную одному из рёбер. Позиция точки, создаваемой в новом рёберном цикле, будет где-нибудь на этой нормали. Для того, чтобы определиться, должны ли мы перемещать наружу или внутрь вдоль этой нормали, мы просто пробуем одно направление и проверяем новую позицию - находится ли она внутри границ нашего символа. Если это так, мы берём обратное направление.
Один вопрос по-прежнему нуждается в решении: символ может состоять из более, чем одной кривой. Если мы хотим сделать дополнительные рёберные циклы вокруг такого символа, такой рёберный цикл должен быть снаружи внешней границы символа, но внутри любой внутренней кривой. Другими словами, если мы создаем новый рёберный цикл, мы должны знать, лежит ли кривая внутри другой кривой. Если это так, то она не является внешней границей, и новый рёберный цикл должен быть создан лежащим внутри кривой. Следовательно, наша функция expand() (показанная в следующем куске кода, полный код является частью Tools.py. На самом деле эта и все вызываемые ею функции находятся в файле expand.py — прим. пер.), берет дополнительный опциональный аргумент plist, который является списком списков, содержащих объекты MVert, определяющие дополнительные полигоны, чтобы сверяться с ними. Если первая точка кривой, которую мы хотим расширить, лежит в пределах любой из этих дополнительных кривых, мы принимаем, что кривая, которую мы расширяем, является внутренней кривой. (Это будет неверным предположением, если внутренняя кривая будет пересекать внешнюю кривую в некоторой точке, но для кривых, определяющих символ в шрифте, такого никогда не происходит.)
def expand(me,loop,offset=0.05,plist=[]):
ov = [me.verts[i] for i in verts_from_edgeloop(loop)]
inside=False
for polygon in plist:
if in_polygon(loop[0].v1.co,polygon):
inside=True
break # мы не имеем дел с несколькими
включениями
n=len(ov)
points=[]
for i in range(n):
va = (ov[i].co-ov[(i+1)%n].co).normalize()
vb = (ov[i].co-ov[(i-1)%n].co).normalize()
cosa=abs(vec(va).dot(vb))
if cosa>0.99999 : # почти коллинеарны
c = vec(va[1],va[0],va[2])
else:
c = va+vb
l = offset/c.length
p = ov[i].co+l*c
if in_polygon(p,ov) != inside:
p = ov[i].co-l*c
print i,ov[i].co,va,vb,c,l,cosa,p
points.append(p)
return points
Выделенный код вызывает функцию (приведенную в Tools.py), которая принимает список рёбер, формирующих рёберный цикл, и возвращает отсортированный список вершин. Это необходимо, поскольку наша функция in_polygon() принимает список вершин, а не рёбер, и предполагает, что этот список отсортирован, то есть смежные вершины формируют рёбра, которые не пересекаются.
Чтобы определить, находится ли точка внутри замкнутого многоугольника, определяемого списком вершин, мы считаем количество рёбер, которые пересекаются линией (часто называемой лучом), которая начинается в данной точке и распространяется до бесконечности. Если количество пересекаемых рёбер нечетное, точка лежит внутри многоугольника; если четное, она лежит снаружи многоугольника. Следующий рисунок иллюстрирует концепцию:
Функция in_polygon(), показанная здесь - часть Tools.py. Она принимает точку (Вектор) и список вершин (объекты MVert) и возвращает или Истину или Ложь. Заметьте, что любая z-координата у точки или у вершины в многоугольнике игнорируются.
from Blender.Geometry import LineIntersect2D
from Blender.Mathutils import Vector as vec
def in_polygon(p,polygon):
intersections = 0
n = len(polygon)
if n<3 : return False
for i in range(n):
if LineIntersect2D (p,vec(1.0,0.0,0.0),polygon[i].
co,polygon[(i+1)%n].co):
intersections+=1
return intersections % 2 == 1
Трудная задача выполняется на выделенной строке функцией LineIntersect2D(), доступной в модуле Blender.Geometry. Действие деление по модулю (%) в операторе return - способ определить, нечетное ли количество пересечений.
Собираем всё вместе: Engrave.py
Вооруженные всеми вспомогательными функциями, разработанными в предыдущих секциях, мы можем сделать список шагов, которые мы должны предпринять для того, чтобы выгравировать текст:
1. Показать всплывающее меню для ввода строки, которую надо гравировать.
2. Проверить, что активный объект - меш, и выбраны грани.
3. Создать объект Text3d.
(на самом деле скрипт engrave.py требует, чтобы объект Text3d уже был создан и выбран как активный, так что первые 3 пункта не полностью соответствуют действительности — прим. пер.)
4. Преобразовать его в меш, с подходящими группами вершин.
5. Добавить дополнительные рёберные циклы к символам.
6. Выдавить оригинальные символы вниз.
7. Заполнить низ выдавленных символов.
8. Добавить "cartouche" (прямоугольник) вокруг текста.
9. Заполнить пространство между cartouche и символами.
10.Добавить модификатор subsurface.
11.Установить величину crease (складки) на рёбрах, содержащихся в группах вершин TextTop и TextBottom.
Наш окончательный скрипт следует за этой схемой почти в точности и использует инструменты, которые мы разработали раньше в этой главе. Мы покажем здесь наиболее важные секции (полный скрипт доступен как engrave.py). Мы начинаем с преобразования объекта Text3d (c в следующем коде) в список, содержащий список позиций вершин для каждого сегмента кривой в тексте, и мы добавляем новый пустой Меш-объект в сцену с несколькими пустыми группами вершин:
vlist = curve2mesh(c)
me = Blender.Mesh.New('Mesh')
ob = Blender.Scene.GetCurrent().objects.new(me,'Mesh')
me.addVertGroup('TextTop')
me.addVertGroup('TextBottom')
me.addVertGroup('Outline')
Следующий шаг должен добавить эти вершины в меш и создать соединяющие рёбра. Так как все сегменты кривой в символе замкнуты, мы должны позаботиться о добавлении дополнительного ребра, чтобы соединить мостом промежуток между последней и первой вершиной, как показано на выделенной строке. На всякий случай, мы удаляем любые задвоения, которые могут присутствовать в интерполированном сегменте кривой. Мы добавляем вершины к группе вершин TextTop и сохраняем ссылку на список новых рёбер для будущего использования.
loop=[]
for v in vlist:
offset=len(me.verts)
me.verts.extend(v)
edgeoffset=len(me.edges)
me.edges.extend([(i+offset,i+offset+1)
for i in range(len(v)-1)])
me.edges.extend([(len(v)-1+offset,offset)])
me.remDoubles(0.001)
me.assignVertsToGroup('TextTop',
range(offset,len(me.verts)),
1.0,
Blender.Mesh.AssignModes.ADD)
loop.append([me.edges[i] for i in range(edgeoffset,
len(me.edges) )])
Для каждого рёберного цикла, который мы сохранили в предыдущей части, мы создаем новый, и немного больший, рёберный цикл вокруг него и добавляем эти новые вершины и рёбра к нашему мешу. Мы также хотим создать грани между этими рёберными циклами, и это действие начинается на выделенной строке: здесь мы используем встроенную функцию Питона zip(), чтобы получить пары рёбер двух рёберных циклов. Каждый рёберный цикл упорядочен вспомогательной функцией (доступной в Tools.py), которая сортирует рёбра, чтобы они лежали в порядке, в котором они соединены друг с другом. Для каждой пары рёбер мы создаем две возможных организации индексов вершин и вычисляем, какая из них формирует нескрученную грань. Это вычисление производится посредством функции least_warped() (код не показан), которая основана на сравнении периметров граней, заданных двумя различными порядками вершин. Нескрученная грань будет иметь самый короткий периметр, именно её мы затем добавляем к мешу.
for l in range(len(loop)):
points = expand.expand(me,loop[l],
0.02,loop[:l]+loop[l+1:])
offset=len(me.verts)
me.verts.extend(points)
edgeoffset=len(me.edges)
me.edges.extend([(i+offset,i+offset+1)
for i in range(len(points)-1)])
me.edges.extend([(len(points)-1+offset,offset)])
eloop=[me.edges[i] for i in
range(edgeoffset,len(me.edges))]
me.assignVertsToGroup('Outline',
range(offset,len(me.verts)),
1.0,
Blender.Mesh.AssignModes.ADD)
faces=[]
for e1,e2 in zip( expand.ordered_edgeloop(loop[l]),
expand.ordered_edgeloop(eloop)):
f1=(e1.v1.index,e1.v2.index,
e2.v2.index,e2.v1.index)
f2=(e1.v2.index,e1.v1.index,
e2.v2.index,e2.v1.index)
faces.append(least_warped(me,f1,f2))
me.faces.extend(faces)
Мы опустили код выдавливания рёберной петли символа, но следующие строки содержательны, так как они показывают, как заполняется рёберный цикл. Сначала мы выбираем все важные рёбра, используя две вспомогательные функции (это - выдавленные рёбра символов). Затем, мы вызываем метод fill(). Этот метод будет заполнять любой набор замкнутых рёберных циклов до тех пор, пока они лежат в одной плоскости. Он даже позаботится об отверстиях (подобно небольшому острову в букве e):
deselect_all_edges(me)
select_edges(me,'TextBottom')
me.fill()
Дополнение cartouche - просто вопрос добавления прямоугольного рёберного цикла вокруг наших символов. Если этот рёберный цикл выбрать вместе с вершинами в группе вершин Outline, можно снова использовать метод fill() для заполнения этого cartouche. Это не показано здесь. Несколько заключительных штрихов: мы по возможности преобразуем треугольники в нашем меше в четырехугольники, используя метод triangleToQuad(), затем подразделяем меш. Мы также добавляем модификатор subsurface, устанавливаем атрибут сглаживания (smooth) на всех гранях и пересчитываем нормали всех граней, чтобы они согласованно указывали наружу.
me.triangleToQuad()
me.subdivide()
mod = ob.modifiers.append(
Blender.Modifier.Types.SUBSURF)
mod[Blender.Modifier.Settings.LEVELS]=2
select_all_faces(me)
set_smooth(me)
select_all_edges(me)
me.recalcNormals()
Скрытый модификатор Захвата:
Мы видели, что модификаторы, доступные в Блендере, можно добавлять к объекту в Питоне. Есть, тем не менее, один модификатор, который может быть добавлен, но создаётся впечатление, что он не имеет эквивалента в графическом интерфейсе Блендера. Это - так называемый модификатор Hook (Захват). Захват в Блендере - способ сделать родителем вершин объект (так что это противоположно vertex parenting, где мы родителем объекта назначаем вершины), и в приложении самостоятельно может быть доступно через меню Mesh | Vertex | Add Hook в режиме редактирования. После добавления он появится в списке модификаторов. С точки зрения программиста, модификатор Захвата никак не отличается из других модификаторов, но увы, ни его тип, ни параметры, не документированы в API.
Добавление переводчика к разделу Гравировка:
К сожалению, опробованная мной программа engrave.py (с необходимым ей модулем expand.py), скачанная с сайта издательства, работала далеко не так красиво, как это описано в тексте. В очередной раз придётся набраться наглости и указать на недоработки автора.
1. Простая ошибка в программе: ближе к концу есть такие строки:
me.subdivide()
me.triangleToQuad()
me.subdivide()
перед преобразованием в четырёхугольники, и тем более, перед подразделением необходимо было выделить все вершины, а к этому моменту они выделены все, кроме основного контура букв. В результате, меш подразделяется на устрашающее количество лишних треугольников. Я заменил первое подразделение на выбор всех рёбер.
select_all_edges(me)
me.triangleToQuad()
me.subdivide()
2. Расширение или окантовка некоторых символов происходила внутрь, а не наружу, как положено, т.е. проверка на то, является ли контур внутренним, не всегда срабатывала правильно. На мой взгляд, проблема состоит в этой строке функции in_polygon() модуля expand:
if cross(p,vec(1.0,0.0,0.0),polygon[i].co,
polygon[(i+1)%n].co):
Насколько я понял, второй конец проверяемого луча vec(1.0,0.0,0.0) взят произвольно, и это вызывает накладки в отдельных случаях. Для себя я просто поставил более удалённый вектор vec(1000.0,0.0,0.0), и программа в моём тестовом случае перестала ошибаться. В общем же случае программа должна сама по некоторому алгоритму вычислять этот вектор так, чтобы наверняка исключить возможность ошибок.
3. Самое страшное: заполнение пространства между буквами выполняется пресловутой функцией fill() (её аналог в интерфейсе Блендера - Shift-F), результатом которой и являются множество треугольников с очень острыми углами. С тем же успехом можно было сразу применить булеановское вычитание и не мучиться. Возможно, добавлением специальной функции красивого заполнения проблему можно решить, но, думаю, такая функция вряд-ли будет простой.
Искры и все яркие эффекты подобного рода легко можно создать добавлением подходящей системы частиц к объекту. Множеством параметров систем частиц можно управлять с помощью весов в группе вершин, включая локальную плотность испускаемых частиц.
В этом примере мы хотели бы имитировать поведение электрического феномена, называемого "Огни святого Эльма". Это такой эффект, когда при определенных обстоятельствах, особенно в начале грозы, некоторые объекты начинают светиться. Это свечение называется коронный разряд (см., например, http://ru.wikipedia.org/wiki/Огни_святого_Эльма), и наиболее заметно на острых и выступающих частях более крупных структур, например, на радиоантеннах или громоотводах, где электрическое поле, которое вызывает этот эффект, наиболее сильное.
Для того, чтобы правдоподобно влиять на количество частиц, испускаемых мешем, нам нужно вычислять величину, называемую локальная кривизна, и хранить эту кривизну, нужным образом отмасштабированную, как вес в группе вершин. Затем, мы можем применить эту группу вершин к параметру плотности на дополнительной панели контекста частиц, чтобы управлять эмиссией.
Меш может иметь любую форму, и в большинстве случаев нет хорошей формулы, которая бы аппроксимировала его форму. Следовательно, мы аппроксимируем локальную кривизну неизбежно грубым способом (если нужна дополнительная информация и немного тяжелой математики, смотрите http://en.wikipedia.org/wiki/Mean_curvature), вычисляя среднюю рёберную кривизну всех связанных с вершиной рёбер для каждой вершины в меше. Здесь мы определяем рёберную кривизну как скалярное произведение нормализованной вершинной нормали и вектора ребра (то есть, вектор, формируемый от вершины к её соседке). Это произведение будет отрицательным, если ребро изгибается вниз относительно нормали, и положительным, если оно изгибается вверх. Мы обратим этот знак, так как нам более привычно понятие положительной кривизны для пиков, а не для впадин. По-другому можно посмотреть на это так: в областях положительной кривизны угол между вершинной нормалью и ребром, начинающемся в той же вершине, больше 90°.
Следующий рисунок иллюстрирует концепцию - он изображает серию вершин, связанных рёбрами. У каждой вершины показана связанная с ней вершинная нормаль (стрелками). Вершины, обозначенные как a, имеют положительную кривизну, те, что обозначены b - отрицательную кривизну. Две из показанных вершин помечены буквой c, они находятся в области нулевой кривизны - в этих местах поверхность плоская, и вершинная нормаль перпендикулярна рёбрам.
Расчет локальной кривизны
Функцию, которая вычисляет локальную кривизну для каждой вершины в меше, и возвращает список нормализованных весов, можно осуществить следующим образом:
from collections import defaultdict
def localcurvature(me,positive=False):
end=defaultdict(list)
for e in me.edges:
end[e.v1.index].append(e.v2)
end[e.v2.index].append(e.v1)
weights=[]
for v1 in me.verts:
dvdn = []
for v2 in end[v1.index]:
dv = v1.co-v2.co
dvdn.append(dv.dot(v1.no.normalize()))
weights.append((v1.index,sum(dvdn)/max(len(dvdn),
1.0)))
if positive:
weights = [(v,max(0.0,w)) for v,w in weights]
minimum = min(w for v,w in weights)
maximum = max(w for v,w in weights)
span = maximum - minimum
if span > 1e-9:
return [(v,(w-minimum)/span) for v,w in weights]
return weights
Функция localcurvature() принимает меш и один опциональный аргумент, и возвращает список кортежей с индексом вершины и её весом. Если дополнительный аргумент - Истина, любой рассчитанный отрицательный вес отвергается.
Сложная работа выполняется на выделенных строках. Здесь мы проходим циклом над всеми вершинами, и затем, во внутреннем цикле, проверяем каждое связанное с текущей вершиной ребро, чтобы извлечь вершину на другом конце из предварительно рассчитанного словаря. Затем мы вычисляем dv как рёберный вектор и добавляем скалярное произведение этого рёберного вектора и нормализованной вершинной нормали в список dvdn.
weights.append((v1.index,sum(dvdn)/max(len(dvdn),1.0)))
Предшествующая строка может выглядеть странно, но она добавляет кортеж, состоящий из индекса вершины и средней кривизны, где среднее число получается вычислением суммы всех величин кривизны по каждому ребру из списка, и деления её на количество величин в списке. Поскольку список может быть пустым (это случается, когда меш содержит не связанные вершины), мы предохраняемся от ошибки деления на 0, деля её на длину списка или на единицу, в зависимости от того, что больше. Таким образом, мы сохраняем наш код более удобочитаемый, избегая оператора if.
Схема кода: curvature.py
С функцией localcurvature() в нашем расположении, сам скрипт вычисления кривизны становится совсем кратким (полный скрипт доступен как curvature.py):
if __name__ == "__main__":
try:
choice = Blender.Draw.PupMenu("Normalization%t|Only
positive|Full range")
if choice>0:
ob = Blender.Scene.GetCurrent().objects.active
me = ob.getData(mesh=True)
try:
me.removeVertGroup('Curvature')
except AttributeError:
pass
me.addVertGroup('Curvature')
for v,w in localcurvature(me,
positive=(choice==1)):
me.assignVertsToGroup('Curvature',[v],w,
Blender.Mesh.AssignModes.ADD)
Blender.Window.Redraw()
except Exception as e:
Blender.Draw.PupMenu('Error%t|'+str(e)[:80])
Выделенные строки показывают, что мы удаляем возможно существующую группу вершин Curvature из Меш-объекта внутри блока try, и отлавливаем исключение AttributeError, которое будет вызвано, если группа отсутствует. Затем, мы снова добавляем группу с тем же именем, так что она будет полностью пустая. Последняя выделенная строка показывает, как мы добавляем отдельно каждую вершину, поскольку любая вершина может иметь отличающийся от других вес.
Все действия окружены конструкцией try … except , которая поймает любые исключения, и они появятся во всплывающем информационном сообщении, если произойдёт что-то необычное. Наиболее вероятно, это будет в ситуациях, когда пользователь забудет выбрать Меш-объект.
Собираем всё это вместе: Огни святого Эльма
Иллюстрация испускания из заострённого стержня была сделана моделированием простого объекта стержня вручную, и, затем, вычислением кривизны с помощью curvature.py.
Затем, была добавлена система частиц и параметр плотности (density) в панели Extra был настроен на группу вершин Curvature. Стержню и системе частиц были даны отдельные материалы: простой серый и белое Хало соответственно. Частицы были симулированы для 250 кадров, и для иллюстрации представлен кадр 250.
Арматура может считаться основой анимации, поскольку деформирует меш управляемым способом, который можно задавать ключами в данных кадрах, необходима для аниматоров, чтобы придавать позы их персонажам удобно контролируемым способом.
Реализация арматуры Блендера обеспечивает риггера и аниматора подавляюще большим количеством возможностей, но в конце концов арматура в первую очередь набор связанных костей, где каждая кость деформирует часть меша. Перемещения этих костей друг относительно друга могут быть обусловлены несколькими различными ограничениями.
Хотя кости можно конфигурировать для работы так, чтобы они влияли через envelope (конверт), тем самым деформируя любую вершину целевого меша в пределах определенного радиуса, их можно также сконфигурировать, чтобы деформировать только те вершины, которые принадлежат группе вершин с именем, совпадающим с именем этой кости. Такая деформация в дальнейшем управляется весом вершины в группе вершин, давая нам возможность точной настройки влияния кости.
Чтобы проиллюстрировать основные возможности арматуры, мы создадим риг простой модели часов. Часы - это единый меш, состоящий из трех отдельных, не соединённых между собой субмешей - body (тело), little hand (маленькая рука), и big hand (большая рука). (Здесь автор, типа, пошутил. В английском языке стрелки часов почему-то называются «hand», что одновременно означает «ладонь» или «рука». Ну в статье стрелки и выполнили в виде реальных рук. Я долго не мог решить, как же лучше перевести эти little hand и big hand. - прим. пер.) Вершины каждой руки часов принадлежат двум отдельным вершинным группам - одна половина часовой руки (Arm), подключена к центру часов, и для конца руки (или ладони, Hand) отдельно. Эта настройка позволяет создать мультяшную анимацию наподобие карикатуры, где мы, например, можем сделать след конца руки фактическим движением.
Схема кода: clock.py
Мы должны предпринять следующие шаги, чтобы оснастить наши часы предлагаемым способом:
1. Импортировать данные меша
2. Создать меш часов
3. Создать вершинные группы
4. Создать объект арматуры
5. Создать кости в составе арматуры.
6. Связать модификатор с арматурой
Перевод из схемы в код - почти один в один, только нужно повторить множество инструкций для каждой из костей (полный код доступен как clock.py):
me=Blender.Mesh.New('Clock')
me.verts.extend(clockmesh.Clock_verts)
me.faces.extend(clockmesh.Clock_faces)
scn=Blender.Scene.GetCurrent()
ob=scn.objects.new(me)
scn.objects.active=ob
me.addVertGroup('BigHand')
me.assignVertsToGroup('BigHand',
clockmesh.Clock_vertexgroup_BigHand,
1.0, Blender.Mesh.AssignModes.ADD)
… <аналогичный код для вершинных групп LittleHand,
BigArm и LittleArm опущен> …
ar = Blender.Armature.New('ClockBones')
ar.envelopes=False
ar.vertexGroups=False
obbones = scn.objects.new(ar)
mod = ob.modifiers.append(Blender.Modifier.Types.ARMATURE
mod[Blender.Modifier.Settings.OBJECT]=obbones
mod[Blender.Modifier.Settings.ENVELOPES]=False
mod[Blender.Modifier.Settings.VGROUPS]=True
ar.makeEditable()
bigarm = Blender.Armature.Editbone()
bigarm.head = vec(0.0,0.0 ,0.57)
bigarm.tail = vec(0.0,0.75,0.57)
ar.bones['BigArm'] = bigarm
bighand = Blender.Armature.Editbone()
bighand.head = bigarm.tail
bighand.tail = vec(0.0,1.50,0.57)
bighand.parent = bigarm
ar.bones['BigHand'] = bighand
… <аналогичный код для маленькой руки опущен> …
ar.update()
obbones.makeParent([ob])
Важные моменты выделены. Сначала, мы отключаем envelopes и свойства vertexGroups у объекта арматуры. Это может показаться странным, но эти свойства являются остатками от того времени, когда арматура не была модификатором, приложенным к мешу, а работала через родительское (parented) влияние на Меш-объект (по крайней мере, насколько Я могу судить, доступная документация немного невнятна в этом месте). Мы определяем, какое влияние использовать, устанавливая свойства в модификаторе арматуры.
После связывания арматурного модификатора с нашим Меш-объектом, мы создадим нашу арматуру кость за костью. Прежде, чем мы добавим какие-либо кости в арматуру, мы должны вызвать её метод makeEditable(). Заметьте, что этот режим редактирования для арматур отличен от режима редактирования для других объектов, которые можно задавать с помощью функции Blender.Window.editMode()! Как только мы закончим, мы возвращаемся в нормальный режим снова, вызывая метод update().
Вы можете обратить внимание, что при создании нашей арматуры мы создём экземпляры объектов Editbone. Вне режима редактирования эти те же кости ссылаются на объекты типа Bone. Оба ссылаются на одну и ту же кость, но предлагают различную функциональность и атрибуты, подходящие для режима редактирования или для режима объектов. Для того, чтобы снабдить нас тем же подходом, Блендер также предоставляет объекты PoseBone для манипуляции костями в режиме Позы.
Кости позиционируются в арматуре определением позиций их головы и хвоста (тупой и острый концы соответственно, в представлении кости как восьмиугольника). Для соединения костей не достаточно сделать позицию хвоста одной кости равной позиции головы другой. Для того, чтобы кость следовала за перемещениями другой кости, она должна быть дочерней к ней. Отношения родитель-потомок осуществляются установкой в атрибут parent потомка ссылки на объект родительской кости. В нашем примере, у нас каждая кость ладони является потомком своей соответствующей кости руки.
Кости в составе арматуры индексируются их именем. Если свойство модификатора арматуры VGROUPS установлено, имя кости должно быть идентично имени группы вершин, на которую она влияет.
Последняя строка кода нашего примера также важна; необходимо сделать арматуру родителем Меш-объекта. Это может показаться излишним в ситуациях, где арматура и меш остаются в одном и том же месте, и перемешаются только отдельные кости в арматуре; но если не сделать этого, это приведёт к неустойчивому отображению меша при интерактивном изменении позы (Вы должны переводить меш в режим редактирования и обратно, например, чтобы видеть эффект от позы на арматуре, который полностью непригоден для работы). Результат нашей оснастки будет выглядеть похожим на это (мы установили режим отображения арматуры в x-ray, чтобы сделать её видимой через меш):
Отрендеренный результат выглядит так:
Мы могли бы захотеть ограничить движение отдельных костей до точных вращений вокруг оси z, и это можно сделать добавлением ограничений (constraints). Мы столкнемся с ограничениями в следующем разделе.
Всё, что мы узнали уже об остнастке, может быть применено также к creepycrawlies.py. Если мы хотим расширить функциональность сгенерированной модели, мы можем соединить модификатор арматуры со сгенерированным мешем. Мы также создадим объект арматуры с подходящим набором костей.
Наша задача уже облегчена, потому что мы уже сгруппировали вершины частей тела в модуле mymesh, так что связывание их с группой вершин и соответствующей костью тривиально. Не таким тривиальным будет создание самих костей, так как их может быть много, и их нужно разместить и соединить правильным способом.
Давайте посмотрим на то, как могли бы быть осуществлены некоторые существенные элементы (полный код смотрите в creepycrawlies.py). Сначала мы должны создать арматуру и сделать её редактируемой для добавления костей:
ar = Blender.Armature.New('BugBones')
ar.autoIK = True
obbones = scn.objects.new(ar)
ar.makeEditable()
Мы можем также задать любые атрибуты, которые изменяют поведение арматуры или способ её отображения. Здесь мы просто включаем свойство autoIK, так как это сделает манипуляции хвостом нашего создания, возможно очень длинным, намного проще для аниматора.
Следующий шаг - это создание костей для каждого набора вершин. Список vgroup в следующем коде содержит кортежи (vg,vlist,parent,connected), где vg - имя группы вершин а vlist - список индексов вершин, принадлежащих этой группе. Каждая кость, которую мы создаем, может иметь родителя и может физически быть соединена с родителем. Эти условия задаются частями кортежа parent (родитель) и connected (соединён):
for vg,vlist,parent,connected in vgroup:
bone = Blender.Armature.Editbone()
bb = bounding_box([verts[i] for i in vlist])
Для каждой кости, которую мы создаем, мы вычисляем габаритный ящик (bounding box) всех вершин в группе, на которые эта кость будет влиять. Дальше мы должны разместить кость. При способе, которым мы настраивали наше создание, все сегменты его тела вытягивались вдоль оси y, за исключением крыльев (wing) и ног (leg). Они вытягивались вдоль оси x. Мы сначала проверяем этот факт, и соответственно устанавливаем переменную axis (ось):
axis=1
if vg.startswith('wing') or vg.startswith('leg'):
axis = 0
Кости в составе арматуры индексируются по имени и позиции концов костей, сохраненных в их атрибутах head (голова) и tail (конец) соответственно. Так, если у нас есть родительская кость, и мы хотим определить её среднее значение координаты y, мы можем вычислить это следующим способом:
if parent != None :
parenty = (ar.bones[parent].head[1] +
ar.bones[parent].tail[1])/2.0
Мы вычисляем эту позицию, поскольку такие части как, например, ноги и крылья имеют родительские кости (то есть, они перемещаются вместе с родительской костью), но не подсоединены головой к хвосту. Мы разместим эти кости, начиная в центре родительской кости, и для этого нам нужна позиция родителя по y. Кости сегментов, лежащих вдоль оси y, сами позиционируются вдоль оси y, и, таким образом, имеют нулевые координаты x и z. Координаты x и z ног и сегментов крыльев берутся из их габаритных ящиков. Если кость подсоединена (connected), мы просто устанавливаем позицию её головы в копию позиции хвоста родителя (выделено ниже).
Класс Блендера Vector предоставляет функцию copy(), но как ни странно, нет функции __copy__(), так что он не будет играть по правилам с функциями из модуля Питона copy.
if connected:
bone.head = ar.bones[parent].tail.copy()
else:
if axis==1:
bone.head=Blender.Mathutils.Vector(0,
bb[1][0],0)
else:
bone.head=Blender.Mathutils.Vector(bb[0][1],
parenty,bb[2][1])
Положение хвоста кости рассчитывается аналогичным образом:
if axis==1:
bone.tail=Blender.Mathutils.Vector(0,bb[1][1],0)
else:
bone.tail=Blender.Mathutils.Vector(bb[0][0],
parenty, bb[2][0])
Последние шаги в создании кости - это добавление её к арматуре и установка специфичных для костей опций и всех родительских связей.
ar.bones[vg] = bone
if parent != None :
bone.parent=ar.bones[parent]
else:
bone.clearParent()
if connected:
bone.options=Blender.Armature.CONNECTED
Заметьте, что в предыдущем коде важен порядок действий: атрибут parent может быть присвоен или очищен только у костей, которые добавлены к арматуре, а опция CONNECTED может быть установлена только у кости, имеющей родителя.
Кроме того, мы должны остерегаться здесь некоторой специфичности Блендера. Родитель может быть установлен у кости назначением в её атрибут parent. Если у него нет родителя, этот атрибут возвращает None. Тем не менее, мы не можем назначить None в этот атрибут, мы должны использовать функцию clearParent(), чтобы удалить родительские отношения.
Материалы — это то, что дает объекту внешнее проявление. В Блендере, материалы чрезвычайно разносторонни, и из-за этого довольно сложны. Почти любым аспектом того, как ведёт себя луч света при отражении от объекта, можно управлять, и не только простыми параметрами, но также картами изображений и нодовыми сетями.
Вплоть до 16 материалов может быть связано с объектом и, в пределах объекта, индивидуальные его части могут ссылаться на один из этих 16 материалов. На объектах Text3d, каждый индивидуальный символ может ссылаться на различный материал, и для кривых это так же для каждой управляющей точки.
С точки зрения разработчика, назначение материала объектам - процесс из двух шагов. Сначала, мы должны определить новый материал, и затем мы должны назначить материал или материалы на объект. Первый шаг может быть опущен, если мы можем сослаться на уже существующие материалы.
Если у объекта, подобному мешу, уже определены грани, тогда мы все еще должны назначать материал для каждой грани. Вновь создаваемые грани будут иметь назначенным активный материал, если активный материал задан.
Небольшой кусок кода иллюстрирует, как мы можем назначать материалы на Меш-объект. Здесь мы назначаем материал с белым рассеянным цветом для всех граней с чётным номером, и с черным рассеянным цветом для всех граней с нечетным номером на Меш-объекте в переменной ob.
me=ob.getData(mesh=1)
mats=[ Blender.Material.New(), Blender.Material.New()]
mats[0].rgbCol=[1.0,1.0,1.0]
mats[1].rgbCol=[0.0,0.0,0.0]
ob.setMaterials(mats)
ob.colbits=3
for f in me.faces:
if f.index%2 == 0 :
f.mat=0
else:
f.mat=1
Выделенная строка гарантирует, что индексы материалов, используемые для каждой грани, относятся к материалам, назначенным на объект. (Также возможно связать материалы с меш-данными, как мы увидим в следующей секции.)
В Блендере и Меш-объект и объект Блендера верхнего уровня, содержащий Меш-объект, могут иметь свой собственный список из 16 материалов. Это удобно, если нам нужно множество экземпляров копии одного и того же меша, но с приложенными различными материалами. Тем не менее, в некоторых ситуациях, мы можем захотеть приложить некоторые или все материалы к Мешу, а не к объекту. Это управляется атрибутом объекта colbits. Этот атрибут состоит из 16 битов, и каждый из них указывает использовать материал от Объекта или от Меша. Мы уже видели пример с этим атрибутом в предыдущей секции.
Объект Кривой (Curve) также может иметь собственный набор материалов, и выбор фактического материала подчиняется тем же правилам, что и для Меш-объекта. Метаболлы также имеют свой собственный набор материалов, и переключение между комплектами материалов производится так же, но в отличие от многих типов объектов, которые состоят из частей (смотри следующую секцию), нет способа соединять различные материалы с различными элементами в пределах Метаболла (это истиннно также и в графическом интерфейсе пользователя: кнопки на панели Links and Materials контекста Редактирования существуют, чтобы назначать индексы материалов индивидуальным элементам метаболла, но они не дают эффекта). Используется только первый слот списка материалов.
Заметьте, что объекты, которые не визуализируются сами, как например, арматуры и решетки, не имеют связанных материалов (таким образом, любые материалы, связанные с Объектом верхнего уровня, содержащим арматуру или решетку, будут проигнорированы). Некоторые объекты, которые не имеют связанных материалов, могут иметь связанные с ними текстуры. Объекты Мира и Лампы, например, можно связать с текстурами, чтобы управлять их цветами.
В пределах меша каждая грань может иметь собственный, связанный с ней материал. Этот материал идентифицируется своим индексом в списке материалов и сохраняется в атрибуте mat. В пределах объекта Text3d, каждый символ может иметь собственный материал, опять же идентифицируемый своим индексом в списке материалов. На этот раз, этот индекс не хранится непосредственно в атрибуте, но может быть установлен или извлечен методами accessor, которые принимают индекс символа в тексте в качестве аргумента.
Секциям внутри Кривой (объекты CurNurb), можно назначить индекс материала их методом setMatIndex(). Индекс мог быть извлечен из них соответствующим методом getMatIndex(). Заметьте, что связь материала с кривыми, которые состоят из единственной линии без настроенной выдавленной ширины или связанного объекта скоса, не будет иметь видимых эффектов, так как эти кривые не рендерятся.
Следующий кусок кода показывает как назначать различные материалы различным символам в пределах объекта Text3d. Сам код прост, но как Вы можете заметить, мы определяем список из трех материалов, но используем только один. Это расточительно, но необходимо, чтобы обойти специфику в функции setMaterial(). Её аргумент индекса материала должен быть смещён на один, например, индекс 2 имеет отношение ко второму материалу в списке, тем не менее, самый большой индекс может пройти не смещённым на единицу. Так если мы хотели бы использовать два материала, мы должны бы использовать индексы 1 и 2, чтобы иметь доступ к материалам 0 и 1, но фактический список материалов должен содержать три материала, в противном случае мы не сможем передать 2 в качестве аргумента в setMaterial().
mats=[Material.New(),Material.New(),Material.New()]
mats[0].rgbCol=[1.0,1.0,1.0]
mats[1].rgbCol=[0.0,0.0,0.0]
mats[2].rgbCol=[1.0,0.0,0.0]
ob.setMaterials(mats)
ob.colbits=3
txt=ob.getData()
for i in range(len(txt.getText())):
txt.setMaterial(i,1+i%2)
Выделенный код показывает коррекцию на 1. Полный код представлен как TextColors.py.
Один важный аспект работы с материалами, с которым мы пока не имели дела - цвета вершин. В мешах каждая вершина может иметь собственный цвет вершины. Цвет вершины отличается от материала, но будут ли цвета вершин вызывать какие-то видимые эффекты, контролируется флагами режима материала. Чтобы использовать любые цвета вершины в материале, должен быть установлен бит VColPaint вызовом метода setMode(). Когда используется этот режим, цвета вершин определяют диффузный (рассеянный) цвет материала, тогда как все обычные атрибуты материалов управляют способом, которым этот диффузный цвет будет рендериться. Обычное использование для цветов вершин - это запекание дорогих в вычислительном отношении эффектов, как например, ambient occlusion. Поскольку цвета вершин можно рендерить очень быстро, ambient occlusion может быть аппроксимирована этим способом, даже в настройке реального времени, как например, в игровом движке. (Аппроксимирована, поскольку при этом не будет такой же реакции на изменения в освещении.)
Цвета вершин сохраняются как объекты Mesh.MCol (основаны на кортежах RGBA) в атрибуте грани col. Атрибут col является списком, содержащим ссылку на объект MCol для каждой вершины в грани. У такого размещения проявится смысл, когда Вы поймёте, что фактически материалы связаны с гранями, а не с вершинами. Когда цвета вершин различны, они линейно интерполируются через грань.
Присваивать атрибуту грани col возможно, если только у меша был установлен его атрибут vertexColors в Истину.
Следующий пример показывает, как мы можем установить цвета вершин меша. Мы выбираем градации серого в зависимости от координаты z вершин (выделено).
import Blender
ob=Blender.Scene.getCurrent().objects.active
me=ob.getData(mesh=1)
me.vertexColors=True
for f in me.faces:
for i,v in enumerate(f.verts):
g = int(max(0.0,min(1.0,v.co.z))*255)
f.col[i].r=g
f.col[i].g=g
f.col[i].b=g
mats=[Blender.Material.New()]
mats[0].setMode(Blender.Material.Modes['VCOL_PAINT'])
ob.setMaterials(mats)
ob.colbits=1
ob.makeDisplayList()
Blender.Window.RedrawAll()
Полный код доступен как VertexColors.py.
Как последний штрих в нашей деятельности с гравюрой, мы добавим два материала. Один индекс материала назначим вершинам на поверхности, и другой вершинам в выточенных канавках. Этим методом мы можем, например, создать проявление вновь созданной надписи на куске выветренного камня.
Так как мы ранее определили несколько удобных групп вершин, назначение индексов материала будет вопросом итерации над всеми гранями и назначения в каждую вершину грани подходящего индекса материала в зависимости от того, членом какой вершинной группы является вершина. Функция, показанная ниже, принимает чуть более общий подход, так как она принимает меш и список регулярных выражений, и назначает индекс материала на каждую грань в зависимости от принадлежности к группе вершин, которая имеет имя, соответствующее одному из регулярных выражений.
Эти функции делают очень легким назначение одинакового индекса материала во все группы вершин, которые имеют аналогичные имена, например все хвосты и сегменты грудной клетки меша, создаваемого creepycrawlies.py (они все имеют такие имена как, например, tail.0, tail.1, , и так далее).
Функция доступна в Tools.py. Она зависит от функции Питона re.search(), которая сопоставляет регулярное выражение со строкой. Выделенная строка показывает, что мы вставляем строку регулярного выражения в так называемые якоря (^ и $). Этим путём регулярное выражение, такое как например, aaaa, сопоставится только с группой вершин с именем aaaa, а не с именем aaaa.0, так что мы сможем различить их, если мы хотим. Если же мы хотим соответствия всем именам групп вершин, которые начинаются с tail, мы могли бы, например, передать регулярное выражение tail.* .
Регулярные выражения являются чрезвычайно мощным способом сопоставления строк. Если Вы незнакомы с ними, Вы должны обратиться к документации по модулю Питона re (http://docs.python.org/library/re.html). Можно начать, например, с http://wiki.intuit.ru.
Другая вещь, которую нужно отметить в этой функции — использование операций с множествами. Они немного ускорят процесс, так как операции с множествами в Питоне чрезвычайно быстрые. Мы используем их здесь, чтобы проверять множество вершин (или, скорее, их индексов), которые составляют грань, на то, что все они входят в множество индексов вершин, находящихся в некоторой группе вершин. Мы заранее вычисляем оба множества индексов вершин, те, которые принадлежат группе вершин и индексы вершин каждой грани, и храним их в словарях для легкого доступа. Таким образом, мы создаем эти множества только однажды, для каждой группы вершин и для каждой грани соответственно, вместо воссоздания каждого множества всякий раз, когда мы сопоставляем регулярное выражение. Для больших мешей это потенциально сохранит много времени (за счет памяти).
import re
def matindex2vertgroups(me,matgroups):
if len(matgroups)>16 :
raise ArgumentError("number of groups larger than
number of materials possible (16)")
groupnames = me.getVertGroupNames()
vertexgroupset={}
for name in groupnames:
vertexgroupset[name]=set(me.getVertsFromGroup(name))
print name,len(vertexgroupset[name])
faceset={}
for f in me.faces:
faceset[f.index]=set([v.index for v in f.verts])
for i,matgroup in enumerate(matgroups):
for name in groupnames:
if re.search('^'+matgroup+'$',name):
for f,vset in faceset.items():
if vset.issubset(vertexgroupset[name]) :
me.faces[f].mat = i
break
В этой главе, мы видели как сделать жизнь легче для наших конечных пользователей, определяя вершинные группы у мешей, чтобы упростить выбор определенных характеристик. Мы также видели, как назначать материалы вершинам, и как создавать новые материалы, если нужно. Первые шаги были предприняты, чтобы оснастить (rig) меш. В частности, мы узнали:
• Как определять вершинные группы
• Как назначать вершины в вершинные группы
• Как назначать материалы граням
• Как назначать вершинам вершинные цвета
• Как устанавливать свойства рёбер
• Как добавлять модификатор
• Как покрывать кожей кости
Далее мы выйдем за пределы статики, и увидим как управлять перемещением объектов.