2011/08/18

ブログ引っ越すかも。。

投稿する際に使用するエディタを「以前のエディタ」にしないとエディタがうまく
動いてくれないようです。。。

なんだかなぁ。。といった感じなので、ブログを引っ越す予定です。
引っ越しが完了したら、このブログは更新されなくなります。
(今までもあまり更新されてないですけどね。。笑)

新しいブログは

http://bl4etc.blog.fc2.com/

です。

Bloggerに越してきてあまり投稿してないのに。。残念。。

2011/08/11

Gimpのプラグインを作ってみました(Simple Sprite Sheet Maker)

複数の画像を一枚にまとめたファイル(Sprite Sheetというらしいですが)が必要になったので、
あれこれ探していたんですが、なかなか見つかりませんでした。。

手始めに先日購入したPixelmator用のプラグインとして作れないかどうか試してみたんですが
Pixelmatorだと(というよりもQuartz Composerプラグインだと?)、複数の画像ファイルやレイヤーを
扱うような処理はとっても作りにくいことがわかって断念しました。
(おしいところまでは作ったんですが、どうも納得できなくて)

次にGimpのプラグインで既存のものがないかと探してみると、SpriteSheet.scmというのが見つかりました。
さっそく使ってみたんですが、期待していた動きとは違っていて画像を横一列にしか配置してくれない
感じでした。

だったらということで、自分で作ってみることにしました。
参考にしたのは
GIMP Python Document
・http://zwell.net/content/pygimp.html
などです。


いきなりですが、ソースコードから。
コメントなどを英語で書く練習もかねてますが、間違いだらけな気が。。笑

#!/usr/bin/env python

'''
SimpleSpriteSheet

This script help to generate sprite sheet for game texture.

When you use this script, all images size must be the same.
Otherwize, you could not get the good result.

Option help:
  1. Sprite cell width
     Set the width of individual Sprite.
     The width of Sprite should be the same as the height of the image.
     When the width of the image is bigger, the image is trimmed.
    
  2. Sprite cell height
     Set the height of individual Sprite.
     The height of Sprite should be the same as the height of the image.
     When the height of the image is bigger, the image is trimmed.
    
  3. Columns per row
     Set the number of the lateral cells.
    
  4. Image source
     When "Layers" is chosen, this script use the layer of current image from the top.
     If "Directory" is chosen, this script use the image files in a directory.
    
  5. Acive layers only
     This option becomes effective when the image source is "Layers".
     If this option is "Yes", script ignores the invisible layers.
    
  6. Directory
     This option becomes effective when the image source is "Directory".
     Script use the image in this directory.
    
  7. Image filter
     This option becomes effective when the image source is "Directory".
     Script processes ignores other image types.
'''

import os
from gimpfu import *


IMG_SOURCE_LAYER = 0
IMG_SOURCE_DIRECTORY = 1
IMG_FILTER_PNG = 0
IMG_FILTER_JPG = 1

SOURCE_FILTER_DICT = {
    IMG_FILTER_PNG: ('.png', '.PNG'),
    IMG_FILTER_JPG: ('.jpg', 'jpeg'),
    }


def plugin_main(timg, tdrawable, spriteWidth, spriteHeight, columnCount,
                imageSource, activeLayersOnly, directory, imageFilter):
    '''
    '''
    newImg = None
    # "Layers" source
    if imageSource == IMG_SOURCE_LAYER:
        newImg = _make_sprite_sheet(timg, spriteWidth, spriteHeight, columnCount)
    # "Directory" source
    elif imageSource == IMG_SOURCE_DIRECTORY:
        # Filter the image file by extention
        extFilters = SOURCE_FILTER_DICT.get(imageFilter)
        filesInDir = os.listdir(directory)
        if extFilters:
            sourceNames = [os.path.join(directory, fname) for fname in filesInDir
                       if os.path.splitext(fname)[1] in extFilters]
        else:
            sourceNames = [os.path.join(directory, fname) for fname in filesInDir]
        # Create the temporary image that contains source images(layers)
        loadedImage = _load_layerd_iamge(sourceNames, spriteWidth, spriteHeight, columnCount)
        if loadedImage:
            # Make the sprite sheet from the image which contains the layers.
            newImg = _make_sprite_sheet(loadedImage, spriteWidth, spriteHeight, columnCount)
            gimp.delete(loadedImage)

    # Show the result image
    if newImg:
        gimp.pdb.gimp_display_new(newImg)

def _get_sprite_row(index, columnCount):
    '''
    index: origin 0
    RETURN: origin 0
    '''
    row  = int(math.ceil(float(index + 1) / float(columnCount)))
    return row - 1

def _get_sprite_column(index, columnCount):
    '''
    index: origin 0
    RETURN: origin 0
    '''
    return index % columnCount

def _get_sprite_position(index, columnCount):
    '''
    RETURN: Tuple (row index, column index)
                  index : origin 0
    '''
    return (_get_sprite_row(index, columnCount),
            _get_sprite_column(index, columnCount))
  
def _get_image_size(sourceCount, spriteWidth, spriteHeight, columnCount):
    '''
    RETURN: Tuple (image widht), image height)
    '''
    cols = columnCount if sourceCount >= columnCount else sourceCount
    w = cols * spriteWidth
    h = (_get_sprite_row(sourceCount, columnCount) + 1) * spriteHeight
  
    return w, h

def _load_layerd_iamge(filenames, spriteWidth, spriteHeight, columnCount):
    '''
    RETURN: gimp.Image
    '''
    imageW, imageH = _get_image_size(len(filenames), spriteWidth, spriteHeight, columnCount)

    newImg = gimp.Image(imageW, imageH, RGB)
  
    for fname in filenames:
        newLayer = gimp.pdb.gimp_file_load_layer(newImg, fname)
        gimp.pdb.gimp_image_add_layer(newImg, newLayer, len(newImg.layers))

    return newImg

def _make_sprite_sheet(image, spriteWidth, spriteHeight, columnCount):
    '''
    return gimp.Image
    '''
    # Validate arguments
    if not image or spriteWidth <= 0 or spriteHeight <= 0 or columnCount <= 0:
        return None

    # Resize image
    visibleLayerCount = len([la for la in image.layers if la.visible])
    if visibleLayerCount == 0:
        print 'No visible layers.'
        return None
    imageW, imageH = _get_image_size(visibleLayerCount, spriteWidth, spriteHeight, columnCount)
    tmpImg = gimp.Image(imageW, imageH, RGB)
    tmpLayer = gimp.Layer(tmpImg, 'Sprite-Sheet', imageW, imageH,
                          RGBA_IMAGE, 100, NORMAL_MODE)
    tmpImg.add_layer(tmpLayer)
    gimp.pdb.gimp_drawable_fill(tmpLayer, TRANSPARENT_FILL)

    # Destination layer pixel region
    dstPixRgn = tmpLayer.get_pixel_rgn(0, 0, imageW, imageH)
  
    # Arange the images
    gimp.progress_init()

    progressBarUnit = 100.0 / float(len(image.layers))
    for idx, layer in enumerate(image.layers):
        row, col = _get_sprite_position(idx, columnCount)
        dstX = col * spriteWidth
        dstY = row * spriteHeight

        pixelsX = layer.width if layer.width <= spriteWidth else spriteWidth
        pixelsY = layer.height if layer.height <= spriteHeight else spriteHeight

        # Source layer pixel region
        srcPixRgn = layer.get_pixel_rgn(0, 0, pixelsX, pixelsY)

        # Copy source pixel regin to destination.
        dstPixRgn[dstX:dstX + pixelsX, dstY:dstY + pixelsY] = srcPixRgn[0:pixelsX, 0:pixelsY]
        gimp.progress_update(idx * progressBarUnit)
        ## === SLOWER ===
        ## for x in range(pixelsX):
        ##     for y in range(pixelsY):
        ##         dstPixRgn[dstX + x, dstY + y] = srcPixRgn[x, y]

        ## === SLOWEST ===
        ## for x in range(pixelsX):
        ##     for y in range(pixelsY):
        ##         channels, pixel = gimp.pdb.gimp_drawable_get_pixel(layer, x, y)               
        ##         gimp.pdb.gimp_drawable_set_pixel(tmpLayer, dstX + x, dstY + y, channels, pixel)

    return tmpImg


