5 Действия при изменениях кадров

Кроме тех многих мест, с которыми мы столкнулись, где Питон может быть использован в Блендере, мы теперь посмотрим на скрипты, которые можно использовать для действия при определенных событиях. В этот тип скриптов входят две изюминки - скриптсвязи и обработчики пространства.

Скриптсвязи (Script links) являются скриптами, которые могут быть ассоциированы с объектами Блендера (Меши, Камеры, и так далее, а также Сцены и объекты Мира), и их можно настроить так, чтобы они автоматически срабатывали в следующих случаях:

• Сразу перед рендером кадра

• Сразу после рендера кадра

• Когда кадр сменяется

• Когда объект скорректирован

• Когда данные объекта скорректированы

Объекты Сцены можно связать с ассоциированными с ними скриптами, которые могут вызываться в двух дополнительных случаях:

• При загрузке .blend файла

• При сохранении .blend файла

Обработчики пространства (Space handlers) являются скриптами на Питоне, которые вызываются всякий раз, когда окно 3D-вида перерисовывается или обнаружено действие клавиатуры или мыши. Их основным применением является расширение возможностей интерфейса пользователя Блендера.

В этой главе вы узнаете:

• Что такое скриптсвязи и обработчики пространства

• Как осуществлять деятельность при каждом изменении кадров в анимации

• Как ассоциировать дополнительную информацию с объектом

• Как сделать, чтобы объект появлялся или исчезал, изменяя его слой или прозрачность

• Как осуществить схему, при которой с объектом в каждом кадре связывается различные меши

• Как прибавить функциональности 3D-виду


Анимация видимости объектов

Часто текущей задачей в создании анимации является желание заставить объект исчезать или тускнеть в определенном кадре, либо ради самого эффекта, или чтобы заменить объект другим для достижения некоего драматического воздействия (как например, взрыв или превращение кролика в мяч).

Существует много способов создавать эти эффекты, и большинство из них конкретно не связаны со скриптсвязями, реагирующими на изменения кадра (многие могут также просто быть ключами анимации). Тем не менее, мы посмотрим на два метода, которые можно легко приспособить ко всем ситуациям такого рода, даже к тем, которые не легко осуществить ключами. Например, нам требуется несколько специфическое поведение параметра, которое легко можно сформулировать в выражении, но неудобно ловить в IPO.


Потускнение материала

Нашим первым примером будет изменение диффузного цвета материала. Это будет подобно простому изменению прозрачности (transparency), но на иллюстрациях такие изменения легче увидеть с диффузным цветом.

Нашей целью является выцветание диффузного цвета от черного к белому и обратно, в течении двухсекундного периода. Мы, следовательно, определяем функцию setcolor(), которая принимает материал и изменяет его диффузный цвет (атрибут rgbColor). Предполагается, что частота 25 кадров в секунду и, следовательно, первая строка получает номер текущего кадра и выполняет деление по модулю, чтобы определить, какая часть текущей целой секунды пройдена.

Выделенная строка в следующем куске кода определяет, в четной или нечетной секунде мы находимся. Если мы в четной секунде, мы наращиваем диффузный цвет до белого, так что мы просто сохраняем нашу вычисленную долю. Если мы в нечетной секунде, мы затемняем диффузный цвет до черного, так что мы вычитаем долю из максимально возможного значения (25). Наконец, мы масштабируем нашу величину в пределы между 0 и 1 и назначаем её во все три цветовых компонента для получения оттенка серого:

import Blender

def setcolor(mat):

  s = Blender.Get('curframe')%25

  if int(Blender.Get('curframe')/25.0)%2 == 0:

    c = s

  else:

    c = 25-s

  c /= 25.0

  mat.rgbCol = [c,c,c]


if Blender.bylink and Blender.event == 'FrameChanged':

  setcolor(Blender.link)

Скрипт заканчивается важной проверкой: Blender.bylink будет Истиной (True), только в том случае, если этот скрипт был вызван как script handler, и в этом случае Blender.event содержит тип события. Мы хотим действовать только при изменении кадра, так что мы проверяем здесь наличие этого события. Если эти условия удовлетворены, мы передаем Blender.link в нашу функцию setcolor(), так как там содержится объект, с которым наш связанный скрипт ассоциирован - в нашем случае это будет объект Material (Материал). (Этот скрипт доступен как MaterialScriptLink.py в файле scriptlinks.blend.)

Следующим пунктом в нашем списке нужно соединить скрипт с объектом, чей материал мы хотим изменить. Следовательно, мы выбираем объект, и в Окне Кнопок выбираем панель Script. В панели Scriptlinks (Скриптсвязи), мы включаем скриптсвязи (кнопка Enable Script Links) и выбираем кнопку MaterialScriptLinks. (Если нет кнопки MaterialScriptLinks, тогда выбранному объекту не был назначен материал. Убедитесь в том, что он есть.) Там должна теперь появиться надпись Select Script link с кнопкой New (Новый). Щелкните на New, появится выпадающий список с доступными скриптами (файлы в текстовом редакторе). В нашем случае мы выбираем MaterialScriptLink.py, и на этом всё. Мы можем теперь протестировать нашу скриптсвязь, изменяя кадр в 3D-виде (с помощью клавиш стрелок). Цвет нашего объекта должен изменяться при изменении номера кадра. (Если у цвета не видно изменений, проверьте тип отображения в 3D-виде, должно быть solid или shaded.)



Изменение слоев

Если мы хотим изменить видимость объекта, изменение назначенного слоя (слоёв) - более распространённая и мощная техника, чем изменения свойств материала. Изменение назначенного слоя имеет, например, преимущество в том, что мы можем сделать объект полностью невидимым для ламп, которые сконфигурированы на освещение только определенных слоёв, и множество аспектов анимации (например, отклонение частиц воздействием полей) также могут быть ограничены определенными слоями. Также, изменение слоев не ограничено объектами со связанными с ними материалами. Вы можете точно так же легко изменить слой для Лампы или Камеры.

В нашем следующем примере мы хотим назначить объекту слой 1, если количество пройденных секунд - четное, и слой 2, если время в секундах нечетное. Скрипт, осуществляющий это, очень подобен нашему скрипту, изменяющему материал. Реальная работа производится посредством функции setlayer(). Первая строка вычисляет слой, в котором объект должен находиться в текущем кадре, а следующая строка (выделенная) назначает список индексов слоя (состоящий из единственного слоя в данном случае) атрибуту layers объекта. Последние две строки функции setlayer() гарантируют, что изменение слоя действительно станет видимым в Блендере.

import Blender

def setlayer(ob):

  layer = 1+int(Blender.Get('curframe')/25.0)%2

  ob.layers = [ layer ]

  ob.makeDisplayList()

  Blender.Window.RedrawAll()

if Blender.bylink and Blender.event == 'FrameChanged':

  setlayer(Blender.link)

Как и в нашем предыдущем скрипте, последние строки нашего скрипта проверяют, что он был вызван как скриптсвязь и по событию изменения кадров, и если это так, передают связанный объект в функцию setlayer(). (Скрипт доступен как OddEvenScriptlink.py в файле scriptlinks.blend.)

Все, что осталось сделать, это назначить скрипт как скриптсвязь (scriptlink) выбранному объекту. Снова, это выполняется в Окне Кнопок | панель Script, щелкая по кнопке Enabling Script Links в панели Scriptlinks (если это необходимо, она могла все ещё быть выбранной после нашего предыдущего примера. Это глобальный выбор, то есть, включено или выключено для всех объектов). На этот раз мы выбираем скриптсвязи объекта вместо скриптсвязей материала и щелкаем на New, чтобы выбрать OddEvenScriptlink.py из выпадающего списка.


Обратный отсчет - анимация таймера с помощью скриптсвязи

Одна из возможностей использовать скриптсвязи, которые действуют при изменении кадров - возможность модифицировать текущий меш, или с помощью изменения вершин Меш-объекта, или посредством ассоцииации с объектом Блендера полностью другого меша. Это невозможно при использовании IPO, так как они ограничены ключами формы, которые интерполируются между предопределёнными формами с той же топологией меша (то же число вершин, соединенных таким же образом). Это истинно также для объектов кривых и текста.

Одним из применений этой техники будет создание объекта счетчика, который отобразит время в секундах с тех пор, как началась анимация. Это выполнено изменением текста объекта Text3d с помощью его метода setText(). Функция setcounter() в следующем коде делает как раз это вместе с необходимыми действиями, чтобы скорректировать отображение в Блендере. (Скрипт доступен как CounterScriptLink.py в файле scriptlinks.blend.)

import Blender

objectname='Counter'

scriptname='CounterScriptLink.py'

def setcounter(counterob):

  seconds = int(Blender.Get('curframe')/25.0)+1

  counterob.getData().setText(str(seconds))

  counterob.makeDisplayList()

  Blender.Window.RedrawAll()

if Blender.bylink:

  setcounter(Blender.link)

else:

  countertxt   = Blender.Text3d.New(objectname)

  scn      = Blender.Scene.GetCurrent()

  counterob   = scn.objects.new(countertxt)

  setcounter(counterob)


  counterob.clearScriptLinks([scriptname])

  counterob.addScriptLink(scriptname,'FrameChanged')

Этот скрипт может быть ассоцирован в виде скриптсвязи с любым объектом Text3d, как показано прежде. Тем не менее, если запустить его с помощью Alt + P из текстового редактора, он создаст новый объект Text3d и присоединит себя к этому объекту как связанный скрипт. Выделенная строка показывает такую же простую проверку на его наличие, как в предыдущих скриптах, но в данном случае мы выполняем также некоторое действие в случае, если скрипт не был вызван как скриптсвязь (после else). Последние две выделенных строки показывают, как мы соединяем скрипт с вновь созданным объектом. Сначала, мы удаляем (clear) любую скриптсвязь с тем же именем, которая, возможно, была связана раньше. Это делается, чтобы предотвратить связывание этой же скриптсвязи более одного раза, что допустимо, но едва ли полезно. Затем, мы добавляем скрипт как скриптсвязь, которая будет вызываться, когда происходит изменение кадра (FrameChanged). Скриншот показывает 3D-вид с кадром из анимации вместе с окном Кнопок (слева вверху), который содержит список ассоциаций скриптсвязей с объектом.

Заметьте, что хотя возможно соединить скриптсвязь с объектом Блендера из скрипта на Питоне, скриптсвязи для него должны быть включены вручную, чтобы действительно работать! (Во вкладке ScriptLinks). В API Питона Блендера нет функциональности, позволяющей сделать это из скрипта.



Я буду следить за вами

Иногда, при работе с сложным объектом, трудно следить за важной деталью, так как она может быть загорожена другими частями геометрии. В такой ситуации, было бы неплохо подсвечивать определенные вершины, чтобы они каким-то образом оставались видимыми, независимо от ориентации и режима редактирования.

Обработчики пространства (Space handlers) предоставляют нам способ выполнять действия всякий раз, когда окно 3D-вида перерисовывается, или когда обнаружено действие клавиатуры или мыши. Эти действия также могут включать рисование в области 3D-вида, так что мы сможем добавить подсвечивание (Highlight) в любом месте, где нам нравится.

Как нам определить, какие вершины мы хотели бы подсветить? Блендер уже предоставляет нам унифицированный способ группировать наборы вершин в виде вершинных групп, так что всё, что мы должны сделать, это позволить пользователю указать, какую группу вершин он хотел бы подсветить. Затем мы сохраним имя этой выбранной группы вершин как свойство объекта (object property). Свойства объектов предназначены для использования в игровом движке, но нет причин, почему мы не можем использовать их в качестве средства постоянно хранить нашу выбранную группу вершин.

И так, снова у нас есть скрипт, который будет вызываться двумя способами: как обработчик пространства (то есть, всякий раз, когда окно 3D-вида перерисовывается, чтобы выделить наши вершины), или при запуске его из текстового редактора с помощью Alt + P, чтобы подсказать пользователю выбрать группу вершин для подсвечивания.


Схема программы: AuraSpaceHandler.py

Следующая схема показывает, какие шаги мы предпримем в каждой ситуации:

1. Получить активный объект и меш.

2. Если запущено автономно:

◦ Получить список групп вершин

◦ Предложить на выбор

◦ Сохранить выбор как свойство объекта

3. Иначе:

◦ Получить свойство, которое содержит группу вершин

◦ Получить список координат вершин

◦ Для каждой вершины:

▪ нарисовать маленький диск

Результирующий код доступен как AuraSpaceHandler.py в файле scriptlinks.blend:

# SPACEHANDLER.VIEW3D.DRAW

Он начинается со строки комментария, которая является существенной, так как она сигнализирует Блендеру, что это - скрипт обработчика пространства, который может быть связан с 3D-видом (в настоящее время никакую другую область нельзя связать с обработчиком пространства) и должен быть вызван по событию обновления изображения redraw.

import Blender

from Blender import *

scn = Scene.GetCurrent()

ob  = scn.objects.active

if ob.type == 'Mesh':

  me = ob.getData(mesh = True)

  if Blender.bylink:

    p=ob.getProperty('Highlight')

    vlist = me.getVertsFromGroup(p.getData())

    matrix = ob.matrix

    drawAuras([me.verts[vi].co*matrix for vi in vlist],

         p.getData())

  else:

    groups = ['Select vertexgroup to highlight%t']

    groups.extend(me.getVertGroupNames())

    result = Draw.PupMenu( '|'.join(groups) )

    if result>0:

     try:

       p=ob.getProperty('Highlight')

       p.setData(groups[result])

     except:

       ob.addProperty('Highlight',groups[result])

Далее скрипт приступает к извлечению активного объекта из текущей сцены и получает меш объекта, если его тип - Mesh. На выделенной строке мы проверяем, запущен ли скрипт как обработчик пространства, и если это так, мы выбираем свойство с именем Highlight (подсветка). Данные этого свойства является именем группы вершин, которую мы хотим подсветить. Мы продолжаем, получая список всех вершин в этой вершинной группе и получая матрицу объекта. Нам она нужна, поскольку позиции вершин загружены относительно матрицы объекта. Затем, мы создаем список позиций вершин и передаем его вместе с именем группы вершин в функцию drawAuras(), которая позаботится о фактическом рисовании.

Вторая выделенная строка показывает начало кода, который будет выполняться, если мы запускаем скрипт из текстового редактора. Он создаёт строку, состоящую из имен всех групп вершин, связанных с активным объектом, разделенных символами трубы (|) и с добавленным подходящим названием. Эта строка передаётся в функцию PupMenu(), которая отобразит меню, и возвратит выбор пользователя, либо -1, если ничего не было выбрано.

Если была выбрана группа вершин, мы пытаемся извлечь свойство Highlight. Если это получается, мы записываем в данные этого свойства имя выбранной группы вершин. Если свойство еще не существовало, мы добавляем новое с именем Highlight и в качестве данных также присваиваем имя выбранной группы вершин.

Затем мы должны убедиться, что скриптсвязи включены (окно Кнопок | панель Scripts | Scriptlinks. Щелкните на Enable Script Links, если это еще не было сделано). Обратите внимание, что Блендеру все равно, имеем ли мы дело с обработчиками пространства или скриптсвязями, поскольку они включаются одинаково.

Последним шагом в использовании нашего обработчика пространства будет ассоциация его с 3D-видом. Чтобы сделать это, включите галочку Draw: AuraSpaceHandler.py в меню View - Space Handler Scripts окна 3D-вида.



Использование тем

Код, который мы еще не видели, имеет дело с фактическим рисованием подсветки и именем группы вершин, чтобы идентифицировать то, что мы выделяем. Он начинается с определения цвета, который мы используем для подсветки, и текста, извлекая их из текущей темы. Таким образом пользователь может настраивать эти цвета удобным способом из окна Пользовательских настроек:

theme   = Window.Theme.Get()[0]

