Browse Source

Add a helper script to transform vertical_frames (strip animations) into Lua+sheet_2d (sprite sheet).

master
poikilos 10 months ago
parent
commit
f9ce1563c6
  1. 276
      pyenliven/mttexture.py
  2. 14
      utilities/recompose-anim

276
pyenliven/mttexture.py

@ -0,0 +1,276 @@
'''
Transpose an animation from type="vertical_frames" to type="sheet_2d"
Usage:
recompose-anim <path> <aspect_w> <aspect_h> <frames_w> <frames_h> <length>
Options:
path The image file.
aspect_w The width of each frame in the multi-frame file.
aspect_h The height of each frame in the multi-frame file.
frames_w The number of frames across the new image should be.
frames_h The number of frames down the new image should be.
length Total length of the animation in seconds (which is the
necessary argument when type="vertical_frames"). This
value is only required since this program shows Lua in
standard output: The value will be automatically
converted to a frame_length (single-frame time in
seconds) argument for the node which is the argument
necessary when type="sheet_2d".
Examples:
# [Mod] Mom and Pop Furniture [mapop]:
recompose-anim mp_channel_rainbow.png 64 48 8 8 2
recompose-anim mp_channel_cube.png 40 40 5 5 3
recompose-anim mp_channel_blast.png 64 64 8 8 5
# ^ 5 5 is optional, but ideal since 1200/48 is 25 as square textures
# are efficient (but power of 2 textures such as 512x512 are better
# and more efficient on older video cards such as on old iPhones)
'''
from __future__ import print_function
import sys
import os
from collections import OrderedDict
from PIL import Image
from pprint import pformat
def echo0(*args, **kwargs):
dst = sys.stderr
if 'file' in kwargs:
dst = kwargs['file']
del kwargs['file']
print(*args, file=dst, **kwargs)
def usage():
echo0(__doc__)
echo0()
option_metas = [
{
'name': "path",
},
{
'name': "aspect_w",
'type': "int",
},
{
'name': "aspect_h",
'type': "int",
},
{
'name': "frames_w",
'type': "int",
},
{
'name': "frames_h",
'type': "int",
},
{
'name': "length",
'type': "int",
},
]
class Framer:
def __init__(self):
self.frames = []
def load_vertical_strip(self, image, aspect_w, aspect_h):
results = OrderedDict()
results['-- in_count'] = 0
# results['-- out_count'] = 0 # set by save methods instead.
w, h = image.size
inverse_ar = float(aspect_h) / float(aspect_w)
# ^ inverse aspect ratio
frame_h = int(round(inverse_ar*w))
if h % frame_h != 0:
raise ValueError(
"Height {} of image is not evenly divisible by the"
" calculated frame_height {}--calculated using"
" frame_height = (aspect_h/aspect_w)*image_w = ({}/{})*{}"
"".format(h, frame_h, aspect_h, aspect_w, w)
)
top = 0
left = 0
right = w # exclusive
bottom = top + frame_h # exclusive
while top < h:
frame = image.crop((left, top, right, bottom))
results['-- in_count'] += 1
self.frames.append(frame)
# results['-- out_count'] += 1
top += frame_h
bottom += frame_h
return results
def save_sheet_2d(self, path, frames_w, frames_h):
results = OrderedDict()
if not self.frames:
raise RuntimeError("You must load first.")
frame_w, frame_h = self.frames[0].size
w = frame_w * frames_w
h = frame_h * frames_h
image = Image.new('RGBA', (w, h), 0)
top = 0
index = 0
dest_count = frames_w * frames_h
time_scale = float(dest_count) / float(len(self.frames))
if time_scale > 1.0:
time_scale = 1.0 # just leave some blank
inv_time_scale = 1.0 / time_scale
last_dest_frame = 0
for y_frame in range(frames_h):
left = 0
for x_frame in range(frames_w):
if index >= len(self.frames):
# The animation is complete.
break
source_index = int(round(inv_time_scale * float(index)))
echo0("{} saved as frame {}".format(source_index, index))
image.paste(self.frames[source_index], (left, top))
# ^ paste copies alpha (alpha_composite does overlay)
last_dest_frame = index
left += frame_w
index += 1
top += frame_h
results['-- out_count'] = last_dest_frame + 1
image.save(path)
return results
def recompose_anim(options):
"""Recompose a vertical strip animation
Convert from Minetest animation type="vertical_frames" to
type="sheet_2d" (transpose to horizontal then split into rows).
Args:
options (dict): Configure how to read and convert the animation.
For all options, see docstring of transpose-anim Python
file.
Returns:
dict: The values that should be used in the texture table (or
any of the tiles tables) in the minetest.register_node call.
Additional values are in comment notation:
'-- in_count' (the detected input frame count)
'-- out_count' (the generated output frame count)
"""
path = options.get('path')
if path is None:
raise ValueError("You must specify a path.")
results = OrderedDict()
if not os.path.isfile(path):
raise FileNotFoundError(path)
for option_meta in option_metas:
name = option_meta['name']
value = options.get(name)
if value is None:
raise ValueError(
"{} is required.".format(name)
)
option_type_name = option_meta.get('type')
if option_type_name:
if type(value).__name__ != option_type_name:
raise ValueError(
"{} should be a(n) {} but got {} {}."
"".format(name, option_type_name,
type(value).__name__, pformat(value))
)
framer = Framer()
img = Image.open(options['path'])
rgba = img.convert("RGBA")
load_results = framer.load_vertical_strip(
rgba,
options['aspect_w'],
options['aspect_h'],
)
nameNoExt, dotExt = os.path.splitext(options['path'])
results['name'] = nameNoExt+"_sheet_2d"+dotExt
load_results.update(framer.save_sheet_2d(
results['name'],
options['frames_w'],
options['frames_h'],
))
# ^ update gets the new '-- out_count'
results['type'] = "sheet_2d"
results['frames_w'] = options['frames_w']
results['frames_h'] = options['frames_h']
results.update(load_results) # Add the frame count comment(s)
results['frame_length'] = options['length'] / load_results['-- in_count']
# ^ It is the in_count not out_count, since 'length' is based on original
time_scale = (
float(load_results['-- out_count'])
/ float(load_results['-- in_count'])
)
inv_time_scale = 1.0 / time_scale
results['frame_length'] = results['frame_length'] * inv_time_scale
# ^ frame_length should be scaled by the output vs input
# (inverse since time becomes longer if frame count became less)
return results
def main():
options = {}
for i, option_meta in enumerate(option_metas):
argi = i + 1
if len(sys.argv) <= argi:
usage()
sys.stderr.write("Error: You must specify all options. Missing {}"
"".format(option_meta['name']))
missing_count = len(option_metas) - (len(sys.argv) - 1)
if missing_count > 1:
echo0(" (and the rest after it above).")
else:
echo0(".")
echo0('- If the texture is used in the mod, you can find'
' potential value(s) for in the Lua file'
' (Use same values as used in register_node).'
''.format())
return 1
arg = sys.argv[argi]
if option_meta.get('type') == "int":
try:
arg = int(arg)
except ValueError:
usage()
echo0("Error: {} should be a number but got {}."
"".format(option_meta['name'], arg))
return 1
options[option_meta['name']] = arg
if not os.path.isfile(options['path']):
# Avoid an exception & use CLI-style (pipe & filter) logic
usage()
echo0('Error: "{}" does not exist.'.format(options['path']))
return 1
results = recompose_anim(options)
echo0("")
echo0("-- Patching instructions (change the following arguments in")
echo0(" the animation table which is in the Tile definition table):")
echo0("-- Remove:")
echo0('- type = "vertical_strip",')
echo0('- aspect_w = {},'.format(options['aspect_w']))
echo0('- aspect_h = {},'.format(options['aspect_h']))
echo0('- length = {},'.format(options['length']))
frame_length = results['frame_length']
echo0("-- Add:")
new_path = results['name']
del results['name']
for key, value in results.items():
if isinstance(value, str):
print(' {} = "{}",'.format(key, value))
else:
print(' {} = {},'.format(key, value))
print(' frame_length = {},'.format(frame_length))
print('-- Also (in Tile definition, but outside of animation table)')
print('-- change texture name (or overwrite original "{}"'
''.format(os.path.basename(options['path'])))
print('- & keep Lua same) with:')
print('-- name="{}"'.format(new_path))
return 0

14
utilities/recompose-anim

@ -0,0 +1,14 @@
#!/usr/bin/env python
from __future__ import print_function
import sys
import os
SCRIPTS_DIR = os.path.dirname(os.path.realpath(__file__))
REPO_DIR = os.path.dirname(SCRIPTS_DIR)
sys.path.insert(0, REPO_DIR) # let import find pyenliven when it's not installed
from pyenliven.mttexture import main
if __name__ == "__main__":
sys.exit(main())
Loading…
Cancel
Save