8 Рендеринг (визуализация) и обработка изображений

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

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

• Автоматизировать процесс рендера

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

• Создавать билборды из сложных объектов

• Манипулировать изображениями, в том числе результатами рендера, используя библиотеку обработки изображений Python Imaging Library (PIL)

• Построить сервер, который создает изображения по-требованию, которые могут быть использованы как вопросы в CAPTCHA


Различные виды - комбинирование множества направлений камеры

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

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



Блендер является отличным средством, которое предоставляет Вам возможность не только моделировать, анимировать и настраивать рендер, но имеет также функциональность, необходимую для композиции. Одна из областей, которая не слишком выделяется, это "манипуляция изображениями". Блендер, конечно же, имеет окно редактора UV/Image, но оно разработано очень специфически для манипулирования UV-раскладками и для просмотра изображений, а не для работы с ними. Редактор нодов также способен на изощрённую обработку изображений, но у него нет документированного API, так что его нельзя сконфигурировать из скрипта.

Конечно, Блендер не может делать всё, и, несомненно, он не пытается конкурировать с графическими пакетами, такими как GIMP (www.gimp.org), но некоторые встроенные функции обработки изображений были бы кстати. (Каждым изображением можно управлять на уровне пикселей, но это довольно медленный процесс для больших изображений, и нам по-прежнему придётся осуществлять высокоуровневую функциональность, такую, как например, альфа-смешивание или поворот изображений).

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

Python Imaging Library (PIL), библиотека, которую мы будем использовать, свободно доступна и просто устанавливается. Следовательно, это не должно стать проблемой для среднего пользователя. Тем не менее, возможно осуществить функциональность простой вставки (мы увидим ниже), просто используя модуль Image Блендера, мы предоставим её в полном коде минималистского модуля pim, в котором будет только необходимый минимум, чтобы иметь возможность использовать наш пример без необходимости устанавливать PIL. Эта независимость имеет цену: наша функция paste() - почти в 40 раз медленнее, чем такая же из PIL, и результирующее изображение может сохраняться только в формате TARGA (.tga). Но вы, наверное, и не заметите этого, так как Блендер может просто отлично отображать TARGA файлы. Полный код оснащен некоторой хитростью, позволяющей использовать модуль PIL (в случае, если он доступен), и наш заменяющий модуль в противном случае. (Это не показано в книге.)


The Python Imaging Library (PIL) (Библиотека Питона формирования изображений)

PIL - это пакет с открытыми исходными текстами, свободно доступный на сайте http://www.pythonware.com/ products/pil/index.htm. Он состоит из множества модулей Питона и основной библиотеки, который поставляется для Windows в скомпилированном виде (и его очень легко скомпилировать для Linux, или даже он может быть уже доступен в дистрибутиве). Просто следуйте за инструкциями на сайте, чтобы установить его (но не забывайте использовать правильную версию питона при установке PIL; если у Вас установлено более одной версии Питона, используйте для установки ту же, что использует Блендер).


Схема кода - combine.py

Какие шаги мы должны предпринять, чтобы создать наше комбинированное изображение? Нам понадобится:

1. Создать камеры, если нужно.

2. Настроить и откадрировать камеры на предмет.

3. Отрендерить виды со всех камер

4. Объединить рендеренные изображения в единственную картинку.

Код начинается с импорта всех необходимых модулей. Из пакета PIL нам нужен модуль Image, но мы импортируем его под другим именем (pim), чтобы предотвратить столкновение с именем модуля Image Блендера, который мы также используем:

from PIL import Image as pim

import Blender

from Blender import Camera, Scene, Image, Object, 

Mathutils, Window

import bpy

import os

Первая функция-утилита, с которой мы столкнёмся, это - paste(). Эта функция объединяет четыре изображения в одно. Изображения передаются как имена файлов, а результат сохраняется как result.png, если не задано другое имя файла. Мы принимаем все четыре изображения, чтобы иметь одинаковые размеры, которые мы определяем, открывая первый файл, как изображение PIL и анализируя атрибут размера size (выделено в следующем коде). Изображения будут разделены и разграничены небольшой линией с однотонным цветом. Её ширина и цвет жестко кодируется в переменных edge и edgecolor, хотя Вы могли бы решить передавать их как аргументы:

def paste(top,right,front,free,output="result.png"):

  im = pim.open(top)

  w,h= im.size

  edge=4

  edgecolor=(0.0,0.0,0.0)

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

  comp = pim.new(im.mode,(w*2+3*edge,h*2+3*edge),

          edgecolor)

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

  comp.paste(im,(edge,edge))

Вставка трёх остальных изображений следует за той же схемой: открыть изображение и вставить его в правильной позиции. Наконец, комбинированное изображение сохраняется (выделено). Тип сохраняемого файла определяется его расширением (например, png), но его можно было бы переопределить, передав аргумент формата методу save(). Заметьте, что не было никакой причины определять формат для входных файлов, так как тип изображения функция open() определяет по их содержанию.

  im = pim.open(right)

  comp.paste(im,(w+2*edge,edge))

  im = pim.open(front)

  comp.paste(im,(edge,h+2*edge))

  im = pim.open(free)

  comp.paste(im,(w+2*edge,h+2*edge))

  comp.save(output)

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

def render(camera):

  cam = Object.Get(camera)

  scn = Scene.GetCurrent()

  scn.setCurrentCamera(cam)

  context = scn.getRenderingContext()

Так как нам может понадобиться использовать эту функцию в фоновом процессе, мы используем метод renderAnim() контекста рендера, а не метод render(). Дело в том, что метод render() не может быть использован в фоновом процессе. Следовательно, мы устанавливаем в значение текущего кадра как начальный, так и конечный кадры, чтобы гарантировать, что функция renderAnim() отрендерит единственный кадр. Мы также устанавливаем displayMode на 0, чтобы предотвратить появление дополнительного окна рендера (выделено в следующем куске кода):

  frame = context.currentFrame()

  context.endFrame(frame)

  context.startFrame(frame)

  context.displayMode=0

  context.renderAnim()

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

  filename= context.getFrameFilename()

Так как номер кадра будет одинаковым для вида каждой камеры, что мы рендерим, мы должны переименовать этот файл, в противном случае он будет переписан. Следовательно, мы создаём новое подходящее имя, состоящее из пути к кадру, который мы только что рендерили, и имени камеры. Мы используем переносимые функции обработки пути из питонового модуля os.path, так, чтобы всё работало одинаково хорошо как под Windows, так и под Linux, например.

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

  camera = os.path.join(os.path.dirname(filename),camera)

  try:

    os.remove(camera)

  except:

    pass

  os.rename(filename,camera)

  return camera

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

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



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

Код содержит много трудной математики, так что мы начнём, импортируя необходимые функции:

from math import asin,tan,pi,radians

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

def frame(cameras,bb):

  maxx = max(v.x for v in bb)

  maxy = max(v.y for v in bb)

  maxz = max(v.z for v in bb)

  minx = min(v.x for v in bb)

  miny = min(v.y for v in bb)

  minz = min(v.z for v in bb)

  wx=maxx-minx

  wy=maxy-miny

  wz=maxz-minz

  m=Mathutils.Vector((wx/2.0,wy/2.0,wz/2.0))

  maxw=max((wx,wy,wz))/2.0

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

  sins=[]

  for cam in cameras:

    p=Mathutils.Vector(Object.Get(cam).getLocation(

                      'worldspace'))

    d=(p-m).length

    sins.append(maxw/d)

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

  maxsin=max(sins)

  angle=asin(maxsin)

  for cam in cameras:

    Object.Get(cam).getData().lens = 16.0/tan(angle)

Другая удобная функция - та, которая создаёт четыре камеры и устанавливает их на сцену размещенными должным образом вокруг начала координат. Функция в принципе простая, но немного усложнена, поскольку она пытается заново использовать существующие камеры с тем же именем, чтобы предотвратить нежелательное размножение камер, если скрипт будет работать неоднократно. Словарь cameras индексируется по имени и содержит список позиций, поворотов, и величин lens:

def createcams():

  cameras = {

    'Top'  : (( 0.0,  0.0,10.0),( 0.0,0.0, 0.0),35.0), 

    'Right': ((10.0,  0.0, 0.0),(90.0,0.0,90.0),35.0),

    'Front': (( 0.0,-10.0, 0.0),(90.0,0.0, 0.0),35.0), 

    'Free' : (( 5.8, -5.8, 5.8),(54.7,0.0,45.0),35.0)}