textcolor = [float(v)/255 for v in theme.get( 

       Window.Types.VIEW3D ).text_hi[:3]]

color   = [float(v)/255 for v in 

       theme.get(Window.Types.VIEW3D).active[:3]]

В первой строке извлекается список тем, которые присутствуют. Первая из них является активной темой. Из этой темы мы извлекаем пространство VIEW3D, и его атрибут text_hi является списком из четырех целых, представляющим цвет RGBA. Мы удаляем из списка альфа-компоненту и преобразуем его в список трех чисел с плавающей точкой (floats) в дипазоне [0, 1], которые мы используем как цвет нашего текста. Таким же образом мы создаем цвет подсветки из атрибута active.

Нашей следующей проблемой будет нарисовать подсветку в форме диска в специфическом месте. Так как размер диска совсем небольшой (его можно скорректировать изменением переменной size), мы можем аппроксимировать его достаточно хорошо формой восьмиугольника. Мы загружаем список координат x и y такого восьмиугольника в список diskvertices:

size=0.2

diskvertices=[( 0.0, 1.0),( 0.7, 0.7), 

        ( 1.0, 0.0),( 0.7,-0.7), 

        ( 0.0,-1.0),(-0.7,-0.7), 

        (-1.0, 0.0),(-0.7, 0.7)]


def drawDisk(loc):

  BGL.glBegin(BGL.GL_POLYGON)

  for x,y in diskvertices:

    BGL.glVertex3f(loc[0]+x*size,loc[1]+y*size,loc[2])

  BGL.glEnd()

Само рисование восьмиугольника сильно зависит от функций, предоставляемых модулем Блендера BGL (выделено в предыдущем коде). Мы начинаем с установки режима рисования многоугольника, затем добавляем вершину для каждого кортежа в списке diskvertices. Позиция, переданная в функцию drawDisk(), будет центром, а вершины будут целиком лежать в круге с радиусом, равным размеру size. Когда мы вызываем функцию glEnd(), будет нарисован многоугольник, заполненный внутри текущим цветом.

Вы можете спросить, каким образом эти функции рисования знают, как перевести местоположение в 3D в координаты на экране, и тут есть действительно больше, чем кажется на первый взгляд, как мы увидим в следующей части кода. Необходимая функция, вызываемая для сообщения графической системе, как преобразовать 3D-координаты в координаты экрана, не включена в функцию drawDisk() (в предшествующем куске кода). Дело в том, что вычисление этой информации отдельно для каждого диска должно привести к лишней потере в производительности, так как эта информация одинаковая для каждого диска, который мы рисуем.

Следовательно, мы определяем функцию drawAuras(), которая принимает список locations (позиции) и аргумент groupname (имя группы, строкового типа). Она вычислит параметры преобразования, вызовет drawDisk() для каждой позиции в списке, и, затем, добавит имя группы как на-экранную этикетку приблизительно справа от центра подсветки. Модуль Блендера Window предоставляет нам функцию GetPerspMatrix(), которая извлекает матрицу для правильного преобразования точки в пространстве 3D в точку на экране. Эта матрица размером 4x4 является объектом Питона, который должен быть преобразован в единственный список чисел с плавающей точкой, чтобы его могла использовать графическая система. Выделенные строки в следующем коде заботятся об этом. Следующие три строки сбрасывают режим проецирования и сообщают графической системе использовать нашу должным образом преобразованную перспективную матрицу для вычисления экранных координат. Заметьте, что изменение этих режимов проецирования и других настроек графики не влияет на то, как сам Блендер рисует объекты на экране, так как эти настройки сохраняются перед вызовом нашего скрипта обработчика и восстанавливаются впоследствии:

def drawAuras(locations,groupname):

  viewMatrix = Window.GetPerspMatrix()

  viewBuff = [viewMatrix[i][j] for i in xrange(4)  

        for j in xrange(4)]

  viewBuff = BGL.Buffer(BGL.GL_FLOAT, 16, viewBuff)

  BGL.glLoadIdentity()

  BGL.glMatrixMode(BGL.GL_PROJECTION)

  BGL.glLoadMatrixf(viewBuff)

  BGL.glColor3f(*color)

  for loc in locations:

    drawDisk(loc)

  n=len(locations)

  if n>0:

    BGL.glColor3f(*textcolor)

    x=sum([l[0] for l in locations])/n

    y=sum([l[1] for l in locations])/n

    z=sum([l[2] for l in locations])/n

    BGL.glRasterPos3f(x+2*size,y,z)

    Draw.Text(groupname,'small')

По окончании предварительных вычислений мы можем установить цвет, которым мы рисуем наши диски с помощью функции glColor3f(). Так как мы сохранили цвет в виде списка трех чисел с плавающей точкой, а функция glColor3f() принимает три отдельных аргумента, мы распаковываем этот список с помощью оператора звездочки. Затем, мы вызываем drawDisk() для каждого элемента в списке locations.

OpenGL функции Блендера:

Документация по модулю Блендера BGL включает множество функций из библиотеки OpenGL. Многие из этих функций включены в большом количестве вариантов, которые выполняют одно и то же действие, но принимают свои аргументы различными способами. Например, BGL.glRasterPos3f() тесно связана с BGL.glRasterPos3fv(), которая принимает список трех чисел с плавающей точкой единичной точности вместо трех отдельных аргументов. За подробностями обратитесь к документации по API модулей Blender.BGL и Blender.Draw и к справочнику по OpenGL на http://www.opengl.org/sdk/docs/man/.

