9 Расширение вашего инструментария

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



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

• Построить список активов, например, карты изображений, и заархивировать их

• Публиковать отрендеренное изображение автоматически через FTP

• Расширить функциональность встроенного редактора поиском с регулярными выражениями

• Ускорить вычисления, используя Psyco - компилятор-на-лету

• Добавить управление версиями к вашим скриптам с помощью Subversion


В Сеть и дальше - публикация готового рендера на FTP

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

File Transfer Protocol (FTP) (Протокол передачи файлов), который мы будем использовать, несколько сложнее, чем, например, протокол HTTP, так как он использует больше одной связи. К счастью для нас, все сложности FTP-клиента хорошо изолированы в стандартном модуле Питона ftplib. Мы не только импортируем класс FTP этого модуля, но также множество других стандартных модулей Питона, особенно для обработки путей файлов (os.path) и для чтения файлов стандарта .netrc (который позволит нам сохранять пароли за пределами нашего скрипта, если нам нужны пароли для регистрации на FTP-сервере). Мы обсудим каждый модуль, когда понадобится.

from ftplib import FTP

import os.path

import re

import netrc

import tempfile

from Blender import Image,Registry,Draw

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

Следовательно, мы определяем небольшую функцию-утилиту, которая проверяет наличие переменной HOME в окружении (она всегда есть на Unix-подобных операционных системах, и на некоторых версиях Windows). Если таковой нет, она проверяет наличие переменной USERPROFILE (присутствует в большинстве версий Windows, включая XP, где она обычно указывает на каталог C:\Documents и Settings\<имя пользователя>). Если она присутствует, функция устанавливает переменную HOME в значение, содержащееся в этой переменной USERPROFILE:

def sethome():

  from os import environ

  if not 'HOME' in environ:

    if 'USERPROFILE'in environ:

     environ['HOME'] = environ['USERPROFILE']

Наша следующая задача в том, чтобы выяснить, на какой FTP-сервер пользователь хочет загрузить результат рендера. Мы запоминаем это в ключе реестра Блендера, чтобы не надоедать пользователю с приглашением всякий раз, когда он хочет отправить свой рендер. Функция getftphost() принимает аргумент reuse (повторное использование), который может быть использован для очистки этого ключа, если он установлен в False (для обеспечения возможности выбора другого FTP-сервера), но переписать интерфейс пользователя, чтобы предлагать ему такую возможность, мы оставляем в качестве упражнения читателю.

Фактический код начинает с поиска ключа реестра (с диска, если необходимо, следовательно, по умолчанию аргумент True, выделено). Если там нет ключа, или он не содержит имя сервера, мы запрашиваем у пользователя имя FTP-сервера посредством всплывающего окна. Если пользователь его не вводит, мы заканчиваем, возбуждая исключение. В противном случае, мы сохраняем имя хоста - сначала создаём словарь, если он ещё не существует, и сохраняем этот словарь в реестр Блендера. Наконец, мы возвращаем сохранённое имя хоста.

def getftphost(reuse=True):

  dictname = 'ftp'

  if reuse == False:

    Registry.RemoveKey(dictname)


  d = Registry.GetKey(dictname,True)

  if d == None or not 'host' in d:

    host = Draw.PupStrInput("Ftp hostname:", "", 45)

    if host == None or len(host) == 0 :

     raise Exception("no hostname specified")

    if d == None :

     d ={}

    d['host'] = host

    Registry.SetKey(dictname,d,True)

  return d['host']

Нам нужна другая вспомогательная функция, чтобы убедиться, что на диск в качестве изображения Блендера сохранено последнее отрендеренное изображение, которое присутствует как изображение с именем Render Result, но это изображение не пишется на диск автоматически. Функция imagefilename() принимает изображение Блендера как аргумент, и, во-первых, проверяет, существует ли корректное имя файла, связанное с ним (выделено). Если нет, она создает имя файла из имени изображения, добавляя расширение .tga (изображения можно сохранять только как файлы TARGA). Затем создаётся полный путь из этого имени файла и пути к временному каталогу. Теперь, когда у нас есть корректное имя файла, мы его сохраняем, вызывая метод save(), и возвращая имя файла:

def imagefilename(im):

  filename = im.getFilename()

  if filename == None or len(filename) == 0:

    filename = im.getName()+'.tga'

    filename = os.path.join(tempfile.gettempdir(),

                filename)

    im.setFilename(filename)

  im.save()

  return filename

Когда мы загружаем файл на FTP-сервер, мы хотим убедиться, что мы не перезапишем существующий файл. Если мы обнаружим, что файл с данным именем уже существует, мы хотели бы иметь функцию, которая создаёт новое имя файла предсказуемым способом, похожим на то, как ведёт себя Блендер при создании имён для объектов Блендера. Мы хотели бы сохранять расширение файла, так что мы не можем просто прилепить к имени цифровой суффикс. Функция nextfile(), следовательно, сначала разделяет имя пути и часть с расширением. Она использует функции split() и splitext() из модуля os.path, чтобы оставить нам чистое имя.

Если имя уже заканчивается на суффикс, состоящий из точки и некоторого числа (например, .42), мы хотели бы увеличить это число. Это именно то, что выполняет довольно пугающая выделенная строка. Функция sub() модуля Питона re принимает регулярное выражение как первый аргумент (мы используем здесь сырую строку, так что нам не надо экранировать обратную косую черту), и проверяет, соответствует ли это регулярное выражение своему третьему аргументу (name, в данном случае). Регулярное выражение, используемое здесь, (\.(\d+)$) совпадает с точкой, за которой следуют одна или более десятичных цифр, но только, если эти цифры являются последними символами. Если есть соответствие образцу, он заменяется вторым аргументом функции sub(). В нашем случае, замена - это не простая строка, а лямбда-функция (то есть, безымянная), в которую мы передаём объект сопоставления, и ожидаем, что она вернёт строку.

Мы окружили часть цифр нашего регулярного выражения круглыми скобками, теперь мы можем просто извлечь эти цифры (без первоначальной точки), вызвав метод group() объекта сопоставления. Мы передаем ему 1 в качестве аргумента, так как первые открывающие скобки обозначают первую группу (группа 0 является всем образцом целиком). Мы преобразуем эту строку цифр в целое, используя встроенную функцию int(), добавляем к ней 1, и преобразуем её обратно в строку с функцией str(). До того, как этот результат автоматически будет возвращён из лямбда-функции, мы снова добавляем точку, чтобы соответствовать нашему желаемому образцу.

Мы завершаем проверкой, отличается ли результирующее имя от оригинального. Если они совпадают, значит оригинальное имя не соответствовало нашему образцу, и мы просто добавляем .1 к имени. Наконец, мы восстанавливаем полное имя файла, добавляя расширение, и вызывая функцию join() из модуля os.path, чтобы добавить путь платформо-независимым способом:

def nextfile(filename):

  (path,base) = os.path.split(filename)

  (name,ext) = os.path.splitext(base)

  new = re.sub(r'\.(\d+)$',

         lambda m:'.'+ str(1+int(m.group(1))),

         name)

  if new == name :

    new = name + '.1'

  return os.path.join(path,new+ext)

Теперь мы полностью готовы заняться реальной работой загрузки файла на FTP-сервер. Сначала мы удостоверимся, что наше окружение имеет переменную HOME, вызывая функцию sethome(). Затем, мы извлекаем имя хоста FTP-сервера, на который мы хотим загрузить (вполне законно, между прочим, ввести IP-адрес вместо имени хоста):

if __name__ == "__main__":

  sethome()

  host = getftphost()

Далее, мы извлекаем данные учётной записи пользователя для выбранного хоста из файла .netrc, если он присутствует (выделено). Это может закончиться неудачей по различным причинам (могло не быть .netrc-файла, или данные хоста отсутствуют в файле); в этом случае будет возбуждено исключение. Если это случится, мы сообщаем об этом пользователю и требуем имя пользователя и пароль с помощью всплывающего окна:

  try:

    (user,acct,password) = \

        netrc.netrc().authenticators(host)

  except:

    acct=None

    user = Draw.PupStrInput(

       'No .netrc file found, enter username:',

       "",75)

    password = Draw.PupStrInput('Enter password:',"",75)