Я это уже вроде упоминал, но скажу здесь ещё раз. Категорически не рекомендуется вставлять числа (да и строки тоже) в текст программы. Расстояние 10.0 взято совершенно произвольно. А вдруг размер объекта окажется больше? Откуда взялось 5.8, человеку, незнакомому с математикой, вообще будет непонятно, хотя в данном случае это просто длина ребра куба, длина диагонали которого равна 10.0 (sqrt((10.0**2)/3.0)≈5.8). Правильным было бы объявить в начале программы константу, равную 10.0, а расстояние для камеры 'Free' вычислять из неё. Тогда для работы с объектом больших или меньших размеров потребовалось бы изменить значение всего одной константы. - дополнение переводчика.


Для каждой камеры в словаре cameras мы проверяем, существует ли она уже как объект Блендера. Если это так, мы проверяем, имеет ли этот объект Блендера связанный с ним объект Камеры. Если последнее не является истиной, мы создаем перспективную камеру с тем же именем, как объект верхнего уровня (выделено), и ассоциируем его с объектом верхнего уровня посредством метода link():

  for cam in cameras:

    try:

     ob = Object.Get(cam)

     camob = ob.getData()

     if camob == None:

       camob = Camera.New('persp',cam)

       ob.link(camob)

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

    except ValueError:

     ob = Object.New('Camera',cam)

     Scene.GetCurrent().link(ob)

     camob = Camera.New('persp',cam)

     ob.link(camob)

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

    ob.setLocation(cameras[cam][0])

    ob.setEuler([radians(a) for a in cameras[cam][1]])

    camob.lens=cameras[cam][2]

    Blender.Redraw()

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

def run():

  ob = Scene.GetCurrent().objects.active

  cameras = ('Top','Right','Front','Free')

  frame(cameras,ob.getBoundBox())

  files = []

  for cam in cameras:

    files.append(render(cam))

Мы поместим скомбинированное изображение в тот же каталог, что и отдельные виды, и назовём его result.png:

  outfile = os.path.join(os.path.dirname(

             files[0]),'result.png')

Мы затем называем нашу функцию paste(), передавая список имён файлов компонентов, развёрнутый в индивидуальные аргументы оператором звездочка (*), и, последний штрих, загружаем файл результата как изображение Блендера и показываем его в окне редактора изображений (выделено ниже). Функция reload (перегрузка) необходима чтобы удостовериться, что предыдущее изображение с тем же самым именем будет обновлено:

  paste(*files, output=outfile)

  im=Image.Load(outfile)

  bpy.data.images.active = im

  im.reload()

  Window.RedrawAll()

Функция run() умышленно не создаёт никаких камер, поскольку пользователь может захотеть сделать это сам. Сам окончательный скрипт заботится о создании камер, но это можно изменить довольно легко, достаточно закомментировать строку. После проверки, если скрипт работает автономно, он просто создает камеры и вызывает метод run:

if __name__ == "__main__":

  createcams()

  run()

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


Рабочий процесс - как продемонстрировать вашу модель

Скрипт можно использовать следующим образом:

1. Поместите ваш предмет в начало координат (позиция (0, 0, 0)).

2. Создайте подходящие условия освещения.

3. Запустите combine.py.

Скрипт можно загрузить в текстовый редактор, чтобы запустить его с помощью Alt + P, но Вы также можете поместить его в каталог scripts Блендера, чтобы сделать его доступным из меню Scripts | Render.


Now, strip — создание киноленты из анимации

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

Хотя тут больше изображений для объединения, чем при нескольких видах камер, код для создания такой киноплёнки довольно похож.



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

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

def strip(files,name='Strip',cols=4):

  rows = int(len(files)/cols)

  if len(files)%int(cols) : rows += 1


  im = pim.open(files.pop(0))

  w,h= im.size

  edge=2

  edgecolor=(0.0,0.0,0.0)


  comp =  pim.new(im.mode, 

          (w*cols+(cols+1)*edge,

          h*rows+(rows+1)*edge),

          edgecolor)


  for y in range(rows):

    for x in range(cols):

     comp.paste(im,(edge+x*(w+edge),edge+y*(h+edge)))

     if len(files)>0:

       im = pim.open(files.pop(0))

     else:

       comp.save(name,format='png')

       return Image.Load(name)