Если число подсветок, которые мы нарисовали, не нулевое, мы задаём цвет рисования в textcolor и затем вычисляем средние координаты всех подсвечиваемых вершин. Затем мы используем функцию glRasterPos3f(), чтобы установить стартовую позицию текста, который мы хотим отобразить в этих усреднённых координатах с небольшим пространством, добавленным к x-координате, чтобы немного сместить текст вправо. Затем функция Блендера Draw.Text() отобразит имя группы небольшим шрифтом в выбранной позиции.


Снова о мешах — создание отпечатков

Хотя мягкие тела (softbody) и имитаторы ткани (cloth), которые доступны в Блендере, во многих ситуациях делают свою работу отлично, иногда Вам может понадобиться иметь больше управления над процессом деформации меша, или Вы захотите сымитировать какое-либо специфическое поведение, которое совсем не охвачено встроенными системами симуляции Блендера. Это упражнение показывает, как вычислять деформацию меша, которого коснулся, но не порвал другой меш. Оно не сможет быть физически точным. Мы стремимся к тому, чтобы получить вероятные результаты для твердых вещей, касающихся легко деформируемой или клейкой поверхности, например, палец, продавливающий масло, или колесо, едущее по мягкой обочине.

На рисунке ниже приведены несколько возможных отпечатков. Дорожки созданы анимированием катящейся автомобильной шины по подразделенной плоскости:



В следующей части мы обратимся к объекту, меш которого будет деформироваться в качестве исходного, и к объекту или объектам, делающим деформацию в качестве цели. В некотором смысле, это очень похоже на ограничение (constraint) и мы могли бы осуществить эти деформации как pycontraints. Тем не менее, это не будет исполнимым, поскольку ограничения оцениваются всякий раз, когда исходный меш или цели двигаются; этим самым вызывается интерфейс пользователя, что приведёт к мучительным остановкам, так как расчет пересечений и результирующей деформации мешей требует интенсивных вычислений. Следовательно, мы выбираем метод, где мы вычисляем и кешируем результаты всякий раз, когда сменяется кадр.

Наш скрипт предоставит несколько функций, он должен:

• Вычислить и кешировать деформации при каждом изменении кадра

• Изменить координаты вершин, когда присутствует кешированная информация

А при автономном запуске, скрипт должен:

• Сохранять и восстанавливать первоначальный меш

• Подсказывать пользователю возможные цели

• Ассоциировать себя как скриптсвязь с исходным объектом

• Возможно, удалять себя как скриптсвязь

Важное соображение в проектировании скрипта - как мы будем сохранять или кешировать оригинальный меш и промежуточные, деформированные меши. Поскольку мы не изменяем топологию меша (то есть, то, как вершины соединены друг с другом), а только координаты вершин, будет достаточно хранить только эти координаты. Это оставляет нас с вопросом: где сохранять эту информацию.

Если мы не хотим писать наше собственное устойчивое решение для сохранений, у нас есть два выбора:

• Использовать реестр Блендера

• Ассоциировать данные с исходным объектом в виде свойства

Реестр (registry) Блендера легко использовать, но мы должны иметь какой-то метод ассоциации данных с объектом, поскольку, возможно, пользователь захочет соединить больше, чем один объект с вычислителем отпечатков. Мы могли бы использовать имя объекта как ключ, но если пользователь захочет изменить это имя, мы потеряем ссылку на сохранённую информацию, в то время как функциональность скриптсвязи должна там все еще оставаться. Тогда пользователь сам должен стать ответственным за удаление сохранённых данных, если имя объекта было изменено.

Ассоциация всех данных в виде свойства (property) избавит от страданий, вызванных переименованиями, и данные будут очищаться при удалении объекта, но типы данных, которые можно сохранять в свойствах, ограничены целым, действительным с плавающей точкой, или строкой. Существуют способы преобразования произвольных данных в строки, используя стандартный модуль Питона pickle, но, к несчастью, такому сценарию препятствуют две проблемы:

• Координаты вершин в Блендере - экземпляры объекта Vector, а они не поддерживают протокол pickle

• Размер строкового свойства ограничен 127 символами, и этого слишком мало, чтобы сохранить даже один кадр с координатами вершин для меша средних размеров

Несмотря на недостатки использования реестра, мы будем использовать его для разработки двух функций - одну для сохранения координат вершин для данного номера кадра, и одну для извлечения этих данных и применения их к вершинам меша. Сначала мы определяем вспомогательную функцию ckey(), которая возвращает ключ для использования с функциями реестра, исходя из имени объекта, чьи данные меша мы хотим кешировать:

def ckey(ob):

  return meshcache+ob.name


Не все реестры - одно и то же

Не перепутайте реестр Блендера с реестром Windows. Оба предназначены для аналогичных целей - обеспечить устойчивую память для всех типов данных, но это разные объекты. Фактические данные в реестре Блендера, которые записаны на диск, по умолчанию находятся в каталоге .blender/scripts/bpydata/config/, и это местоположение может быть изменено заданием параметра datadir с помощью Blender.Set().

Наша функция storemesh() принимает в качестве аргументов объект и номер кадра. Первым действием нужно извлечь координаты вершин из данных меша, связанных с объектом. Затем она извлекает все данные, сохранённые в реестре Блендера для объекта, с которым мы имеем дело, и мы передаем дополнительный параметр True (Истина), указывающий, что если нет данных в памяти, GetKey() должна проверить их наличие на диске. Если совсем нет никаких данных, сохранённых для нашего объекта, GetKey() возвращает None, и в этом случае мы инициализируем наш кеш пустым словарём.

Впоследствии, мы сохраняем координаты нашего меша в этом словаре, проиндексированном номером кадра (выделено в следующем куске кода). Мы преобразуем этот номер кадра из целого в строку, которую нужно использовать в качестве фактического ключа, поскольку функция Блендера SetKey() принимает ключи строкового типа при сохранении данных реестра на диск, и вызовет исключение, если она сталкивается с целым. Последняя строка снова вызывает SetKey() с дополнительным аргументом True, чтобы указать, что мы хотим сохранять данные также на диск.

def storemesh(ob,frame):

  coords = [(v.co.x,v.co.y,v.co.z) for v in

       ob.getData().verts]

  d=Blender.Registry.GetKey(ckey(ob),True)

  if d == None: d={}

  d[str(frame)]=coords

  Blender.Registry.SetKey(ckey(ob),d,True)

Функция retrievemesh() принимает в качестве аргументов объект и номер кадра. Если она находит кешированные данные для данного объекта и кадра, она назначает загруженные координаты вершинам в меше. Сначала мы определим два новых исключения, означающие некоторые специфические ошибочные состояния, с которыми retrievemesh() может столкнуться:

class NoSuchProperty(RuntimeError): pass;

class NoFrameCached(RuntimeError): pass;

retrievemesh() вызовет исключение NoSuchProperty, если объект не имеет связанных кешированных данных меша, и исключение NoFrameCached если данные присутствуют, но не для указанного кадра. Выделенная строка в следующем коде заслуживает некоторого внимания. Мы выбираем связанные данные меша у объекта с mesh=True. Это даст завёрнутый (wrapped) меш, а не копию, так что любые данные вершин, к которым мы получаем доступ, или изменяем, ссылаются на фактические данные. Также, мы сталкиваемся со встроенный функцией Питона zip(), которая принимает два списка и возвращает список, состоящий из кортежей двух элементов, по одному из каждого списка. Это эффективно позволяет просматривать два списка параллельно. В нашем случае, эти списки - список вершин и список координат и мы просто преобразуем эти координаты в векторы и назначаем их в атрибут co каждой вершины:

def retrievemesh(ob,frame):

  d=Blender.Registry.GetKey(ckey(ob),True)

  if d == None:

    raise NoSuchProperty("no property %s for object %s"

       %(meshcache,ob.name))

  try:

    coords = d[str(frame)]

  except KeyError:

    raise NoFrameCached(("frame %d not cached on" + 

              "object %s") %(frame,ob.name))

  for v,c in zip(ob.getData(mesh=True).verts,coords):

    v.co = Blender.Mathutils.Vector(c)

Чтобы завершить наш набор функций кеша, мы определяем функцию clearcache(), которая пытается удалять данные в реестре, связанные с нашим объектом. Конструкция try … except … обеспечивает, чтобы при отсутствии сохранённых данных, действие было молча проигнорировано:

def clearcache(ob):

  try:

    Blender.Registry.RemoveKey(ckey(ob))

  except:

    pass


Пользовательский интерфейс

Наш скрипт будет использоваться не только как скриптсвязь, связанная с объектом, но он также будет использоваться автономно (по нажатию Alt + P в текстовом редакторе, например), чтобы обеспечить пользователя средствами для идентификации цели, которая создаст отпечаток, чтобы очищать кеш, и, чтобы ассоциировать скриптсвязь с активным объектом. При использовании таким образом, он обеспечивает конечного пользователя несколькими управляющими меню, показанными на скриншотах. Первый показывает возможные действия:



Второй скриншот показывает всплывающее меню с предложением выбрать объект из списка Меш-объектов, из которого пользователь может выбрать, чем создавать отпечаток:



Мы сначала определяем вспомогательную функцию, которая будет использована выпадающим меню, обеспечивающим пользователя выбором Меш-объектов, для использования в качестве цели при создании отпечатка. getmeshobjects() принимает аргумент scene и возвращает список имен всех Меш-объектов. Как показано на скриншоте, список объектов-целей включает в том числе исходный объект. Хотя это законно, но вряд ли очень полезно:

def getmeshobjects(scene):

  return [ob.name for ob in scene.objects if

      ob.type=='Mesh']

Само меню осуществляется функцией targetmenu(), определенной следующим образом:

def targetmenu(ob):

  meshobjects=getmeshobjects(Blender.Scene.GetCurrent())

  menu='Select target%t|'+ "|".join(meshobjects)

  ret = Blender.Draw.PupMenu(menu)

  if ret>0:

    try:

     p = ob.getProperty(impresstarget)

     p.setData(meshobjects[ret-1])

    except:

     ob.addProperty(impresstarget,meshobjects[ret-1])

Она выбирает список из всех Меш-объектов в сцене и представляет этот список на выбор пользователю, используя функцию Блендера Draw.PupMenu(). Если пользователь выбирает один из пунктов меню (возвращённая величина будет больше нуля, смотри выделенную строку предыдущего кода), будет загружено имя этого Меш-объекта как свойство, связанное с нашим объектом. Константа impresstarget определяется в другом месте в качестве имени для свойства. Сначала, программа независимо проверяет, есть ли там уже такое свойство, связанное с объектом, вызывая метод getProperty(), и присваивает данные свойству, если есть. Если метод getProperty() вызывает исключение, поскольку свойство еще не существует, тогда мы добавляем новое свойство к объекту и назначаем ему данные с помощью единственного вызова addProperty().

