111 lines
3.3 KiB
Python
111 lines
3.3 KiB
Python
"""Character reference: topology and part names exported from a master character.
|
|
|
|
The reference JSON lets every character file be compared with the master
|
|
character without opening it:
|
|
|
|
- ``parts``: inner mouth / eye object names, with the character name replaced
|
|
by ``{asset}`` (e.g. ``GEO_{asset}_Eye_L``).
|
|
- ``body``: vertex/edge/face counts and the face vertex indices of the body
|
|
mesh, used to compare vertex order.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import zlib
|
|
|
|
import bpy
|
|
import numpy as np
|
|
|
|
from .. import config
|
|
from .mesh import polygon_arrays
|
|
|
|
FORMAT_VERSION = 1
|
|
|
|
|
|
def templated(name, asset):
|
|
"""Replace the ``asset`` token of an underscore separated name by ``{asset}``."""
|
|
return "_".join("{asset}" if token == asset else token for token in name.split("_"))
|
|
|
|
|
|
def part_objects(objects):
|
|
keywords = [k.lower() for k in config.CHAR_PART_KEYWORDS]
|
|
return [ob for ob in objects if any(k in ob.name.lower() for k in keywords)]
|
|
|
|
|
|
def body_name(asset):
|
|
return config.CHAR_BODY_NAME.format(asset=asset)
|
|
|
|
|
|
def _encode(array):
|
|
return base64.b64encode(zlib.compress(np.ascontiguousarray(array, dtype=np.int32).tobytes())).decode("ascii")
|
|
|
|
|
|
def _decode(text):
|
|
return np.frombuffer(zlib.decompress(base64.b64decode(text)), dtype=np.int32)
|
|
|
|
|
|
def topology_signature(mesh):
|
|
_, totals, loop_verts = polygon_arrays(mesh)
|
|
digest = hashlib.sha1(totals.tobytes() + loop_verts.tobytes()).hexdigest()
|
|
return {
|
|
"vertices": len(mesh.vertices),
|
|
"edges": len(mesh.edges),
|
|
"faces": len(mesh.polygons),
|
|
"hash": digest,
|
|
"loop_totals": _encode(totals),
|
|
"loop_vertices": _encode(loop_verts),
|
|
}
|
|
|
|
|
|
def decode_topology(body):
|
|
return _decode(body["loop_totals"]), _decode(body["loop_vertices"])
|
|
|
|
|
|
def reference_path(prefs=None):
|
|
if prefs is not None and getattr(prefs, "char_reference_path", ""):
|
|
return bpy.path.abspath(prefs.char_reference_path)
|
|
return os.environ.get("WTD_CHAR_REFERENCE", "") or config.CHAR_REFERENCE_PATH
|
|
|
|
|
|
_cache: dict[str, tuple[float, dict]] = {}
|
|
|
|
|
|
def load_reference(path):
|
|
"""Load a reference JSON, cached by modification time. None if missing."""
|
|
if not path or not os.path.isfile(path):
|
|
return None
|
|
mtime = os.path.getmtime(path)
|
|
cached = _cache.get(path)
|
|
if cached and cached[0] == mtime:
|
|
return cached[1]
|
|
with open(path, encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
_cache[path] = (mtime, data)
|
|
return data
|
|
|
|
|
|
def export_reference(scene, asset, path):
|
|
"""Write the reference of the character ``asset`` found in ``scene``."""
|
|
body = scene.objects.get(body_name(asset))
|
|
if body is None or body.type != "MESH":
|
|
raise ValueError(f"Body mesh '{body_name(asset)}' not found in the scene")
|
|
if body.mode == "EDIT":
|
|
body.update_from_editmode()
|
|
|
|
parts = sorted({templated(ob.name, asset) for ob in part_objects(scene.objects)})
|
|
data = {
|
|
"format_version": FORMAT_VERSION,
|
|
"source_asset": asset,
|
|
"source_file": bpy.data.filepath,
|
|
"parts": parts,
|
|
"body": topology_signature(body.data),
|
|
}
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
json.dump(data, fh, indent=2)
|
|
_cache.pop(path, None)
|
|
return data
|