#
# Register script
#
register(
    'python_fu_SSS',
    'Simple sprite sheet maker(S.S.S).',
    'Simple sprite sheet maker(S.S.S).',
    'Junichi Kawanishi aka jkani4 (jkani4@gmail.com)',
    'Copyright 2011 Junichi Kawanishi',
    '2011',
    '/Filters/_SSS...',
    '', # image types
    [(PF_INT, 'spriteWidth', 'Sprite cell width:', 128),
     (PF_INT, 'spriteHeight', 'Sprite cell height:', 128),
     (PF_INT, 'columnCount', 'Columns per row:', 8),
     (PF_RADIO, 'imageSource', 'Image source:', IMG_SOURCE_LAYER, (('Layer', IMG_SOURCE_LAYER), ('Directory', IMG_SOURCE_DIRECTORY))),
     (PF_TOGGLE, 'activeLayersOnly', 'Active layers only:', 1),
     (PF_DIRNAME, 'directory', 'Directory:', 0),
     (PF_OPTION, 'imageFilter', 'Image filter', IMG_FILTER_PNG, ['*.png', '*.jpg'])],
    [],
    plugin_main)

#
# Start
#
main()

ううむ、私のブログ、サイドバーが広いのでソースコードが見づらくてごめんなさい。
レイヤーの画像を並べている処理(_make_sprite_sheet)で、SLOWERとかSLOWESTとか
コメントアウトされている部分があります。
この部分は、あるImageから別のImageに画像をコピーする処理をしています。

SLOWESTとしてコメントアウトされた方法で実装してみたところ、
処理そくどがめちゃくちゃ遅かったので、ボツになりました。
(1pixelづつコピーしてるし、別のScript-fuを呼び出してるので遅いのは当然か。。)

次にSLOWERとしてコメントアウトされた方法に修正しました。
まだ、懲りずに1pixelづつコピーしてます。。(笑
結構速くなりましたが、まだ全然だめな感じでいた。

そして最後、画像の矩形を一気にコピーするように修正すると、
我慢できる処理速度になりました。めでたしめでたし。


さてさて、実際に上記のスクリプトを~/Library/Application Support/Gimp/plug-ins(私の環境はMacなので)において、
Gimpを起動すると、フィルタメニューの項目にSSS...というものが表示されます。
SSS(Simple Sprite Sheet)の略なんですが、略す必要があるかと言われると。。ないです。(笑


使い方はその名の通り、シンプル(なはずです)。
このスクリプトの使い方は2種類あります。
(1) 現在の画像のレイヤーを順番に(上のレイヤーから順に)ならべる。
(2) 指定したディレクトリ中の画像ファイルを順番にならべる。

シンプルなだけあって、制約もあります。
並べる画像のサイズはすべて同じサイズでないといけません。
そうしないと、期待した結果が得られない可能性が高いです。


さて、スクリプトを起動すると以下のような画面が表示されます。

設定項目の説明ですが、
1. Sprite cell width
並べる画像の幅を指定します。
2. Sprite cell height
並べる画像の高さを指定します。
3. Columns per row
一行につき画像を何枚ならべるかを指定します。
4. Image source
Layerが選択されていた場合には、画像のレイヤーを上から順に並べて行きます。
Directoryが選択されている場合には、指定したディレクトリ中の画像を並べます。
5. Active layers only
Image sourceでLayerが選択された場合のみ有効です。
可視レイヤーのみを対象に処理を行います。
6. Directory
Image sourceでDirectoryが選択された場合のみ有効です。
読み込む画像の場所を指定します。
7. Image filter
Image sourceでDirectoryが選択された場合のみ有効です。
読み込む画像を拡張子でフィルタします。



処理結果のサンプル
Image sourceでLayerを選択して、5つのレイヤーを持っている画像に対して実行してみました。
Columns per rowは3です。
画像が左上から順番に並びました。



いやはや、なかなかいい経験でした。