poikilos
1 year ago
2 changed files with 290 additions and 0 deletions
@ -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 |
|||
|
@ -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…
Reference in new issue