Compare commits

...

2 Commits

  1. 276
      pyenliven/mttexture.py
  2. 79
      pyenliven/objanalyze.py
  3. 4
      utilities/blender/count_objects.py
  4. 19
      utilities/objanalyze
  5. 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

79
pyenliven/objanalyze.py

@ -0,0 +1,79 @@
#!/usr/bin/env python
'''
Count the number of meshes in OBJ files. In the Wavefront OBJ format,
these meshes are technically called "Objects" and the OBJ file can have
any number of them. They are created by the "o" command in the file.
Alternative (OBJ or non-OBJ):
- For support of any file that can be imported into Blender, try pasting
count_objects.py's contents (found in
EnlivenMinetest/utilities/blender/) into a Blender script Window
(Follow instructions in docstring at top of file).
'''
from __future__ import print_function
import sys
import os
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()
def obj_stats(path):
results = {
'mesh_count': 0,
}
if not os.path.isfile(path):
raise FileNotFoundError(path)
lineN = 0
with open(path, 'rb') as stream:
for rawL in stream:
lineN += 1 # counting numbers start at 1
line = rawL.strip()
if not line.strip():
# blank line
continue
parts = line.split()
if parts[0] == b"o":
results['mesh_count'] += 1
else:
pass
# echo0('line {}: Does not start with "o": {}'
# ''.format(lineN, parts))
return results
def main():
if len(sys.argv) < 2:
usage()
echo0("You must provide an OBJ filename.")
return 1
path = sys.argv[1]
if not path.lower().endswith(".obj"):
usage()
echo0("Error: Only OBJ files can be analyzed. Try pasting"
" count_objects.py's contents into a Blender script window"
" (Follow instructions in docstring at top of file)")
" to analyze any file you can import into Blender."
return 1
if not os.path.isfile(path):
# Avoid an exception & use CLI-style (pipe & filter) logic
usage()
echo0('Error: "{}" does not exist.'.format(path))
return 1
stats = obj_stats(path)
print(stats['mesh_count'])
return 0
if __name__ == "__main__":
sys.exit(main())

4
utilities/blender/count_objects.py

@ -13,6 +13,10 @@ Usage:
the utilities/blender directory at
github.com/poikilos/EnlivenMinetest).
- "Text", "Run Script".
Alternatives:
OBJ files can be analyzed in the terminal with a terminal Python script:
objanalyze <file>
'''
import bpy

19
utilities/objanalyze

@ -0,0 +1,19 @@
#!/usr/bin/env python
'''
Count the number of meshes in OBJ files. In the Wavefront OBJ format,
these meshes are technically called "Objects" and the OBJ file can have
any number of them. They are created by the "o" command in the file.
'''
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.objanalyze import main
if __name__ == "__main__":
sys.exit(main())

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