147 lines
6.2 KiB
Python
147 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Read a Blockbench project back into the module JSON.
|
|
|
|
bb_import.py <armory.jar> <placement.bbmodel> [--write]
|
|
|
|
Only cubes tagged `[edit:<file>#<target>]` are read; everything else in the file
|
|
is reference geometry. For each one the cube's box is turned back into the
|
|
transform that would produce it:
|
|
|
|
D = T(t) @ R @ S the world matrix MIAPI must end up with
|
|
own = inv(slot_chain) @ D the transform the module file actually stores
|
|
|
|
`own` is then decomposed to translation/Euler/scale, which is all a MIAPI
|
|
transform can hold. That step can lose something: the limb slots are a rotation
|
|
against a non-uniform scale, and inverting one puts shear into `own` that the
|
|
triple cannot carry - so the result is measured against the cube it came from
|
|
and the error is printed in pixels. A tenth of a pixel is nothing; a whole one
|
|
means the pose asked for cannot be expressed and wants a different rotation.
|
|
|
|
Blockbench composes a cube's rotation as ZYX and MIAPI composes its own as XYZ,
|
|
so nothing here passes Euler triples between the two - only matrices.
|
|
"""
|
|
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'
|
|
|
|
|
|
def local_box(res, path, variant='default'):
|
|
"""The model's own extents, before anything is applied to it."""
|
|
q = mc.model_quads(res.model(path, variant), res)
|
|
pts = np.vstack([x.pts for x in q])
|
|
return (pts.min(0) + pts.max(0)) / 2, (pts.max(0) - pts.min(0)) / 2, q
|
|
|
|
|
|
def target_info(res, spec):
|
|
"""Where a tag points: the module file, its model, and the chain above it."""
|
|
where, key = spec.split('#')
|
|
kind, name = where.split('/')
|
|
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']
|
|
if kind == 'socket':
|
|
path = os.path.join(REPO, SOCKETS, f'{name}.json')
|
|
slot = (chest if name.startswith('arm') else pants)[name]['transform']
|
|
part = {'arm_left': 'left_arm', 'arm_right': 'right_arm',
|
|
'leg_left': 'left_leg', 'leg_right': 'right_leg'}[name]
|
|
keys = ['data', 'replace', 'slots', key, 'transform']
|
|
model = 'miapi:models/item/armor/gems/medium/[material.texture].json'
|
|
return path, keys, mc.transform_matrix(slot), mc.PIVOTS[part], model
|
|
path = os.path.join(REPO, SPACE, f'{name}.json')
|
|
i = int(key.replace('model', ''))
|
|
with open(path) as fh:
|
|
entry = json.load(fh)['data']['replace']['model'][i]
|
|
origin = entry['transform'].get('origin')
|
|
if origin == 'body':
|
|
outer = mc.transform_matrix(chest['chest_back']['transform'])
|
|
elif origin == 'head':
|
|
helmet = json.loads(res.read('data/tm_armory/miapi/modules/armor/helmet.json'))['slots']
|
|
outer = mc.transform_matrix(helmet['hat']['transform'])
|
|
else:
|
|
outer = np.eye(4)
|
|
keys = ['data', 'replace', 'model', i, 'transform']
|
|
return path, keys, outer, mc.PIVOTS[origin], entry['path']
|
|
|
|
|
|
def main(jar, model_path, write):
|
|
res = mc.Resources([jar, os.path.join(REPO, 'src/main/resources')])
|
|
doc = json.load(open(model_path))
|
|
np.set_printoptions(precision=3, suppress=True)
|
|
|
|
edits = [e for e in doc['elements'] if '[edit:' in e.get('name', '')]
|
|
if not edits:
|
|
print('no [edit:...] cubes in that file'); return 1
|
|
print(f'{len(edits)} editable cube(s)\n')
|
|
|
|
for cu in edits:
|
|
spec = cu['name'].split('[edit:')[1].split(']')[0]
|
|
path, keys, outer, pivot, model = target_info(res, spec)
|
|
c_local, h_local, quads = local_box(res, model)
|
|
|
|
# the cube, back in world coordinates
|
|
lo = np.asarray(cu['from'], float); hi = np.asarray(cu['to'], float)
|
|
c_bb = (lo + hi) / 2
|
|
h_bb = (hi - lo) / 2
|
|
r_bb = bb.euler_zyx_to_matrix(cu.get('rotation', [0, 0, 0]))
|
|
c_world = bb.to_world(c_bb)
|
|
r_world = bb.rot_to_bb(r_bb) # conjugation is its own inverse
|
|
|
|
scale = np.where(h_local > 1e-9, h_bb / np.where(h_local > 1e-9, h_local, 1), 1.0)
|
|
s = np.diag(scale)
|
|
t = (c_world - np.asarray(pivot, float)) - r_world @ s @ c_local
|
|
|
|
d = np.eye(4); d[:3, :3] = r_world @ s; d[:3, 3] = t
|
|
own = np.linalg.inv(outer) @ d
|
|
tr = mc.decompose(own)
|
|
|
|
# what MIAPI will actually render, after the lossy merge
|
|
got = mc.merge(mc.transform_matrix(tr), outer, True)
|
|
pts = np.vstack([q.pts for q in quads])
|
|
want = (np.column_stack([pts, np.ones(len(pts))]) @ d.T)[:, :3]
|
|
have = (np.column_stack([pts, np.ones(len(pts))]) @ got.T)[:, :3]
|
|
err = np.linalg.norm(want - have, axis=1).max()
|
|
|
|
flag = '' if err < 0.05 else (' <-- lossy, check it' if err < 0.5 else ' <-- CANNOT BE EXPRESSED')
|
|
print(f'{spec}')
|
|
print(f' world centre {(c_world).round(3)} size {(h_bb*2).round(3)}')
|
|
print(f' -> t {[round(v,4) for v in tr["translation"].values()]}'
|
|
f' r {[round(v,4) for v in tr["rotation"].values()]}'
|
|
f' s {[round(v,4) for v in tr["scale"].values()]}')
|
|
print(f' residual after MIAPI\'s merge: {err:.4f}px{flag}')
|
|
|
|
if write:
|
|
with open(path) as fh:
|
|
doc_j = json.load(fh)
|
|
node = doc_j
|
|
for k in keys[:-1]:
|
|
node = node[k]
|
|
keep = node[keys[-1]].get('origin')
|
|
node[keys[-1]] = {
|
|
'rotation': {a: round(float(tr['rotation'][a]), 4) for a in 'xyz'},
|
|
'translation': {a: round(float(tr['translation'][a]), 4) for a in 'xyz'},
|
|
'scale': {a: round(float(tr['scale'][a]), 4) for a in 'xyz'},
|
|
}
|
|
if keep:
|
|
node[keys[-1]]['origin'] = keep
|
|
with open(path, 'w') as fh:
|
|
json.dump(doc_j, fh, indent=2); fh.write('\n')
|
|
print(f' wrote {os.path.relpath(path, REPO)}')
|
|
print()
|
|
if not write:
|
|
print('(dry run - pass --write to apply)')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv[1], sys.argv[2], '--write' in sys.argv))
|