6 Ключи формы, кривые IPO, и Позы

Мы уже сталкивались с кривыми IPO в Главе 4, Pydrivers and Constraints, когда мы обсуждали Pydrivers, но с IPO можно делать гораздо больше, чем просто управлять одним IPO посредством другого. Например, API Блендера обеспечивает нас средствами для создания IPO из ничего, что позволяет определить движение, которое не легко воссоздать, устанавливая ключевые кадры вручную. Кроме того, некоторые типы кривых IPO имеют отчасти отличающееся поведение по сравнению с теми, с которыми мы до сих пор сталкивались. Ключи Формы и Позы - примеры (наборов) кривых IPO, которые отличаются от, например, IPO позиции. Мы столкнемся как с ключами формы, так и Позами позже в этой главе, но мы начнём с рассмотрения того, как мы можем определять IPO из ничего.

В этой главе мы изучим, как:

• определять кривые IPO

• определять ключи формы у меша

• определять IPO для этих ключей формы

• позировать арматуры

• группировать изменения поз в действия


Обидчивый субъект - определение IPO из ничего

Множество путей движения объектов трудно моделировать вручную, например, когда мы хотим, чтобы объект следовал в точности по математической кривой, или если мы хотим скоординировать перемещение многочисленных объектов по некоторому пути, что не легко выполнить копированием кривых IPO или определением Управляющих объектов IPO (drivers).

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


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

Скрипт orbit.py, который мы разработаем, будет предпринимать следующие шаги:

1. Определение точки середины пути между выбранными объектами.

2. Определение протяженности выбранных объектов.

3. Определение IPO для объекта один.

4. Определение IPO для объекта два.

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

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



Длина диагонали габаритного ящика может быть вычислена определением как максимального так и минимального значений для каждой из координат x, y, и z. Компоненты вектора, представляющего эту диагональ, являются разницами между этими максимумом и минимумом. Длина диагонали впоследствии получается взятием квадратного корня суммы квадратов компонент x, y, и z. Функция diagonal() довольно кратко реализована, так как она использует много встроенных функций Питона. Она принимает список векторов в виде аргумента, а затем проходит циклом по каждой из компонент (выделено. Компоненты x, y, и z объекта Vector в Блендере могут быть доступны по индексам 0, 1, и 2 соответственно):

def diagonal(bb):

  maxco=[]

  minco=[]

  for i in range(3):

    maxco.append(max(b[i] for b in bb))

    minco.append(min(b[i] for b in bb))

  return sqrt(sum((a-b)**2 for a,b in zip(maxco,minco)))

Она определяет пределы для каждой компоненты, используя встроенные функции max() и min(). В конце она возвращает длину, спаривая каждый минимум и максимум с помощью функции zip().

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

obs=Blender.Scene.GetCurrent().objects.selected

if len(obs)!=2:

  Draw.PupMenu('Please select 2 objects%t|Ok')

else:

  loc0 = obs[0].getLocation()

  loc1 = obs[1].getLocation()


  bb0 = obs[0].getBoundBox()

  bb1 = obs[1].getBoundBox()


  w = (diagonal(bb0)+diagonal(bb1))/4.0

Прежде, чем мы сможем вычислить траектории обоих объектов, мы сначала создадим два новых и пустых объекта кривых IPO:

  ipo0 = Ipo.New('Object','ObjectIpo0')

  ipo1 = Ipo.New('Object','ObjectIpo1')

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

  for i,icu in enumerate((Ipo.OB_LOCX,

              Ipo.OB_LOCY,Ipo.OB_LOCZ)):

    ipo0[icu]=(1,loc0[i])

    ipo0[icu][30]=loc1[i]


    ipo1[icu]=(1,loc1[i])

    ipo1[icu][30]=loc0[i]

    ipo0[icu].interpolation = 

IpoCurve.InterpTypes.BEZIER

    ipo1[icu].interpolation = 

IpoCurve.InterpTypes.BEZIER

