Передаем данные из GIMP в Blender

Не так давно столкнулся с интересной задачей при создании 2D-анимации в Blender: мне нужно было сделать плоскую сетку по форме объекта из PNG-изображения с прозрачным фоном. На обычную плоскость ее натянуть нельзя, так как предполагалось, что объект будет деформироваться при помощи скелета и shape keys. И таких сеток нужно было создать довольно много. Создавать их вручную, расставляя вершины по контуру картинки, как-то очень уж трудоемко – захотелось этот процесс как-то оптимизировать. И тут я вспомнил, что GIMP умеет преобразовывать маски в кривые, которые затем можно сохранить как SVG и импортировать в Blender. Осталось лишь заскриптовать эту последовательность действий!

Я решил, что переносить SVG вручную из одной программы в другую я тоже не хочу – пусть будет условно одна-единственная кнопка, по нажатию на которую слой из GIMP переносится в текущий открытый проект Blender. Подобное взаимодействие двух приложений можно реализовать при помощи технологий RPC (remote procedure call) – в частности XML-RPC, который позволяет через HTTP на клиенте вызвать серверную функцию, передав ей параметры, и затем получить результат. Преимущество XML-RPC в том, что он полностью скрывает транспортный механизм такого вызова – в скриптовых языках он выглядит просто как обычный вызов функции. Сервером я решил сделать плагин для Blender, клиентом – плагин для GIMP. Оба плагина я написал на Python, где протокол XML-RPC реализован в стандартной библиотеке. В GIMP и Blender используются разные версии Python, поэтому код работы с XML-RPC немного отличается.

Серверная часть выглядит достаточно тривиально: нужна лишь функция, которая принимает на вход строку, содержащую SVG – эта функция регистрируется как серверная функция в объекте SimpleXMLRPCServer:

import os
import bpy

import threading
import tempfile
from xmlrpc.server import SimpleXMLRPCServer

HOST = "127.0.0.1"
PORT = 8000

def svg_to_curve(svg:str):
    tmp = tempfile.NamedTemporaryFile(delete=False, mode="w")
    tmp.write(svg)
    tmp.close()
    bpy.ops.import_curve.svg(filepath=tmp.name, filter_glob='*')
    os.unlink(tmp.name)
    return {}

def launch_server():
    server = SimpleXMLRPCServer((HOST, PORT))
    server.register_function(svg_to_curve)
    server.serve_forever()

(для краткости я опустил служебный код для регистрации плагина)

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

На стороне GIMP делается следующее: текущему слою создается маска из альфа-канала, из маски создается выделение (gimp_image_select_item), из выделения, в свою очередь – кривая (plug_in_sel2path). Кривая экспортируется в SVG (gimp_vectors_export_to_string), а затем мы просто вызываем удаленную функцию svg_to_curve, после чего удаляем все служебные объекты.

import xmlrpclib
from gimpfu import *

def export_svg(svg):
    proxy = xmlrpclib.ServerProxy("http://localhost:8000/")
    try:
        proxy.svg_to_curve(svg)
    except xmlrpclib.Fault as err:
        pdb.gimp_message(err.faultString)

def layer_to_blender_curve(image, layer):
    if not pdb.gimp_item_is_group(layer):
        mask = layer.mask
        if not mask:
            mask = layer.create_mask(ADD_ALPHA_TRANSFER_MASK)
            layer.add_mask(mask)
        pdb.gimp_image_select_item(image, CHANNEL_OP_REPLACE, mask)
        path = pdb.plug_in_sel2path(image, None)
        pdb.gimp_selection_none(image)
        
        vector_name = pdb.gimp_path_list(image)[1][0]
        vec = pdb.gimp_image_get_vectors_by_name(image, vector_name)
        vec.name = "mask_path"
        
        svg = pdb.gimp_vectors_export_to_string(image, path)
        export_svg(svg)
        
        pdb.gimp_image_remove_vectors(image, vec)
        pdb.gimp_layer_remove_mask(layer, 0)

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

