create-mekanism-modular/tools/bb_export.py

169 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Write the armour placement out as a Blockbench project.
bb_export.py <armory.jar> [out.bbmodel]
Everything that positions a piece is drawn: the wearer, Armory's plates, this
mod's bezels, and - as the only unlocked cubes - the pieces whose transforms are
ours to change. Each of those carries its destination in its own name, in an
`[edit:...]` tag, so the file is the only thing `bb_import.py` needs.
The reference geometry is locked rather than merely present. A gem is aimed at a
bezel, and a bezel that can be nudged by accident is a bezel that stops being an
answer.
"""
import json
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mcmodel as mc
import ARMOUR_EDITOR as ed
import blockbench as bb
REPO = ed.REPO
SOCKETS = ed.SOCKETS
SPACE = 'src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space'
# hue per body part, so a group reads as a unit in the outliner
COLOURS = {'left_arm': 1, 'right_arm': 2, 'left_leg': 3, 'right_leg': 4,
'body': 5, 'head': 6}
def box_cube(name, quads, matrix, pivot, colour, locked):
"""One drawn thing, already composed, as a Blockbench cube."""
pts = np.vstack([q.pts for q in quads])
pts = (np.column_stack([pts, np.ones(len(pts))]) @ np.asarray(matrix).T)[:, :3]
pts = pts + np.asarray(pivot, float)
centre, lo, hi, r = bb.oriented_box(pts, matrix)
return bb.cube(name,
origin=bb.to_bb(centre),
lo=bb.to_bb(centre)[0] * 0 + bb.to_bb(centre) + lo, # placeholder
hi=bb.to_bb(centre) + hi,
rotation=bb.matrix_to_euler_zyx(bb.rot_to_bb(r)),
colour=colour, locked=locked)
def main(jar, out):
class A:
pass
args = A()
args.scene = 'sockets'; args.jar = [jar]; args.variant = 'default'
args.gem = 'medium'; args.chain = False; args.exact_merge = False
args.layer1 = args.layer2 = 'miapi:item/armor/base/iron/layer_1'
res = mc.Resources([jar, os.path.join(REPO, 'src/main/resources')])
elements, outliner = [], []
textures, tex_index = [], {}
def texture(ref):
"""Embed a texture once, and hand back the index a face refers to it by."""
if ref is None:
return None
if ref not in tex_index:
try:
img = res.image(ref)
except (KeyError, OSError):
tex_index[ref] = None
return None
tex_index[ref] = len(textures)
textures.append(bb.texture_entry(ref.split('/')[-1], img, len(textures)))
return tex_index[ref]
def add(grp_children, name, quads, matrix, pivot, colour, locked):
pts = np.vstack([q.pts for q in quads])
pts = (np.column_stack([pts, np.ones(len(pts))]) @ np.asarray(matrix).T)[:, :3]
pts = pts + np.asarray(pivot, float)
centre, lo, hi, r = bb.oriented_box(pts, matrix)
c = bb.to_bb(centre)
for q in quads:
texture(q.texture)
faces = bb.faces_from_quads(quads, matrix, r, tex_index)
cu = bb.cube(name, origin=c, lo=c + lo, hi=c + hi,
rotation=bb.matrix_to_euler_zyx(bb.rot_to_bb(r)),
colour=colour, locked=locked, faces=faces)
elements.append(cu)
grp_children.append(cu['uuid'])
# ---------------------------------------------------------------- wearer
body = []
for part, (o, s, off, mirror) in mc.HUMANOID.items():
if part == 'hat':
continue
q = mc.cube_quads(o, s, off, 0.0, mirror, (64, 64),
'minecraft:entity/player/wide/steve', 0)
add(body, f'{part} (wearer)', q, np.eye(4), mc.PIVOTS[part], 8, True)
outliner.append(bb.group('WEARER (locked reference)', body))
# ------------------------------------------------------------ the limbs
chest = json.loads(res.read('data/tm_armory/miapi/modules/armor/chestplate.json'))['slots']
pants = json.loads(res.read('data/tm_armory/miapi/modules/armor/pants.json'))['slots']
slots = {'arm_left': chest['arm_left'], 'arm_right': chest['arm_right'],
'leg_left': pants['leg_left'], 'leg_right': pants['leg_right']}
gem_q = mc.model_quads(res.model('miapi:models/item/armor/gems/medium/'
'[material.texture].json', 'default'), res)
for part, slot, model in ed.PLATES:
kids = []
limb_tr = slots[slot]['transform']
limb = mc.transform_matrix(limb_tr)
piv = mc.PIVOTS[part]
col = COLOURS[part]
plate = mc.model_quads(res.model(f'miapi:models/item/armor/model/{model}/'
'[material.texture].json', 'default'), res)
add(kids, f'{slot} plate', plate, limb, piv, col, True)
try:
bez = mc.model_quads(res.model(f'cmmodular:models/item/armor/model/{slot}/'
'socket/[material.texture].json', 'default'), res)
add(kids, f'{slot} bezel', bez, limb, piv, col, True)
except KeyError:
pass
path = os.path.join(REPO, SOCKETS, f'{slot}.json')
with open(path) as fh:
gem_tr = json.load(fh)['data']['replace']['slots']['gem']['transform']
world = mc.merge(mc.transform_matrix(gem_tr), limb, True)
add(kids, f'{slot} GEM [edit:socket/{slot}#gem]', gem_q, world, piv, col, False)
outliner.append(bb.group(part, kids, origin=bb.to_bb(piv)))
# ------------------------------------------------------------ space gear
helmet = json.loads(res.read('data/tm_armory/miapi/modules/armor/helmet.json'))['slots']
# The `hat` slot is what puts a helmet on the head rather than the neck.
for part, name, outer_tr, path_in_repo in (
('body', 'life_support_socket', chest['chest_back']['transform'],
f'{SPACE}/life_support_socket.json'),
('head', 'helmet_socket', helmet['hat']['transform'],
f'{SPACE}/helmet_socket.json')):
kids = []
piv = mc.PIVOTS[part]
outer = mc.transform_matrix(outer_tr)
with open(os.path.join(REPO, path_in_repo)) as fh:
module = json.load(fh)
for i, entry in enumerate(module['data']['replace']['model']):
tr = entry['transform']
if tr.get('origin') not in mc.BODY_PARTS:
continue # inventory model; MIAPI never draws it on a body
q = mc.model_quads(res.model(entry['path'], 'default'), res)
world = mc.merge(mc.transform_matrix(tr), outer, True)
add(kids, f'{name} [edit:space/{name}#model{i}]', q, world, piv,
COLOURS[part], False)
if kids:
outliner.append(bb.group(f'{part} - {name}', kids, origin=bb.to_bb(piv)))
doc = bb.project('cmmodular placement', outliner, elements, textures)
with open(out, 'w') as fh:
json.dump(doc, fh, indent=1)
editable = [e['name'] for e in elements if not e['locked']]
print(f'{out}\n {len(elements)} cubes, {len(outliner)} groups, '
f'{len(textures)} textures')
print(' editable:')
for n in editable:
print(f' {n}')
if __name__ == '__main__':
main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else 'placement.bbmodel')