Книга: Написание скриптов для Blender 2.49

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

Схема кода - 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.376. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз