Skip to content

Instantly share code, notes, and snippets.

@Bentroen
Last active August 9, 2018 07:23
Show Gist options
  • Save Bentroen/456b20fc707a2874565d8ddd6d107d15 to your computer and use it in GitHub Desktop.
Save Bentroen/456b20fc707a2874565d8ddd6d107d15 to your computer and use it in GitHub Desktop.
#PokeCA Model De-animator | Extract frames from animated Pokémon
########################################################################################
# #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