Заметьте, что ключ позиции первого объекта в кадре 1 является текущей позицией, а ключ позиции в кадре 30 - позиция второго объекта. Для другого объекта всё с точностью до наоборот. Мы установили режимы интерполяции этих кривых на "Bezier", чтобы получить плавное движение. У нас теперь есть две кривые IPO, которые делают взаимообмен позиций двух объектов, но расчёт пока таков, что они будут двигаться сквозь друг друга.

Следовательно, нашим следующим шагом нужно добавить ключ в кадре 15 со скорректированной компонентой z. Ранее мы вычислили w, чтобы запомнить половину расстояния, на которое нужно сдвинуться каждому с пути другого. Здесь мы добавляем это расстояние к компоненте z в середине пути для первого объекта и вычитаем его для другого:

  mid_z = (loc0[2]+loc1[2])/2.0

  ipo0[Ipo.OB_LOCZ][15] = mid_z + w

  ipo1[Ipo.OB_LOCZ][15] = mid_z - w

Наконец, мы добавляем новые кривые IPO к нашим объектам:

  obs[0].setIpo(ipo0)

  obs[1].setIpo(ipo1)

Полный код доступен как swap2.py в файле orbit.blend. Результирующие пути двух объектов схематически отображены на следующем скриншоте:




Много проглотил - определение поз

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

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