Отрендеренное изображение было сохранено как объект Блендера Image с именем Render Result. Следующая вещь, которую мы делаем - извлекаем ссылку на это изображение и убеждаемся, что оно сохранено на диск. Функция imagefilename(), которую мы определили раньше, возвращает имя файла загруженного изображения.

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

  im = Image.Get('Render Result')

  filename = imagefilename(im)


  ftp = FTP(host,user,password,acct)

  files = ftp.nlst()

Хм, автор так аккуратно обрабатывает ситуации отсутствия файла .netrc, имени с паролем в нём, сохранённости рендеренного изображения, а о работоспособности FTP-сервера вообще не упоминает. По-моему, ситуацию отсутствия связи, а также неверности логина или пароля тоже необходимо обрабатывать через try/except. - прим. пер.

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

  dstfilename = os.path.basename(filename)

  while dstfilename in files:

    dstfilename = nextfile(dstfilename)

Затем, мы выгружаем наш файл изображения, вызывая метод storbinary(). Этот метод принимает имя целевого файла с префиксом STOR, как первый аргумент, и открытый файловый дескриптор как второй аргумент. Мы предоставляем последний, вызывая встроенную функцию Питона open() с именем нашего файла изображения в качестве единственного аргумента. (Если нужна дополнительная информация о довольно диковинном поведении модуля ftplib, ссылка на его документацию: http://docs.python.org/library/ftplib.html.) Мы грациозно заканчиваем связь в FTP-сервером, вызывая метод quit(), и сообщаем пользователю о завершении задачи, показывая сообщение с упоминанием имени целевого файла, так как оно может отличаться от ожидаемого, если существует файл с аналогичным именем:

  ftp.storbinary('STOR '+dstfilename,open(filename))


  ftp.quit()


  Draw.PupMenu('Render result stored as "%s"%s|Ok'

         %(dstfilename,'%t'))

Полный код доступен как ftp.py в файле ftp.blend. Его можно запустить из текстового редактора, но в общем случае, несомненно, значительно удобнее поместить ftp.py в каталог скриптов Блендера. Скрипт сконфигурирован так, чтобы он был доступен в меню Файл | Экспорт (File | Export).


Весенняя уборка - архивация неиспользуемых изображений

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

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

Функции работы с файлами предоставляются модулями Питона os и os.path, а ZIP-файлами, которые могут использоваться как в Windows так и на открытых платформах, можно манипулировать с помощью модуля zipfile. ZIP-файл, в который мы перемещаем неиспользованные файлы, мы назовём Attic.zip:

import Blender

from os import walk,remove,rmdir,removedirs

import os.path

from zipfile import ZipFile

zipname = 'Attic.zip'

Первой задачей будет сгенерировать список всех файлов в каталоге, где находится наш .blend-файл. Функция listfiles() использует функцию walk() из модуля Питона os, чтобы рекурсивно обойти дерево каталогов и построить список файлов при обходе.

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

Строка, содержащая оператор yield, возвращает как результат один файл за один раз, так что наша функция может быть использована как итератор. (Для дополнительной информации об итераторах, смотрите online-документацию по адресу http://docs.python.org/reference/simple_stmts.html#yield) Мы соединяем соответствующее имя файла и путь, чтобы сформировать полное имя, и нормализуем его (то есть, удаляем двойные разделители пути и тому подобное); хотя нормализация здесь не строго необходима, поскольку walk() должна возвращать любые пути в нормализованной форме:

def listfiles(dir):

  for root,dirs,files in walk(dir):

    for file in files:

     if not file.startswith('.'):

       yield os.path.normpath(

         os.path.join(root, file))

    for d in dirs:

     if d.startswith('.'):

       dirs.remove(d)

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

def run():

  Blender.UnpackAll(Blender.UnpackModes.USE_ORIGINAL)

Функция GetPaths() из модуля Blender выдаёт список всех файлов, используемых .blend-файлом (за исключением самого этого .blend-файла). Мы передаем ей аргумент absolute установленным в Истину, чтобы извлекать имена файлов с полным путём вместо относительных путей от текущего каталога для того, чтобы сравнить их должным образом со списком, произведённым функцией listfiles().

Снова мы также нормализуем эти имена файлов. Выделенная строка показывает, как мы извлекаем абсолютный путь текущего каталога, передавая условное обозначение для текущего каталога Блендера ( // ) в функцию expandpath():

  files = [os.path.normpath(f) for f in 

       Blender.GetPaths(absolute=True)]

  currentdir = Blender.sys.expandpath('//')

Затем мы создаём объект ZipFile в режиме write (записи). Это отбросит любой существующий архив с тем же именем, и позволит нам добавлять файлы в архив. Полное имя архива строится соединением текущего каталога Блендера и имени, которое мы хотим использовать для архива. Использование функции join() из модуля os.path обеспечивает нам создание полного имени платформо-независимым образом. Мы установили аргумент debug (отладка) объекта ZipFile в значение 3, чтобы сообщать о чём-либо необычном на консоль при создании архива:

  zip = ZipFile(os.path.join(currentdir,zipname),'w')

  zip.debug = 3

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

Архив создаётся проходом цикла по списку всех файлов в текущем каталоге Блендера и сравниванием их со списком файлов, использованных нашим .blend-файлом. Любой файл с таким расширением, как например, .blend или .blend1 пропускается (выделено), как и сам архив. Файлы добавляются к ZIP-файлу использованием метода write(), который принимает в качестве параметра имя файла с путём относительно архива (и, следовательно, текущего каталога). Этот путь удобнее для распаковки архива в новом месте. Любые ссылки на файлы за пределами текущего дерева каталогов не затрагиваются функцией relpath(). Любой файл, который мы добавляем к архиву, помечается для удаления добавлением его к списку removefiles. Наконец, мы закрываем архив - важный шаг, поскольку, если его опустить, мы можем остаться с запорченным архивом:

  removefiles = []

  for f in listfiles(currentdir):

    if not (f in files 

       or os.path.splitext(f)[1].startswith('.blend') 

       or os.path.basename(f) == zipname):

     rf = os.path.relpath(f,currentdir)

     zip.write(rf)

     removefiles.append(f)


  zip.close()

Последней задачей будет удаление файлов, которые мы переместили в архив. Функция remove() из модуля Питона os выполнит это, но мы также хотим удалить любой каталог, который остался пустым после удаления файлов. Следовательно, для каждого файла, который мы удаляем, нам надо определить имя его каталога. Мы также удостоверяемся, этот каталог не указывает на текущий каталог, потому что мы хотим быть абсолютно уверены, что мы не удаляем его, так как это место, где находятся наши .blend-файлы. Хотя это маловероятный сценарий, что можно открыть .blend-файл в Блендере и удалить сам этот .blend файл, что могло бы оставить каталог пустым. Если мы удалим этот каталог, любое последующее (авто) сохранение должно потерпеть неудачу. Функция relpath() возвращает точку, если каталог, переданный как первый аргумент, указывает на тот же каталог, что и каталог, переданный как второй аргумент. (Функция samefile() является более надежной и прямой, но не доступна в Windows.)

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

  for f in removefiles:

    remove(f)

    d = os.path.dirname(f)

    if os.path.relpath(d,currentdir) != '.':

     try:

       removedirs(d)

     except OSError:

       pass


if __name__ == '__main__':

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


Расширение редактора - поиск с регулярными выражениями

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

Регулярные выражения очень мощны и множество программистов любят их универсальность (и множество других ненавидят их ужасную неудобочитаемость). Любите Вы или ненавидите их, они очень выразительные: сопоставление любого десятичного числа можно просто выразить как, например, \d+ (одна или более цифр). Если Вы ищете слово, которое пишется по буквам по-разному в Британском или Американском вариантах английского, как например, colour/color, Вы можете делать сопоставление с любым из них с помощью выражения colou?r (color с необязательным u).

Следующий код покажет, что встроенный редактор Блендера может быть оснащён этим полезным средством поиска просто с помощью нескольких строк кода. Представленный скрипт должен быть установлен в каталоге скриптов Блендера, и его можно будет затем вызывать из меню текстового редактора как Text | Text Plugins | Regular Expression Search, или комбинацией горячих клавиш Alt + Ctrl + R. При этом появится небольшое всплывающее поле ввода, где пользователь может ввести регулярное выражение (там будет запомнено последнее введенное регулярное выражение), и если пользователь щелкнет по кнопке OK или нажмёт Enter, курсор будет установлен в первом из мест, которые соответствуют регулярному выражению, с выделением сопоставленного выражения.



Чтобы зарегистрировать скрипт в качестве текстового плугина с назначенной горячей клавишей, первые строки скрипта состоят из привычных заголовков, дополненных пунктом Shortcut: (выделено ниже):

#!BPY

"""

Name: 'Regular Expression Search'

Blender: 249

Group: 'TextPlugin'

Shortcut: 'Ctrl+Alt+R'

Tooltip: 'Find text matching a regular expression'

"""

Следующим шагом нужно импортировать необходимые модули. Питон предоставляет нам стандартный модуль re, который хорошо документирован (онлайн документации достаточно даже для пользователей-новичков, незнакомых с регулярными выражениями. По-русски почитать можно, например, здесь: http://www.intuit.ru/department/pl/python/6/4.html — прим. пер.), и мы импортируем модуль Блендера bpy. В этой книге мы не часто используем этот модуль, так как он помечен, как экспериментальный, но в этом случае мы нуждаемся в нём, чтобы узнать, какой текстовый буфер является активным:

from Blender import Draw,Text,Registry

import bpy

import re

Для того, чтобы сигнализировать о любых ошибках, как например, ошибочное регулярное выражение, или о том, что не нашлось ни одного сопоставления, мы определяем простую функцию popup():

def popup(msg):

  Draw.PupMenu(msg+'%t|Ok')

  return

Поскольку мы хотим помнить последнее регулярное выражение, которое ввёл пользователь, мы используем реестр Блендера и, следовательно, мы определяем ключ для использования:

keyname = 'regex'

Функция run() связывает всю функциональность вместе; она извлекает активный текстовый буфер и завершается, если его не нашлось:

def run():


  txt = bpy.data.texts.active

  if not txt: return

Далее, она извлекает позицию курсора внутри этого буфера:

  row,col = txt.getCursorPos()

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

  d=Registry.GetKey(keyname)

  try:

    default = d['regex']

  except:

    default = ''

  pattern = Draw.PupStrInput('Regex: ',default,40)

  if pattern == None or len(pattern) == 0 : return

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

  try:

    po = re.compile(pattern)

  except:

    popup('Illegal expression')

    return

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

  first = True

  for string in txt.asLines(row):

    if first :

     string = string[col:]

    mo = re.search(po,string)

Если есть сопоставление, мы отмечаем его начало в пределах строки и его длину (должным образом исправленную, если это строка первая) и устанавливаем позицию курсора на текущую строку и в начало сопоставления (выделено). Мы также устанавливаем "позицию выделения" в позицию сопоставления плюс длина сопоставления, таким образом наше сопоставление будет выделено, и затем делаем возврат. Если нет сопоставления в пределах строки, мы увеличиваем индекс строки row и продолжаем цикл.

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

    if mo != None :

     i = mo.start()

     l = mo.end()-i

     if first :

       i += col

     txt.setCursorPos(row,i)

     txt.setSelectPos(row,i+l)

     break

    row += 1

    first = False


  else :

    popup('No match')

  Registry.SetKey(keyname,{'regex':pattern})

if __name__ == '__main__':

  run()

Полный код доступен как regex.py в файле regex.blend, но может быть размещён в каталоге скриптов Блендер с подходящим именем, как например, textplugin_regex.py.


Расширение редактора - взаимодействие с Subversion

При активной разработке скриптов может оказаться сложно следить за изменениями или возвращаться к предыдущим версиям. Это не уникально для написания скриптов Питона в Блендере, поэтому системы управления версиями развиваются уже много лет. Одна из хорошо известных, и широко используемых - это Subversion (http://subversion.tigris.org). В этом разделе мы показываем, как может быть дополнен редактор, чтобы отправлять или обновлять текстовые файлы из хранилища.

Взаимодействие с хранилищем Subversion не предусмотрено встроенными модулями Питона, так что мы должны получить эту библиотеку где-нибудь еще. Секция загрузок сайта http://pysvn.tigris.org содержит и исходные коды и бинарные дистрибутивы для многих платформ. Не забудьте получить правильную версию, так как поддерживаемая версия Subversion и версия Питона могут отличаться. Скрипт, который мы разрабатывали здесь, протестирован на Subversion 1.6.x и Питоне 2.6.x, но должен также работать с более ранними версиями Subversion.

Мы осуществим функциональность отправления (commit) текстового файла в хранилище и обновления (update) файла (то есть, получение самой последней исправленной версии из хранилища). Если мы пытаемся отправить файл, который пока не является частью хранилища, мы добавляем его, но мы не будем разрабатывать инструменты для создания хранилища или проверки рабочей копии. Такие инструменты, как, например, TortoiseSVN в Windows (http://tortoisesvn.tigris.org/) или множество инструментов для открытых платформ значительно лучше это делают. Мы просто принимаем подтверждённый (checked-out) рабочий каталог, где мы храним наши текстовые файлы Блендера. (Этот рабочий каталог может отличаться от вашего каталога проекта Блендера.)


Отправка (commit) файла в хранилище

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

Так же как и расширение, позволившее нам производить поиск с помощью регулярных выражений, этот скрипт начинается с подходящего заголовка, чтобы идентифицировать его как плугин текстового редактора, и чтобы назначить клавиатурное сокращение. Мы определяем мнемосхему Ctrl + Alt + C для отправки (выделено), так же как мы определим Ctrl + Alt + U для обновления в своем сопровождающем скрипте. Мы также импортируем необходимые модули, особенно модуль pysvn:

#!BPY

"""

Name: 'SVNCommit'

Blender: 249

Group: 'TextPlugin'

Shortcut: 'Ctrl+Alt+C'

Tooltip: 'Commit current textbuffer to svn'

"""

from Blender import Draw,Text,Registry

import bpy

import pysvn

def popup(msg):

  Draw.PupMenu(msg+'%t|Ok')

  return

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

def run():


  txt = bpy.data.texts.active

  if not txt: return


  fn = txt.getFilename()

  if fn == None or len(fn) == 0:

    popup('No filename defined: save it first')

    return

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

  svn = pysvn.Client()

  info = svn.info(fn)

  if info == None:

    popup('not yet added to repository, '+ \

       'will do that now')

    svn.add(fn)

Затем, мы сводим текущее содержимое текстового буфера, соединяя все строки в нём в единственный блок данных, и записываем его в файловый объект, который мы открыли для файла, связанного с буфером:

  file=open(fn,'wb')

  file.write('\n'.join(txt.asLines()))

  file.close()

Этот файл будет отправлен в хранилище с помощью метода checkin(), которому мы передаем довольно неинформативное сообщение отправки. Было бы хорошей идеей предложить пользователю создать более заметное сообщение. Наконец, мы сообщаем пользователю результат отправки.


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

  version = svn.checkin(fn,'Blender commit')

  popup('updated to rev. '+str(version))


if __name__ == '__main__':

  run()

Полный код доступен как textplugin_commit в файле svn.blend, но должен быть установлен в каталоге скриптов Блендера.


Обновление (updating) файла из хранилища

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

Кроме проверки, сохранен ли текстовый буфер, и добавлен ли уже файл к хранилищу, мы должны также проверить, является ли наша текущая версия - новой или измененной по сравнению с версией в хранилище. Если это так, мы предлагаем пользователю выбор: отвергнуть эти изменения и вернуться к версии в хранилище, или подтвердить и отправить версию, находящуюся в текстовом буфере. (Третий вариант, объединение различий, у нас не предусмотрен; хотя Subversion, несомненно, способно сделать это, по крайней мере для текстовых файлов, но лучше это предоставить более универсальным инструментам, таким как TortoiseSVN.)

Первая часть скрипта очень похожа на скрипт отправки. Основное различие - это другое клавиатурное сокращение:

#!BPY

"""

Name: 'SVNUpdate'

Blender: 249

Group: 'TextPlugin'

Shortcut: 'Ctrl+Alt+U'

Tooltip: 'Update current textbuffer from svn'

"""

from Blender import Draw,Text,Registry

import bpy

import re

import pysvn

def popup(msg):

  Draw.PupMenu(msg+'%t|Ok')

  return

Функция run() тоже начинается почти также, она извлекает активный текстовый буфер (если он есть) и проверяет, имеет ли текстовый буфер связанное имя файла (выделено). Она также проверяет, было ли имя файла уже добавлено к хранилищу, и если нет, исправляет это, вызывая метод add(), и сообщает об этом пользователю посредством всплывающего окна:

def run():


  txt = bpy.data.texts.active

  if not txt: return


  fn = txt.getFilename()

  if fn == None or len(fn) == 0:

    popup('No filename defined: save it first')

    return

  svn = pysvn.Client()

  info = svn.info(fn)

  if info == None:

    popup('not yet added to repository, '+ \

       'will do that now')

    svn.add(fn)

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

  file=open(fn,'wb')

  file.write('\n'.join(txt.asLines()))

  file.close()


  if svn.status(fn)[0].text_status == 

pysvn.wc_status_kind.modified:

    c=Draw.PupMenu('file probably newer than '+ \

     'version in repository%t|Commit|Discard changes')

    if c==1:

     svn.checkin(fn,'Blender')

     return

    elif c==2:

     svn.revert(fn)

После извлечения версии из хранилища мы обновляем содержание нашего текстового буфера:

  txt.clear()

  file=open(fn)

  txt.write(file.read())

  file.close()

Наконец, мы сообщаем пользователю с помощью всплывающего окна, какой номер версии содержится в текстовом буфере, снова вызывая метод status() и получая значение поля commit_revision:

  popup('updated to rev. ' 

      +str(svn.status(fn)[0].entry.commit_revision))


if __name__ == '__main__':

  run()

Полный код доступен как textplugin_svnupdate в файле svn.blend, и, подобно сопряженному с ним скриптом для отправки, он должен быть размещён в каталоге скриптов Блендера.


Работа с хранилищем

Хотя полный урок по работе с Subversion выходит за рамки этой книги, вероятно, будет полезным набросать схему рабочего процесса для проекта Блендера, в котором скриптовые компоненты пишутся через систему контроля версий.

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

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

1. Подтвердить (Check out) хранилище скриптов внутри нашего каталога проекта Блендера (это называется рабочая копия хранилища).

2. Создать скрипт в нашем .blend файле во встроенном редакторе.

3. Сохранить этот скрипт в рабочую копию.

4. Каждый раз, когда мы что-то изменяем, мы нажимаем Ctrl + Alt + C, чтобы отправить наши изменения.

5. Каждый раз, когда мы начинаем работать с нашим скриптом снова, мы нажимаем сначала Ctrl + Alt + U, чтобы сразу увидеть, не изменил ли кто-нибудь еще что-нибудь.

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


The need for speed (жажда скорости) — использование Psyco

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

Тем не менее, существуют ситуации, где любое увеличение скорости весьма приветствуется. Из всех примеров, которые мы видели в этой книге, Pynodes, вероятно, наиболее интенсивные в вычислительном отношении, так как инструкции выполняются над каждым видимым пикселем в текстуре или шейдере, и часто даже много больше времени уходит на пиксель, если принять во внимание oversampling. Экономия нескольких миллисекунд от скрипта, который тратит меньше секунды на выполнение, не даст слишком многого, но экономия 20% времени рендера составит существенную экономию времени при рендере 500 кадров.

Ввод Psyco: Psyco - расширение Питона, которое пытается ускорять выполнение скрипта, компилируя часто используемые части скрипта в машинные инструкции, и сохраняя их для многократного использования. Этот процесс часто называется компиляция-на-лету (just-in-time compilation, JIT), и она родственна JIT-компиляторам на других языках, таких как Java. (Они аналогичны по концепции, но совершенно отличаются в реализации из-за того, что в Питоне динамическая типизация. Это никак не затрагивает разработчиков скриптов на Питоне.) Важно то, что Psyco может быть использовано в любом скрипте без каких-либо изменений в коде, за исключением добавления нескольких строк.

Psyco доступен как бинарный пакет для Windows, и может быть скомпилирован из исходных кодов на других платформах. Полные инструкции доступны на вебсайте Psyco: http://psyco.sourceforge.net/.

Проверьте, что Вы устанавливаете версию, которая подходит вашей версии Питона, поскольку, хотя сайт указывает, что версия, скомпилированная для Питона 2.5 должна работать также для 2.6, она все еще может потерпеть неудачу, поэтому лучше использовать версию, специально скомпилированную для 2.6.

На сайте http://psyco.sourceforge.net/ я не смог найти бинарной версии Psyco для Питона 2.6 под Windows (в Линуксе-то его легко самостоятельно скомпилировать). Но Гугл помог — такая версия обнаружилась здесь: http://www.voidspace.org.uk/python/modules.shtml#psyco. Вполне рабочая. Ввиду фактического умирания проекта Psyco (в википедии написано, что его преемником является PyPy – странная штука «Питон-на-Питоне», в данный момент не слишком работоспособная), сомневаюсь, что он когда-нибудь заработает для 3-го Питона, и, соответственно, для новых версий Блендера. - прим. пер.

Итак, какое увеличение скорости мы могли бы ожидать? Это трудно оценить, но достаточно легко измерить! Просто рендерите кадр и отмечайте время, которое потребовалось, затем импортируйте psyco в ваш код, рендерите снова и отмечайте различие. Если оно значимое, оставляйте в коде, в противном случае, Вы можете снова его удалить.

На следующей таблице указаны некоторые результаты для тестовой сцены, приведенной в psyco.blend, но ваши данные могут отличаться. Также заметьте, что тестовая сцена является довольно оптимистическим сценарием, так как большая часть оказалась покрыта текстурой, генерируемой Pynode. Если бы её было меньше, прирост в скорости бы уменьшился, но это дает оценку того, что возможно с Psyco. Показатель в два раза для важного кода легко достижим. В следующей таблице перечислены некоторые иллюстрирующие примеры времени расчёта:

Время в секундах

Нетбук

Без Psyco 52.7

С Psyco 26.3


Стационарный компьютер

Без Psyco 14.01

С Psyco 6.98


Включение Psyco

Следующий код показывает дополнительные строки, которые нужны для включения psyco в нашем ранее встречавшемся Pynode raindrops (капли дождя). Изменения указаны жирным шрифтом.

<... все остальные строки остаются прежними ...>

__node__ = Raindrops

try:

  import psyco

  psyco.bind(Raindrops.__call__)

  print 'Psyco configured'

except ImportError:

  print 'Psycho not configured, continuing'

  pass  

Так что, по сути, было добавлено только несколько строк после определения Pynode. Убедитесь, что вы щелкнули на кнопке Update (обновить) на Pynode, иначе код не будет перекомпилирован, и изменения не будут видны.

Предшествующий код просто пытается импортировать модуль psyco. Если это терпит неудачу (по любой причине), в консоли выводится информационное сообщение, но, тем не менее, код будет работать правильно. Если он импортируется, мы указываем Psyco оптимизировать метод __call__(), вызывая функцию bind() со ссылкой на этот метод __call__ в качестве аргумента, и сообщаем пользователю в консоли, что мы успешно сконфигурировали Psyco.


Итог

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

• Как построить список активов, таких как карты изображений, и заархивировать их

• Как опубликовать отрендеренные изображения автоматически через FTP

• Как расширить функциональность встроенного редактора поиском с регулярными выражениями

• Как ускорить вычисления, используя Psyco - компилятор-на-лету

• Как добавить управление версиями к вашим скриптам с помощью Subversion

Загрузка...