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