Основной интерфейс пользователя определяется на верхнем уровне скрипта. Он проверяет, что не был запущен как скриптсвязь, затем предоставляет пользователю несколько действий на выбор:

if not Blender.bylink:

  ret = Blender.Draw.PupMenu('Impress scriptlink%t|'+

                'Add/Replace scriptlink|'+

                'Clear cache|Remove ' +  

                'all|New Target')

  active = Blender.Scene.GetCurrent().objects.active

  if ret > 0:

    clearcache(active)

  if ret== 1:

    active.clearScriptLinks([scriptname])

    active.addScriptLink(scriptname,'FrameChanged')

    targetmenu(active)

  elif ret== 2:

    pass

  elif ret== 3:

    active.removeProperty(meshcache)

    active.clearScriptLinks([scriptname])

  elif ret== 4:

    targetmenu(active)

Любой правильный выбор очистит кеш (выделено), и в последующих проверках выполняются необходимые действия, связанные каждым индивидуальным выбором: Add/Replace scriptlink удалит скриптсвязь, если она уже присутствует, чтобы предотвратить появление дубликатов и, затем, добавит её к активному объекту. Затем будет представлено меню целей, чтобы выбрать Меш-объект для использования при создании отпечатка. Так как мы уже очистили кеш, то второй выбор, Clear cache, не делает ничего специфического, так что здесь у нас просто оператор pass (пропустить). Remove All попытается удалить кеш и отсоединить себя как скриптсвязь, и, наконец, меню New target представит меню выбора целей, чтобы дать возможность пользователю выбрать новый целевой объект, не удаляя никаких кешированных результатов.

Если скрипт выполняется как скриптсвязь, мы сначала проверяем, что мы действуем при событии FrameChanged, затем пытаемся извлечь любые сохраненные координаты вершин для текущего кадра (выделено в следующем коде). Если нет ранее загруженных данных, мы должны вычислить эффекты от целевого объекта для этого кадра. Следовательно, мы получаем список целевых объектов для рассматриваемого объекта, вызывая вспомогательную функцию gettargetobjects() (на данный момент будет возвращен список только из одного объекта), и для каждого объекта мы вычисляем эффект для нашего меша с помощью вызова impress(). Затем, мы сохраняем эти, возможно изменённые, координаты вершин и корректируем дисплейный список, чтобы графический интерфейс пользователя Блендера знал, как отображать наш измененный меш:

elif Blender.event == 'FrameChanged':

  try:

    retrievemesh(Blender.link,Blender.Get('curframe'))

  except Exception as e: # мы ловим что-нибудь

    objects = gettargetobjects(Blender.link)

    for ob in objects:

     impress(Blender.link,ob)

    storemesh(Blender.link,Blender.Get('curframe'))

  Blender.link.makeDisplayList()

Теперь нам осталось фактическое вычисление отпечатка целевого объекта в нашем меше.


Вычисление отпечатка

Определение эффекта создания отпечатка от целевого объекта будем достигать следующим образом:

Для каждой вершины в меше, получающем отпечаток:

1. Определить, расположена ли она внутри целевого объекта, и если это так:

2. Установить позицию вершины в позицию ближайшей вершины на объекте, создающем отпечаток

Здесь есть несколько важных вопросов. Позиция вершины в меше сохранена относительно матрицы преобразования объекта. Другими словами, если мы хотим сравнить координаты вершин в двух разных мешах, мы должны преобразовать каждую вершину матрицами преобразования их соответствующих объектов перед выполнением любого сравнения.

Также, объект Blender.Mesh имеет метод pointInside(), который возвращает Истину, если данная точка находится внутри меша. Тем не менее, он будет работать только в надежно закрытых мешах, так что пользователь должен проверить, что объекты, которые создают отпечатки на самом деле закрыты. (Они могут иметь внутренние пузыри, но их поверхности не должны содержать рёбер, которые не примыкают в точности к 2 граням. Эти так называемые non-manifold рёбра можно выбрать в режиме выбора рёбер с помощью Select | Non Manifold в 3D-виде или нажав Ctrl + Shift + Alt + M.)

Наконец, перемещение вершин к ближайшей вершине на целевом объекте может быть совсем неточным, если целевой меш довольно грубый. Производительность разумная, тем не менее, было бы хорошо иметь для сравнения несколько точек, так как наш алгоритм - довольно неумелый, поскольку сначала определяет точку внутри меша, и затем отдельно вычисляет ближайшую вершину, дублируя множество вычислений. Тем не менее, так как производительность приемлема даже для мешей, состоящих из сотен точек, мы будем придерживаться нашего подхода, поскольку он сохраняет нашу программу простой и спасает нас от необходимости писать и тестировать очень сложный код.

Реализация начинается с функции, возвращающей расстояние до ближайшей вершины к данной точке pt и её координаты:

def closest(me,pt):

  min = None

  vm = None

  for v in me.verts:

    d=(v.co-pt).length

    if min == None or d

     min = d

     vm = v.co

  return min,vm