(ПЕРИСТАЛЬТИКА (peristalsis) - волнообразные сокращения, распространяющиеся вдоль некоторых полых органов в теле человека. Эти сокращения возникают самопроизвольно и характерны для таких полых органов, которые снабжены круговыми и продольными мышцами (например, обычно они наблюдаются в кишечнике). Перистальтика усиливается благодаря растяжению стенок полого органа. Как только стенки органа растягиваются, происходит сокращение круговых мышц. Перед их растяжением происходит расслабление круговых мышц и сокращение продольных, благодаря чему содержимое органа (чаще всего, кишечника) продвигается в дистальном направлении. - с сайта http://vocabulary.ru— пожелание приятного аппетита от переводчика ☺)

Для того, чтобы синхронизировать масштабирование индивидуальных костей таким образом, чтобы оно следовало по цепочке от родителя к ребенку, мы должны отсортировать наши кости, поскольку атрибут bones объекта Pose, который мы получаем, вызвав getPose() в арматуре, является словарём. Цикл по ключам или значениям этого словаря будет возвращать эти величины в произвольном порядке.

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

В следующем коде, мы используем список имен bones, чтобы хранить имена костей Позы в правильном порядке. Мы выталкиваем (pop) кость из списка костей Позы pbones и добавляем её имя достаточно долго, пока она ещё не добавлена (выделено). (Я не cмог адекватно перевести это предложение, потому что здесь логика и программы и текста, как мне кажется, дают сбой, подробнее смотрите ниже — прим. пер.) Мы сравниваем имена вместо объектов костей Позы, поскольку текущая реализация костей Позы надежно не поддерживает оператора in:

def sort_by_parent(pbones):

  bones=[]

  if len(pbones)<1 : return bones

  bone = pbones.pop(0)

  while(not bone.name in bones):

    bones.append(bone.name)

Затем, мы получаем родителя кости, которую мы только что добавили к нашему списку, и настолько долго, насколько мы можем просматривать цепь родителей, мы включаем такого родителя (или, точнее, его имя) в наш список перед текущим элементом (выделено ниже). Если цепь не может следовать дальше, мы выталкиваем новую кость Позы. Когда больше нет костей, метод pop() вызовет исключение IndexError, и мы выходим из нашего цикла while:

    parent = bone.parent

    while(parent):

     if not parent.name in bones:

       bones.insert(bones.index(bone.name),

             parent.name)

     bone = parent

     parent = parent.parent

    try:

     bone = pbones.pop(0)

    except IndexError:

     break

  return bones


Чем дольше я пытался разобраться с логикой этой функции, чтобы адекватно перевести два предыдущих абзаца, тем сильнее мне это не нравилось, ибо логики я не наблюдал. Тогда я немного потестировал эту функцию в файле peristaltic.blend, и убедился, что она правильно работает не во всех случаях. Цепочка костей в файле по направлению от родительских к дочерним выглядит так: ['Bone', 'Bone.001', 'Bone.002', 'Bone.003', 'Bone.004', 'Bone.005']. Если на вход функции список pbones приходит в таком порядке: ["Bone.001", "Bone.002", "Bone.003", "Bone.004", "Bone.005", "Bone"], то результат получается таким, каким надо, но если на вход придёт, например, список ["Bone.002", "Bone.001", "Bone.003", "Bone.004", "Bone.005", "Bone"] (первые два элемента поменяны местами), то на выходе будет всего 3 кости: ['Bone', 'Bone.001', 'Bone.002']. Вот мой исправленный вариант функции:

def sort_by_parent(pbones):

   bones=[]

   while True:  # Бесконечный цикл гарантирует перебор 

         # всех костей из входного списка

     try:

      bone = pbones.pop(0)

     except IndexError:

      break # Единственное условие выхода из цикла

     if not bone.name in bones:

       bones.append(bone.name)

       parent = bone.parent

       while(parent):

        if not parent.name in bones:

          bones.insert(bones.index(bone.name),

                parent.name)

        bone = parent

        parent = parent.parent

   return bones

- Добавление переводчика.


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

scn = Blender.Scene.GetCurrent()

arm = scn.objects.active

if arm.getType()!='Armature':

  Blender.Draw.PupMenu("Selected object is not an " +

             "Armature%t|Ok")

else:

  adata = arm.getData()

Затем, мы делаем арматуру редактируемой и убеждаемся, что у каждой кости задана опция HINGE (выделено). Преобразование списка опций в множество (set) и обратно в список после добавления опций HINGE является способом удостовериться, что эта опция появится в списке только один раз.

  adata.makeEditable()

  for ebone in adata.bones.values():

    ebone.options = 

     list(set(ebone.options)|

       set([Blender.Armature.HINGE]))

  adata.update()

Поза связана с объектом арматуры, а не со своими данными, так что мы получаем её из объекта arm, используя метод getPose(). Позы кости очень похожи на обычные IPO, но они должны быть связаны с действием (action), которое группирует эти позы. При работе с Блендером интерактивно действие создаётся автоматически, как только мы вставим ключевой кадр в позу, но в скрипте мы должны явно создать действие, если оно ещё не присутствует (выделено):

  pose = arm.getPose()

  action = arm.getAction()

  if not action:

    action = Blender.Armature.NLA.NewAction()

    action.setActive(arm)

Следующим шагом нужно отсортировать кости Позы в порядке цепи от родительских к дочерним, используя нашу ранее определенную функцию. Всё, что осталось сделать, это двигаться по временной шкале через десять кадров за 1 шаг и задавать ключи для масштаба каждой кости на каждом шаге, увеличивая масштаб, если номер кости в последовательности соответствует нашему шагу и восстанавливая его, если нет. Одна из результирующих кривых IPO показана на скриншоте. Заметьте, что нашей предварительной установкой атрибута HINGE в каждой кости, мы предотвратили распространение масштабирования на детей кости:

  bones = sort_by_parent(pose.bones.values())


  for frame in range(1,161,10):

    index = int(frame/21)-1

    n = len(bones)

    for i,bone in enumerate(bones):

     if i == index :

       size = 1.3

     else :

       size = 1.0

     pose.bones[bone].size=Vector(size,size,size)

     pose.bones[bone].insertKey(arm,frame, 

              Blender.Object.Pose.SIZE)

Полный код доступен как peristaltic.py в файле peristaltic.blend.



Применение peristaltic.py к арматуре

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

1. Добавьте арматуру к сцене

2. Перейдите в режим редактирования, и выдавите любое число костей из конца первой кости.

3. Перейдите в объектный режим и добавьте меш, отцентрированный в позиции арматуры. Любой меш будет работать, но в нашей иллюстрации, мы используем цилиндр со множеством подразбиений.

4. Выберите меш, затем с Shift'ом выберите арматуру. Теперь как арматура, так и Меш-объект выбраны, но в то же время арматура является активным объектом.

5. Нажмите Ctrl + P и выберите armature. В появившемся после этого меню, выберите Create from bone heat. Это создаст группу вершин в меше для каждой кости в арматуре. Эти группы вершин будут использованы для деформации меша, когда мы ассоциируем арматуру с мешем в качестве модификатора.

6. Выберите меш и добавьте модификатор armature. Наберите имя арматуры в поле Ob: и убедитесь, что выбран переключатель Vert. Group, а Envelopes - нет.

7. Выберите арматуру и запустите peristaltic.py. В результате будет анимированный Меш-объект, имеющий сходство с прохождением куска через узкую гибкую трубу. Несколько кадров показаны на иллюстрации:



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




Get down с ритмом - синхронизация ключей формы со звуком

(Словосочетание Get Down имеет такое количество самых разнообразных значений, что я не рискнул выбирать из них и оставляю без перевода — прим. пер.)

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

Анимация, которую мы создадим, зависит главным образом от манипуляции ключами формы (shape keys). Ключи Формы можно представлять как искажения базового меша. Меш может иметь много таких искажений и каждому из них даётся определённое имя. Интересно то, что Блендер предоставляет нам возможность интерполяции между базовой формой и любой из искаженных форм непрерывным способом, позволено даже смешивать вклады от разных форм.

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

Анимирование установкой ключевых кадров в Блендере означает создание кривых IPO и манипуляция ими, как мы уже видели раньше. На самом деле, кривые IPO Shape или Key очень похожи на другие типы IPO и управляются практически так же. Основное различие между, например, IPO Объекта и IPO Формы - в том, что индивидуальные кривые IPO Формы проиндексированы не некоторой встроенной числовой константой (как например, Ipo.OB_LOCX для Объектов), а строкой, поскольку пользователь может определить любое количество именованных форм.

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


Манипуляция звуковыми файлами

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

Как упомянуто в предыдущем разделе, API Блендера не обеспечивает большого количества инструментов для работы со звуковыми файлами, в основном модуль Sound обеспечивает нас способом загрузки и воспроизведения звуковых файлов, но на этом и всё. Нет способа получить доступ к индивидуальным точкам волны, закодированным в файле.

К счастью, в стандартный дистрибутив Питона включен модуль wave, который обеспечивает нас средствами для чтения файлов в обыкновенном формате .wav. Хотя он поддерживает только несжатый формат, этого будет достаточно, так как этот формат является очень распространённым, и большинство инструментов работы со звуком, как например, Audacity, могут преобразовывать в этот формат. С этим модулем мы можем открыть .wav-файл, определить частоту сэмплов и длительность звукового клипа, и получить индивидуальные сэмплы. Как мы увидим в объяснении следующего кода, мы все еще должны преобразовывать эти сэмплы в величины, которые мы можем использовать как значения ключей для наших ключей формы, но тяжелую работу уже сделали для нас.


Схема кода: Sound.py

Вооружившись знаниями о том, как создавать кривые IPO и получать доступ к .wav-файлам, мы можем наметить следующую схему программы:

1. Определить, имеет ли активный объект пригодные заданные формы, и предложить выбрать их них.

2. Позволить пользователю выбрать .wav-файл.

3. Определить количество звуковых сэмплов в секунду в файле (частота дискретизации).

4. Вычислить количество необходимых кадров анимации, основываясь на длительности звукового файла и показателе количества видеокадров в секунду.

5. Затем, для каждого кадра анимации:

◦ Усреднить звуковые сэмплы, проходящие в этом кадре

◦ Установить величину смешивания выбранной кривой IPO этому среднему (нормализованному) числу

Полный код доступен как Sound.py в файле sound000.blend и объясняется следующим образом:

import Blender

from Blender import Scene,Window,Draw

from Blender.Scene import Render


import struct

import wave

Мы начинаем, импортируя необходимые модули, включая модуль Питона wave, чтобы иметь доступ к нашему .wav-файлу и модуль struct, который предоставляет функции для манипулирования двоичными данными, которые мы получим из .wav-файла.

Затем, мы определяем вспомогательную функцию, показывающую всплывающее меню в середине нашего экрана. Она ведёт себя просто подобно стандартной функции PupMenu() из модуля Draw, но устанавливает курсор в позицию середины экрана с помощью функций GetScreenSize() и SetMouseCoords() из модуля Блендера Window:

def popup(msg):

  (w,h)=Window.GetScreenSize()

  Window.SetMouseCoords(w/2,h/2)

  return Draw.PupMenu(msg)

Основная часть работы будет осуществляться функцией sound2active(). Она принимает два аргумента - имя .wav-файла, для использования и имя ключа формы для анимации, основанной на информации из .wav-файла. Сначала, мы пытаемся создавать объект WaveReader, вызывая функцию open() модуля wave (выделено). Если это не удаётся, мы показываем ошибку во всплывающем окне и выходим:

def sound2active(filename,shapekey='Pop out'):

  try:

    wr = wave.open(filename,'rb')

  except wave.Error,e:

    return popup(str(e)+'%t|Ok')

Затем мы делаем некоторые разумные проверки: мы сначала проверяем, является ли .wav-файл МОНО файлом. Если Вы хотите использовать стерео файл, преобразуйте его сначала в моно, например с помощью свободного пакета Audacity (http://audacity.sourceforge.net/). Затем мы проверяем, имеем ли мы дело с несжатым .wav-файлом, поскольку модуль wave не может работать с другими типами. (большинство .wav-файлов являются несжатыми, но если нужно, Audacity также может их преобразовать), и мы проверяем, что сэмплы 16-битовые. Если любая из этих проверок терпит неудачу, мы выводим соответствующее сообщение об ошибке:

  c = wr.getnchannels()

  if c!=1 : return popup('Only mono files are '+

              'supported%t|Ok')

  t = wr.getcomptype()

  w = wr.getsampwidth()

  if t!='NONE' or w!=2 : 

   return popup('Only 16-bit, uncompresses files '+

          'are supported%t|Ok')

Теперь, когда мы можем работать с файлом, мы получаем его частоту дискретизации (frame rate, количество аудио сэмплов в секунду) и общее число байт (как ни странно, используя неуклюже названную функцию getnframes() из модуля wave). Затем, мы считываем все эти байты и сохраняем их в переменной b.

  fr= wr.getframerate()

  n = wr.getnframes()


  b = wr.readframes(n)

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

  scn     = Scene.GetCurrent()

  context   = scn.getRenderingContext()

  seconds   = float(n)/fr

  sampleratio = fr/float(context.framesPerSec())

Как упомянуто раньше, модуль wave дает нам доступ ко множеству свойств .wav-файла и к сырым (raw) аудио сэмплам, но не предоставляет никаких функций для преобразования этих сырых сэмплов в удобные для использования целые величины. Следовательно, нам нужно сделать это самостоятельно. К счастью, это не так уж трудно, как это может показаться. Поскольку мы знаем, что 16-битовые аудио сэмплы представлены как 2-х байтовое целое в формате "меньший-в-конце" ("little-endian"), мы можем использовать функцию unpack() из модуля Питона struct, чтобы эффективно преобразовывать список байтов в список целых, передавая подходящую спецификацию формата. (Вы можете прочитать больше о .wav-файлах здесь https://ccrma.stanford.edu/courses/422/ проекты/WaveFormat/, на русском здесь: http://www.fpga-cpld.ru/wave.html, работа с модулем struct описана здесь: http://world-python.org/article/tutorialmodules/32-modul-struct.html — прим. пер.)

  samples  = struct.unpack('<%dh'%n,b)

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

Затем для каждого кадра мы вычисляем от стартового кадра до последнего кадра (исключительно) среднее значение аудио сэмплов, которые попадают на каждый видеокадр суммированием этих аудио сэмплов (находятся в списке samples) и деля на количество этих аудио сэмплов за видеокадр (выделено в следующем куске кода).

Мы задаём выбранный ключ формы в величину в дипазоне [0:1], так что мы должны нормализовать рассчитанные средние числа, определяя минимальную и максимальную величины, и вычислить масштаб:

  staframe = context.startFrame()

  endframe = int(staframe +

          seconds*context.framesPerSec())


  popout=[]

  for i in range(staframe,endframe):

    popout.append(sum(samples[int( 

    (i-1)*sampleratio):int(i*sampleratio)])/sampleratio)

  minvalue = min(popout)

  maxvalue = max(popout)

  scale = 1.0/(maxvalue-minvalue)

Наконец, мы получаем активный объект в текущей сцене и получаем его IPO Формы (выделено). Мы заканчиваем, устанавливая величину ключа формы для каждого кадра в рассматриваемом нами диапазоне в масштабированное среднее аудио сэмплов:

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


  ipo = ob.getData().getKey().getIpo()


  for i,frame in enumerate(range(staframe,endframe)):

    ipo[shapekey][frame]=(popout[i]-minvalue)*scale

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

if __name__ == "__main__":

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


  try:

    shapekeys = ob.getData().getKey(

          ).getIpo().curveConsts

    key = popup(('Select a shape key%t|'+

          '|').join(shapekeys))

    if key>0:

      Window.FileSelector (lambda f:sound2active(f,

       shapekeys[key-1]), 

      "Select a .wav file",

      Blender.Get('soundsdir'))

  except:

    popup('Not a mesh or no shapekeys defined%t|Ok')

Если мы смогли извлечь список ключей формы, мы предоставляем пользователю всплывающее меню для выбора из этого списка. Если пользователь выбирает один из пунктов, переменная key будет положительной и мы предоставляем пользователю диалог выбора файлов (выделено). В этот диалог передаётся lambda-функция, которая будет вызвана, если пользователь выберет файл, с передачей имени этого выбранного файла в качестве аргумента. В нашем случае мы создаём эту lambda-функцию, чтобы вызвать функцию sound2active(), определённую ранее с этим именем файла и выбранным ключом формы.

Начальный каталог, который будет представлен пользователю в выборе файлов, определяется последним аргументом в функции FileSelector(). Мы задали его параметром Блендера soundsdir. Это обычно // (то есть, относительный путь, указывающий на тот же каталог, где находится .blend-файл, с которым пользователь работает), но может быть установлен в окне Пользовательских настроек (секция File Paths) на нечто другое.


Анимация меша .wav-файлом: последовательность действий

Теперь, когда у нас есть наш скрипт Sounds.py, мы можем применить его следующим образом:

1. Выбрать Меш-объект

2. Добавить ключ формы "Basis" к нему (окно Кнопок, контекст редактирования, панель Shapes). Он будет соответствовать наименее искаженной форме меша.

3. Добавить второй ключ формы и дать ему значимое имя.

4. Отредактировать этот меш, чтобы он представлял наиболее искаженную форму.

5. В режиме объектов, запустить Sound.py из текстового редактора, нажимая Alt + P.

6. Выбрать имя ключа формы, определенное раньше (не "Basis"), из выпадающего меню.

7. Выбрать .wav-файл для выполнения.

Результатом будет объект с кривой IPO для выбранного ключа формы, который будет колебаться согласно ритму звука, как показано на следующем скриншоте:




Итог

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

• определять кривые IPO

• определять ключи формы на меше

• определять кривые IPO для этих ключей формы

• позировать арматуры

• группировать изменения поз в действия

В следующей главе мы должны узнать, как создавать заказные текстуры и шейдеры.


Загрузка...