Функция render(), которую мы определяем, принимает количество пропускаемых кадров в виде аргумента и рендерит любое количество кадров между начальным и конечным кадрами. Эти начальный и конечный кадры могут быть заданы пользователем в панели кнопок рендера. Эти кнопки рендера также содержат величину шага, но эта величина не доступна в API Питона. Это означает, что наша функция будет несколько избыточнее, чем нам хотелось бы, так как мы должны создать цикл, который рендерит каждый отдельный кадр (выделено в следующем коде) вместо прямого вызова renderAnim(). Следовательно, мы должны манипулировать атрибутами startFrame и endFrame контекста рендера (как и раньше), но мы будем осторожными и восстановим эти атрибуты перед возвратом списка имён файлов отрендеренных картинок. Если бы мы не нуждались в каком-либо программном контроле значения величины пропуска, мы могли бы просто заменить вызов render() вызовом renderAnim():

def render(skip=10):

  context = Scene.GetCurrent().getRenderingContext()

  filenames = []

  e = context.endFrame()

  s = context.startFrame()

  context.displayMode=0

  for frame in range(s,e+1,skip):

    context.currentFrame(frame)

    context.startFrame(frame)

    context.endFrame(frame)

    context.renderAnim()

    filenames.append(context.getFrameFilename())

  context.startFrame(s)

  context.endFrame(e)

  return filenames

После определения этих функций сам скрипт теперь просто вызывает render(), чтобы создавать изображения, и strip(), чтобы объединить их. Результирующее изображение Блендера перезагружается (reload) для его обновления на экране, если изображение с таким именем уже присутствовало, затем все окна перерисовываются (выделено):

def run():

  files = render()

  im=strip(files)

  bpy.data.images.active = im

  im.reload()

  Window.RedrawAll()

if __name__ == "__main__":

  run()

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


Рабочий процесс — использование strip.py

Создать ленту анимационных кадров можно следующим образом:

1. Создать вашу анимацию. (Ага, это, конечно самый простой пункт ;) -прим. пер.)

2. Запустить strip.py из текстового редактора.

3. Комбинированное изображение появится в окне редактора UV/image.

4. Сохранить изображение с именем по вашему выбору.


Рендер билбордов

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

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

Блендер поставляется со множеством инструментов, позволяющих уменьшить количество необходимой памяти при рендере множества копий объекта; различные Меш-объекты могут ссылаться на одни и те же данные меша, как, например, при DupliVerts. (Объекты-потомки, которые копируются в позицию каждой вершины родительского объекта. Смотри http://www.is.svitonline.com/sailor/doc/man/specmod/dupliverts.htm более подробно.) Дублирование объектов в системах частиц также позволяет нам создавать множество экземпляров того же самого объекта без действительного дублирования всех данных. Эти методы могут предотвратить потери огромного количества памяти, но детализированные объекты все еще могут требовать процессорных мощностей для рендера, поскольку их детали все еще должны быть отрендерены.

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

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

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

1. Спозиционировать две камеры, спереди и справа от детального объекта.

2. Откадрировать обе камеры, чтобы они захватывали весь объект с одинаковым углом.

3. Отрендерить прозрачные изображения с premultiplied (заранее перемноженным) альфа-каналом и без неба.

4. Создать простой объект из двух перпендикулярных квадратов.

5. Наложить каждое отрендеренное изображение на квадрат.

6. Скрыть детальный объект от рендера.

7. Необязательно, скопировать простой объект в систему частиц (пользователю может не понадобиться автоматизировать эту часть, если он захочет расставить простые объекты вручную).

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

Способ избежать этого - отрегулировать отрендеренные цвета, перемножив их с величиной альфы и контекст рендера имеет необходимые атрибуты, чтобы включить такой режим. Мы не должны забывать отмечать изображения, рендеренные как "premultiplied", при использовании их в качестве текстур, в противном случае они будут выглядеть слишком тёмными. Различие проиллюстрировано на следующем скриншоте, где мы скомпоновали и расширили правильно premultiplied левую половину и отрендеренную с небом правую половину. У ствола дерева справа проявляется светлый край. (Посмотрите отличную книгу Роджера Викса "Foundation Blender Compositing", если нужна дополнительная информация.)



Буковое дерево (использованное на этой и последующих иллюстрациях) - это высокодетальная модель (свыше 30,000 граней), созданная Yorik van Havre с помощью свободного пакета моделирования растений ngPlant. (Смотри его вебсайт для большего количества отличных примеров: http://yorik.uncreated.net/ greenhouse.html). Далее первый набор изображений показывает буковое дерево спереди и результирующий рендер передней грани билборда слева. (немного темнее из-за premultiplication).




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




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



Нашей первой проблемой будут некоторые ранее используемые функции, которые мы писали для презентации модели с несколькими видами. Эти функции находятся в текстовом буфере с именем combine.py, и мы не сохраняли его во внешний файл. Мы создадим наш скрипт cardboard.py как новый текстовый буфер в том же .blend файле, где и combine.py, и хотим ссылаться на последний так же, как на внешний модуль. Блендер позволяет это делать, так как он ищет модуль в текущих текстовых буферах, если он не может найти внешний файл.

Поскольку внутренние текстовые буферы не имеют информации о том, когда они последний раз изменялись, мы должны убедиться, что загружена самая последняя версия. Об этом позаботится функция reload(). Если мы её не выполним, Блендер не сможет обнаружить возможных изменений в combine.py, что могло бы провести нас к использованию его более старой скомпилированной версии:

import combine

reload(combine)

Мы не будем использовать заново функцию render() из combine.py, поскольку сейчас у нас другие требования для рендеренных изображений, которые мы наложим на билборды. Как уже объяснялось, мы должны убедиться, что мы не получим никаких светлых краёв в местах с частичной прозрачностью, так что мы заранее включаем premultiply в альфа-канале (выделено). Мы восстанавливаем контекст рендера в 'рендер неба' (rendering the sky) обратно до возврата из этой функции, поскольку легко забыть установить его обратно вручную, и Вы можете потратить время на удивление, куда подевалось ваше небо:

def render(camera):

  cam = Object.Get(camera)

  scn = Scene.GetCurrent()

  scn.setCurrentCamera(cam)

  context = scn.getRenderingContext()

  frame = context.currentFrame()

  context.endFrame(frame)

  context.startFrame(frame)

  context.displayMode=0

  context.enablePremultiply()

  context.renderAnim()

  filename= context.getFrameFilename()

  camera = os.path.join(os.path.dirname(filename),camera)

  try:

    os.remove(camera) # удаление, в противном случае

             # переименование

             # потерпит неудачу в windows

  except:

    pass

  os.rename(filename,camera)


  context.enableSky()

  return camera

Каждое отрендеренное изображение должно быть преобразовано в подходящий материал, чтобы наложить его на квадрат с UV-отображением. Функция imagemat() будет делать это просто; она принимает объект Блендера Image в качестве аргумента и возвращает объект Материала. Этот материал будет сделан полностью прозрачным (выделено), но эта прозрачность и цвет модифицируются текстурой, которую мы назначаем в первый текстурный канал (вторая выделенная строка). Тип текстур установлен в Image и, поскольку мы визуализировали эти изображения с premultiplied альфа-каналом, мы используем метод setImageFlags(), чтобы указать, что мы хотим использовать этот альфа-канал, и устанавливаем атрибут premul изображения в Истину:

def imagemat(image):

  mat = Material.New()

  mat.setAlpha(0.0)

  mat.setMode(mat.getMode()|Material.Modes.ZTRANSP)

  tex = Texture.New()

  tex.setType('Image')

  tex.image = image

  tex.setImageFlags('UseAlpha')

  image.premul=True

  mat.setTexture(0,tex,Texture.TexCo.UV, 

          Texture.MapTo.COL|Texture.MapTo.ALPHA)

  return mat

Каждая грань, к которой мы применяем материал, должна иметь UV-раскладку. В нашем случае, это будет самой простой из возможных раскладок, так как квадратная грань будет отображена так, чтобы в точности соответствовать прямоугольному изображению. Это часто называют сбросом отображения, и следовательно, функция, которую мы определим, называется reset(). Она принимает объект Блендера MFace, который мы считаем четырёхугольником, и присваивает его атрибуту uv список 2D-векторов, по одному для каждой вершины. Эти векторы размещают каждую из вершин в углах изображения:

def reset(face):

  face.uv=[vec(0.0,0.0),vec(1.0,0.0),

       vec(1.0,1.0),vec(0.0,1.0)]

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

def cardboard(left,right):

  mesh = Mesh.New('Cardboard')

  verts=[(0.0,0.0,0.0),(1.0,0.0,0.0),

      (1.0,0.0,1.0),(0.0,0.0,1.0),

      (0.5,-0.5,0.0),(0.5,0.5,0.0),

      (0.5,0.5,1.0),(0.5,-0.5,1.0)]

  faces=[(0,1,2,3),(4,5,6,7)]

  mesh.verts.extend(verts)

  mesh.faces.extend(faces)


  mesh.addUVLayer('Reset')

  mesh.activeUVLayer='Reset'

Затем мы создаем подходящие материалы из обоих изображений, и назначаем эти материалы в атрибут меша materials. Далее, мы сбрасываем (reset) UV-координаты обеих граней, и назначаем им материалы (выделено). Мы обновляем (update) меш, чтобы сделать изменения видимыми до возврата из функции:

  mesh.materials=[imagemat(left),imagemat(right)]


  reset(mesh.faces[0])

  reset(mesh.faces[1])

  mesh.faces[0].mat=0

  mesh.faces[1].mat=1


  mesh.update()

  return mesh

Чтобы заменить меш дублированием объекта системой частиц, мы строим утилиту setmesh(). Она принимает имя объекта со связанной системой частиц и Меш-объект как аргументы. Она находит Объект по имени, и извлекает первую систему частиц (выделено в следующем куске кода). Объект дублирования находится в атрибуте duplicateObject. Заметьте, что этот атрибут только для чтения, так что к настоящему времени нет возможности поменять объект из Питона. Но мы можем заменить данные объекта и, мы это делаем посредством передачи Меш-объекта в метод link(). Оба объекта, эмиттер и объект дублирования системой частиц изменятся, так что мы удостоверимся, что изменения станут видимыми, вызывая метод makeDisplayList() для них обоих перед запуском обновления изображения (redraw) всех окон Блендера:

def setmesh(obname,mesh):

  ob = Object.Get(obname)

  ps = ob.getParticleSystems()[0]

  dup = ps.duplicateObject

  dup.link(mesh)

  ob.makeDisplayList()

  dup.makeDisplayList()

  Window.RedrawAll()

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

def run():

  act_ob = Scene.GetCurrent().objects.active

  act_ob.restrictRender = False

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

  renderstate = {}

  for ob in Scene.GetCurrent().objects:

    renderstate[ob.getName()] = ob.restrictRender

    if not ob.getType() in ('Camera','Lamp' ):

     ob.restrictRender = True

  act_ob.restrictRender = False

Как только всё настроено, чтобы рендерить только активный объект, мы рендерим переднее и правое изображения с должным образом откадрированными камерами, просто подобно тому, как мы это делали в скрипте combine.py. Фактически, здесь мы заново используем функцию frame() (выделено):

  cameras = ('Front','Right')

  combine.frame(cameras,act_ob.getBoundBox())

  images={}

  for cam in cameras:

    im=Image.Load(render(cam))

    im.reload()

    images[cam]=im

  bpy.data.images.active = im

  Window.RedrawAll()

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

  for ob in Scene.GetCurrent().objects:

    ob.restrictRender = renderstate[ob.getName()]


  mesh = cardboard(images['Front'],images['Right'])

  act_ob.restrictRender = True

  setmesh('CardboardP',mesh)

Последние строки кода создают камеры, необходимые для рендера билбордов (если эти камеры в данный момент отсутствуют), вызывая функцию createcams() из модуля combine до вызова run():

if __name__ == "__main__":

  combine.createcams()

  run()

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


Рабочий процесс - использование cardboard.py

Допустим, что у вас есть высокополигональный объект, и что Вы хотели бы преобразовать его в набор билбордов, тогда работа могла бы выглядеть примерно так:

1. Создать объект с именем CardboardP.

2. Назначить систему частиц на этот объект.

3. Создать временный куб.

4. Назначить временный куб дублированным объектом в первой системе частиц объекта CarboardP.

5. Выбрать (сделать активным) объект, который будет отрендерен как набор билбордов.

6. Запустить cardboard.py.

7. Выбрать первоначальную камеру и отрендерить сцену.

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



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




Генерация вопросов CAPTCHA

Во многих ситуациях, как например, блогах, форумах, и онлайн-опросах (можно назвать ещё несколько), операторы вебсайтов хотят избежать автоматизированных почтовых отправлений от спамботов, но не хотят напрягать посетителей-людей регистрацией с аутентификацией. В таких ситуациях, которые стали обычными, посетителю предлагают так называемый вопрос CAPTCHA (http://ru.wikipedia.org/wiki/CAPTCHA). Вопрос CAPTCHA (или просто Captcha) в самой своей простой форме - изображение, которое должно быть трудным для компьютерного распознавания, но простым для расшифровки человеком, обычно это искаженное или смазанное слово или число.

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

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

Мы разработаем наш сервер как веб-сервер, который будет реагировать на запросы, адресованные ему как URL'ы в форме http::/captcha?text=, и который возвращает PNG-изображение - 3D-представление этого текста. Таким образом, будет легко внедрить этот сервер в архитектуру, в которой некоторое программное обеспечение, например блог, может легко использовать эту функциональность, просто подключаясь к нашему серверу через HTTP. Пример сгенерированного вопроса показан на иллюстрации:



Разработка сервера CAPTCHA

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

import BaseHTTPServer

import re

import os

import shutil

Модуль BaseHTTPServer определяет два класса, которые вместе включают полную реализацию сервера HTTP. Класс BaseHTTPServer реализует основной сервер, который будет слушать поступающие HTTP-запросы на некотором сетевом порту, и мы используем этот класс, как есть.

При получении корректного HTTP-запроса BaseHTTPServer пошлет этот запрос обработчику запросов. Наша реализация такого обработчика запросов, основанная на BaseHTTPRequestHandler, довольно скудна, так как ожидается, что всё, что он будет делать - запрашивать поля GET и HEAD в форме captcha?text=abcd. Следовательно, всё мы должны сделать - переписать методы do_GET() и do_HEAD() базового класса.

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

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

class CaptchaRequestHandler(

     BaseHTTPServer.BaseHTTPRequestHandler):

  def do_GET(self):

    f=self.do_HEAD()

    shutil.copyfileobj(f,self.wfile)

    f.close()

Метод do_HEAD() сначала определяет, получили ли мы правильный запрос (то есть, URI в форме captcha?text=abcd), вызывая метод gettext() (выделено, определяется позже в коде). Если URI некорректен, метод gettext(), возвращает None и тогда do_HEAD() возвращает клиенту ошибку File not found (Файл не найден), вызывая метод send_error() базового класса:

  def do_HEAD(self):

    text=self.gettext()

    if text==None:

       self.send_error(404, "File not found")

       return None

Если был запрошен корректный URI, фактическое изображение генерируется методом captcha(), который возвращает имя файла сгенерированного изображения. Если этот метод терпит неудачу по любой причине, клиенту возвращается Internal server error (Внутренняя ошибка сервера):

    try:

       filename = self.captcha(text)

    except:

       self.send_error(500, "Internal server error")

       return None

Если все прошло хорошо, мы открываем файл изображения, отсылаем клиенту ответ 200 (показывающий успешную операцию), и возвращаем заголовок Content-type, устанавливающий, что мы возвращаем png-изображение. Затем мы используем функцию fstat() с номером handle открытого файла в качестве аргумента, чтобы извлечь длину сгенерированного изображения и вернуть её как заголовок Content-Length (выделено), сопроводив временем модификации и пустой строкой, означающей конец заголовков перед возвратом открытого файлового объекта f:

    f = open(filename,'rb')

    self.send_response(200)

    self.send_header("Content-type", 'image/png')

    fs = os.fstat(f.fileno())

    self.send_header("Content-Length", str(fs[6]))

    self.send_header("Last-Modified", 

         self.date_time_string(fs.st_mtime))

    self.end_headers()

    return f

Метод gettext() проверяет, что запрос, передаваемый нашему обработчику запросов в переменной пути - правильный URI, сверяя его с регулярным выражением. Функция match() из модуля Питона re возвращает MatchObject (объект сопоставления), если регулярное выражение соответствует параметру, и None, если нет. Если есть соответствие, мы возвращаем содержание первой группы объекта сопоставления (символы, которые соответствуют выражению между круглыми скобками в регулярном выражении, в нашем случае значение текстового аргумента), в противном случае мы возвращаем None:

  def gettext(self):

    match = re.match(r'^.*/captcha\?text=(.*)$',

            self.path)

    if match != None:

     return match.group(1)


    return None

Теперь мы добрались до задачи, специфичной для Блендера - сгенерировать рендеренный в 3D текст, который будет возвращён в виде png изображения. Метод captcha() принимает текст для рендера как аргумент, и возвращает имя файла сгенерированного изображения. Мы допускаем, что освещение и камера в .blend файле, в котором мы запускаем captcha.py, настроены правильно, чтобы удобочитаемо отображать наш текст. Следовательно, метод captcha() просто настраивает правильным образом объект Text3d и рендерит его.

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

  def captcha(self,text):

    import Blender

    scn = Blender.Scene.GetCurrent()

    text_ob = None

    for ob in scn.objects:

     if ob.name == 'Text' :

       text_ob = ob.getData()

       break

Если не нашлось никакого ранее используемого объекта Text3d, создаём новый :

    if text_ob == None:

     text_ob = Blender.Text3d.New('Text')

     ob=scn.objects.new(text_ob)

     ob.setName('Text')

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


Чего-то не хватает

Документация API Блендера имеет небольшой пропуск: как будто не существует способа настроить другой шрифт для объекта Text3d. Тем не менее, есть недокументированный метод setFont(), который принимает объект Font в качестве аргумента. Код, выполняющий изменение шрифта должен выглядеть похожим на это:

fancyfont=Text3d.Load('/usr/share/fonts/ttf/myfont.ttf') 

text_ob.setFont(fancyfont)

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


Последним шагом нужно обновить дисплейный список Блендера для этого объекта, чтобы наши изменения были отрендерены:

    text_ob.setText(text)

    text_ob.setExtrudeDepth(0.3)

    text_ob.setWidth(1.003)

    text_ob.setSpacing(0.8)

    text_ob.setExtrudeBevelDepth(0.01)

    ob.makeDisplayList()

Как только наш объект Text3d будет на месте, нашей следующей задачей станет отрендерить его изображение в файл. Сначала мы извлекаем контекст рендера из текущей сцены и устанавливаем displayMode в 0, чтобы предотвратить появление дополнительного окна рендера:

    context = scn.getRenderingContext()

    context.displayMode=0

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

    context.imageSizeX(160)

    context.imageSizeY(120)

    context.setImageType(Blender.Scene.Render.PNG)

    context.enableRGBAColor()

    context.alphaMode=2

Даже если мы рендерим простое неподвижное изображение, мы используем метод renderAnim() контекста рендера, поскольку иначе результаты рендерятся не в файл, а только в буфер. Следовательно, мы устанавливаем начальный и конечный кадры анимации в 1 (точно так же, как и текущий кадр), чтобы удостовериться, что мы генерируем простой одиночный кадр. Затем мы используем метод getFrameFilename(), чтобы получить имя файла (с полным путём) отрендеренного кадра (выделено). Далее мы одновременно сохраняем это имя файла и возвращаем его как результат:

    context.currentFrame(1)

    context.sFrame=1

    context.eFrame=1

    context.renderAnim()

    self.result=context.getFrameFilename()

    return self.result

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

def run(HandlerClass = CaptchaRequestHandler,

    ServerClass = BaseHTTPServer.HTTPServer,

    protocol="HTTP/1.1"):

    port = 8080

    server_address = ('', port)

    HandlerClass.protocol_version = protocol

    httpd = ServerClass(server_address, HandlerClass)

    httpd.serve_forever()

if __name__ == '__main__':

    run()

Полный код доступен как captcha.py в файле captcha.blend, и сервер можно запустить несколькими путями: из текстового редактора (с Alt + P), из меню Scripts | render | captcha, или запустив Блендер в фоновом режиме из командной строки. Чтобы остановить сервер снова, необходимо завершить Блендер. Обычно это можно сделать посредством нажатия Ctrl + C в консоли или в окне DOSbox.


Предупреждение

Заметьте, что этот сервер реагирует на чьи угодно запросы, а это далеко небезопасно. Как минимум он должен быть запущен через межсетевой экран, который ограничивает доступ к нему только для сервера, которому требуются вопросы Captcha. Прежде чем запускать его в любом месте, которое может быть доступно из Интернета, вы должны тщательно подумать о безопасности вашей сети!


Итог

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

• Автоматизацию процесса рендера

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

• Создание билбордов из сложных объектов

• Манипуляцию изображениями, в том числе результатами рендера, используя библиотеку обработки изображений Python Imaging Library (PIL)

• Построение сервера, создающего изображения по-требованию, которые могут быть использованы как вопросы CAPTCHA

В последней главе мы взглянем на некоторые служебные задачи.


Загрузка...