Функция impress() принимает исходный и целевой объект как аргументы и модифицирует меш-данные исходного объекта, если целевой меш делает отпечаток. Первая вещь, которую она делает - извлечение матриц преобразования объектов. Как указано ранее, они будут нужны для преобразования координат вершин, чтобы их можно было сравнивать. Мы также извлекаем обратную матрицу исходного объекта. Она будет нужна, чтобы преобразовать координаты в пространство исходного объекта.

Выделенная строка извлекает завёрнутые (wrapped) меш-данные исходного объекта. Нам нужны завёрнутые данные, поскольку нам может понадобиться изменить координаты некоторых вершин. Следующие две строки извлекают копии меш-данных. Нам нужны эти копии, чтобы преобразование, которое мы выполним, не повлияло на фактические меш-данные. Вместо копирования мы могли бы пропустить аргумент mesh=True, что должно было бы дать нам ссылку на объект Nmesh вместо объекта Mesh. Тем не менее, объекты Nmesh не завернуты и обозначены как устаревшие. Также, у них отсутствует метод pointInside(), который нам нужен, так что мы выбираем самостоятельное копирование мешей.

Затем, мы преобразуем эти копии мешей соответствующими их объектам матрицами преобразования. Использование метода этих мешей transform() спасает нас от цикла по каждой вершине и самостоятельного умножения координат вершин на матрицу преобразования, и этот метод, наверное, несколько быстрее, так как transform() полностью выполнен на языке C:

from copy import copy

def impress(source,target):

  srcmat=source.getMatrix()

  srcinv=source.getInverseMatrix()

  tgtmat=target.getMatrix()

  orgsrc=source.getData(mesh=True)

  mesrc=copy(source.getData(mesh=True))

  metgt=copy(target.getData(mesh=True))


  mesrc.transform(srcmat)

  metgt.transform(tgtmat)


  for v in mesrc.verts:

    if metgt.pointInside(v.co):

     d,pt = closest(metgt,v.co)

     orgsrc.verts[v.index].co=pt*srcinv

Последняя часть функции impress() - это цикл по всем вершинам в преобразованном исходном меше и проверка на нахождение вершины внутри (преобразованного) целевого меша. Если это так, функция определяет, какая вершина в целевом меше ближайшая, и устанавливает задетую вершину в оригинальном меше в эти координаты.

Этот оригинальный меш не преобразован, так что мы должны преобразовать эту ближайшую точку в пространство исходного объекта, умножая координаты на обратную матрицу преобразования. Поскольку вычисления преобразования являются дорогостоящими, модификация преобразованного меша и преобразование всего меша обратно в конечном счете может взять деликатное время. Содержание ссылки на не преобразованный меш и просто преобразование обратно отдельных точек может, следовательно, оказаться предпочтительным, если только сравнительно немного вершин попали в отпечаток. Полный скрипт доступен как ImpressScriptLink.py в файле scriptlinks.blend. Следующая иллюстрация показывает возможный результат. Здесь мы создали небольшую анимацию шара (icosphere), прокатив его по грязи (подразделенная плоскость) и погружая в неё.



При работе со скриптом важно иметь в виду, что когда отпечаток вычисляется, ни одна из вершин меша, который получает отпечаток, не должна быть расположена внутри цели раньше, чем она сдвинется. Если это случилось, то, возможно, вершина будет сметена вслед за перемещением цели, исказив исходный меш вдоль пути. Например, чтобы сделать иллюстрацию колесного следа в грязи, мы анимируем катящееся колесо вдоль пути, рассчитывая отпечатки, которые оно делает в каждом кадре. В первом кадре, который мы анимируем, мы должны убедиться, что колесо не касается плоскости земли, иначе она может быть искажена, поскольку, если вершина плоскости земли оказалась внутри колеса и близко ко внутреннему ободу, она будет перемещена на ближайшую вершину на этом ободе. Если колесо катится медленно, эта вершина останется близко к этому внутреннему ободу, и тем самым фактически будет клеиться к этому движущемуся внутреннему ободу, разрывая в процессе плоскость земли. Тот же разрушительный процесс может произойти, если целевой объект очень маленький по сравнению с исходным мешем или перемещается очень быстро. В этих обстоятельствах, вершина может проникнуть в целевой объект так быстро, что ближайшая вершина не будет находиться на ведущей поверхности, создающей отпечаток, но окажется где-нибудь в другом месте цели, и в результате вершины будут притянуты ко внешней стороне вместо проталкивания внутрь. В иллюстрации катящейся шины трактора, мы тщательно спозиционировали шину в первом кадре, чтобы она находилась строго справа от подразделенной плоскости перед ключевым кадром, начинающим катящееся движение влево. Показанное изображение взято из кадра 171 без какого-либо сглаживания или применённых материалов к плоскости.




Итог

В этой главе мы узнали как привязывать изменения к развитию кадров анимации и как обращаться к информации о состоянии объекта. Мы также видели как сменять слои, например, чтобы объект на рендере стал невидимым. В подробностях, мы увидели:

• Что такое скриптсвязи и обработчики пространства

• Как выполнять действия при каждом изменении кадров в анимации

• Как ассоциировать дополнительную информацию с объектом

• Как заставить объект появляться или исчезать изменением слоя или изменением прозрачности

• Как осуществить схему, соединяющую разные меши с объектом в каждом кадре

• Как расширить функциональность 3D-вида Далее: добавление ключей формы и кривых IPO.


Загрузка...