LIB_poser_pmd.py
1    #############################################################################
2    #
3    # ---------- GNU-License Standardtext ---------------------------------------
4    # This program is free software; you can redistribute it and/or modify
5    # it under the terms of the GNU General Public License as published by
6    # the Free Software Foundation.
7    #
8    # This program is distributed in the hope that it will be useful,
9    # but WITHOUT ANY WARRANTY; without even the implied warranty of
10   # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11   # GNU General Public License for more details.
12   #
13   # You should have received a copy of the GNU General Public License along
14   # with this program; if not, write to the Free Software Foundation, Inc.,
15   # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16   # ---------------------------------------------------------------------------
17   #
18   # You can find a copy of the GNU-License here:
19   # http://www.gnu.org/licenses/licenses.html#GPL
20   #
21   # If you are unsure, or in case of any questions, feel free to contact me.
22   #
23   #############################################################################
24   
25   # 2021/10/14 --> Now compatible with Python 3.x
26   
27   from __future__ import division, print_function
28   
29   import struct, re, sys, os, time
30   from collections import namedtuple, OrderedDict
31   import uuid
32   import random
33   
34   if sys.version_info.major > 2:
35       # Python 3 (Poser 12 and above)
36       basestring = str
37       map = lambda a, b: [a(_b) for _b in b]
38       long = int
39   
40   OD = OrderedDict  # just a shortcut
41   
42   VERTEX = namedtuple("VERTEX", "x y z")
43   DELTA = namedtuple("DELTA", "index, vertex")
44   
45   
46   class OutputStream(object):
47       def __init__(self, handle, close=False):
48           self.handle = handle
49           self.close = close
50   
51 def __enter__(self): 52 self.__save = sys.stdout 53 sys.stdout = self.handle 54
55 def __exit__(self, exc_type, exc_val, exc_tb): 56 if self.close: 57 self.handle.close() 58 sys.stdout = self.__save 59 60
61 def append_morph(targets, 62 morphname, 63 actorname, 64 UUID=None, 65 deltas=None): 66 """ 67 Creates a morphtarget with name <<morphname>> in OrderedDict 68 <<targets>> for actor <<actorname>>. 69 70 If no UUID is given (<<UUID>> is None), a random UUID is 71 generated for this morph. 72 73 It is allowed to leave <<deltas>> empty (deltas = None). 74 If deltas is not empty, it must be a list of [ 75 [delta-index, delta.x, delta.y, delta.z], 76 [...], 77 ] 78 e.g.: [ 79 [0, -0.001, 0.234, 0.543], 80 [1, -0.001, 0.236, 0.543], 81 [2, -0.0, 0.0, 0.0], 82 [3, 0.0, 0.234, 0.543], 83 ] 84 Lines with empty entries like [delta-index, 0.0, 0.0, 0.0] 85 will be ignored to keep the list as short as possible. 86 87 If a Wavefront-object file should be used as morphdata, 88 use function append_morph_from_obj(target, filename) instead. 89 90 :param targets: OrderedDict or PoserPmdFile object (morph is appended to this object). 91 :param morphname: Name of the morph as (type string). 92 :param actorname: Name of the actor this morph is made for (type string). 93 :param UUID: uuid (type string) 94 :param deltas: morphdata (type list) 95 :return: None 96 """ 97 98 obj = targets # save orginal target if changed 99 if isinstance(targets, PoserPmdFile): 100 targets = targets.targets 101 102 if not isinstance(targets, OrderedDict): 103 raise TypeError("targets must be %s" % OrderedDict) 104 105 if not len(morphname): 106 raise ValueError("morphname is an empty string") 107 108 if not len(actorname): 109 raise ValueError("actorname is an empty string") 110 111 if deltas is None: 112 deltas = list() 113 114 target = targets.setdefault(morphname, OrderedDict()) 115 actor = target.setdefault(actorname, OrderedDict()) 116 actor["UUID"] = UUID or new_uuid() 117 actor["numb_deltas"] = len(deltas) 118 119 # Make a copy of deltas and filter out entries with 0.0, 0.0, 0.0 120 actor["deltas"] = [DELTA(index=i, vertex=v) for i, v in deltas if i >= 0 and v != [0.0, 0.0, 0.0]] 121 122 return actor 123 124
125 def append_morph_from_obj( 126 targets, # class OrderedDict or PoserPmdFile 127 filename, # string with a path to a wafront file 128 morphname=None, # name of the morph to append 129 actorname=None): # actor this morph should be used for 130 """ 131 Append a morph from a file (wavefront object format) 132 """ 133 if isinstance(targets, PoserPmdFile): 134 targets = targets.targets 135 136 if not isinstance(targets, OrderedDict): 137 raise TypeError("targets must be %s" % OrderedDict) 138 139 if not os.path.isfile(filename): 140 raise IOError("File not found: %s" % filename) 141 142 dirname, fname = os.path.split(filename) 143 144 if morphname is None: 145 morphname = dirname.rsplit(os.sep, 1)[-1] 146 147 if actorname is None: 148 actorname = fname.rsplit(".", 1)[0] 149 150 deltas = list() 151 index = 0 152 for line in open(filename, "r"): 153 l = line.strip().split() 154 if len(l) == 4 and l[0] == "v": 155 deltas.append(DELTA(index=index, vertex=map(float, l[1:]))) 156 index += 1 157 158 return append_morph(targets, 159 morphname=morphname, 160 actorname=actorname, 161 deltas=deltas) 162 163
164 def new_uuid(*parms): 165 # Return a independent random uuid 166 if len(parms) == 2: 167 parms = str(parms[0]) + ":" + str(parms[1]) 168 while len(parms) < 16: 169 parms += chr(random.randint(ord("A"), ord("Z"))) 170 return uuid.UUID(parms) 171 172 return uuid.uuid4() 173 174
175 class PoserObject(object): 176 __slots__ = ("_name", "_data") 177 178 @property 179 def InternalName(self): 180 return self._name 181
182 @InternalName.setter 183 def InternalName(self, name): 184 self._name = name 185
186 def __init__(self, internalname=None): 187 self._name = internalname 188 self._data = OrderedDict() 189
190 def __getattr__(self, item): 191 if item[0] == "_": 192 return self.__getattribute__(item) 193 194 return self._data[item] 195
196 def __setattr__(self, key, value): 197 if key[0] == "_": 198 return super(PoserObject, self).__setattr__(key, value) 199 200 d = self._data.get(key) 201 if d is None: 202 self._data[key] = value 203 else: 204 if not isinstance(d, list): 205 d = self._data[key] = list(d, ) 206 d.append(value) 207
208 def __contains__(self, item): 209 return item in self._data 210
211 def get_actor(self, actorname): 212 assert isinstance(actorname, basestring) 213 actors = self._data.setdefault("actors", OrderedDict()) 214 return actors.get(actorname, None) 215
216 def add_actor(self, actordata, actorname=None): 217 """ 218 Add <PoserObject> actordata to the list of actors. 219 If an actor with this name allready exists, it will be 220 overwritten with the new one. 221 222 If <str> actorname is given, this name is set 223 into this actor as new InternalName. 224 Raises TypeError if actordata is the wrong type. 225 226 :param actordata: <PoserObject> 227 :param actorname: <str> actorname or <None> 228 :return: None 229 """ 230 231 if isinstance(actordata, self.__class__): 232 actors = self._data.setdefault("actors", OrderedDict()) 233 if actorname is not None: 234 actordata._name = actorname 235 actors[actordata.InternalName] = actordata 236 else: 237 raise TypeError("actordata must be type %s" % self.__class__) 238
239 def write(self, indent=0, fh=sys.stdout): 240 t = "\t" * indent 241 with OutputStream(fh): 242 for k, v in self._data.items(): 243 if k == "actors": 244 for actor in v: 245 print("%sactor %s" % (t, actor._name)) 246 print("\t%s{" % t) 247 actor.write(indent=indent + 1, fh=fh) 248 print("\t%s}\n" % t) 249 elif isinstance(v, PoserObject): 250 print("%s%s %s" % (t, k, v._name or "")) 251 print("%s\t{" % t) 252 v.write(indent=indent + 1, fh=fh) 253 print("%s\t}\n" % t) 254 elif isinstance(v, tuple): 255 print("%s%s" % (t, k)) 256 for entry in v: 257 print("%s%s" % (t, entry)) 258 elif isinstance(v, list): 259 if len(v) == 0: 260 continue 261 262 if isinstance(v[0], DELTA): 263 print("%s%s" % (t, k)) 264 print("%s\t{" % t) 265 for delta in v: 266 print("%s\td %d" % (t, delta.index), "%0.10f %0.10f %0.10f" % delta.vertex) 267 print("%s\t}\n" % t) 268 else: 269 for entry in v: 270 if isinstance(entry, PoserObject): 271 print("%s%s %s" % (t, k, entry._name)) 272 print("\t%s{" % t) 273 entry.write(indent=indent + 1, fh=fh) 274 print("\t%s}" % t) 275 else: 276 print("%s%s %s" % (t, k, entry)) 277 278 else: 279 print("%s%s %s" % (t, k, v)) 280 281
282 class PoserPmdFile(object): 283 def __init__(self, fh=None): 284 if isinstance(fh, basestring): 285 fh = open(fh, "rb", 1) 286 assert hasattr(fh, "write") 287 288 if fh is not None: 289 self.filename = fh.name 290 self.fh = fh # Filehandle to read from. 291 if fh.mode[0] == "r": 292 self.check() # Check file-signature 293 else: 294 self.filename = self.fh = None 295 296 self.num_targets = None # No target read yet. 297 self.targets = OrderedDict() # storage for Morph-Structure 298
299 def set_file(self, param): 300 """ 301 Set a file to use for input 302 :param param: <str> filename or <file> filehandle 303 :return: None 304 """ 305 if isinstance(param, basestring): 306 self.fh = open(param, "rb", 1) 307 else: 308 self.fh = param 309
310 def check(self): 311 """ 312 Check file-signature. 313 Raise IOError if signature is not "PZMD". 314 :return: True 315 """ 316 self.fh.seek(0) 317 s = self.fh.read(4) 318 if s != "PZMD": 319 raise IOError("Not a Poser PMD file") 320 return True 321
322 def _unpack(self, fmt, size): 323 result = struct.unpack("!" + fmt, self.fh.read(size)) 324 return result[0] 325
326 def read_all(self): 327 """ 328 Calls read_target() and read_deltas() 329 :return: None 330 """ 331 self.read_targets() 332 self.read_deltas() 333
334 def read_targets(self): 335 """ 336 Read all morphtargets from open file (filehandle in self.fh) 337 :return: <OrderedDict>self.targets 338 """ 339 if self.fh.closed: 340 raise IOError("File '%s' is closed" % self.fh.name) 341 342 self.fh.seek(0x10) 343 self.num_targets = self._read_uint(4) 344 345 self.targets.clear() 346 347 for i in range(self.num_targets): 348 morphname = self._read_str() 349 actorname = self._read_str() 350 target = self.targets.setdefault(morphname, OrderedDict()) 351 actor = target.setdefault(actorname, OrderedDict()) 352 actor["numb_deltas"] = self._read_uint(4) 353 actor["morphpos"] = self._read_uint(4) 354 355 self.read_uuids(self.fh.tell()) 356 357 return self.targets 358
359 def read_deltas(self): 360 """ 361 Read deltas from open file (filehandle in self.fh) 362 into self.targets. 363 364 :return: None 365 """ 366 if self.targets is None: 367 self.read_targets() 368 369 def read_one(pos): 370 result = list() 371 self.fh.seek(pos) 372 indices = self._read_uint(4) 373 for i in range(indices): 374 result.append(DELTA(self._read_uint(4), VERTEX(*self._read_vector()))) 375 376 return result 377 378 for morphname, v in self.targets.items(): 379 for actor, data in v.items(): 380 data["deltas"] = read_one(data["morphpos"]) 381
382 def read_uuids(self, pos): 383 """ 384 Read all uuids starting at file-position <pos>. 385 :param pos: fileposition 386 :return: None 387 """ 388 self.fh.seek(pos) 389 for name, target in self.targets.items(): 390 for actorname, actordata in target.items(): 391 actordata["UUID"] = self._read_str() 392
393 def _read_str(self, length=1): 394 strlen = self._read_uint(length) 395 s = self.fh.read(strlen) 396 res = re.match(r"^(.*?)pmd_\d$", s) 397 if res: 398 s = res.groups(1)[0] 399 return s 400
401 def _read_int(self, size): 402 if size == 1: 403 return self._unpack("b", size) 404 if size == 2: 405 return self._unpack("h", size) 406 if size == 4: 407 return self._unpack("i", size) 408 raise ValueError("invalid int size: " + size) 409
410 def _read_uint(self, size): 411 if size == 1: 412 return self._unpack("B", size) 413 if size == 2: 414 return self._unpack("H", size) 415 if size == 4: 416 return self._unpack("I", size) 417 raise ValueError("invalid int size: " + size) 418
419 def _read_vector(self): 420 return struct.unpack("!fff", self.fh.read(12)) 421
422 def _read_float(self): 423 return self._unpack("f", 4) 424
425 def _write_header(self): 426 self.fh.seek(0) 427 self.fh.write("PZMD") 428 self._write_uint(0, 4) 429 self._write_uint(0, 4) 430 self._write_uint(0, 4) 431 self._write_uint(0, 4) 432
433 def _write_morphcount(self, value): 434 self.fh.seek(0x10) 435 self._write_uint(value, 4) 436
437 def write_morphs_to_pmd(self, filename=None): 438 """ 439 Write existing structure to PMD-file. 440 441 To create a fitting morph-structure, do the following 442 (Note: beside of the morphname a actorname is required, 443 to have morphs with the same names for different actors 444 in one file): 445 446 fh = open("filename.pmd", "wb") 447 morph_obj = PoserPmdFile(fh) 448 for morphname in <your_list_of_morphnames>: 449 for actorname in <your_list_of_actornames>: 450 # morph for this actor 451 actor = append_morph(morph_obj.targets, 452 morphname, 453 actorname, 454 uuid, # default:None 455 deltas # default: None 456 ) 457 # If not None, deltas must be a list of vertices. Empty 458 # entries (like [0.0, 0.0, 0.0]) are ignored. 459 # One may generate deltas on the fly: 460 # actor["deltas"] = <my deltalist> 461 # actor["numb_deltas"] = <number of vertices in this actor> 462 morph_obj.write_morphs_to_pmd() 463 464 One may set another filename to func<write_morphs_to_pmd> 465 for special cases. Probably because a PMD-file is just read-in 466 and manipulated. You can use the same object to save in another 467 file, while giving a filename as argument: 468 morph_obj.write_morph_to_pmd(str<filename>). 469 Note: a previously opend file is re-opened after writing to 470 the new file. 471 472 """ 473 prev_name = prev_mode = None 474 prev_pos = 0 475 if filename is not None: 476 if not self.fh.closed: 477 prev_name = self.fh.name 478 prev_mode = self.fh.mode 479 prev_pos = self.fh.tell() 480 else: 481 if not self.fh.mode[0] == "w": 482 raise IOError("File not open for writing") 483 484 self.fh.close() 485 self.fh = open(filename, "wb") 486 self._write_header() 487 488 morph_cnt = 0 489 uuid_list = list() 490 self.fh.seek(0x14) 491 for morphname, targets in self.targets.items(): 492 for actorname, target in targets.items(): 493 uuid_list.append(target.get("UUID") or new_uuid()) 494 morph_cnt += 1 495 self._write_str(morphname) 496 self._write_str(actorname) 497 self._write_uint(0, 4) # just to move pointer 498 target["morph_pos"] = self.fh.tell() 499 self._write_uint(0, 4) 500 501 for uuid in uuid_list: 502 self._write_str(uuid) 503 del uuid_list 504 505 for morphname, targets in self.targets.items(): 506 for actorname, target in targets.items(): 507 deltas = target.get("deltas", []) 508 indices = len(deltas) 509 self._write_uint_to(self.fh.tell(), target.get("morph_pos"), 4) 510 self._write_uint(indices, 4) 511 for delta in deltas: 512 self._write_uint(delta.index, 4) 513 self._write_vertex(delta.vertex) 514 515 # write number of morphs 516 self._write_morphcount(morph_cnt) 517 self.fh.close() 518 519 if prev_name is not None: 520 self.fh = open(prev_name, prev_mode) 521 self.fh.seek(prev_pos) 522
523 def _write_str(self, s): 524 s = bytearray(s.encode("utf-8", "ignore")) 525 self.fh.write(struct.pack("!b%db" % len(s), len(s), *s)) 526
527 def _write_float(self, value): 528 assert isinstance(value, float) 529 self.fh.write(struct.pack("!f", value)) 530
531 def _write_vertex(self, vertex): 532 assert isinstance(vertex, VERTEX) 533 self.fh.write(struct.pack("!fff", *vertex)) 534
535 def _write_int(self, value, size): 536 assert isinstance(value, (int, long)) 537 if size == 1: 538 fmt = "!b" 539 elif size == 2: 540 fmt = "!h" 541 elif size == 4: 542 fmt = "!i" 543 else: 544 raise ValueError("wrong format") 545 self.fh.write(struct.pack(fmt, value)) 546
547 def _write_uint(self, value, size): 548 assert isinstance(value, (int, long)) 549 if size == 1: 550 fmt = "!B" 551 elif size == 2: 552 fmt = "!H" 553 elif size == 4: 554 fmt = "!I" 555 else: 556 raise ValueError("wrong format") 557 558 self.fh.write(struct.pack(fmt, value)) 559
560 def _write_uint_to(self, value, pos, size): 561 assert isinstance(value, (int, long)) 562 oldpos = self.fh.tell() 563 self.fh.seek(pos) 564 self._write_uint(value, size) 565 self.fh.seek(oldpos) 566
567 def get_morph(self, morphname, actorname=None): 568 """ 569 Get a morph by it's name or uuid. 570 For uuid no actor must be given. For a request by name, 571 an actorname should be given, or the first morph with a 572 fitting name will be returned. 573 574 Returns a dict() with the following entries: 575 numb_deltas: number of deltas (vertices) in this actor, 576 morphname: morphname (as requested), 577 actorname: actorname, as requested or the first found 578 if no actorname was given, 579 deltas : actual delta-data (list of <namedtuple>DELTA) 580 581 or 582 None, if no fitting morph was found. 583 """ 584 if re.match("[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}", morphname): 585 # uuid, ignore actor 586 for name, target in self.targets.items(): 587 for acname, data in target.items(): 588 if data.get("UUID") == morphname: 589 return dict(numb_deltas=data.get("numb_deltas"), 590 morphname=name, 591 actorname=acname, 592 deltas=data.get("deltas") 593 ) 594 else: 595 for name, target in self.targets.items(): 596 if name != morphname: 597 continue 598 if actorname is None: 599 data = target.items()[0] 600 return dict(numb_deltas=data.get("numb_deltas"), 601 morphname=name, 602 actorname=acname, 603 deltas=data.get("deltas") 604 ) 605 # if we have an actorname: 606 for acname, data in target.items(): 607 if actorname == acname: 608 return dict(numb_deltas=data.get("numb_deltas"), 609 morphname=name, 610 actorname=acname, 611 deltas=data.get("deltas") 612 ) 613 return None # nothing found 614 615
616 def write_deltas(data, indent=0, prefix="d", ignore_empty=False, fh=sys.stdout): 617 """ 618 Write the deltas from delta-list <data> to file. 619 620 No headers are written, just the list. 621 622 :param data: list of <DELTA> entries 623 :param indent: <int>, first column to write to (indentation) 624 :param prefix: <str> written in front of vertices on each line 625 :param ignore_empty: <bool>, flag wether vertices with (0,0,0) are written or not 626 :param fh: <file>, filehandle to write to 627 :return: None 628 """ 629 tab = "\t" * indent 630 with OutputStream(fh): 631 if isinstance(data, list): 632 if prefix is not None: 633 for delta in data: 634 if (not ignore_empty) and delta.vertex == (0.0, 0.0, 0.0): 635 continue 636 print("%s%s %05d" % (tab, prefix, delta.index), "%0.10f %0.10f %0.10f" % delta.vertex) 637 638 else: 639 for delta in data: 640 if (not ignore_empty) and delta.vertex == (0.0, 0.0, 0.0): 641 continue 642 print("%s" % tab + "%0.10f %0.10f %0.10f" % delta.vertex) 643 else: 644 raise AttributeError("wrong Datatype: %s" % data.__class__) 645 646
647 def write_all_deltas(target, indent=0, prefix="d", ignore_empty=False, fh=sys.stdout): 648 """ 649 Writes all deltas from <PoserPmdFile>target to one file. 650 Parameters: See write_deltas() 651 652 For each morph a header is written as comment. 653 Example: 654 ###################################################################### 655 ### Morphname : Custom_Morph 656 ### Actor : rThumb1 657 ### UUID : BFE48146-8C6D-47DF-9462-EFEE63C921B7 658 ### Deltas : 101 659 ### Deltas used: 48 660 ###################################################################### 661 662 :param target: 663 :param indent: 664 :param prefix: 665 :param ignore_empty: 666 :param fh: 667 :return: 668 """ 669 if isinstance(target, PoserPmdFile): 670 target = target.targets 671 672 assert isinstance(target, OrderedDict) 673 tab = "\t" * indent 674 with OutputStream(fh): 675 for morphname, morph in target.items(): 676 assert isinstance(morph, OrderedDict) 677 for actorname, data in morph.items(): 678 numb_deltas = data["numb_deltas"] 679 assert isinstance(data, OrderedDict) 680 print("%s#" % tab, "-" * 50) 681 print("%s# Morphname : %s" % (tab, morphname)) 682 print("%s# Actor : %s" % (tab, actorname)) 683 print("%s# UUID : %s" % (tab, data["UUID"])) 684 print("%s# Deltas : %d" % (tab, numb_deltas)) 685 print("%s#" % tab, "-" * 50) 686 if numb_deltas == 0: 687 print("%s# E M P T Y" % tab) 688 else: 689 write_deltas(data["deltas"], indent=indent + 1, prefix=prefix, 690 ignore_empty=ignore_empty, fh=fh) 691 print() 692 693
694 def write_morph_as_obj(data, fh=sys.stdout): 695 """ 696 Write morph from <list> data as Wavefront obj to <file> fh. 697 :param data: list of <DELTA> 698 :param fh: <file> filehandle 699 :return: None 700 """ 701 if isinstance(data, list): 702 idx = 0 703 with OutputStream(fh): 704 for delta in data: 705 while delta.index > idx: 706 print("v %0.10f %0.10f %0.10f" % (0, 0, 0)) 707 idx += 1 708 709 print("v %0.10f %0.10f %0.10f" % delta.vertex) 710 idx += 1 711 else: 712 raise AttributeError("wrong Datatype: %s" % data.__class__) 713 714
715 def export_as_obj(obj, filepath): 716 """ 717 Export morphs in <PoserPmdFile> obj as Wavefront obj-files 718 for each morph contained in obj to path <filepath>. 719 720 obj: PoserPmdFile() 721 filepath: Directory (must exist). 722 723 For each actor in <obj> a sub-directory with actorname is created 724 under base-directory <filepath>. 725 726 For each morphname an obj-file is created inside the related 727 actor-directory. 728 729 E.g: If <obj> contains one morphname 'Custom_morph' for actors 730 'hip' and 'chest', export_as_obj('c:/new_morphs') will result in: 731 c:/new_morphs 732 hip 733 Custom_morph.obj 734 chest 735 Custom_morph.obj 736 737 All existing files will be overwritten. 738 """ 739 assert isinstance(filepath, basestring) 740 if not isinstance(obj, PoserPmdFile): 741 raise TypeError("target must be %s" % PoserPmdFile) 742 if not os.path.isdir(filepath): 743 raise ValueError("filepath is not a directory") 744 745 def stripped(s): 746 """:rtype: basestring""" 747 return re.subn("[:. ]", "_", s) 748 749 for morphname, target in obj.targets.items(): 750 for actorname, data in target.items(): 751 morphpath = os.path.join(filepath, stripped(actorname)) 752 if not os.path.exists(morphpath): 753 os.mkdir(morphpath) 754 filename = os.path.join(morphpath, stripped(morphname) + ".obj") 755 756 with open(filename, "w") as fh: 757 with OutputStream(fh): 758 try: 759 fname = obj.filename 760 except AttributeError: 761 fname = "unknown" 762 print("#" * 70) 763 print("### Imported from %s" % fname) 764 print("### Exported %s" % time.asctime()) 765 print("### by LIB_poser_pmd.py (c) 2016 by F. Hartmann (aka ADP)") 766 print("#" * 70) 767 print("### Morphname :", morphname) 768 print("### Actor :", actorname) 769 print("### UUID :", data.get("UUID")) 770 print("### Deltas :", data.get("numb_deltas")) 771 print("### Deltas used:", len(data.get("deltas"))) 772 print("#" * 70) 773 write_morph_as_obj(data.get("deltas"), fh) 774 775
776 def morphs_2_poser_object(obj, poser_obj=None, version=10, 777 usedeltas=True, 778 ignoreEmptyMorphs=True, 779 indent=0): 780 """ 781 Setup and return a Poser-like obj-structure from <PoserPmdFile> obj. 782 Mainly used to construct an object to write a CR2-file. 783 784 """ 785 if not isinstance(obj, PoserPmdFile): 786 raise TypeError("target must be %s or %s" % (PoserPmdFile, OrderedDict)) 787 788 targets = obj.targets 789 poser_o = poser_obj or PoserObject() 790 poser_o.actors = list() 791 792 def get_actor(actorname, default=None): 793 for ac in poser_o.actors: 794 if ac._name == actorname: 795 return ac 796 return default 797 798 for morphname, target in targets.items(): 799 if not isinstance(target, OrderedDict): 800 raise TypeError("wrong datatype") 801 802 for actorname, data in target.items(): 803 uuid = data.get("UUID") 804 deltas = data.get("deltas") 805 indices = len(deltas) 806 if indices == 0 and ignoreEmptyMorphs: 807 continue 808 809 actor = get_actor(actorname) 810 if actor is None: 811 actor = PoserObject(actorname) 812 poser_o.actors.append(actor) 813 814 actor.channels = PoserObject() 815 actor.channels.groups = PoserObject() 816 actor.channels.groups.groupNode = PoserObject("Morph") 817 actor.channels.groups.groupNode.parmNode = list() 818 819 actor.animatableOrigin = 0 820 actor.animatableOrientation = 0 821 822 actor.channels.groups.groupNode.parmNode.append(morphname) 823 824 if indices != 0: 825 if not "targetGeom" in actor.channels: 826 actor.channels.targetGeom = list() 827 828 targetGeom = PoserObject(morphname) 829 actor.channels.targetGeom.append(targetGeom) 830 831 targetGeom.name = morphname 832 targetGeom.initValue = 0 833 targetGeom.hidden = 0 834 targetGeom.enabled = 1 835 targetGeom.forceLimits = 1 836 targetGeom.min = -100000 837 targetGeom.max = 100000 838 targetGeom.trackingScale = 0.02 839 targetGeom.keys = PoserObject() 840 targetGeom.keys.static = 0 841 targetGeom.keys.k = "0 0" 842 targetGeom.masterSynched = 1 843 targetGeom.interpStyleLocked = 0 844 targetGeom.valueOpDeltaAdd = ("Figure", "BODY", morphname) 845 targetGeom.strength = 1.0 846 targetGeom.deltaAddDelta = 1.0 847 848 if uuid is not None: 849 targetGeom.uuid = uuid 850 851 targetGeom.indices = indices 852 targetGeom.numbDeltas = data.get("numb_deltas") 853 if deltas is not None and usedeltas and targetGeom.numbDeltas > 0: 854 targetGeom.deltas = deltas 855 targetGeom.targetExtendedInfo = PoserObject() 856 targetGeom.targetExtendedInfo.subdivDeltas = PoserObject("0") 857 else: 858 targetGeom.useBinaryMorph = 1 859 860 targetGeom.blendType = 0 861 targetGeom.afterBlend = 1 862 863 else: 864 # valueParm 865 if not "valueParm" in actor.channels: 866 actor.channels.valueParm = list() 867 868 valueParm = PoserObject(morphname) 869 actor.channels.valueParm.append(valueParm) 870 871 valueParm.name = morphname 872 valueParm.initValue = 0 873 valueParm.hidden = 0 874 valueParm.enabled = 1 875 valueParm.forceLimits = 0 876 valueParm.min = -100000 877 valueParm.max = 100000 878 valueParm.trackingScale = 1 879 valueParm.keys = PoserObject() 880 valueParm.keys.static = 0 881 valueParm.keys.k = "0 0" 882 valueParm.interpStyleLocked = 0 883 884 return poser_o 885 886
887 def export2poser_pose(filename, obj, usedeltas=False, poserversion=10, poserbuild=None): 888 """ 889 Write a poser.CR2/PZ2 file according to the content of <type PoserPmdFile> obj. 890 if <type bool>usedeltas is False, a PMD-File with the same name (but extension ".pmd") 891 should exist in the same folder, because a reference to the PMD-file 892 will be used (injectPMDFileMorphs). 893 No deltas are written to the file in this case. 894 895 If <bool>usedeltas is True, deltas are written directly into the CR2/PZ2-file (old methode). 896 897 poserversion: written to the file as given. 898 poserbuild: ignored if None. 899 """ 900 dir_name = os.path.dirname(filename) 901 base_name = os.path.basename(filename) 902 pmd_file = os.path.join(dir_name, base_name.rsplit(".", 1)[0] + ".pmd") 903 if not os.path.isdir(dir_name): 904 raise IOError("directory does not exist: %s" % filename) 905 906 if not isinstance(obj, (PoserPmdFile, OrderedDict)): 907 raise TypeError("target must be %s or %s" % (PoserPmdFile, OrderedDict)) 908 909 poser_obj = PoserObject() 910 version = poser_obj.version = PoserObject() 911 if not usedeltas: 912 poser_obj.injectPMDFileMorphs = "%s.pmd" % filename.rsplit(".", 1)[0] 913 version.number = poserversion 914 if poserbuild: 915 version.build = poserbuild 916 morphs_2_poser_object(obj, poser_obj=poser_obj, usedeltas=usedeltas) 917 figure = poser_obj.figure = PoserObject() 918 figure.conformScale = 0 919 figure.conformMorphs = 0 920 figure.conformForceEndPoint = 0 921 figure.conformFollowOrigin = 0 922 figure.conformTranslations = 0 923 924 with open(filename, "w") as fh: 925 print("{", file=fh) 926 poser_obj.write(fh=fh, indent=0) 927 print("}", file=fh) 928 929
930 def yield_morphnames(obj): 931 """ 932 Yields each morphname in <PoserObject>obj 933 :param obj: PoserObject 934 """ 935 for entry in obj.targets.keys(): 936 yield entry 937 938
939 def yield_actornames(obj): 940 """ 941 Yields each actorname in <PoserObject>obj 942 :param obj: PoserObject 943 """ 944 seen = set() 945 for morphname, target in obj.targets.items(): 946 for name, _ in target.items(): 947 if not name in seen: 948 seen.add(name) 949 print(name) 950 yield name 951 952
953 def get_names_lists(obj): 954 data = dict(morphnames=set(), actornames=set()) 955 for morphname, target in obj.targets.items(): 956 data["morphnames"].add(morphname) 957 for actorname, _ in target.items(): 958 data["actornames"].add(actorname) 959 return data 960 961
962 def list_morphs(obj, order="actor"): 963 """ 964 Prints a list of morphs/actors (used for testing) 965 :param obj: PoserPmdFile 966 :param order: if order contains "actor", the list of 967 morphnames per actor is printed 968 (e.g.: <actorname>: <morph>, <morph>, ...) 969 else a list of morphs and their actors 970 (e.g.: <morphname>: <actor>, <actor>, ...). 971 :return: None 972 """ 973 if not isinstance(obj, PoserPmdFile): 974 raise TypeError("given obj must be %s" % PoserPmdFile) 975 if "actor" in order.lower(): 976 actors = OrderedDict() 977 for morphname, target in obj.targets.items(): 978 for actorname, data in target.items(): 979 morphs = actors.setdefault(actorname, list()) 980 if len(data.get("deltas")) == 0: 981 morphs.append(morphname + " (empty, maybe a master-dial)") 982 else: 983 morphs.append(morphname) 984 985 for actorname, data in actors.items(): 986 print( "\nActor %s:" % actorname) 987 for entry in data: 988 print("\t" + entry) 989 else: 990 morphs = OrderedDict() 991 for morphname, target in obj.targets.items(): 992 morph = morphs.setdefault(morphname, list()) 993 for actorname, data in target.items(): 994 l = len(data.get("deltas")) 995 if l == 0: 996 morph.append(actorname + " (empty, maybe a master-dial)") 997 else: 998 morph.append(actorname) 999 1000 for morphname, data in morphs.items(): 1001 print("\nMorph %s:" % morphname) 1002 for actorname in data: 1003 print("\t%s" % actorname) 1004 1005 1006 if __name__ == "__main__": 1007 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 1008 # !!!!!!!!!!!!!!! !!!!!!!!!!!!!! 1009 # !!!!!!!!!!!!!!! Set your own pathes and filenames !!!!!!!!!!!!!! 1010 # !!!!!!!!!!!!!!! !!!!!!!!!!!!!! 1011 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 1012 1013 basepath = "/home/fredi/Schreibtisch/Poser" 1014 myPmdInFile = os.path.join(basepath, "/home/fredi/Desktop/Poser/LaFemme2/Runtime/Geometries/RPublishing/LaFemme/INJ All Face Morphs.pmd") 1015 1016 with open(myPmdInFile) as fh: 1017 pmd_obj = PoserPmdFile(fh) 1018 pmd_obj.read_targets() 1019 pmd_obj.read_deltas() 1020 1021 a = pmd_obj.get_morph("FEC24F7F-E1A2-4A75-AF6C-C9FC435442A4") 1022 b = pmd_obj.get_morph(morphname="EyesUp", actorname="head") 1023 print("a =", a.get("numb_deltas"), a.get("morphname"), a.get("actorname")) 1024 print("b =", b.get("numb_deltas"), b.get("morphname"), b.get("actorname")) 1025 export2poser_pose(os.path.join(basepath, "testmorph.pz2"), pmd_obj) 1026 1027 print(tuple(yield_actornames(pmd_obj))) 1028 print(tuple(yield_morphnames(pmd_obj))) 1029 data = get_names_lists(pmd_obj) 1030 print(data) 1031 list_morphs(pmd_obj, order="actor") 1032