Исходники плагинов вы можете найти в репозитории https://github.com/gecko0307/image2curve.

Недостатком данного решения является то, что на стороне Blender будет постоянно работать HTTP-сервер на localhost:8000, так что вы в это время не сможете привязать к этому порту ничего другого. В Python есть способы получить случайный незанятый номер порта, чтобы не конфликтовать с другими серверами, однако в этом случае придется как-то передать порт в GIMP, что, как мне кажется, несколько усложняет весь процесс и добавляет лишнюю точку отказа.

Винтажные фильтры – обновление

Обновилась коллекция винтажных фильтров для GIMP: добавлены три новых фильтра (Amaro, Brannan, Toaster), а также поддержка виньетирования. Все фильтры теперь объединены в один: при запуске скрипта выводится диалоговое окно с выбором фильтра и другими опциями.

Скачать можно здесь.

Журнал “FPS” №27

Вышел 27 номер электронного PDF-журнала “FPS”, посвященного разработке игр, программированию, компьютерной графике и звуку.

Читайте в этом номере:

> Подборка новостей по Blender
> Тон Розендаль о будущем интерфейса Blender
> GIMP: цветокоррекция на Python
> От мольберта – к дисплею. Заметки о цифровой живописи
> Физический движок своими руками. Часть IV
> Математика в dlib
> Ranges: диапазоны в D
> Игровые новости из мира Linux
> Право на творчество

Номер доступен для онлайн-чтения и загрузки на сервисе Issuu.com, Документах Google и Dropbox.

Последние новости по проекту вы можете узнать в публичной странице журнала в социальной сети Google+: http://gplus.to/fpsmag. Добавляйте нас в круги, оставляйте свои комментарии и отписывайтесь в нашем сообществе.

Архив номеров журнала здесь.

Ломографические фильтры для GIMP

Пять винтажных фильтров для GIMP.

Скачать

Кстати, остальные мои скрипты для GIMP теперь лежат здесь.

Thumbnailer для GIMP

Терпеть не могу рутинную, механическую работу! Сейчас вот пришлось вручную изготовлять миниатюры изображений произвольного разрешения, центрируя и уменьшая их до квадрата размером 128х128. Не стал долго мучиться, написал для GIMP скрипт на Python – и решил сразу выложить, вдруг кому-нибудь тоже пригодится:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from gimpfu import *

def python_fu_thumbnailer(image, layer, width, height):
    origWidth = pdb.gimp_image_width(image)
    origHeight = pdb.gimp_image_height(image)
    newWidth = origWidth
    newHeight = origHeight
    
    newX = 0
    newY = 0
    
    if (origWidth > origHeight):
        newWidth = origHeight
        newX = -(origWidth/2 - newWidth/2)
    elif (origWidth < origHeight):
        newHeight = origWidth
        newY = -(origHeight/2 - newHeight/2)

    pdb.gimp_layer_resize(layer, newWidth, newHeight, newX, newY)
    pdb.gimp_image_resize_to_layers(image)
    
    pdb.gimp_context_set_interpolation(INTERPOLATION_LANCZOS)
    pdb.gimp_image_scale(image, width, height)

register(
    "python-fu-thumbnailer",
    "Thumbnailer",
    "Thumbnailer 0.1",
    "Timur Gafarov",
    "(c) Copyright 2013 Timur Gafarov",
    "09-02-2013",
    "Make a thumbnail...",
    "RGB*, GRAY*",
    [
        (PF_IMAGE, "image", "Target image", None),
        (PF_DRAWABLE, "drawable", "Target layer", None),
        (PF_SPINNER, "width", "Width:", 128, (1, 262144, 1)),
        (PF_SPINNER, "height", "Height:", 128, (1, 262144, 1))
    ],
    [],
    python_fu_thumbnailer, 
    menu = "/Python-Fu/Transform")

main()

Тестировал с GIMP 2.7.4 и Python 2.7.1.