1 /** 2 Root Puppet Object 3 4 Copyright: 5 Copyright © 2020-2026, Inochi2D Project 6 7 License: 8 $(LINK2 https://github.com/Inochi2D/inochi2d/blob/main/LICENSE, BSD 2-clause License) 9 10 Authors: 11 Luna Nielsen 12 */ 13 module inochi2d.puppet; 14 import inochi2d.common; 15 import inochi2d.nodes; 16 import inochi2d.param; 17 import inochi2d.animation; 18 import inochi2d.core; 19 import inp.format; 20 import nulib.io.stream; 21 import nulib; 22 import numem; 23 24 // NOTE: Puppet has some legacy functionality, this allows turning that off. 25 version (IN_NO_LEGACY) { 26 } else { 27 pragma(msg, "WARNING: Legacy features are enabled, these may be removed in future updates."); 28 version = IN_LEGACY; 29 } 30 31 /** 32 Puppet properties 33 */ 34 class PuppetProperties : NuObject, ISerializable, IDeserializable!ModelState { 35 public: 36 37 /** 38 Parent puppet object 39 */ 40 Puppet parent; 41 42 /** 43 Name of the puppet 44 */ 45 nstring name; 46 47 /** 48 Author of the puppet 49 */ 50 nstring author; 51 52 /** 53 Thumbnail of the puppet. 54 */ 55 Texture thumbnail; 56 57 /** 58 Pixels-per-meter for the physics system 59 */ 60 float physicsPixelsPerMeter = 1000; 61 62 /** 63 Gravity for the physics system 64 */ 65 float physicsGravity = 9.8; 66 67 /** 68 Whether the puppet should preserve pixel borders. 69 This feature is mainly useful for puppets which use pixel art. 70 */ 71 bool graphicsUsePointFiltering = false; 72 73 /** 74 Constructs a new properties object. 75 */ 76 this(Puppet puppet) @nogc { 77 this.parent = puppet; 78 } 79 80 /** 81 Serializes the type. 82 */ 83 void onSerialize(ref DataNode object) @nogc { 84 85 // General Properties. 86 object["name"] = name[]; 87 object["author"] = author[]; 88 object["thumbnail"] = parent.textureCache.find(thumbnail); 89 90 // Physics properties. 91 object["physicsPixelsPerMeter"] = physicsPixelsPerMeter; 92 object["physicsGravity"] = physicsGravity; 93 94 // Graphics properties 95 object["graphicsUsePointFiltering"] = graphicsUsePointFiltering; 96 } 97 98 /** 99 Deserializes the type. 100 */ 101 void onDeserialize(ref DataNode object, ref ModelState state) @nogc { 102 103 // 0.8 backwards compatibility. 104 object.tryGetRef(state, author, "rigger", author); 105 object.tryGetRef(state, author, "artist", author); 106 107 object.tryGetRef(state, name, "name"); 108 object.tryGetRef(state, author, "author", author); 109 object.tryGetRef(state, physicsPixelsPerMeter, "pixelsPerMeter"); 110 object.tryGetRef(state, physicsGravity, "gravity"); 111 } 112 } 113 114 /** 115 A puppet 116 */ 117 class Puppet : NuRefCounted, ISerializable, IDeserializable!ModelState { 118 private: 119 @nogc: 120 121 // The drawlist that the puppet passes to its nodes. 122 DrawList drawList_; 123 124 // A list of parts that are not masked by other parts 125 weak_vector!Visual visuals_; 126 127 // A list of parameters attached to the puppet. 128 vector!Parameter parameters_; 129 130 // A dictionary of named animations 131 vector!Animation animations_; 132 133 // Extended Vendor Data 134 weak_map!(string, ubyte[]) vendorData_; 135 136 // 137 // LEGACY CONSTRUCTS 138 // 139 version (IN_LEGACY) { 140 141 // A list of parameters that are driven by drivers 142 weak_map!(Parameter, SimplePhysics) driven_; 143 144 // A list of drivers that need to run to update the puppet 145 SimplePhysics[] drivers_; 146 } 147 148 void scanParts(ref Node node) { 149 node.findVisuals(visuals_); 150 151 // Legacy physics system. 152 version (IN_LEGACY) { 153 node.findNodes!SimplePhysics(drivers_); 154 driven_.clear(); 155 foreach (driver; drivers_) { 156 foreach (Parameter param; driver.affectedParameters) 157 driven_[param] = driver; 158 } 159 } 160 } 161 162 Node findNode(Node n, string name) @nogc { 163 164 // Name matches! 165 if (n.name == name) 166 return n; 167 168 // Recurse through children 169 foreach (child; n.children) { 170 if (Node c = findNode(child, name)) 171 return c; 172 } 173 174 // Not found 175 return null; 176 } 177 178 Node findNode(Node n, GUID guid) @nogc { 179 180 // Name matches! 181 if (n.guid == guid) 182 return n; 183 184 // Recurse through children 185 foreach (child; n.children) { 186 if (Node c = findNode(child, guid)) 187 return c; 188 } 189 190 // Not found 191 return null; 192 } 193 194 /// Loads textures from a DataNode into the texture cache. 195 void loadTextures(ref DataNode node) { 196 assert(textureCache !is null, "Texture cache is invalid!"); 197 assert(node.isArray, "Not a texture cache array!"); 198 199 texLoadLoop: foreach (i, ref DataNode texture; node.array) { 200 TextureData textureData; 201 202 // Skip invalid texture indices. 203 if (!texture.isObject || "encoding" !in texture || "data" !in texture) 204 continue; 205 206 uint encoding = texture["encoding"].tryCoerce!int(-1); 207 ubyte[] data = texture["data"].blob; 208 209 // Invalid data? 210 if (encoding == -1 || data.length == 0) 211 continue; 212 213 // Handle different encodings. 214 switch (encoding) { 215 case INP_TEX_FMT_PNG: 216 case INP_TEX_FMT_TGA: 217 textureData = TextureData.load(data); 218 break; 219 220 case INP_TEX_FMT_BC7: 221 assert(0, "BC7 not implemented yet!"); 222 continue texLoadLoop; 223 224 default: 225 // Unknown format. 226 continue texLoadLoop; 227 } 228 textureCache.insert(Texture.createForData(textureData.move()), i); 229 } 230 } 231 232 protected: 233 234 /** 235 Serializes a puppet into an existing object. 236 */ 237 void onSerialize(ref DataNode object) @nogc { 238 object["properties"] = properties.serialize(); 239 object["nodes"] = root.serialize(); 240 object["param"] = parameters_.serialize(); 241 object["animations"] = animations_.serialize(); 242 } 243 244 /** 245 Deserializes a puppet 246 */ 247 void onDeserialize(ref DataNode object, ref ModelState state) @nogc { 248 249 // Invalid type. 250 if (!object.isObject) 251 return; 252 253 // Just set to basic initialized object if none was found. 254 object.tryGetRef(state, properties, "properties", properties); 255 256 // Legacy "meta" key. 257 if ("meta" in object) { 258 object.tryGetRef(state, properties, "meta", properties); 259 object["meta"].tryGetRef(state, properties.graphicsUsePointFiltering, "preservePixels"); 260 } 261 262 // Legacy "physics" key. 263 if ("physics" in object) { 264 object["physics"].tryGetRef(state, properties.physicsPixelsPerMeter, "pixelsPerMeter"); 265 object["physics"].tryGetRef(state, properties.physicsGravity, "gravity"); 266 } 267 268 // Add root node if it was found. 269 if ("nodes" in object) { 270 object.tryGetRef(state, root, "nodes", root); 271 } 272 273 if (auto params = "param" in object) { 274 if ((*params).isArray) { 275 parameters_.resize((*params).length); 276 foreach (i, ref param; (*params).array) { 277 parameters_[i] = param.tryDeserializeParam(state); 278 } 279 } 280 } 281 282 if (auto anim = "animation" in object) { 283 (*anim).deserialize(animations_, state); 284 } 285 } 286 287 void onFinalize(ref ModelState state) @nogc { 288 289 // Finally update link etc. 290 this.root.finalize(state); 291 this.root.updateTransform(); 292 293 foreach (parameter; parameters_) { 294 parameter.finalize(this, state); 295 } 296 297 foreach (ref animation; animations_) { 298 animation.finalize(this); 299 } 300 this.rescanNodes(); 301 } 302 303 public: 304 305 /** 306 Properties for the puppet. 307 */ 308 PuppetProperties properties; 309 310 /** 311 The root node of the puppet 312 */ 313 Node root; 314 315 /** 316 INP Texture slots for this puppet 317 */ 318 TextureCache textureCache; 319 320 /** 321 Extended vendor data 322 */ 323 ubyte[][string] extData; 324 325 /** 326 Whether parameters should be rendered 327 */ 328 bool renderParameters = true; 329 330 /** 331 Whether drivers should run 332 */ 333 version (IN_LEGACY) bool enableDrivers = true; 334 335 /** 336 Puppet render transform 337 338 This transform does not affect physics 339 */ 340 Transform transform; 341 342 /** 343 The active draw list for the puppet. 344 */ 345 final @property DrawList drawList() @nogc => drawList_; 346 347 /** 348 A read-only slice of the root visuals being rendered. 349 */ 350 final @property Visual[] visuals() => visuals_; 351 352 /** 353 A read-only slice of drivers 354 */ 355 version (IN_LEGACY) final @property SimplePhysics[] drivers() => drivers_; 356 357 /** 358 A read-only slice of animations attached to this puppet. 359 */ 360 final @property Animation[] animations() => animations_[]; 361 362 /** 363 A read-only slice of animations attached to this puppet. 364 */ 365 final @property Parameter[] parameters() => parameters_[]; 366 367 // Destructor 368 ~this() { 369 nogc_delete(properties); 370 nogc_delete(drawList_); 371 nogc_delete(textureCache); 372 } 373 374 /** 375 Constructs a new, empty puppet. 376 377 Params: 378 cache = The texture cache to use during construction. 379 root = The node to put as the root of the puppet. 380 */ 381 this(TextureCache cache = null, Node root = null) { 382 this.properties = nogc_new!PuppetProperties(this); 383 this.textureCache = cache ? cache : nogc_new!TextureCache(); 384 this.drawList_ = nogc_new!DrawList(); 385 386 // Setup root node 387 this.root = root ? root : nogc_new!Node(this); 388 this.root.setPuppet(this); 389 this.root.name = "Root"; 390 } 391 392 /** 393 Creates a new puppet from a node tree 394 */ 395 this(Node root) { 396 this(null, root); 397 } 398 399 version (WebAssembly) { 400 } else { 401 402 /** 403 Loads a $(D Puppet) from a file. 404 405 Params: 406 path = Path to the file to load. 407 sink = A sink to write logs to. 408 409 Notes: 410 Not available when compiling for WebAssembly. 411 */ 412 static Result!Puppet fromFile(string path, IOSink sink = IOSink.init) @nogc { 413 import nulib.io.stream.file : FileStream; 414 415 if (FileStream fstream = nogc_new!FileStream(path, "r+b")) { 416 return Puppet.fromStream(fstream, sink); 417 } 418 return error!Puppet("Could not open file."); 419 } 420 } 421 422 /** 423 Loads a $(D Puppet) from a Stream. 424 425 Params: 426 stream = The readable stream to load the puppet from. 427 sink = A sink to write logs to. 428 */ 429 static Result!Puppet fromStream(Stream stream, IOSink sink = IOSink.init) @nogc { 430 assert(stream); 431 assert(stream.canRead); 432 433 // Set up model state. 434 ModelState state; 435 state.io = sink; 436 437 // Identify Inochi2D 0.8 models. 438 INPFileFormat fileFormat = stream.detectFormat(); 439 if (fileFormat == INPFileFormat.inp1) { 440 state.doUpgrade08 = true; 441 state.version_ = IN_MAKE_VERSION!(0, 8, 6); 442 state.warning("Inochi2D 0.8's file format is deprecated, it is recommended that you upgrade to Inochi2D 0.9."); 443 } 444 445 auto result = stream.readINP(); 446 if (!result) 447 return error!Puppet(result.error); 448 449 DataNode node = result.get(); 450 if (INP_TAG_PAYLOAD !in node) 451 return error!Puppet("No payload was found in the model!"); 452 453 // Create new puppet and deserialize the data. 454 Puppet puppet = nogc_new!Puppet(nogc_new!TextureCache()); 455 puppet.deserialize(node, state); 456 return ok(puppet); 457 } 458 459 /** 460 Loads a $(D Puppet) from a Stream. 461 462 Params: 463 stream = The readable stream to load the puppet from. 464 */ 465 final bool toStream(Stream stream) @nogc { 466 assert(stream); 467 assert(stream.canWrite); 468 469 // Prepare data node. 470 DataNode data = DataNode.createObject(); 471 data[INP_TAG_PAYLOAD] = DataNode.createObject(); 472 data[INP_TAG_TEXTURES] = DataNode.createArray(); 473 474 // Serialize data 475 return false; 476 } 477 478 /** 479 Serializes a puppet. 480 481 Params: 482 node = The payload DataNode to deserialize from. 483 */ 484 final void serialize(ref DataNode node) { 485 assert(node.isObject, "Target DataNode was not an Object!"); 486 node[INP_TAG_PAYLOAD] = DataNode.createObject(); 487 this.onSerialize(node[INP_TAG_PAYLOAD]); 488 } 489 490 /** 491 Deserializes a Puppet from a payload $(D DataNode). 492 493 Params: 494 node = The DataNode to deserialize from. 495 state = The state of the deserializer. 496 */ 497 final void deserialize(ref DataNode node, ref ModelState state) @nogc { 498 assert(INP_TAG_PAYLOAD in node, "No payload was found!"); 499 assert(node[INP_TAG_PAYLOAD].isObject, "Invalid payload object."); 500 501 // NOTE: Deserialization happens in multiple steps, 502 // 1. Load textures from TEX_SECT, assigning texture IDs. 503 // 2. Deserialize payload, (this MUST be present.) 504 // 3. Finalize any data. 505 if (INP_TAG_TEXTURES in node) { 506 this.loadTextures(node[INP_TAG_TEXTURES]); 507 } 508 509 this.onDeserialize(node[INP_TAG_PAYLOAD], state); 510 this.onFinalize(state); 511 } 512 513 /** 514 Updates the nodes 515 */ 516 final void update(float delta) { 517 drawList_.clear(); 518 root.preUpdate(drawList_); 519 520 // Update parameters 521 if (renderParameters) { 522 foreach (parameter; parameters_) { 523 parameter.update(); 524 } 525 } 526 527 // Ensure the transform tree is updated 528 root.updateTransform(); 529 530 version (IN_LEGACY) { 531 if (renderParameters && enableDrivers) { 532 // Update parameter/node driver nodes (e.g. physics) 533 foreach (driver; drivers_) { 534 driver.updateDriver(delta); 535 } 536 } 537 538 // Update again after drivers. 539 root.updateTransform(); 540 } 541 542 // Update nodes 543 root.update(delta, drawList_); 544 root.postUpdate(drawList_); 545 } 546 547 /** 548 Reset drivers/physics nodes 549 */ 550 version (IN_LEGACY) final void resetDrivers() @nogc { 551 foreach (driver; drivers_) { 552 driver.reset(); 553 } 554 } 555 556 /** 557 Returns the index of a parameter by name 558 */ 559 ptrdiff_t findParameterIndex(string name) @nogc { 560 foreach (i, parameter; parameters_) { 561 if (parameter.name == name) { 562 return i; 563 } 564 } 565 return -1; 566 } 567 568 /** 569 Returns a parameter by GUID 570 */ 571 Parameter findParameter(GUID guid) @nogc { 572 foreach (i, parameter; parameters_) { 573 if (parameter.guid == guid) { 574 return parameter; 575 } 576 } 577 return null; 578 } 579 580 /** 581 Gets if a node is bound to ANY parameter. 582 */ 583 bool getIsNodeBound(Node node) { 584 foreach (i, parameter; parameters_) { 585 if (parameter.hasAnyBindingsTo(node)) 586 return true; 587 } 588 return false; 589 } 590 591 /** 592 Draws the puppet 593 */ 594 final void draw(float delta) { 595 sortNodes(visuals_); 596 597 foreach (visual; visuals_) { 598 if (!visual.enabled) 599 continue; 600 601 visual.draw(delta, drawList_); 602 } 603 } 604 605 /** 606 Removes a parameter from this puppet 607 */ 608 void addParameter(Parameter param) { 609 parameters_ ~= param; 610 } 611 612 /** 613 Removes a parameter from this puppet 614 */ 615 void removeParameter(Parameter param) { 616 parameters_.remove(param); 617 } 618 619 /** 620 Rescans the puppet's nodes 621 622 Run this every time you change the layout of the puppet's node tree 623 */ 624 final void rescanNodes() { 625 this.scanParts(root); 626 } 627 628 /** 629 Finds Node by its name 630 */ 631 T find(T = Node)(string name) @nogc 632 if (is(T : Node)) { 633 return cast(T)findNode(root, name); 634 } 635 636 /** 637 Finds Node by its unique id 638 */ 639 T find(T = Node)(GUID guid) @nogc 640 if (is(T : Node)) { 641 return cast(T)findNode(root, guid); 642 } 643 644 /** 645 Adds a texture to a new slot if it doesn't already exist within this puppet 646 */ 647 final uint addTextureToSlot(Texture texture) @nogc { 648 return textureCache.add(texture); 649 } 650 651 /** 652 Sets thumbnail of this puppet 653 */ 654 final void setThumbnail(Texture texture) @nogc { 655 textureCache.add(texture); 656 this.properties.thumbnail = texture; 657 } 658 659 /** 660 Gets the texture slot index for a texture 661 662 returns -1 if none was found 663 */ 664 final ptrdiff_t getTextureSlotIndexFor(Texture texture) @nogc { 665 return textureCache.find(texture); 666 } 667 668 /** 669 Gets the combined bounds of the puppet 670 */ 671 vec4 getCombinedBounds(bool reupdate = false)() { 672 return root.getCombinedBounds!(reupdate, true); 673 } 674 }