Last active
August 9, 2018 07:23
-
-
Save Bentroen/456b20fc707a2874565d8ddd6d107d15 to your computer and use it in GitHub Desktop.
#PokeCA Model De-animator | Extract frames from animated Pokémon
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
######################################################################################## | |
# #PokeCA Model De-animator by Bentroen # | |
######################################################################################## | |
# # | |
# Usage instrucions: point 'input_path' to a folder containing the 'models' and # | |
# 'textures' folders from the original map's resource pack. The script will # | |
# fetch the correct model according to 'model_list', and export the following # | |
# to 'output_path': every frame to 'models/all_frames/', every first frame to # | |
# 'models/' and first frame of textures to 'textures/'. If the texture itself # | |
# is animated (changes from frame to frame, e.g. Ternion's blue fire), only # | |
# the first frame is retained. # | |
# # | |
######################################################################################## | |
from PIL import Image | |
import os | |
import shutil | |
import json | |
input_path = "resources" | |
output_path = "output" | |
watch_elements = False | |
number_models = True | |
def int_ify(x): | |
if isinstance(x, float) and x.is_integer(): | |
return int(x) | |
else: | |
return x | |
model_list = { | |
"elerind": "coarse_dirt", | |
"judulium": "stone", | |
"lunalium": "jungle_leaves", | |
"laviem": "dark_oak_log", | |
"lavestal": "pink_stained_hardened_clay", | |
"laviandal": "gray_stained_hardened_clay", | |
"krillbard": "quartz_block", | |
"calyx": "acacia_log", | |
"santrikos": "jungle_planks", | |
"katlin": "orange_wool", | |
"karnivora": "black_stained_hardened_clay", | |
"lupus": "hardened_clay", | |
"caniculus": "silver_stained_hardened_clay", | |
"rocleff": "birch_log", | |
"sandregin": "jungle_log", | |
"pyraiz": "cyan_stained_hardened_clay", | |
"magnamic": "purple_stained_hardened_clay", | |
"ferrousaur": "oak_leaves", | |
"glissadiaur": "spruce_leaves", | |
"oinklet": "dark_oak_stairs", | |
"snortchop": "acacia_stairs", | |
"gruntrot": "green_stained_hardened_clay", | |
"sandsylph": "dirt", | |
"cragraunt": "oak_planks", | |
"viser": "granite_smooth", | |
"graspawer": "diorite", | |
"viginoro": "diorite_smooth", | |
"sonyar": "white_stained_hardened_clay", | |
"soninger": "orange_stained_hardened_clay", | |
"sonfargio": "magenta_stained_hardened_clay", | |
"howlinger": "glass", | |
"permagin": "end_portal_frame", | |
"pernchiller": "ender_chest", | |
"bacillus": "brown_stained_hardened_clay", | |
"tindler": "gold_ore", | |
"phlogiston": "iron_ore", | |
"sparkfaere": "coal_ore", | |
"specter": "diamond_ore", | |
"specixel": "diamond_block", | |
"sodler": "sea_lantern", | |
"provender": "jukebox", | |
"wyvern": "obsidian", | |
"mecheladon": "gravel", | |
"currizerier": "lapis_ore", | |
"orthon": "piston", | |
"orthenizer": "sticky_piston", | |
"mediander": "tnt", | |
"lacuscular": "birch_leaves", | |
"meshuge": "redstone_lamp", | |
"glazer": "noteblock", | |
"branular": "crafting_table", | |
"cebraral": "furnace", | |
"miasmar": "end_stone", | |
"drabber": "bookshelf", | |
"folineal": "podzol", | |
"scrapper": "anvil_intact", | |
"brutifan": "anvil_slightly_damaged", | |
"brawfauster": "anvil_very_damaged", | |
"fluster": "jungle_planks", | |
"briller": "birch_planks", | |
"pixeal": "mossy_cobblestone", | |
"joulebo": "andesite", | |
"jitterizer": "andesite_smooth", | |
"joulitterial": "grass", | |
"ramme": "oak_log", | |
"rammeral": "spruce_log", | |
"aphotic": "red_stained_hardened_clay", | |
"shellefer": "sponge_wet", | |
"contretemps": "melon_block", | |
"grapnel": "lime_wool", | |
"incande": "mycelium", | |
"icandador": "nether_brick", | |
"quagger": "lit_pumpkin", | |
"rhizome": "redstone_ore", | |
"daeioldon": "granite", | |
"circumire": "sandstone_stairs", | |
"circigringler": "emerald_ore", | |
"plainolo": "clay", | |
"plainingan": "beacon", | |
"imbue": "prismarine", | |
"dousiler": "dark_prismarine", | |
"blulisiler": "prismarine_bricks", | |
"malicese": "yellow_wool", | |
"quintessence": "dropper", | |
"virago": "red_sand", | |
"fiendler": "iron_bars", | |
"fiendender": "packed_ice", | |
"fienderago": "hay_block", | |
"nymphador": "dark_oak_planks", | |
"bolstern": "smooth_sandstone", | |
"nettiler": "gold_block", | |
"branbar": "redstone_block", | |
"thoristican": "lapis_block", | |
"venin": "white_wool", | |
"chillbit": "chiseled_quartz_block", | |
"frigitice": "quartz_column", | |
"crystallix": "quartz_stairs", | |
"claringilizer": "magenta_wool", | |
"clarivoyanzer": "coal_block", | |
"hivani": "light_blue_stained_hardened_clay", | |
"gregarion": "yellow_stained_hardened_clay", | |
"gregariano": "lime_stained_hardened_clay", | |
"kasainu": "jungle_stairs", | |
"spiritear": "bedrock", | |
"croccal": "netherrack", | |
"crocette": "soul_sand", | |
"cromadillo": "glowstone", | |
"ovy": "brick_slab", | |
"ounoth": "brick_stairs", | |
"foaross": "quartz_ore", | |
"frosso": "sandstone", | |
"frozendo": "chiseled_sandstone", | |
"vultara": "cobblestone_slab", | |
"sinob": "stone_brick_slab", | |
"syrein": "quartz_slab", | |
"fangdrawl": "dark_oak_slab", | |
"zasuke": "nether_brick_stairs", | |
"whiffli": "stonebrick", | |
"whiffer": "mossy_stonebrick", | |
"whifflinzer": "cracked_stonebrick", | |
"malfa": "blue_stained_hardened_clay", | |
"conflar": "pink_wool", | |
"conflagg": "gray_wool", | |
"trumps": "iron_block", | |
"cluckalor": "red_sandstone", | |
"chrall": "cyan_wool", | |
"crassall": "purple_wool", | |
"permafloe": "smooth_red_sandstone", | |
"malicorith": "blue_wool", | |
"archai": "sandstone_slab", | |
"sanskorp": "oak_stairs", | |
"bibbo": "chest", | |
"dragifaere": "red_sandstone_slab", | |
"ternion": "stone_slab", | |
"terragon": "brown_wool", | |
"giltine": "dispenser", | |
"arceus": "sand" | |
} | |
model_path = os.path.join(input_path, "models", "item") | |
texture_path = os.path.join(input_path, "textures") | |
if os.path.exists(output_path): | |
for file in os.listdir(output_path): | |
file_path = os.path.join(output_path, file) | |
if os.path.isdir(file_path): | |
shutil.rmtree(file_path) | |
texture_out_path = os.path.join(output_path, "textures") | |
frame_out_path = os.path.join(output_path, "models") | |
for dir in (texture_out_path, frame_out_path): | |
if not os.path.exists(dir): | |
os.makedirs(dir) | |
for model_number, (model_name, model_file) in enumerate(list(model_list.items())): | |
filename = os.path.join(model_path, model_file + ".json") | |
with open(filename) as json_file: | |
model = json.load(json_file) | |
texture_name = list(model["textures"].values())[0] | |
print("Processing model {}/{}: {}{}(file: {})".format('{:03d}'.format(model_number+1),\ | |
len(model_list), model_name, (14-len(model_name))*" ", filename)) | |
img_name = os.path.join(texture_path, texture_name + '.png') | |
img = Image.open(img_name) | |
frame_count = img.height // img.width | |
if frame_count <= 4: | |
grid_size = 2 | |
elif frame_count <= 16: | |
grid_size = 4 | |
elif frame_count <= 64: | |
grid_size = 8 | |
frame_area = 16//grid_size | |
# append each element to a frame according to texture UV map | |
# (this calculation uses only the first face) | |
frames = [[] for x in range(frame_count)] | |
for count, element in enumerate(model["elements"]): | |
uv = list(element["faces"].values())[0]["uv"] | |
for i, x in enumerate(uv): | |
if x > 16: | |
uv[i] = x % 16 | |
x1, y1, x2, y2 = uv | |
row = int((y1 + y2)/2 // frame_area) | |
col = int((x1 + x2)/2 // frame_area) | |
frame = row*grid_size + col | |
# remap face UVs to use full texture space | |
# values are rounded to three decimal places to fix rounding errors | |
faces = element["faces"].keys() | |
uvs = [face["uv"] for face in element["faces"].values()] | |
for face, uv in zip(faces, uvs): | |
x1, y1, x2, y2 = uv | |
uv = [int_ify(round((round(x, 3) % frame_area), 3)*grid_size) for x in uv] | |
# The calculation above causes UVs to wrap by default (e.g. if frame_area = 8 and | |
# one of the UVs is 8, (8 % 8) will return 0, wrapping to the left of the texture). | |
# This is the intended result if the UV is something like (8, 12), but not if it's | |
# (4, 8), because 8 in this case refers to the rightmost point. The following code | |
# checks if this wrap should occur (uv = 0/touching left or top edge of texture) | |
# or not (uv = 16/touching right or bottom edge of texture), and if that's the case, | |
# sets the UV to frame_area * grid_size (which resolves to 16) to "undo" the wrap | |
if x1 % frame_area == 0 and (x1 + x2)/2 < x1: uv[0] = 16 | |
if x2 % frame_area == 0 and (x1 + x2)/2 < x2: uv[2] = 16 | |
if y1 % frame_area == 0 and (y1 + y2)/2 < y1: uv[1] = 16 | |
if y2 % frame_area == 0 and (y1 + y2)/2 < y2: uv[3] = 16 | |
element["faces"][face]["uv"] = uv | |
element["faces"][face]["texture"] = "#texture" | |
# fix rounding errors in element coordinates | |
from_field = element["from"] | |
to_field = element["to"] | |
element["from"] = [int_ify(round(x, 3)) for x in from_field] | |
element["to"] = [int_ify(round(x, 3)) for x in to_field] | |
if watch_elements: | |
print("element {}/{} | frame {}/{} | col: {} | row: {} | UV: {}" .format( \ | |
count+1, len(model["elements"]), frame+1, len(frames), col, row, uv)) | |
frames[frame].append(element) | |
if number_models: | |
numbered_model_name = '{:03d}'.format(model_number+1) + "_" + model_name | |
else: | |
numbered_mode_name = model_name | |
model_out_path = os.path.join(output_path, "models", "all_frames", numbered_model_name) | |
if not os.path.exists(model_out_path): | |
os.makedirs(model_out_path) | |
for frame_number, frame in enumerate(frames): | |
# copy original model and replace some fields prior to exporting | |
new_model = model | |
new_model["__comment"] = "This model has been generated using Bentroen's model de-animator" | |
new_model["textures"] = {} | |
new_model["textures"]["texture"] = model_name | |
new_model["elements"] = frame | |
# dump as json file | |
model_file_name = os.path.join(model_out_path, model_name + "_" + str(frame_number+1) + '.json') | |
with open(model_file_name, 'w') as json_file: | |
json.dump(new_model, json_file, indent=4, separators=(',', ': ')) | |
# create a copy if it's the first frame | |
if frame_number == 0: | |
src = model_file_name | |
dst = os.path.join(frame_out_path, numbered_model_name + '.json') | |
shutil.copy(src, dst) | |
new_img_size = img.width/grid_size | |
new_img = img.crop((0, 0, new_img_size, new_img_size)) | |
img_name = os.path.join(texture_out_path, model_name + '.png') | |
new_img.save(img_name) | |
img.close() | |
print("\nProcessing done!") | |
print("Shutting down...") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment