diff --git a/cg/blender/scripts/addons/BakeWrangler/LICENSE b/cg/blender/scripts/addons/BakeWrangler/LICENSE new file mode 100644 index 0000000..3877ae0 --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/cg/blender/scripts/addons/BakeWrangler/__init__.py b/cg/blender/scripts/addons/BakeWrangler/__init__.py new file mode 100644 index 0000000..d39c57e --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/__init__.py @@ -0,0 +1,207 @@ +''' +Copyright (C) 2019-2023 Dancing Fortune Software All Rights Reserved + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +''' + +bl_info = { + 'name': 'Bake Wrangler', + 'description': 'Bake Wrangler aims to improve all baking tasks with a node based interface and provides additional bake passes', + 'author': 'DFS', + 'version': (1, 5, 'b11'), + 'blender': (3, 6, 0), + 'location': 'Editor Type > Bake Node Editor', + "warning": "Beta Version", + 'doc_url': 'https://bake-wrangler.readthedocs.io', + "tracker_url": "https://blenderartists.org/t/bake-wrangler-node-based-baking-tool-set/", + "support": "COMMUNITY", + 'category': 'Baking'} + + +import bpy +from . import nodes +from . import status_bar + + +# Preferences +class BakeWrangler_Preferences(bpy.types.AddonPreferences): + bl_idname = __package__ + + def update_icon(self, context): + if not self.show_icon: + status_bar.status_bar_icon.disable_bw_icon() + else: + status_bar.status_bar_icon.ensure_bw_icon() + + # Message prefs + show_icon: bpy.props.BoolProperty(name="Show BW Icon in Status Bar", description="Shows an icon that changes color based on baking state and can be clicked on to bring up the log", default=True, update=update_icon) + text_msgs: bpy.props.BoolProperty(name="Messages to Text editor", description="Write messages to a text block in addition to the console", default=True) + clear_msgs: bpy.props.BoolProperty(name="Clear Old Messages", description="Clear the text block before each new bake", default=True) + wind_msgs: bpy.props.BoolProperty(name="Open Text in new Window", description="A new window will be opened displaying the text block each time a new bake is started", default=False) + wind_close: bpy.props.BoolProperty(name="Auto Close Text Window", description="Close the text window on successful bake completion", default=False) + + # Node prefs + show_node_prefs: bpy.props.BoolProperty(name="Node Defaults", description="Default general node options", default=False) + def_filter_mesh: bpy.props.BoolProperty(name="Meshes", description="Show mesh type objects", default=True) + def_filter_curve: bpy.props.BoolProperty(name="Curves", description="Show curve type objects", default=True) + def_filter_surface: bpy.props.BoolProperty(name="Surfaces", description="Show surface type objects", default=True) + def_filter_meta: bpy.props.BoolProperty(name="Metas", description="Show meta type objects", default=True) + def_filter_font: bpy.props.BoolProperty(name="Fonts", description="Show font type objects", default=True) + def_filter_light: bpy.props.BoolProperty(name="Lights", description="Show light type objects", default=True) + def_filter_collection: bpy.props.BoolProperty(name="Collections", description="Toggle only collections", default=False) + def_show_adv: bpy.props.BoolProperty(name="Expand Advanced Settings", description="Expand advanced settings on node creation instead of starting with them collapsed", default=False) + invert_bakemod: bpy.props.BoolProperty(name="Invert Selected in Bake Modifiers", description="Inverts the selection method used by the Bake Modifiers option from ignoring viewport hidden modifiers to baking them", default=False) + + # Render prefs + show_render_prefs: bpy.props.BoolProperty(name="Render Defaults", description="Default settings for rendering options", default=False) + def_samples: bpy.props.IntProperty(name="Default Bake Samples", description="The number of samples per pixel that new Pass nodes will be set to when created", default=1, min=1) + def_xres: bpy.props.IntProperty(name="Default Bake X Resolution", description="The X resolution new Pass nodes will be set to when created", default=1024, min=1, subtype='PIXEL') + def_yres: bpy.props.IntProperty(name="Default Bake Y Resolution", description="The Y resolution new Pass nodes will be set to when created", default=1024, min=1, subtype='PIXEL') + def_device: bpy.props.EnumProperty(name="Default Device", description="The render device new Pass nodes will be set to when created", items=nodes.node_tree.BakeWrangler_PassSettings.cycles_devices, default='CPU') + def_raydist: bpy.props.FloatProperty(name="Default Ray Distance", description="The ray distance that new Mesh nodes will use when created", default=0.01, step=1, min=0.0, unit='LENGTH') + def_max_ray_dist: bpy.props.FloatProperty(name="Default Max Ray Dist", description="The max ray distance that new Mesh nodes will use when created", default=0.0, step=1, min=0.0, unit='LENGTH') + def_margin: bpy.props.IntProperty(name="Default Margin", description="The margin that new Mesh nodes will use when created", default=0, min=0, subtype='PIXEL') + def_mask_margin: bpy.props.IntProperty(name="Default Mask Margin", description="The mask margin that new Mesh nodes will use when created", default=0, min=0, subtype='PIXEL') + ignore_vis: bpy.props.BoolProperty(name="Objects Always Visible", description="Enable to ignore the visibility of selected objects when baking, making them visible regardless of settings in blender", default=False) + + # Ouput prefs + show_output_prefs: bpy.props.BoolProperty(name="Output Defaults", description="Default settings for output options", default=False) + def_format: bpy.props.EnumProperty(name="Default Output Format", description="The format new Output nodes will use when created", items=nodes.node_tree.BakeWrangler_OutputSettings.img_format, default='PNG') + def_xout: bpy.props.IntProperty(name="Default Output X Resolution", description="The X resolution new Output nodes will be set to when created", default=1024, min=1, subtype='PIXEL') + def_yout: bpy.props.IntProperty(name="Default Output Y Resolution", description="The Y resolution new Output nodes will be set to when created", default=1024, min=1, subtype='PIXEL') + def_outpath: bpy.props.StringProperty(name="Default Output Path", description="The path new Output nodes will use when created", default="", subtype='DIR_PATH') + def_outname: bpy.props.StringProperty(name="Default Output Name", description="The name new Output nodes will use when created", default="Image", subtype='FILE_NAME') + make_dirs: bpy.props.BoolProperty(name="Create Paths", description="If selected path doesn't exist, try to create it", default=False) + auto_open: bpy.props.BoolProperty(name="Auto open bakes", description="Automatically open the baked image in blender if it isn't already open", default=True) + save_packed: bpy.props.BoolProperty(name="Save packed images", description="Prior to baking, save any packed images with changes or they will not apply during the bake", default=False) + save_images: bpy.props.BoolProperty(name="Save unpacked images", description="Prior to baking, save any unpacked images with changes or they will not apply during the bake", default=False) + img_non_color: bpy.props.EnumProperty(name="Non-Color", description="Color space to use as non-color when alternative color spaces are in use", items=nodes.node_tree.BakeWrangler_OutputSettings.img_color_spaces) + + # Performance prefs + fact_start: bpy.props.BoolProperty(name="Disable Add-ons", description="Disable add-ons in the background baking instance (faster load times and some 3rd party add-ons can crash the process)", default=True) + retrys: bpy.props.IntProperty(name="Retries", description="On bake failure retry this many times", default=0) + + # Dev prefs + debug: bpy.props.BoolProperty(name="Debug", description="Enable additional debugging output", default=False) + + def draw(self, context): + layout = self.layout + colprefs = layout.column(align=False) + + coltext = colprefs.column(align=False) + coltext.prop(self, "show_icon") + coltext.prop(self, "text_msgs") + if self.text_msgs: + box = coltext.box() + box.prop(self, "clear_msgs") + box.prop(self, "wind_msgs") + row = box.row(align=True) + row.label(icon='THREE_DOTS') + row.prop(self, "wind_close") + if self.wind_msgs: + row.enabled = True + else: + row.enabled = False + + # Node prefs + box = colprefs.box() + if not self.show_node_prefs: + box.prop(self, "show_node_prefs", icon="DISCLOSURE_TRI_RIGHT", emboss=False) + else: + box.prop(self, "show_node_prefs", icon="DISCLOSURE_TRI_DOWN", emboss=False) + col = box.column(align=False) + row = col.row(align=True) + row.alignment = 'LEFT' + row.label(text="Filter:") + row1 = row.row(align=True) + row1.alignment = 'LEFT' + row1.prop(self, "def_filter_mesh", text="", icon='MESH_DATA') + row1.prop(self, "def_filter_curve", text="", icon='CURVE_DATA') + row1.prop(self, "def_filter_surface", text="", icon='SURFACE_DATA') + row1.prop(self, "def_filter_meta", text="", icon='META_DATA') + row1.prop(self, "def_filter_font", text="", icon='FONT_DATA') + row1.prop(self, "def_filter_light", text="", icon='LIGHT_DATA') + if self.def_filter_collection: + row1.enabled = False + row2 = row.row(align=False) + row2.alignment = 'LEFT' + row2.prop(self, "def_filter_collection", text="", icon='GROUP') + col.prop(self, "def_show_adv") + col.prop(self, "invert_bakemod") + + # Render prefs + box = colprefs.box() + if not self.show_render_prefs: + box.prop(self, "show_render_prefs", icon="DISCLOSURE_TRI_RIGHT", emboss=False) + else: + box.prop(self, "show_render_prefs", icon="DISCLOSURE_TRI_DOWN", emboss=False) + col = box.column(align=False) + col.prop(self, "def_samples", text="Samples") + col1 = col.column(align=True) + col1.prop(self, "def_xres", text="X") + col1.prop(self, "def_yres", text="Y") + col.prop(self, "def_device", text="Device") + col.prop(self, "def_margin", text="Margin") + col.prop(self, "def_mask_margin", text="Mask Margin") + col.prop(self, "def_raydist", text="Ray Distance") + col.prop(self, "def_max_ray_dist", text="Max Ray Dist") + col.prop(self, "ignore_vis") + + # Output prefs + box = colprefs.box() + if not self.show_output_prefs: + box.prop(self, "show_output_prefs", icon="DISCLOSURE_TRI_RIGHT", emboss=False) + else: + box.prop(self, "show_output_prefs", icon="DISCLOSURE_TRI_DOWN", emboss=False) + col = box.column(align=False) + col.prop(self, "def_format", text="Format") + col1 = col.column(align=True) + col1.prop(self, "def_xout", text="X") + col1.prop(self, "def_yout", text="Y") + col2 = col.column(align=True) + col2.prop(self, "def_outpath", text="Image Path") + col2.prop(self, "def_outname", text="Image Name") + col.prop(self, "make_dirs") + col.prop(self, "auto_open") + + # Dev prefs + col = colprefs.column(align=True) + col.prop(self, "fact_start") + col.prop(self, "save_packed") + col.prop(self, "save_images") + col.prop(self, "retrys") + if 'Non-Color' not in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys(): + col.prop(self, "img_non_color") + col.prop(self, "debug") + + + +def register(): + from bpy.utils import register_class + register_class(BakeWrangler_Preferences) + # Add status property to the window manager + bpy.types.WindowManager.bw_status = bpy.props.IntProperty(name="Bake Wrangler Status", default=0) + bpy.types.WindowManager.bw_lastlog = bpy.props.StringProperty(name="Bake Wangler Log", default="") + bpy.types.WindowManager.bw_lastfile = bpy.props.StringProperty(name="Bake Wangler Temp Blend", default="") + nodes.register() + status_bar.register() + + +def unregister(): + from bpy.utils import unregister_class + nodes.unregister() + status_bar.unregister() + unregister_class(BakeWrangler_Preferences) + # Remove status property from window manager + delattr(bpy.types.WindowManager, 'bw_status') diff --git a/cg/blender/scripts/addons/BakeWrangler/baker.py b/cg/blender/scripts/addons/BakeWrangler/baker.py new file mode 100644 index 0000000..16c404e --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/baker.py @@ -0,0 +1,3030 @@ +# Information needed to create a desired output. A solution has a destination file +# name and output format along with a list of the bake passes that are needed and +# how they are combined into the final image +class bake_solution(): + def __init__(self, socket, alpha, vcol): + self.bakepass = {} # Dict of Pass Nodes to process + self.baketile = {} # Dict keyed by Pass Nodes with a dict of UDIM tiles when enabled + self.bakecols = {} # Dict keyed by Object with Pass node and name of color data + self.postproc = None# Material containing post processing information + self.dopost = False # Enabled if post process nodes are set up + self.sock = socket # Socket of the output node for which this is the solution + self.alpha = alpha # Socket with alpha channel input + self.vcol = vcol # Is a vertex color output + self.format = socket.node.get_settings() # Dict of format settings + self.split = socket.node.get_split_objects() # List of objects to split output for + self.get_passes(socket, False, vcol) + if not vcol and alpha.enabled: + self.get_passes(alpha, True, vcol) + + # Clear all results from solution + def clear_results(self): + for key in self.bakepass.keys(): + node = self.bakepass[key] + node.bake_result = None + node.mask_result = None + node.sbake_result = None + node.smask_result = None + node.vcol_result = None + for key in self.baketile.keys(): + self.baketile[key] = {} + + # Moves backwards down the tree from a given input socket and will be called + # recursively until all paths have ended in a bake pass + def get_passes(self, socket, is_alpha, is_vcol): + node = get_input(socket) + if not node: return + + # Add the process, even if it's a pass (add func will sort it out) + self.add_postproc(node, socket, is_alpha, is_vcol) + + if node.bl_idname == 'BakeWrangler_Bake_Pass': + # Add to pass list + self.bakepass[node.name] = node + self.baketile[node.name] = {} + # Initialize results + node.bake_result = None + node.mask_result = None + node.sbake_result = None + node.smask_result = None + node.vcol_result = None + else: + # It's not a pass node, so it must be a post process, recurse + self.dopost = True + for sock in node.inputs: + self.get_passes(sock, is_alpha, is_vcol) + + # Adds the configuration from a post processing node into the post proc material + def add_postproc(self, post, from_sock, is_alpha, is_vcol): + #if self.postproc == None and (post.bl_idname == 'BakeWrangler_Bake_Pass' and not post.use_mask): + # return + if self.postproc == None: + if is_vcol: + self.postproc = post_proc_col.copy() + self.postproc.name = "BW_Post_Col" + self.sock.node.name + self.sock.suffix + else: + self.postproc = post_proc_mat.copy() + self.postproc.name = "BW_Post_" + self.sock.node.name + self.sock.suffix + + nodes = self.postproc.node_tree.nodes + links = self.postproc.node_tree.links + # Reached bake pass node + if post.bl_idname == 'BakeWrangler_Bake_Pass': + if is_vcol: + # Add vertex color attribute node + if post.name not in nodes.keys(): + vcols = nodes.new('ShaderNodeVertexColor') + vcols.name = post.name + # Originating node name stored in the label string so it can be looked up later + vcols.label = post.name + else: + # It's already been set up, but may need another link added + vcols = nodes[post.name] + self.link_postproc(from_sock, vcols.outputs['Color']) + return + self.link_postproc(from_sock, vcols.outputs['Color']) + else: + # Add a masked bake node set up + if post.name not in nodes.keys(): + masked_bake = nodes.new('ShaderNodeGroup') + masked_bake.node_tree = post_masked_bake + masked_bake.name = post.name + # Originating node name stored in the label string so it can be looked up later + masked_bake.label = post.name + # Invert output for smoothness passes + if post.bake_picked in ['SMOOTHNESS']: + masked_bake.inputs['Invert'].default_value = 1.0 + else: + masked_bake.inputs['Invert'].default_value = 0.0 + else: + # It's already been set up, but may need another link added + masked_bake = nodes[post.name] + self.link_postproc(from_sock, masked_bake.outputs['Bake']) + return + # Add and link image inputs + bake = nodes.new('ShaderNodeTexImage') + links.new(bake.outputs['Color'], masked_bake.inputs['Bake']) + mask = nodes.new('ShaderNodeTexImage') + links.new(mask.outputs['Color'], masked_bake.inputs['Mask']) + self.link_postproc(from_sock, masked_bake.outputs['Bake']) + # Channel mapping node + elif post.bl_idname == 'BakeWrangler_Channel_Map': + # Add channel map node + if post.name not in nodes.keys(): + chan_map = nodes.new('ShaderNodeGroup') + chan_map.node_tree = post_chan_map.copy() + chan_map.name = post.name + else: + # It's already set up but may need another link added + chan_map = nodes[post.name] + self.link_postproc(from_sock, chan_map.outputs['Color']) + return + # Configure node internals (initial configuration has Color input set up) + chan_nodes = chan_map.node_tree.nodes + chan_links = chan_map.node_tree.links + for chan in post.inputs.keys(): + if chan in ['Red', 'Green', 'Blue']: + if get_input(post.inputs[chan]): + # Connect up correct input + in_chan = post.inputs[chan].input_channel + if in_chan in ['Red', 'Green', 'Blue']: + chan_links.new(chan_nodes['Input'].outputs[chan], chan_nodes[chan + '_From'].inputs[0]) + chan_links.new(chan_nodes[chan + '_From'].outputs[in_chan], chan_nodes['RGB_Map'].inputs[chan]) + else: + chan_links.new(chan_nodes['Input'].outputs[chan], chan_nodes[chan + '_From_V'].inputs[0]) + chan_links.new(chan_nodes[chan + '_From_V'].outputs[0], chan_nodes['RGB_Map'].inputs[chan]) + # Link to prev + self.link_postproc(from_sock, chan_map.outputs['Color']) + # Mix RGB + elif post.bl_idname == 'BakeWrangler_Post_MixRGB': + # Add mix node + if post.name not in nodes.keys(): + mix_rgb = nodes.new('ShaderNodeMixRGB') + mix_rgb.name = post.name + mix_rgb.blend_type = post.op + mix_rgb.inputs['Fac'].default_value = post.inputs['Fac'].value_fac + mix_rgb.inputs['Color1'].default_value = post.inputs['Color1'].value_rgb + mix_rgb.inputs['Color2'].default_value = post.inputs['Color2'].value_rgb + # Link input + mix_rgb = nodes[post.name] + self.link_postproc(from_sock, mix_rgb.outputs['Color']) + # Split RGB + elif post.bl_idname == 'BakeWrangler_Post_SplitRGB': + # Add split node + if post.name not in nodes.keys(): + split_rgb = nodes.new('ShaderNodeSeparateColor') + split_rgb.name = post.name + # Link input + split_rgb = nodes[post.name] + self.link_postproc(from_sock, split_rgb.outputs[from_sock.links[0].from_socket.name]) + elif post.bl_idname == 'BakeWrangler_Post_JoinRGB': + # Add join node + if post.name not in nodes.keys(): + join_rgb = nodes.new('ShaderNodeCombineColor') + join_rgb.name = post.name + for chan in join_rgb.inputs: + chan.default_value = post.inputs[chan.name].value_col + # Link input + join_rgb = nodes[post.name] + self.link_postproc(from_sock, join_rgb.outputs['Color']) + elif post.bl_idname == 'BakeWrangler_Post_Math': + # Add maths node + if post.name not in nodes.keys(): + maths = nodes.new('ShaderNodeMath') + maths.name = post.name + maths.operation = post.op + for val in post.inputs: + maths.inputs[int(val.identifier)].default_value = val.value + # Link input + maths = nodes[post.name] + self.link_postproc(from_sock, maths.outputs['Value']) + elif post.bl_idname == 'BakeWrangler_Post_Gamma': + # Add gamma node + if post.name not in nodes.keys(): + gamma = nodes.new('ShaderNodeGamma') + gamma.name = post.name + gamma.inputs['Gamma'].default_value = post.inputs['Gamma'].value_gam + # Link input + gamma = nodes[post.name] + self.link_postproc(from_sock, gamma.outputs['Color']) + + # Link post proc to previous entry + def link_postproc(self, from_sock, to_sock): + nodes = self.postproc.node_tree.nodes + links = self.postproc.node_tree.links + if from_sock.node.bl_idname == 'BakeWrangler_Output_Vertex_Cols': + prev_sock = nodes['bw_emit'].inputs['Color'] + elif from_sock.node.bl_idname == 'BakeWrangler_Output_Image_Path': + if from_sock.name == 'Color': + prev_sock = nodes['AlphaSwitch'].inputs['Out'] + else: + prev_sock = nodes['AlphaMap'].inputs['Alpha'] + # Set up value mapping (default uses Value) + if from_sock.input_channel in ['R', 'G', 'B']: + nodes['AlphaMap'].node_tree = nodes['AlphaMap'].node_tree.copy() + amap_nodes = nodes['AlphaMap'].node_tree.nodes + amap_links = nodes['AlphaMap'].node_tree.links + amap_links.new(amap_nodes['Alpha_Map'].outputs[from_sock.input_channel], amap_nodes['Output'].inputs['Alpha']) + elif from_sock.node.bl_idname == 'BakeWrangler_Post_Math': + # Determine which input it is since they have the same name + prev_sock = nodes[from_sock.node.name].inputs[int(from_sock.identifier)] + else: + prev_sock = nodes[from_sock.node.name].inputs[from_sock.name] + links.new(to_sock, prev_sock) + + +# Process the node tree with the given node as the starting point +def process_tree(tree_name, node_name, socket): + # Create a base scene to work from that has every object in it + global base_scene + global mesh_scene + global active_scene + global current_frame + global frame_range + global padding + global anim_seed + base_scene = bpy.data.scenes.new("BakeWrangler_Base") + mesh_scene = bpy.data.scenes.new("BakeWrangler_Mesh") + active_scene = bpy.context.window.scene + current_frame = active_scene.frame_current + frame_range = None + padding = None + anim_seed = False + bpy.context.window.scene = base_scene + # For now use active scenes current animation frame (maybe more advanced options later) + base_scene.frame_current = current_frame + for obj in bpy.data.objects: + base_scene.collection.objects.link(obj) + + # Add a property on objects that can link to a copy made + bpy.types.Object.bw_copy = bpy.props.PointerProperty(name="Object Copy", description="Copy with modifiers applied", type=bpy.types.Object) + bpy.types.Object.bw_copy_frame = bpy.props.IntProperty(name="Object Copy frame", description="Frame number when created", default=current_frame) + bpy.types.Object.bw_strip = bpy.props.PointerProperty(name="Object Stripped", description="Copy with modifiers stripped", type=bpy.types.Object) + bpy.types.Object.bw_strip_frame = bpy.props.IntProperty(name="Object Stripped frame", description="Frame number when created", default=current_frame) + bpy.types.Object.bw_vcols = bpy.props.PointerProperty(name="Object VCol Copy", description="Copy with modifiers intact", type=bpy.types.Object) + bpy.types.Object.bw_auto_cage = bpy.props.PointerProperty(name="Cage", description="Bake Wrangler auto generated cage", type=bpy.types.Object) + + # Get tree position + tree = bpy.data.node_groups[tree_name] + node = tree.nodes[node_name] + err = 0 + solutions = None + global user_prop + user_prop = None + user_data = None + solution_itr = 0 + frames_itr = 0 + batch_itr = 0 + global solution_restart + global frames_restart + global batch_restart + + if debug: _print("> Debugging output enabled", tag=True) + modify_recipe(tree) + + # Each output node will generate a dict of bake solutions which will then be processed. Results + # are isolated within an output and may be regenerated if used in multiple outputs. + if node.bl_idname in ['BakeWrangler_Output_Image_Path', 'BakeWrangler_Output_Vertex_Cols']: + # A single output node, generate its solution list and then process them + solutions = process_output_node(node, socket) + if node.bl_idname == 'BakeWrangler_Output_Vertex_Cols': + _print("> Processing [%s]: Creating %i vertex colors" % (node.get_name(), len(solutions.keys())), tag=True) + _print(">", tag=True) + for key in solutions.keys(): + solution_itr += 1 + if (solution_itr - 1) < solution_restart: continue + err += process_vcol_solution(solutions[key]) + _print("%i" % (solution_itr)) + else: + # Check for frame ranges + frames = sorted(node.frame_range()) + padding = node.frame_range(padding=True) + anim_seed = node.frame_range(animated=True) + if len(frames) <= 1: + if len(frames) == 1: current_frame = frames.pop() + _print("> Processing [%s]: Creating %i images" % (node.get_name(), len(solutions.keys())), tag=True) + _print(">", tag=True) + for key in solutions.keys(): + solution_itr += 1 + if (solution_itr - 1) < solution_restart: continue + err += process_solution(solutions[key]) + _print("%i" % (solution_itr)) + elif len(frames) > 1: + pack_data() + frame_range = frames + if debug: _print("> Frame range: %s" % (frame_range), tag=True) + for frame in frames: + frames_itr += 1 + if (frames_itr - 1) < frames_restart: continue + current_frame = frame + base_scene.frame_current = current_frame + _print("> Processing [%s][Frame %s]: Creating %i images" % (node.get_name(), current_frame, len(solutions.keys())), tag=True) + _print(">", tag=True) + for key in solutions.keys(): + solution_itr += 1 + if (solution_itr - 1) < solution_restart: continue + else: solution_restart = 0 + err += process_solution(solutions[key]) + solutions[key].clear_results() + _print("%i" % (solution_itr)) + _print("%i" % (frames_itr)) + solution_itr = 0 + _print("%i" % (solution_itr)) + free_data() + elif node.bl_idname == 'BakeWrangler_Output_Batch_Bake': + _print("> Batch Mode [%i jobs]" % (len(node.inputs) - 1), tag=True) + # Batch mode has an optional user property that can be incremented, set that up + if node.user_prop_objt and node.user_prop_name: + user_data = node.user_prop_objt + user_prop = node.user_prop_name + if node.user_prop_zero: + user_data[user_prop] = 0 + user_data.update_tag() + # Then similar to above, but update and increment the property between outputs + for batch_input in node.inputs: + batch = get_input(batch_input) + if batch: + batch_itr += 1 + if (batch_itr - 1) < batch_restart: continue + solutions = process_output_node(batch) + if batch.bl_idname == 'BakeWrangler_Output_Vertex_Cols': + _print("> Processing [%s]: Creating %i vertex colors" % (batch.get_name(), len(solutions.keys())), tag=True) + _print(">", tag=True) + for key in solutions.keys(): + solution_itr += 1 + if (solution_itr - 1) < solution_restart: continue + else: solution_restart = 0 + err += process_vcol_solution(solutions[key]) + _print("%i" % (solution_itr)) + solution_itr = 0 + _print("%i" % (solution_itr)) + else: + # Check for frame ranges + frames = batch.frame_range() + if len(frames) <= 1: + if len(frames) == 1: current_frame = frames[0] + _print("> Processing [%s]: Creating %i images" % (batch.get_name(), len(solutions.keys())), tag=True) + _print(">", tag=True) + for key in solutions.keys(): + solution_itr += 1 + if (solution_itr - 1) < solution_restart: continue + else: solution_restart = 0 + err += process_solution(solutions[key]) + _print("%i" % (solution_itr)) + solution_itr = 0 + _print("%i" % (solution_itr)) + elif len(frames) > 1: + frame_range = frames + for frame in frames: + frames_itr += 1 + if (frames_itr - 1) < frames_restart: continue + else: frames_restart = 0 + current_frame = frame + _print("> Processing [%s][Frame %s]: Creating %i images" % (batch.get_name(), current_frame, len(solutions.keys())), tag=True) + _print(">", tag=True) + for key in solutions.keys(): + solution_itr += 1 + if (solution_itr - 1) < solution_restart: continue + else: solution_restart = 0 + err += process_solution(solutions[key]) + solutions[key].clear_results() + _print("%i" % (solution_itr)) + _print("%i" % (frames_itr)) + solution_itr = 0 + _print("%i" % (solution_itr)) + free_data() + frames_itr = 0 + _print("%i" % (frames_itr)) + if user_prop != None: + # Increment user property + user_data[user_prop] += 1 + user_data.update_tag() + _print("" % (batch_itr)) + else: + _print("> Invalid bake tree starting node", tag=True) + return True + return err + + +# To simplify doing the texture normal pass, the recipe will be modified if that pass is in use (maybe other stuff in future) +def modify_recipe(tree): + nodes = tree.nodes + links = tree.links + for node in nodes: + if node.bl_idname == 'BakeWrangler_Bake_Pass' and node.bake_picked == 'TEXNORM' and node.norm_space == 'TANGENT' and node.use_subtraction: + # Replace pass with a normal pass being subtracted from a object normals pass + norms = node + norms.bake_cat = 'CORE' + norms.bake_core = 'NORMAL' + objnorms = nodes.new('BakeWrangler_Bake_Pass') + objnorms.bake_cat = 'PBR' + objnorms.bake_pbr = 'OBJNORM' + # Connect all the same inputs to the objnorm node + for input in norms.inputs: + if input.islinked(): + if input.name == 'Settings': + links.new(input.links[0].from_socket, objnorms.inputs['Settings']) + else: + links.new(input.links[0].from_socket, objnorms.inputs[-1]) + # Create subtraction set up and insert it just after the pass nodes + in_sub = nodes.new('BakeWrangler_Post_MixRGB') + in_sub.op = 'SUBTRACT' + in_sub.inputs['Fac'].value_fac = 1.0 + out_sub = nodes.new('BakeWrangler_Post_MixRGB') + out_sub.op = 'ADD' + out_sub.inputs['Fac'].value_fac = 1.0 + out_sub.inputs['Color2'].value_rgb = [0.5, 0.5, 1.0, 1.0] + links.new(in_sub.outputs['Color'], out_sub.inputs['Color1']) + + for link in norms.outputs['Color'].links: + links.new(out_sub.outputs['Color'], link.to_socket) + + links.new(norms.outputs['Color'], in_sub.inputs['Color1']) + links.new(objnorms.outputs['Color'], in_sub.inputs['Color2']) + in_sub.inputs['Color2'].valid = True + + +# Process an output node into a list of bake solutions +def process_output_node(node, socket=-1): + solutions = {} + idx = 0 + for sock in node.inputs: + if sock.bl_idname == 'BakeWrangler_Socket_Color' and get_input(sock): + if socket > -1: + if socket == idx: + solutions[sock] = bake_solution(sock, node.inputs[idx+1], node.bl_idname == 'BakeWrangler_Output_Vertex_Cols') + else: + solutions[sock] = bake_solution(sock, node.inputs[idx+1], node.bl_idname == 'BakeWrangler_Output_Vertex_Cols') + idx += 1 + return solutions + + +# Process solution for vetex color output, passes will be baked and saved to temp files for transfer +def process_vcol_solution(solution): + err = 0 + # The first step in any solution is to bake all the required passes that haven't been done yet + for key in solution.bakepass.keys(): + if not solution.bakepass[key].vcol_result: + _print("> Pass: [%s] " % (solution.bakepass[key].get_name()), tag=True, wrap=False) + err += process_bake_pass(solution, key, solution.split) + _print(">", tag=True) + + _print("> -Exporting vertex colors:", tag=True, wrap=False) + # Now process each objects output + for obj in solution.bakecols.keys(): + if solution.dopost: + # Set input vertex colors + for node in solution.postproc.node_tree.nodes: + if node.bl_idname == 'ShaderNodeVertexColor': + # Check there is a vcol for this node + if node.label in solution.bakecols[obj].keys(): + node.layer_name = solution.bakecols[obj][node.label] + berr, post_obj, post_vcol = bake_post_vcols(bpy.data.objects[obj].bw_vcols, solution.postproc, solution) + err += berr + if not berr: + verr, file = ipc.bake_verts(verts=post_obj.data.color_attributes[post_vcol], object=obj, name=solution.sock.suffix, type=solution.format['vcol_type'], domain=solution.format['vcol_domain']) + err += verr + if not verr and file: + pickled_verts.append(file) + else: + for bake in solution.bakecols[obj].keys(): + verr, file = ipc.bake_verts(verts=bpy.data.objects[obj].bw_vcols.data.color_attributes[solution.bakecols[obj][bake]], object=obj, name=solution.sock.suffix, type=solution.format['vcol_type'], domain=solution.format['vcol_domain']) + err += verr + if not verr and file: + pickled_verts.append(file) + return err + + +# Process a solution, all passes need to be baked and any post processes done before saving to an image +def process_solution(solution): + err = 0 + # The first step in any solution is to bake all the required passes that haven't been done yet + for key in solution.bakepass.keys(): + if not solution.bakepass[key].bake_result: + _print("> Pass: [%s] " % (solution.bakepass[key].get_name()), tag=True, wrap=False) + err += process_bake_pass(solution, key, solution.split) + ret = 0 + udim = solution.format['img_udim'] + if solution.split: + # Go through each pass and generate just the objects in the split list saving each one + for obj in solution.split: + for key in solution.bakepass.keys(): + bake = solution.bakepass[key].bake_result + if bake: + bake = bake.copy() + mask = solution.bakepass[key].mask_result + if mask: + mask = mask.copy() + ret = process_bake_pass(solution, key, [obj], False, [bake, mask]) + if ret != -1: + err += ret + _print(">", tag=True) + if udim: # Output each udim + udims = [] + for key in solution.baketile.keys(): + for tile in solution.baketile[key].keys(): + if len(solution.baketile[key][tile]) > 2 and tile not in udims: + udims.append(tile) + udims.sort() + if not err: + obname = obj[0].name + if len(obj) > 3: obname = obj[3] + if udim: + for tile in udims: + err += process_output(solution, obname, udim=tile) + else: + err += process_output(solution, obname) + if not solution.split:# or ret == -1: + _print(">", tag=True) + if udim: # Output each udim + udims = [] + for key in solution.baketile.keys(): + for tile in solution.baketile[key].keys(): + if len(solution.baketile[key][tile]) and tile not in udims: + udims.append(tile) + udims.sort() + for tile in udims: + err += process_output(solution, udim=tile) + else: + err += process_output(solution) + return err + + +# Do post processing and generate output image +def process_output(solution, split="", udim=None): + err = 0 + # Check for frame range + frame_str = '' + if frame_range or padding: + if padding: + pad_width = padding + else: + pad_width = len(str(max(frame_range))) + frame_str = '.{frame:0{width}}'.format(frame=current_frame, width=pad_width) + # Get file path + output_path = solution.sock.node.img_path + if udim is not None: + output_name = solution.sock.node.name_with_ext(split + solution.sock.suffix + "." + str(udim) + frame_str) + else: + output_name = solution.sock.node.name_with_ext(split + solution.sock.suffix + frame_str) + output_file = os.path.join(os.path.realpath(output_path), output_name) + + # See if the output exists or if a new file should be created + orig_exists = False + if os.path.exists(output_file): + orig_image = bpy.data.images.load(os.path.abspath(output_file)) + bw_solution_data['images'].append(orig_image) + orig_image.alpha_mode = 'CHANNEL_PACKED' # Prevent alpha changing output color + orig_exists = True + + # Next post processing should take place as well as alpha channel creation if needed + _print("> Image: [%s] " % (output_name), tag=True) + post_alpha = None + if solution.format['img_color_mode'] == 'RGBA': + post_alpha = bpy.data.images.new(output_name + "alpha", solution.format['img_xres'], solution.format['img_yres']) + bw_solution_data['images'].append(post_alpha) + post_alpha.use_generated_float = solution.format['img_use_float'] + post_alpha.alpha_mode = 'NONE' + post_alpha.colorspace_settings.name = solution.format['img_non_color'] + post_alpha.colorspace_settings.is_data = True + # See if this solution will use marginer and need the combined mask + post_mask = None + if solution.format['marginer']: + post_mask = bpy.data.images.new(output_name + "mask", solution.format['img_xres'], solution.format['img_yres']) + bw_solution_data['images'].append(post_mask) + post_mask.alpha_mode = 'NONE' + post_mask.colorspace_settings.name = solution.format['img_non_color'] + post_mask.colorspace_settings.is_data = True + # Create post color image + post_color = bpy.data.images.new(output_name + "color", solution.format['img_xres'], solution.format['img_yres']) + bw_solution_data['images'].append(post_color) + post_color.colorspace_settings.name = solution.format['img_color_space'] + post_color.use_generated_float = solution.format['img_use_float'] + + nodes = solution.postproc.node_tree.nodes + links = solution.postproc.node_tree.links + # Set original image if it was loaded unless clear image is set + if orig_exists and not solution.format['img_clear']: + nodes['bw_input_orig'].image = orig_image + else: + nodes['bw_input_orig'].image = bpy.data.images["bw_default_orig"] + # Images in the post proc need to be set before running it + masks = [] + for node in nodes: + if node.bl_idname == 'ShaderNodeGroup' and node.node_tree == post_masked_bake: + # Set the bake input + bake_img = None + bake_set = solution.bakepass[node.label].get_settings() + if node.inputs['Bake'].is_linked: + bake_img = node.inputs['Bake'].links[0].from_node + if bake_set['interpolate']: + bake_img.interpolation = 'Cubic' + if split: + if udim is not None: + if udim in solution.baketile[node.label].keys(): + bake_img.image = solution.baketile[node.label][udim][2]#.copy() + #bake_img.image.pixels = solution.baketile[node.label][udim][2].pixels[:] + else: + bake_img.image = bpy.data.images["bw_default_orig"] + else: + bake_img.image = solution.bakepass[node.label].sbake_result#.copy() + #bake_img.image.pixels = solution.bakepass[node.label].sbake_result.pixels[:] + else: + if udim is not None: + if udim in solution.baketile[node.label].keys(): + bake_img.image = solution.baketile[node.label][udim][0]#.copy() + #bake_img.image.pixels = solution.baketile[node.label][udim][0].pixels[:] + else: + bake_img.image = bpy.data.images["bw_default_orig"] + else: + bake_img.image = solution.bakepass[node.label].bake_result#.copy() + #bake_img.image.pixels = solution.bakepass[node.label].bake_result.pixels[:] + bake_img.image.scale(solution.format['img_xres'], solution.format['img_yres']) + mask_img = None + if node.inputs['Mask'].is_linked: + if bake_set['use_mask'] or solution.format['marginer']: + mask_img = node.inputs['Mask'].links[0].from_node + bake_set = solution.bakepass[node.label].get_settings() + if split: + if udim is not None: + if udim in solution.baketile[node.label]: + mask_img.image = solution.baketile[node.label][udim][3] + else: + mask_img.image = bpy.data.images["bw_default_orig"] + else: + mask_img.image = solution.bakepass[node.label].smask_result + else: + if udim is not None: + if udim in solution.baketile[node.label]: + mask_img.image = solution.baketile[node.label][udim][1] + else: + mask_img.image = bpy.data.images["bw_default_orig"] + else: + mask_img.image = solution.bakepass[node.label].mask_result + mask_img.image.scale(solution.format['img_xres'], solution.format['img_yres']) + masks.append(node) + else: + links.remove(node.inputs['Mask'].links[0]) + # Add all masks together + add_masks(masks, nodes, solution.postproc.node_tree.links) + + # Bake post material + err += bake_post_material(post_color, post_alpha, solution.postproc, post_mask, bake_set['use_mask']) + + # Apply alpha channel if needed + post_combined = None + if post_alpha: + _print("> -Creating alpha channel", tag=True, wrap=False) + post_combined = bpy.data.images.new(output_name + "combined", solution.format['img_xres'], solution.format['img_yres']) + bw_solution_data['images'].append(post_combined) + post_combined.colorspace_settings.name = solution.format['img_color_space'] + post_combined.use_generated_float = solution.format['img_use_float'] + post_combined.alpha_mode = 'CHANNEL_PACKED' + aerr = alpha_pass(post_color, post_alpha, post_combined) + if not aerr: + _print("", tag=True) + err += aerr + + # Switch into output scene and apply output format + bpy.context.window.scene = output_scene + apply_output_format(output_scene.render.image_settings, solution.format) + solution_image = post_color + if post_combined: solution_image = post_combined + + # Do final processing if needed + if solution.format['marginer']: paint_margin(solution_image, post_mask, solution.format['marginer_size'], solution.format['marginer_fill']) + if solution.format['fast_aa']: fast_aa(solution_image, solution.format['fast_aa_lvl']) + + # Save the image to disk + _print("> -Writing changes to %s" % (output_file), tag=True) + _print(">", tag=True) + solution_image.save_render(output_file, scene=output_scene) + + images_saved.append(output_file) + bpy.context.window.scene = base_scene + + return err + + +# Takes a bake pass node and output format, creating an image in the bake_result and mask_result (if used) +def process_bake_pass(solution, key, split_list=None, exclude=True, imgs=None): + err = False + no_split = True + node = solution.bakepass[key] + format = solution.format + udim = format['img_udim'] + vcol = True if 'vcol' in format.keys() else False + + # Gather pass settings + bake_settings = node.get_settings() + bake_settings['node_name'] = node.name + bake_settings['vcol'] = vcol + bake_meshes = node.get_inputs() + + # Generate the bake and mask images + def pbp_img_gen(imgs=None, node=None, bake_settings=None): + if imgs is None or (len(imgs) and imgs[0] is None): + if imgs is None: _print(" [Mesh Nodes (%i)]" % (len(bake_meshes)), tag=True) + img_bake = bpy.data.images.new(node.get_name(), width=bake_settings["x_res"], height=bake_settings["y_res"]) + bw_solution_data['images'].append(img_bake) + img_mask = None + img_bake.alpha_mode = 'NONE' + if format['img_use_float']: + img_bake.use_generated_float = True + img_bake.colorspace_settings.name = format['img_color_space'] + #img_bake.colorspace_settings.name = 'Linear' + if 'sRGB' in format['img_color_space']: + bake_settings['osl_curv_srgb'] = True + if bake_settings['use_bg_col']: + img_bake.generated_color = bake_settings['bg_color'] + else: + if (bake_settings['bake_type'] in ['NORMAL', 'TEXNORM', 'OBJNORM', 'BEVNORMEMIT', 'BEVNORMNORM', 'CLEARNORM', 'OSL_BENTNORM'] and bake_settings['norm_s'] == 'TANGENT') or (bake_settings['bake_type'] == 'MULTIRES' and bake_settings['multi_pass'] == 'NORMALS'): + img_bake.generated_color = (0.5, 0.5, 1.0, 1.0) + elif bake_settings['bake_type'] == 'CURVATURE': + img_bake.generated_color = (bake_settings["curv_mid"], bake_settings["curv_mid"], bake_settings["curv_mid"], 1.0) + elif bake_settings['bake_type'] == 'OSL_HEIGHT': + img_bake.generated_color = (bake_settings["osl_height_midl"], bake_settings["osl_height_midl"], bake_settings["osl_height_midl"], 1.0) + #if bake_settings['use_mask']: + img_mask = bpy.data.images.new("mask_" + node.get_name(), width=bake_settings["x_res"], height=bake_settings["y_res"]) + bw_solution_data['images'].append(img_mask) + img_mask.alpha_mode = 'NONE' + img_mask.colorspace_settings.name = format['img_non_color'] + img_mask.colorspace_settings.is_data = True + else: + img_bake = imgs[0] + img_mask = None + #if bake_settings['use_mask'] and len(imgs) > 1: + if len(imgs) > 1: + img_mask = imgs[1] + return img_bake, img_mask + + if not vcol: + img_bake, img_mask = pbp_img_gen(imgs=imgs, node=node, bake_settings=bake_settings) + else: + _print(" [Mesh Nodes (%i)]" % (len(bake_meshes)), tag=True) + img_bake = img_mask = None + + # Begin processing bake meshes + for mesh_dat in bake_meshes: + mesh = mesh_dat[0] + multi = (bake_settings['bake_type'] == 'MULTIRES') + + # Determine bake type and object groups + def pbp_type_and_obj_groups(mesh=None): + hi2lo = False + matbk = False + bbbk = False + bake_settings['bbbk'] = False + if mesh.bl_idname == 'BakeWrangler_Sort_Meshes': + input_src = get_input(mesh_dat[1]) + mesh_settings = mesh.get_settings(input=mesh_dat[1]) + mesh_settings['mesh_name'] = input_src.name + mesh_settings['mesh_label'] = input_src.get_name() + if input_src.bl_idname == 'BakeWrangler_Bake_Material': + hi2lo = False + matbk = True + active_meshes = input_src.get_materials() + selected_objs = [] + scene_objs = [] + else: + hi2lo = True + active_meshes = mesh.get_objects('TARGET', mesh_dat[1]) + selected_objs = mesh.get_objects('SOURCE', mesh_dat[1]) + scene_objs = mesh.get_objects('SCENE', mesh_dat[1]) + else: + mesh_settings = mesh.get_settings() + mesh_settings['mesh_name'] = mesh.name + mesh_settings['mesh_label'] = mesh.get_name() + if mesh.bl_idname == 'BakeWrangler_Bake_Material': + matbk = True + active_meshes = mesh.get_materials() + selected_objs = [] + scene_objs = [] + else: + if mesh.bl_idname == 'BakeWrangler_Bake_Billboard': + bbbk = True + bake_settings['bbbk'] = True + active_meshes = mesh.get_objects('TARGET') + selected_objs = mesh.get_objects('SOURCE') + scene_objs = mesh.get_objects('SCENE') + return hi2lo, matbk, bbbk, mesh_settings, active_meshes, selected_objs, scene_objs + + hi2lo, matbk, bbbk, mesh_settings, active_meshes, selected_objs, scene_objs = pbp_type_and_obj_groups(mesh=mesh) + bake_settings['bbbk'] = bbbk + + # Bake is supposed to be split into multiple files based on object names from list + if split_list: + active_split = [] + for act in active_meshes: + msh = act + if hi2lo: + replace_list = [] + for splt in split_list: + replace_list.append(splt[0:3]) + split_list = replace_list + msh = act[0] + if (exclude and msh not in split_list) or (not exclude and msh in split_list): + active_split.append(act) + if len(active_split): + active_meshes = active_split + no_split = False + else: + continue + + if matbk: + _print("> Materials: [%s] [Targets (%i)]" % (mesh_settings['mesh_label'], len(active_meshes)), tag=True) + elif bbbk: + _print("> Billboard: [%s]" % (mesh_settings['mesh_label']), tag=True) + else: + _print("> Mesh: [%s] [Targets (%i)]" % (mesh_settings['mesh_label'], len(active_meshes)), tag=True) + + if mesh_settings['margin'] > 0 or mesh_settings['margin_auto']: + if mesh_settings['margin_auto']: + mesh_settings["margin"] = min(bake_settings["x_res"], bake_settings["y_res"]) / 256 + if mesh_settings["margin"] < 1: mesh_settings["margin"] = 1 + if mesh_settings["margin"] > 32: mesh_settings["margin"] = 32 + # Recalculate margin size based on ratio between bake and output images + bake_ratio = math.sqrt(math.pow(bake_settings["x_res"], 2) + math.pow(bake_settings["y_res"], 2)) # Pythag for diagonal + outp_ratio = math.sqrt(math.pow(format['img_xres'], 2) + math.pow(format['img_yres'], 2)) + margin_ratio = bake_ratio / outp_ratio # Ratio between sizes + mesh_settings["margin"] *= margin_ratio # Multiply margin by ratio to maintain size in output + if debug: _print("> Margin %s" % (mesh_settings["margin"]), tag=True) + + # Process each active mesh + for active in active_meshes: + # Unpack active and selected from active if doing high to low poly auto mapped bake + if hi2lo: + scene_objs = active[2] + selected_objs = active[1] + active = active[0] + # Load in template bake scene with mostly optimized settings for baking + bake_scene_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources", "BakeWrangler_Scene.blend") + with bpy.data.libraries.load(bake_scene_path, link=False, relative=False) as (file_from, file_to): + file_to.scenes.append("BakeWrangler") + bake_scene = file_to.scenes[0] + bake_scene.name = "bake_" + node.get_name() + "_" + mesh.get_name() + "_" + active[0].name + bake_scene.frame_current = current_frame + if anim_seed: + bake_scene.cycles.seed = current_frame + bw_solution_data['scenes'].append(bake_scene) + + # Copy render settings if required + if bake_settings['cpy_render']: + if bake_settings['cpy_from'] in [None, ""]: + bake_settings['cpy_from'] = active_scene + copy_render_settings(bake_settings['cpy_from'], bake_scene) + + # Set up camera if that is ray origin + if 'view_from' in mesh_settings: + bake_scene.collection.objects.link(mesh_settings['view_from']) + bake_scene.camera = mesh_settings['view_from'] + bake_scene.render.bake.view_from = 'ACTIVE_CAMERA' + else: + bake_scene.render.bake.view_from = 'ABOVE_SURFACE' + + # Set the device and sample count to override anything that could have been copied + bake_OSL = False + bake_scene.cycles.device = bake_settings['bake_device'] + if bake_settings['bake_type'].startswith('OSL_') or (mesh_settings['material_replace'] and mesh_settings['material_osl']): + if bake_settings['bake_type'] == 'OSL_HEIGHT': + bake_OSL = True + bake_scene.cycles.device = 'CPU' + bake_scene.cycles.shading_system = True + bake_scene.cycles.samples = bake_settings['bake_samples'] + #bake_scene.cycles.aa_samples = bake_settings['bake_samples'] + bake_scene.cycles.use_adaptive_sampling = bake_settings['bake_usethresh'] + bake_scene.cycles.adaptive_threshold = bake_settings['bake_threshold'] + bake_scene.cycles.time_limit = bake_settings['bake_timelimit'] + + # Set custom world instead of default if enabled + if bake_settings['use_world']: + if bake_settings['the_world'] not in [None, ""]: + bake_scene.world = bake_settings['the_world'] + else: + bake_scene.world = active_scene.world + + # Initialise bake type settings + mesh_settings["cage"] = False + mesh_settings["cage_object"] = None + mesh_settings["cage_obj_name"] = "" + to_active = False + selected = None + + # For material bake, we can skip most of the rest of the steps + if matbk: + # Add a plane, sized correctly and put a copy of the material on it after being prepped + matpln = material_plane.copy() + bw_solution_data['objects'].append(matpln) + matpln.data = material_plane.data.copy() + matpln.name = "mat_plane_" + active[0].name + matpln.dimensions = (mesh_settings['matbk_width'], mesh_settings['matbk_height'], 0.0) + # Do material copy and prep + mat2bk = active[0].copy() + bw_solution_data['materials'].append(mat2bk) + matpln.data.materials.append(mat2bk) + bake_scene.collection.objects.link(matpln) + # Switch into bake scene + bpy.context.window.scene = bake_scene + # Select the target and make it active + bpy.ops.object.select_all(action='DESELECT') + matpln.select_set(True) + bpy.context.view_layer.objects.active = matpln + # Bake the plane + err += bake_solo(img_bake, {'mat': mat2bk}, bake_settings, mesh_settings) + # Switch back to main scene before next pass. Nothing will be deleted so that the file can be examined for debugging. + bpy.context.window.scene = base_scene + # Skip the rest of the steps + continue + + # Determine what strategy to use for this bake + if not multi: + # See if there are valid source objects to do a selected to active bake + for obj in selected_objs: + # Let a duplicate of the target object count if they use different UV Maps + if obj[0] != active[0] or (len(obj) > 1 and len(active) > 1 and obj[1] != active[1]): + to_active = True + break + if mesh_settings["bake_mods"]: to_active = True + # Copy all selected objects over if 'to active' pass + if to_active: + selected = bpy.data.collections.new("Selected_" + active[0].name) + bw_solution_data['collections'].append(selected) + if mesh_settings["bake_mods"]: + # Add the Target to the Sources if it isn't already in the list before processing + active_in_selected = False + for obj in selected_objs: + if obj[0] == active[0]: + active_in_selected = True + break + if not active_in_selected: + selected_objs.append(active) + for obj in selected_objs: + # Let a duplicate of the target object in if they use different UV Maps + if obj[0] != active[0] or (len(obj) > 1 and len(active) > 1 and obj[1] != active[1]) or mesh_settings["bake_mods"]: + copy = prep_object_for_bake(obj[0], invert=mesh_settings["bake_mods_invert"], vcols=[vcol, active[0]]) + bw_solution_data['objects'].append(copy) + selected.objects.link(copy) + # Set UV map to use if one was selected + if len(obj) > 1 and obj[1] not in [None, ""]: + copy.data.uv_layers.active = copy.data.uv_layers[obj[1]] + bake_scene.collection.children.link(selected) + # Add the cage copy to the scene because it doesn't work properly in a different scene currently + if len(active) > 2 and active[2]: + mesh_settings["cage"] = True + mesh_settings["cage_object"] = prep_object_for_bake(active[2], invert=mesh_settings["bake_mods_invert"], vcols=[vcol, active[0]]) + bw_solution_data['objects'].append(mesh_settings["cage_object"]) + mesh_settings["cage_obj_name"] = mesh_settings["cage_object"].name + elif active[0].bw_auto_cage: + mesh_settings["cage"] = True + mesh_settings["cage_object"] = prep_object_for_bake(active[0].bw_auto_cage, invert=mesh_settings["bake_mods_invert"], vcols=[vcol, active[0]]) + bw_solution_data['objects'].append(mesh_settings["cage_object"]) + mesh_settings["cage_obj_name"] = mesh_settings["cage_object"].name + else: + # Collection of base objects for multi-res to link into + base_col = bpy.data.collections.new("Base_" + active[0].name) + bw_solution_data['collections'].append(base_col) + bake_scene.collection.children.link(base_col) + + # Regardless of strategy the following data will be used. Copies are made so other passes can get the originals + if not multi: + if mesh_settings["bake_mods"]: + target = prep_object_for_bake(active[0], strip=True, invert=mesh_settings["bake_mods_invert"], vcols=[vcol, active[0]]) + else: + target = prep_object_for_bake(active[0], vcols=[vcol, active[0]]) + else: + active_obj = active[0] + target = active_obj.copy() + target.data = active_obj.data.copy() + bw_solution_data['objects'].append(target) + + # Check for valid cage now if one is set + if mesh_settings["cage"]: + if len(target.data.polygons) != len(mesh_settings["cage_object"].data.polygons): + _print("> !Cage invalid for [%s]" % (active[0].name), tag=True) + mesh_settings["cage"] = False + mesh_settings["cage_obj_name"] = "" + + # Set UV map to use if one was selected + if len(active) > 1 and active[1] not in [None, ""]: + target.data.uv_layers.active = target.data.uv_layers[active[1]] + + # Materials should be removed from the target copy for To active + if to_active: + target.data.materials.clear() + target.data.polygons.foreach_set('material_index', [0] * len(target.data.polygons)) + target.data.update() + # If no specific cage, but auto cage is enabled, create a cage for the object + if not mesh_settings["cage"] and mesh_settings["auto_cage"]: + mesh_settings["cage"] = True + mesh_settings["cage_object"] = target.copy() + mesh_settings["cage_object"].data = target.data.copy() + mesh_settings["cage_object"].name = "%s.%s" % (target.name, "auto_cage") + mesh_settings["cage_obj_name"] = mesh_settings["cage_object"].name + # Expand the cage object + cage_displace = mesh_settings["cage_object"].modifiers.new("cage_disp", 'DISPLACE') + cage_displace.strength = mesh_settings['acage_expansion'] + cage_displace.direction = 'NORMAL' + cage_displace.mid_level = 0.0 + # Smooth normals + mesh_settings["cage_object"].data.use_auto_smooth = True + mesh_settings["cage_object"].data.auto_smooth_angle = mesh_settings['acage_smooth'] + for poly in mesh_settings["cage_object"].data.polygons: + poly.use_smooth = True + # Clear sharps + for edge in mesh_settings["cage_object"].data.edges: + edge.use_edge_sharp = False + + # Add target before doing mats + if not to_active or bake_OSL: + bake_scene.collection.objects.link(target) + + # Replace materials if required + if bake_settings['bake_cat'] == 'WRANG' or mesh_settings['material_replace']: + replace_materials_for_shader_bake(bake_scene, bake_settings, mesh_settings) + # Add OSL materials to target now so they can be configured by the next step + if bake_settings['bake_type'] == 'OSL_HEIGHT': + target.data.materials.append(osl_height) + # Create unique copies for every material in the scene before anything else is done + unique_mats = make_materials_unique_to_scene(bake_scene, "_" + node.name + "_" + mesh.name + "_" + active[0].name, bake_settings) + for mkey in unique_mats.keys(): + bw_solution_data['materials'].append(unique_mats[mkey]) + + # Add target after doing mats + if to_active and not bake_OSL: + bake_scene.collection.objects.link(target) + + # Make sure a basic material is on the target + if bbbk: + # Modify settings for billboard bake here to use glossy color pass with special material + basic_mat = billboard_mat.copy() + bw_solution_data['materials'].append(basic_mat) + basic_mat.name = billboard_mat.name + node.name + mesh.name + check_has_material(target, None, basic_mat) + else: + basic_mat = bpy.data.materials.new(name="bw_basic_" + node.name + "_" + mesh.name) + bw_solution_data['materials'].append(basic_mat) + basic_mat.use_nodes = True + check_has_material(target, unique_mats, basic_mat) + # Add a material to any objects lacking a material for bent norms + if bake_settings['bake_type'] == 'OSL_BENTNORM': + bent_mat = basic_mat.copy() + bw_solution_data['materials'].append(bent_mat) + bent_mat.name = "bw_bent_" + node.name + "_" + mesh.name + if to_active: + for from_obj in selected.objects: + check_has_material(from_obj, unique_mats, bent_mat) + + # Rotation of target + bake_settings['bb_rot'] = active[0].rotation_euler + + # Copy all scene objects over if not a multi-res pass + if not multi: + scene = bpy.data.collections.new("Scene_" + active[0].name) + bw_solution_data['collections'].append(scene) + for obj in scene_objs: + # Exclude current target if its in the list as well as any source objects as these + # cause problems in blender 3.1 + dups = [active[0]] + for dup in selected_objs: + dups.append(dup[0]) + if obj[0] not in dups: + scene.objects.link(obj[0]) + bake_scene.collection.children.link(scene) + # Add cage object + if mesh_settings["cage"]: + bake_scene.collection.objects.link(mesh_settings["cage_object"]) + + # Switch into bake scene + bpy.context.window.scene = bake_scene + + # OSL setup for cage, swaps cage for target + if (bake_OSL or bbbk) and mesh_settings["cage_object"]: + target.hide_render = True + mesh_settings["cage_object"].data.materials.clear() + mesh_settings["cage_object"].data.polygons.foreach_set('material_index', [0] * len(target.data.polygons)) + mesh_settings["cage_object"].data.update() + mesh_settings["cage_object"].data.materials.append(target.data.materials[0]) + target = mesh_settings["cage_object"] + mesh_settings["cage"] = False + + # Select the target and make it active + bpy.ops.object.select_all(action='DESELECT') + target.select_set(True) + bpy.context.view_layer.objects.active = target + + # If UDIM tiles are enabled, the bake must repeat for each tile and the UV map needs to be modified between + if udim: + tiles = solution.baketile[key] + udims = uv_to_udim(solution, key, target.data.uv_layers.active) + _print("> UDIM: [%s] [Tiles (%s)]" % (active[0].name, len(udims)), tag=True) + # Do pre bake set up before doing each tile + if multi: + bake_multi_res(img_bake, unique_mats, bake_settings, mesh_settings, base_col, target, active_obj, pre_only=True) + elif to_active and not bake_OSL: + err += bake_to_active(img_bake, unique_mats, bake_settings, mesh_settings, selected, pre_only=True) + else: + err += bake_solo(img_bake, unique_mats, bake_settings, mesh_settings, pre_only=True) + for tile in udims: + if not len(tiles[tile]): + # Tile image hasn't been assigned yet, make a copy of the bake image (and mask) + tile_bake = img_bake.copy() + bw_solution_data['images'].append(tile_bake) + tile_bake.pixels = img_bake.pixels[:] + tile_bake.name = "%s.%s" % (img_bake.name, tile) + tile_bake.update() + tile_mask = None + if bake_settings['use_mask'] or format['marginer']: + tile_mask = img_mask.copy() + bw_solution_data['images'].append(tile_mask) + tile_mask.pixels = img_mask.pixels[:] + tile_mask.name = "%s.%s" % (img_mask.name, tile) + tile_mask.update() + tiles[tile] = [tile_bake, tile_mask] + tile_bake = tiles[tile][0] + if split_list and not exclude: + tile_bake = tiles[tile][0].copy() + bw_solution_data['images'].append(tile_bake) + tile_bake.pixels = tiles[tile][0].pixels[:] + tile_bake.update() + if len(tiles[tile]) < 3: + tiles[tile].append(tile_bake) + else: + tiles[tile][2] = tile_bake + # UV map needs to be modified to move the current tile within the 0-1 range + uv_name = target.data.uv_layers.active.name + uv_tile = target.data.uv_layers[uv_name] + focus_udim_tile(uv_tile, tile) + bake_settings['tile_no'] = tile + # Perform bake type needed + if multi: + err += bake_multi_res(tile_bake, unique_mats, bake_settings, mesh_settings, base_col, target, active_obj, False, tile_bake) + elif to_active and not bake_OSL: + err += bake_to_active(tile_bake, unique_mats, bake_settings, mesh_settings, selected, False, tile_bake) + else: + err += bake_solo(tile_bake, unique_mats, bake_settings, mesh_settings, False, tile_bake) + # Switch back to original UVs before next pass + focus_udim_tile(uv_tile, tile, unfocus=True) + # Go back over tiles if masks are needed + if bake_settings['use_mask'] or format['marginer']: + bake_mask(tile_mask, unique_mats, bake_settings, mesh_settings, to_active, target, selected, pre_only=True) + for tile in udims: + tile_mask = tiles[tile][1] + if split_list and not exclude: + tile_mask = tiles[tile][1].copy() + bw_solution_data['images'].append(tile_mask) + tile_mask.pixels = tiles[tile][1].pixels[:] + tile_mask.update() + if len(tiles[tile]) < 4: + tiles[tile].append(tile_mask) + else: + tiles[tile][3] = tile_mask + # UV map needs to be modified to move the current tile within the 0-1 range + uv_name = target.data.uv_layers.active.name + uv_tile = target.data.uv_layers[uv_name] + focus_udim_tile(uv_tile, tile) + bake_settings['tile_no'] = tile + # Set samples to the mask value + bake_scene.cycles.device = bake_settings['bake_device'] + bake_scene.cycles.samples = 1 + #bake_scene.cycles.aa_samples = 1 + err += bake_mask(tile_mask, unique_mats, bake_settings, mesh_settings, to_active, target, selected, False, tile_mask) + # Switch back to original UVs before next pass + focus_udim_tile(uv_tile, tile, unfocus=True) + else: + if vcol: + img_vcol = target.data.color_attributes.new(node.get_name(), format['vcol_type'], format['vcol_domain']) + target.data.color_attributes.active_color = img_vcol + + # Perform bake type needed + if multi: + if vcol: _print("> Multi-Resolution cannot be output to vertex colors, skipping pass", tag=True) + else: err += bake_multi_res(img_bake, unique_mats, bake_settings, mesh_settings, base_col, target, active_obj) + elif to_active and not bake_OSL: + err += bake_to_active(img_bake, unique_mats, bake_settings, mesh_settings, selected) + else: + err += bake_solo(img_bake, unique_mats, bake_settings, mesh_settings) + + # Bake the mask if samples are non zero + if (bake_settings['use_mask'] or format['marginer']) and not vcol: + # Set samples to the mask value + bake_scene.cycles.device = bake_settings['bake_device'] + bake_scene.cycles.samples = 1 + #bake_scene.cycles.aa_samples = 1 + err += bake_mask(img_mask, unique_mats, bake_settings, mesh_settings, to_active, target, selected) + + # Output vcolors to vcol object copy + if vcol: + if active[0].name not in solution.bakecols.keys(): + # Create dict for this object if not done already + solution.bakecols[active[0].name] = {} + # Store name of vcols for this bake node + solution.bakecols[active[0].name][node.name] = img_vcol.name + # Copy vcols to base object copy + copy_vert_cols(name=img_vcol.name, cpy_from=target, cpy_to=active[0].bw_vcols) + + # Switch back to main scene before next pass. Nothing will be deleted so that the file can be examined for debugging. + bpy.context.window.scene = base_scene + + # Return early if no split list item was baked + if split_list and no_split: + if not exclude: + return -1 + else: + return err + + # Finished inputs, return the bakes + if not udim and not vcol: + if exclude: + node.bake_result = img_bake + node.mask_result = img_mask + else: + node.sbake_result = img_bake + node.smask_result = img_mask + elif vcol: + node.vcol_result = img_vcol + return err + + + +# Bake a multi-res pass +def bake_multi_res(img_bake, materials, bake_settings, mesh_settings, base_col, target, original, pre_only=False, tile=None): + # Set multi res levels on copy + multi_mod = None + if not tile: # Tile is assumed to have run a pre only time first + for mod in target.modifiers: + if mod.type == 'MULTIRES': + multi_mod = mod + break + if multi_mod: + multi_mod.levels = 0 + multi_mod.render_levels = multi_mod.total_levels + if bake_settings["multi_samp"] == 'FROMMOD': + src_mod = None + for mod in original.modifiers: + if mod.type == 'MULTIRES': + src_mod = mod + break + if src_mod: + multi_mod.levels = src_mod.levels + multi_mod.render_levels = src_mod.render_levels + elif bake_settings["multi_samp"] == 'CUSTOM': + if bake_settings["multi_targ"] >= 0 and bake_settings["multi_targ"] <= multi_mod.total_levels: + multi_mod.levels = bake_settings["multi_targ"] + if bake_settings["multi_sorc"] >= 0 and bake_settings["multi_sorc"] <= multi_mod.total_levels: + multi_mod.render_levels = bake_settings["multi_sorc"] + + # Next link all the objects from the base scene to hopefully stop any modifier errors + for obj in base_scene.objects: + base_col.objects.link(obj) + obj.select_set(False) + + # Add a bake target image node to each material + for mat in materials.values(): + if not tile: + if debug: _print("> Preparing material [%s] for [Multi-Res %s] bake" % (mat.name, bake_settings['bake_type']), tag=True) + image_node = mat.node_tree.nodes.new("ShaderNodeTexImage") + image_node.image = img_bake + image_node.select = True + mat.node_tree.nodes.active = image_node + else: + # In the case of a tile, the node should already be created and active, so just change image path + image_node = mat.node_tree.nodes.active + image_node.image = tile + + # Bake it + if pre_only: return 0 + return bake(bake_settings['bake_cat'], bake_settings['multi_pass'], bake_settings, mesh_settings, False, True) + + + +# Bake a to-active pass +def bake_to_active(img_bake, materials, bake_settings, mesh_settings, selected, pre_only=False, tile=None): + if not tile: + # Make the source objects selected + if not bake_settings['bbbk']: + for obj in selected.objects: + obj.select_set(True) + + # Add texture node set up to target object + mat = bpy.context.view_layer.objects.active.material_slots[0].material + image_node = mat.node_tree.nodes.new("ShaderNodeTexImage") + image_node.image = img_bake + image_node.select = True + mat.node_tree.nodes.active = image_node + + # Prepare the materials for the bake type + for mat in materials.values(): + if debug: _print("> Preparing material [%s] for [%s] bake" % (mat.name, bake_settings['bake_type']), tag=True) + if not mat.name.startswith('bw_basic_'): prep_material_for_bake(mat.node_tree, bake_settings['bake_type'], bake_settings) + else: + # In the case of a tile, the node should already be created and active, so just change image path + mat = bpy.context.view_layer.objects.active.material_slots[0].material + image_node = mat.node_tree.nodes.active + image_node.image = tile + + # Bake it + if pre_only: return 0 + return bake(bake_settings['bake_cat'], bake_settings['bake_type'], bake_settings, mesh_settings, True, False) + + + +# Bake single object pass +def bake_solo(img_bake, materials, bake_settings, mesh_settings, pre_only=False, tile=None): + # Prepare the materials for the bake type + for mat in materials.values(): + if not tile: + if debug: _print("> Preparing material [%s] for [%s] bake" % (mat.name, bake_settings['bake_type']), tag=True) + prep_material_for_bake(mat.node_tree, bake_settings['bake_type'], bake_settings) + # For non To active bakes, add an image node to the material and make it selected + active for bake image + image_node = mat.node_tree.nodes.new("ShaderNodeTexImage") + image_node.image = img_bake + image_node.select = True + mat.node_tree.nodes.active = image_node + else: + # In the case of a tile, the node should already be created and active, so just change image path + image_node = mat.node_tree.nodes.active + image_node.image = tile + + # Bake it + if pre_only: return 0 + return bake(bake_settings['bake_cat'], bake_settings['bake_type'], bake_settings, mesh_settings, False, False) + + + +# Bake a masking pass +def bake_mask(img_mask, materials, bake_settings, mesh_settings, to_active, target, selected, pre_only=False, tile=None): + if not tile: + # Make sure a basic material is on every object + mat = bpy.data.materials.new(name="bw_mask_" + bake_settings["node_name"] + "_" + mesh_settings["mesh_name"]) + bw_solution_data['materials'].append(mat) + mat.use_nodes = True + objs = [target] + if selected: + for obj in selected.objects: + objs.append(obj) + for obj in objs: + check_has_material(obj, materials, mat) + + # Requires adding a pure while emit shader to all the materials first and changing target image + for mat in materials.values(): + if not tile: + prep_material_for_bake(mat.node_tree, 'MASK', bake_settings) + + # Add image node to material and make it selected + active + if not to_active: + if not tile: + image_node = mat.node_tree.nodes.new("ShaderNodeTexImage") + image_node.image = img_mask + image_node.select = True + mat.node_tree.nodes.active = image_node + else: + # In the case of a tile, the node should already be created and active, so just change image path + image_node = mat.node_tree.nodes.active + image_node.image = tile + + # Add image node to target and make it selected + active (should only be one material at this point) + if to_active: + if not tile: + image_node = bpy.context.view_layer.objects.active.material_slots[0].material.node_tree.nodes.new("ShaderNodeTexImage") + image_node.image = img_mask + image_node.select = True + bpy.context.view_layer.objects.active.material_slots[0].material.node_tree.nodes.active = image_node + else: + # In the case of a tile, the node should already be created and active, so just change image path + image_node = bpy.context.view_layer.objects.active.material_slots[0].material.node_tree.nodes.active + image_node.image = tile + + # Bake it + #mesh_settings["margin"] += mesh_settings["mask_margin"] + if pre_only: return 0 + return bake('PBR', 'MASK', bake_settings, mesh_settings, to_active, False) + + + +# Call actual bake commands +def bake(bake_cat, bake_type, bake_settings, mesh_settings, to_active, multi): + # Set 'real' bake pass. PBR use EMIT rather than the named pass, since those passes don't exist. + if bake_settings['bbbk']: + real_bake_type = 'GLOSSY' + bake_settings['influences'] = set() + bake_settings['influences'].add('DIRECT') + bpy.context.scene.cycles.transparent_max_bounces = mesh_settings['alpha_bounce'] + to_active = False + elif bake_cat in ['PBR', 'WRANG']: + if bake_type in ['OBJNORM', 'BEVNORMNORM', 'OSL_BENTNORM']: real_bake_type = 'NORMAL' + else: real_bake_type = 'EMIT' + elif bake_cat == 'CORE' and bake_type == 'SMOOTHNESS': + real_bake_type = 'ROUGHNESS' + else: + real_bake_type = bake_type + + # Set target of output + target_mode = 'VERTEX_COLORS' if ('vcol' in bake_settings.keys() and bake_settings['vcol']) else 'IMAGE_TEXTURES' + + # Set threads if not default + if bake_settings['threads'] != 0: + bpy.context.scene.render.threads_mode = 'FIXED' + bpy.context.scene.render.threads = bake_settings['threads'] + + # Set tile sizes if not default + if bake_settings['tiles'] in ['IMG', 'CUST']: + if bake_settings['tiles'] == 'IMG': + bpy.context.scene.cycles.tile_size = bake_settings["x_res"] + else: + bpy.context.scene.cycles.tile_size = bake_settings["tile_size"] + + if mesh_settings["margin_extend"]: bpy.context.scene.render.bake.margin_type = 'EXTEND' + else: bpy.context.scene.render.bake.margin_type = 'ADJACENT_FACES' + + if debug: _print("> Real bake type set to [%s], Mode [%s]" % (real_bake_type, target_mode), tag=True) + + # Update view layer to be safe + bpy.context.view_layer.update() + start = datetime.now() + if 'tile_no' in bake_settings.keys(): + _print("> -Baking %s pass [tile %s]: " % (bake_type, bake_settings['tile_no']), tag=True, wrap=False) + del bake_settings['tile_no'] + else: + _print("> -Baking %s pass: " % (bake_type), tag=True, wrap=False) + + # Do the bake. Most of the properties can be passed as arguments to the operator. + err = False + try: + if not multi: + bpy.ops.object.bake( + type=real_bake_type, + pass_filter=bake_settings["influences"], + margin=int(mesh_settings["margin"]), + use_selected_to_active=to_active, + max_ray_distance=mesh_settings["max_ray_dist"], + cage_extrusion=mesh_settings["ray_dist"], + cage_object=mesh_settings["cage_obj_name"], + normal_space=bake_settings["norm_s"], + normal_r=bake_settings["norm_r"], + normal_g=bake_settings["norm_g"], + normal_b=bake_settings["norm_b"], + target=target_mode, + save_mode='INTERNAL', + use_clear=False, + use_cage=mesh_settings["cage"], + ) + else: + bpy.context.scene.render.use_bake_multires = True + bpy.context.scene.render.bake_margin = int(mesh_settings["margin"]) + bpy.context.scene.render.bake_type = bake_type + bpy.context.scene.render.use_bake_clear = False + bpy.ops.object.bake_image() + except RuntimeError as error: + _print("%s" % (error), tag=True) + err = True + else: + _print("Completed in %s" % (str(datetime.now() - start)), tag=True) + return err + + + +# Handle post processes that need to be applied to the baked data to create the desired map +def bake_post(img_bake, settings, format): + post_obj = post_scene.objects["BW_Post_%s" % (settings['bake_type'])] + post_mat = post_obj.material_slots[0].material.node_tree.nodes + post_src = post_mat["bw_post_input"] + post_out = post_mat["bw_post_output"] + + post_img = bpy.data.images.new(img_bake.name + "_POST", width=settings["x_res"], height=settings["y_res"]) + post_img.colorspace_settings.name = format['img_color_space'] + post_img.colorspace_settings.is_data = img_bake.colorspace_settings.is_data + post_img.use_generated_float = img_bake.use_generated_float + + # Switch into post scene and set up the selection state + bpy.context.window.scene = post_scene + bpy.ops.object.select_all(action='DESELECT') + post_obj.select_set(True) + bpy.context.view_layer.objects.active = post_obj + + # Set up standard images + post_src.image = img_bake + post_out.image = post_img + + # Generate image + bpy.context.view_layer.update() + err = False + try: + bpy.ops.object.bake( + type="EMIT", + save_mode='INTERNAL', + use_clear=False, + ) + except RuntimeError as error: + _print(": %s" % (error), tag=True) + err = True + else: + pass + return [err, post_img] + + + +# Bake post processing step for a vcol output +def bake_post_vcols(post_obj, post_mat, solution): + # Add copy of object to post scene + post_cpy = post_obj.copy() + post_cpy.data = post_obj.data.copy() + post_scene.collection.objects.link(post_cpy) + # Clear materials and set it to use post material + post_cpy.data.materials.clear() + post_cpy.data.polygons.foreach_set('material_index', [0] * len(post_cpy.data.polygons)) + post_cpy.data.update() + post_cpy.data.materials.append(post_mat) + # Add vertex color slot to bake into + vcol = post_cpy.data.color_attributes.new(post_mat.name, solution.format['vcol_type'], solution.format['vcol_domain']) + post_cpy.data.color_attributes.active_color = vcol + + # Switch into scene and set up selection + bpy.context.window.scene = post_scene + bpy.ops.object.select_all(action='DESELECT') + post_cpy.select_set(True) + bpy.context.view_layer.objects.active = post_cpy + + # Generate output + bpy.context.view_layer.update() + _print("> -Performing post bake processing", tag=True, wrap=False) + err = False + try: + bpy.ops.object.bake( + type="EMIT", + target='VERTEX_COLORS', + save_mode='INTERNAL', + ) + except RuntimeError as error: + _print(": %s" % (error), tag=True) + err = True + else: + _print("", tag=True) + + return [err, post_cpy, vcol.name] + + + +# Perform bake of a post processing material shader +def bake_post_material(post_col, post_alp, post_mat, post_msk, masked): + post_obj = post_scene.objects["BW_Post"] + post_obj.material_slots[0].material = post_mat + post_mat_nodes = post_mat.node_tree.nodes + post_out_col = post_mat_nodes["bw_post_output"] + post_out_alp = post_mat_nodes["bw_post_output_alpha"] + post_out_msk = post_mat_nodes["bw_post_output_mask"] + output_switch = post_mat_nodes["AlphaSwitch"].inputs["AlphaSwitch"] + mask_switch = post_mat_nodes["AlphaSwitch"].inputs["MaskSwitch"] + use_mask = post_mat_nodes["AlphaSwitch"].inputs["UseMask"] + + # Switch into scene and set up selection + bpy.context.window.scene = post_scene + bpy.ops.object.select_all(action='DESELECT') + post_obj.select_set(True) + bpy.context.view_layer.objects.active = post_obj + + # Set output image + post_out_col.image = post_col + post_out_col.select = True + post_mat_nodes.active = post_out_col + output_switch.default_value = 0.0 + mask_switch.default_value = 0.0 + use_mask.default_value = 0.0 + if masked: + use_mask.default_value = 1.0 + + # Generate output + bpy.context.view_layer.update() + _print("> -Performing post bake processing", tag=True, wrap=False) + err = False + try: + bpy.ops.object.bake( + type="EMIT", + save_mode='INTERNAL', + use_clear=False, + ) + except RuntimeError as error: + _print(": %s" % (error), tag=True) + err = True + else: + post_out_col.select = False + if post_alp is None and post_msk is None: + _print("", tag=True) + + # Set alpha output if enabled + if post_alp: + post_out_alp.image = post_alp + post_out_alp.select = True + post_mat_nodes.active = post_out_alp + output_switch.default_value = 1.0 + mask_switch.default_value = 0.0 + + # Generate alpha output + bpy.context.view_layer.update() + try: + bpy.ops.object.bake( + type="EMIT", + save_mode='INTERNAL', + use_clear=False, + ) + except RuntimeError as error: + _print(": %s" % (error), tag=True) + err = True + else: + post_out_alp.select = False + if post_msk is None: + _print("", tag=True) + + # Set mask output if enabled + if post_msk: + post_out_msk.image = post_msk + post_out_msk.select = True + post_mat_nodes.active = post_out_msk + output_switch.default_value = 0.0 + mask_switch.default_value = 1.0 + + # Generate mask output + bpy.context.view_layer.update() + try: + bpy.ops.object.bake( + type="EMIT", + save_mode='INTERNAL', + use_clear=False, + ) + except RuntimeError as error: + _print(": %s" % (error), tag=True) + err = True + else: + post_out_msk.select = False + _print("", tag=True) + + return err + + + +# Combine alpha channel with color image +def alpha_pass(color, alpha, combined): + stride = 4 + col_px = list(color.pixels) + alp_px = list(alpha.pixels) + + # Sanity check + if len(col_px) != len(alp_px): + _print(": Input/Output pixel count mismatch", tag=True) + return True + + # Write channel + for pixel in range(int(len(col_px)/stride)): + position = pixel * stride + alpha_ch = position + 3 + col_px[alpha_ch] = alp_px[position] # Alpha image is greyscale, so any channel value will do + + combined.pixels = col_px[:] + combined.update() + return False + + + +# Paint a margin around masked objects to the specified pixels or completely fill the empty space +def paint_margin(image, mask, margin, fill, step=12, samples=3): + if fill: margin = -1 + if not margin: return + start = datetime.now() + _print("> -Painting margin: ", tag=True, wrap=False) + # Put this in a try block because it can explode + try: + mpixels, mmask, mbools, mmargins, mw, mh, mmargin_step = marginer.set_up(image, mask, step) + margined = marginer.add_margin(mpixels, mmask, mbools, mmargins, mw, mh, mmargin_step, margin, samples) + marginer.write_back(image, margined) + except Exception as err: + _print("Failed (%s)" % (str(err)), tag=True) + else: + _print("Completed in %s" % (str(datetime.now() - start)), tag=True) + + + +# Move a UDIM tile into the 0-1 UV range +def focus_udim_tile(uvmap, udim, unfocus=False): + # Calculate offset + v_shift = int((udim - 1001) / 10) + u_shift = int((udim - 1001) - (v_shift * 10)) + if unfocus: + v_shift = v_shift * -1 + u_shift = u_shift * -1 + # Move all the UVs by offset + for uv in uvmap.data.values(): + uv.uv[0] -= u_shift + uv.uv[1] -= v_shift + + + +# Take a UV map and split it into UDIM tiles using standard format +def uv_to_udim(solution, key, uvmap): + tiles = solution.baketile[key] + udim = [] + # TODO: Check fastest way to get all the uvs + uvs = [uvmap.data[i].uv[:] for i in range(len(uvmap.data.values()))] + for u, v in uvs: + if int(u) == u and u > 0: u -= 1 + if int(v) == v and v > 0: v -= 1 + tile_no = (int(v) * 10) + int(u) + 1001 + if tile_no not in tiles: + tiles[tile_no] = [] + if tile_no not in udim: + udim.append(tile_no) + udim.sort() + return udim + + + +# Fast AA pass by scaling pixels +def fast_aa(image, level): + img_x = image.size[0] + img_y = image.size[1] + x_lvl = img_x * ((10 - level) * 0.1) + y_lvl = img_y * ((10 - level) * 0.1) + image.scale(int(x_lvl), int(y_lvl)) + image.update() + image.scale(int(img_x), int(img_y)) + image.update() + + + +# Copy vert colors between object copies +def copy_vert_cols(name=None, cpy_from=None, cpy_to=None): + # Set array to size of data and copy it in + from_cols = cpy_from.data.color_attributes[name] + data = [0.0] * (len(from_cols.data) * 4) + from_cols.data.foreach_get('color', data) + # Create new color attrib and copy array into it + cpy_to.data.color_attributes.new(name, from_cols.data_type, from_cols.domain) + to_data = cpy_to.data.color_attributes[name].data + to_data.foreach_set('color', data) + + + +# Add masks together by editing internal node group +def add_masks(masks, nodes, links): + mask_sock = nodes['AlphaSwitch'].inputs['Mask'] + # Simplest case is one mask + prev_adder = None + if len(masks) == 1: + links.new(masks[0].outputs['Mask'], mask_sock) + # At least two masks or more + elif len(masks) > 1: + prev_adder = masks[0] + for idx in range(1, len(masks)): + # Add a mask adder node group + mask_adder = nodes.new('ShaderNodeGroup') + mask_adder.node_tree = internal_add_mask + # Link prev adder to new adder and mask to adder + links.new(prev_adder.outputs['Mask'], mask_adder.inputs['Mask1']) + links.new(masks[idx].outputs['Mask'], mask_adder.inputs['Mask2']) + prev_adder = mask_adder + # Link the last adder to mask socket + if prev_adder: + links.new(prev_adder.outputs['Mask'], mask_sock) + + + +# Clear an existing image to all black all transparent +def clear_image(solution): + # Proceed if clear is set + if solution.node.img_clear: + output_path = solution.node.img_path + output_name = solution.node.name_with_ext() + output_file = os.path.join(os.path.realpath(output_path), output_name) + + # Nothing to do if image doesn't exist + if os.path.exists(output_file): + img = bpy.data.images.load(os.path.abspath(output_file)) + img.generated_type = 'BLANK' + img.generated_color = (0.0, 0.0, 0.0, 0.0) + img.generated_width = img.size[0] + img.generated_height = img.size[1] + img.source = 'GENERATED' + img.filepath_raw = output_file + img.save() + _print("> -Image Cleared", tag=True) + + + +# Take a list of values and return the highest +def pixel_value(channels): + value = 0 + for val in channels: + if val > value: + value = val + return value + + + +# Apply image format settings to scenes output settings +def apply_output_format(target_settings, format): + # Configure output image settings + target_settings.file_format = img_type = format["img_type"] + + # Color mode + target_settings.color_mode = format['img_color_mode'] + # Color Depth + if format['img_color_depth']: + target_settings.color_depth = format['img_color_depth'] + # Compression / Quality for formats that support it + if img_type == 'PNG': + target_settings.compression = format['img_quality'] + elif img_type in ['JPEG', 'JPEG2000']: + target_settings.quality = format['img_quality'] + # Codecs for formats that use them + if img_type == 'JPEG2000': + target_settings.jpeg2k_codec = format['img_codec'] + elif img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']: + target_settings.exr_codec = format['img_codec'] + elif img_type == 'TIFF': + target_settings.tiff_codec = format['img_codec'] + # Additional settings used by some formats + if img_type == 'JPEG2000': + target_settings.use_jpeg2k_cinema_preset = format["img_jpeg2k_cinema"] + target_settings.use_jpeg2k_cinema_48 = format["img_jpeg2k_cinema48"] + target_settings.use_jpeg2k_ycc = format["img_jpeg2k_ycc"] + elif img_type == 'DPX': + target_settings.use_cineon_log = format["img_dpx_log"] + elif img_type == 'OPEN_EXR': + target_settings.use_zbuffer = format["img_openexr_zbuff"] + + + +# Copy render settings from source scene to active scene +def copy_render_settings(source, target): + # Copy all Cycles settings + for setting in source.cycles.bl_rna.properties.keys(): + if setting not in ["rna_type", "name"]: + setattr(target.cycles, setting, getattr(source.cycles, setting)) + for setting in source.cycles_curves.bl_rna.properties.keys(): + if setting not in ["rna_type", "name"]: + setattr(target.cycles_curves, setting, getattr(source.cycles_curves, setting)) + # Copy SOME Render settings + for setting in source.render.bl_rna.properties.keys(): + if setting in ["dither_intensity", + "filter_size", + "film_transparent", + "use_freestyle", + "threads", + "threads_mode", + "hair_type", + "hair_subdiv", + "use_simplify", + "simplify_subdivision", + "simplify_child_particles", + "simplify_subdivision_render", + "simplify_child_particles_render", + "use_simplify_smoke_highres", + "simplify_gpencil", + "simplify_gpencil_onplay", + "simplify_gpencil_view_fill", + "simplify_gpencil_remove_lines", + "simplify_gpencil_view_modifier", + "simplify_gpencil_shader_fx", + "simplify_gpencil_blend", + "simplify_gpencil_tint", + ]: + setattr(target.render, setting, getattr(source.render, setting)) + + + +# Pretty much everything here is about preventing blender crashing or failing in some way that only happens +# when it runs a background bake. Perhaps it wont be needed some day, but for now trying to keep all such +# things in one place. Modifiers are applied or removed and non mesh types are converted. +def prep_object_for_bake(object, strip=False, invert=False, vcols=[False, None]): + # Create a copy of the object to modify and put it into the mesh only scene + if ((not object.bw_copy or object.bw_copy_frame != current_frame) and not strip and not vcols[0])\ + or ((not object.bw_strip or object.bw_strip_frame != current_frame) and strip and not vcols[0])\ + or (not object.bw_vcols and vcols[0]) or user_prop: + copy = object.copy() + copy.bw_copy = None + copy.bw_strip = None + copy.bw_vcols = None + copy.data = object.data.copy() + bw_solution_data['objects'].append(copy) + copy.name = ("BW_SMOD_" if strip else ("BW_VCOL_" if vcols[0] else "BW_")) + object.name + base_scene.collection.objects.link(copy) + bpy.context.view_layer.update() + else: + # Object already preped + retcpy = (object.bw_strip if strip else (object.bw_vcols if vcols[0] else object.bw_copy)) + ret = retcpy.copy() + ret.data = retcpy.data.copy() + return ret + + # Can't modify object for vertex colors as vert count might change + if vcols[0] and object == vcols[1] and not strip: + object.bw_vcols = copy + bw_solution_data['objects'].append(copy) + mesh_scene.collection.objects.link(copy) + base_scene.collection.objects.unlink(copy) + + # Create copy of the copy for return object + ret = copy.copy() + ret.data = copy.data.copy() + return ret + + # Objects need to be selectable and visible in the viewport in order to convert them + copy.hide_select = False + copy.hide_viewport = False + + # If ignoring visibility is enabled, also make the object shown for render + if ignorevis: + copy.hide_render = False + + # Apply active shape keys to copy so that modifiers can be applied + if hasattr(copy.data, "shape_keys") and copy.data.shape_keys is not None: + copy.shape_key_add(name="BW_Combined", from_mix=True) + for skey in copy.data.shape_keys.key_blocks: + copy.shape_key_remove(skey) + + # Make obj the only selected + active + bpy.ops.object.select_all(action='DESELECT') + copy.select_set(True) + bpy.context.view_layer.objects.active = copy + # Deal with mods + if len(copy.modifiers): + for mod in copy.modifiers: + show_vp = mod.show_viewport + if invert: show_vp = not show_vp + if mod.show_render and (not strip or not show_vp): + # A mod can be disabled by invalid settings, which will throw an exception when trying to apply it + try: + if object.type == 'MESH': + bpy.ops.object.make_local() + bpy.ops.object.modifier_apply(modifier=mod.name) + except: + _print("> Error applying modifier '%s' to object '%s'" % (mod.name, object.name), tag=True) + bpy.ops.object.modifier_remove(modifier=mod.name) + else: + bpy.ops.object.modifier_remove(modifier=mod.name) + + # Deal with object type + if object.type != 'MESH': + # Apply render resolution if its set before turning into a mesh + if object.type == 'META': + if copy.data.render_resolution > 0: + copy.data.resolution = copy.data.render_resolution + else: + if copy.data.render_resolution_u > 0: + copy.data.resolution_u = copy.data.render_resolution_u + if object.data.render_resolution_v > 0: + copy.data.resolution_v = copy.data.render_resolution_v + # Convert + bpy.ops.object.convert(target='MESH') + + # Meta objects seem to get deleted and a new object replaces them, breaking the reference + if object.type == 'META': + copy = bpy.context.view_layer.objects.active + + # Link copy to original, remove from base scene and add to mesh scene + if strip: + object.bw_strip = copy + object.bw_strip_frame = current_frame + else: + object.bw_copy = copy + object.bw_copy_frame = current_frame + mesh_scene.collection.objects.link(copy) + base_scene.collection.objects.unlink(copy) + + # Return copy of copy + ret = copy.copy() + ret.data = copy.data.copy() + return ret + + +# Takes a materials node tree and makes any changes necessary to perform the given bake type. A material must +# end with principled shader(s) and mix shader(s) connected to a material output in order to be set up for any +# emission node bakes. +def prep_material_for_bake(node_tree, bake_type, bake_settings): + # Bake types with built-in passes don't require any preparation + if not node_tree or (bake_settings['bake_cat'] != 'PBR' and bake_type not in ['MASK', 'OSL_BENTNORM']): + return + + # Mask is a special case where an emit shader and output can just be added to any material + elif bake_type == 'MASK': + nodes = node_tree.nodes + + # Add white emit and a new active output + emit = nodes.new('ShaderNodeEmission') + emit.inputs['Color'].default_value = [1.0, 1.0, 1.0, 1.0] + outp = nodes.new('ShaderNodeOutputMaterial') + node_tree.links.new(emit.outputs['Emission'], outp.inputs['Surface']) + outp.target = 'CYCLES' + + # Make all outputs not active + for node in nodes: + if node.type == 'OUTPUT_MATERIAL': + node.is_active_output = False + + outp.is_active_output = True + return + + + # The material has to have a node tree and it needs at least 2 nodes to be valid + elif len(node_tree.nodes) < 2: + # AOV bake only requires that material has the named AOV node + if bake_type == 'AOV' and len(node_tree.nodes): + pass + else: + return + + # All other bake types use an emission shader with the value plugged into it + + # A material can have multiple output nodes. Blender seems to preference the output to use like so: + # 1 - Target set to current Renderer and Active (picks first if multiple are set active) + # 2 - First output with Target set to Renderer if no others with that target are set Active + # 3 - Active output (picks first if mutliple are active) + # + # Strategy will be to find all valid outputs and evaluate if they can be used in the same order as above. + # The first usable output found will be selected and also changed to be the first choice for blender. + # Four buckets: Cycles + Active, Cycles, Generic + Active, Generic + nodes = node_tree.nodes + node_cycles_out_active = [] + node_cycles_out = [] + node_generic_out_active = [] + node_generic_out = [] + node_selected_output = None + + # Collect all outputs + for node in nodes: + if node.type == 'OUTPUT_MATERIAL': + if node.target == 'CYCLES': + if node.is_active_output: + node_cycles_out_active.append(node) + else: + node_cycles_out.append(node) + elif node.target == 'ALL': + if node.is_active_output: + node_generic_out_active.append(node) + else: + node_generic_out.append(node) + + # Select the first usable output using the order explained above and make sure no other outputs are set active + node_outputs = node_cycles_out_active + node_cycles_out + node_generic_out_active + node_generic_out + for node in node_outputs: + input = node.inputs['Surface'] + if not node_selected_output: # and material_recursor(node): + node_selected_output = node + node.is_active_output = True + else: + node.is_active_output = False + + if not node_selected_output: + # For an AOV bake, just add an output now if there wasn't one + if bake_type == 'AOV': + node_selected_output = nodes.new('ShaderNodeOutputMaterial') + node_selected_output.is_active_output = True + else: + return + + # Output has been selected. An emission shader will now be built, replacing mix shaders with mix RGB + # nodes and principled shaders with just the desired data for the bake type. Recursion used. + if debug: _print("> Chosen output [%s] descending tree:" % (node_selected_output.name), tag=True) + class linkcls(): + def __init__(self): + self.link_data = None + self.link_list = [] + def set_links(self, link_data): self.link_data = link_data + def new(self, lto, lfrom): self.link_list.append([self.link_data, lto, lfrom]) + def remove(self, link): self.link_data.remove(link) + def create(self): + for link in self.link_list: + if debug: _print("> Linking: %s to %s" % (link[1].node.name, link[2].node.name), tag=True) + link[0].new(link[1], link[2]) + link_list = linkcls() + ret = prep_material_rec(node_selected_output, None, node_selected_output.inputs['Surface'], bake_type, bake_settings, link_list) + if bake_type != 'OSL_BENTNORM': + link_list.create() + return ret + + + +# Takes a node of type OUTPUT_MATERIAL, BSDF_PRINCIPLED or MIX/ADD_SHADER. Starting with an output node it will +# recursively generate an emission shader to replace the output with the desired bake type. The link_socket +# is used for creating node tree links. Also added dealing with Custom Node Groups. +def prep_material_rec(node, socket, link_socket, bake_type, bake_settings, link_list, parent=None): + if debug: print("%s:%s->%s:%s" % (link_socket.node.type, link_socket.name, node.name, (socket.name if socket else "None"))) + tree = node.id_data + nodes = tree.nodes + links = link_list + links.set_links(tree.links) + # Helper to get links or values for node inputs + def link_or_value(socket): + if socket.is_linked: return follow_input_link(socket.links[0]).from_socket, 0 + else: return socket.default_value, 1 + # Helper to either create a link or set the default value + def map_input_or_value(proxy, proxy_input, socket): + sock, is_val = link_or_value(socket) + if is_val: proxy.inputs[proxy_input].default_value = sock + else: links.link_data.new(sock, proxy.inputs[proxy_input]) + # Helper to check both inputs to mix/add are connected + def check_both_inputs(node): + if node.type == 'MIX_SHADER': + input1 = 1 + input2 = 2 + else: + input1 = 0 + input2 = 1 + recon_link = 0 + if node.inputs[input1].is_linked: + recon_link = 1 + elif node.inputs[input2].is_linked: + recon_link = 2 + return [(node.inputs[input1].is_linked) and (node.inputs[input2].is_linked), recon_link] + # Cases: + if node.type == 'OUTPUT_MATERIAL': + # Start of shader. Create new emission shader and connect it to the output + next = follow_input_link(link_socket.links[0]) + next_node = next.from_node + next_sock = next.from_socket + node_emit = nodes.new('ShaderNodeEmission') + if bake_settings['bbbk']: + # billboard bake needs to respect alpha by creating a separate alpha mix + bbbk_node = nodes.new('ShaderNodeBsdfTransparent') + bbbk_mix = nodes.new('ShaderNodeMixShader') + bbbk_mix.inputs['Fac'].default_value = 0.0 + bake_settings['bbbk_sock'] = bbbk_mix.inputs['Fac'] + # Link it all up + links.new(node_emit.outputs['Emission'], bbbk_mix.inputs[2]) + links.new(bbbk_node.outputs['BSDF'], bbbk_mix.inputs[1]) + sock_toout = bbbk_mix.outputs['Shader'] + else: + sock_toout = node_emit.outputs['Emission'] + # Link output + links.new(sock_toout, link_socket) + + # AOV bake will try to find and connect the named AOV node to the shader now and return + if bake_type == 'AOV': + # Helper to find named AOV + def find_aov(named, nodes): + first = not len(named) + for node in nodes: + if node.type == 'OUTPUT_AOV': + if first or node.name == named: + return node + return None + aov_node = find_aov(bake_settings['aov_name'], nodes) + if aov_node: + if bake_settings['aov_input'] == 'COL': + map_input_or_value(node_emit, 'Color', aov_node.inputs['Color']) + else: + socket, is_val = link_or_value(aov_node.inputs['Value']) + if is_val: node_emit.inputs['Color'].default_value = [socket, socket, socket, socket] + else: links.new(socket, node_emit.inputs['Color']) + return True + else: + # Set it to black if not found? + node_emit.inputs['Color'].default_value = [0,0,0,0] + return False + else: + # Recurse + ret = prep_material_rec(next_node, next_sock, node_emit.inputs['Color'], bake_type, bake_settings, links, parent) + return ret + + if node.type in ['MIX_SHADER', 'ADD_SHADER']: + # If there aren't two inputs then the node is essentially muted, so a delete with reconnect should be the answer.. + check_inputs = check_both_inputs(node) + '''if not check_inputs[0]: + if check_inputs[1] is not None: + return prep_material_rec(check_inputs[1].from_node, check_inputs[1].from_socket, link_socket, bake_type, bake_settings, links, parent) + else: return False''' + # Mix shader needs to generate a mix RGB maintaining the same Fac input if linked + mix_node = nodes.new('ShaderNodeMixRGB') + mix_node.label = node.label + mix_node.location = node.location + mix_node.mute = node.mute + mix_node.blend_type = 'MIX' + if bake_settings['bbbk']: # duplicate everything for alpha + bbmix_node = nodes.new('ShaderNodeMixRGB') + bbmix_node.blend_type = 'MIX' + if node.type == 'MIX_SHADER' and node.inputs['Fac'].is_linked: + # Connect Fac input + links.new(follow_input_link(node.inputs['Fac'].links[0]).from_socket, mix_node.inputs['Fac']) + if bake_settings['bbbk']: links.new(follow_input_link(node.inputs['Fac'].links[0]).from_socket, bbmix_node.inputs['Fac']) + else: + if node.type == 'MIX_SHADER': + # Set Fac value to match instead + mix_node.inputs['Fac'].default_value = node.inputs['Fac'].default_value + if bake_settings['bbbk']: bbmix_node.inputs['Fac'].default_value = node.inputs['Fac'].default_value + else: + # Add shader is a Fac of 1 + mix_node.inputs['Fac'].default_value = 1 + mix_node.blend_type = 'ADD' + if bake_settings['bbbk']: + bbmix_node.inputs['Fac'].default_value = 1 + bbmix_node.blend_type = 'ADD' + # Connect mix output to previous socket + links.new(mix_node.outputs['Color'], link_socket) + if bake_settings['bbbk']: links.new(bbmix_node.outputs['Color'], bake_settings['bbbk_sock']) + # Recurse + if node.type == 'MIX_SHADER': + input1 = 1 + input2 = 2 + else: + input1 = 0 + input2 = 1 + if not check_inputs[0]: + if check_inputs[1] == 1: + nextA = follow_input_link(node.inputs[input1].links[0]) + nextB = False + elif check_inputs[1] == 2: + nextA = False + nextB = follow_input_link(node.inputs[input2].links[0]) + else: + return False + else: + nextA = follow_input_link(node.inputs[input1].links[0]) + nextB = follow_input_link(node.inputs[input2].links[0]) + if nextA: + if bake_settings['bbbk']: bake_settings['bbbk_sock'] = bbmix_node.inputs['Color1'] + branchA = prep_material_rec(nextA.from_node, nextA.from_socket, mix_node.inputs['Color1'], bake_type, bake_settings, links, parent) + else: + mix_node.inputs['Color1'].default_value = [0,0,0,0] + branchA = True + if nextB: + if bake_settings['bbbk']: bake_settings['bbbk_sock'] = bbmix_node.inputs['Color2'] + branchB = prep_material_rec(nextB.from_node, nextB.from_socket, mix_node.inputs['Color2'], bake_type, bake_settings, links, parent) + else: + mix_node.inputs['Color2'].default_value = [0,0,0,0] + branchB = True + return branchA and branchB + + if node.type == 'GROUP' and socket: + # First the 'group' should be replaced with a copy along with adding a RGBA output and connecting it + node.node_tree = node.node_tree.copy() + if bake_settings['bbbk']: + node.node_tree.outputs.new('NodeSocketColor', 'BW_BBOut') + links.new(bake_settings['bbbk_sock'], node.outputs[-1]) + node.node_tree.outputs.new('NodeSocketColor', 'BW_Out') + links.new(link_socket, node.outputs[-1]) + # Set links to the internal tree + links.set_links(node.node_tree.links) + # Entering a custom group, has some code duplication with exiting + # Descending node tree, origin needs to be added to parent stack (using a copy to deal with branching) + if parent: + gparent = parent.copy() + gparent.append(node) + else: + gparent = [node] + gout = None + gsoc = 0 + # Get the index of the socket as names may not be unique then find internal socket + for soc in node.outputs: + if socket == soc: break + else: gsoc += 1 + for gnode in node.node_tree.nodes: + if gnode.type == 'GROUP_OUTPUT' and gnode.is_active_output: + gout = gnode + break + # Quick sanity check + if not gout or not gout.inputs[gsoc].is_linked: + if debug: _print("> Error: Group node active output socket not linked", tag=True) + return True + # Follow the next node to be considered, use the added RGBA socket as link_sock (should be 2nd last) + next = follow_input_link(gout.inputs[gsoc].links[0]) + if bake_settings['bbbk']: bake_settings['bbbk_sock'] = gout.inputs[-3] + return prep_material_rec(next.from_node, next.from_socket, gout.inputs[-2], bake_type, bake_settings, links, gparent) + + if node.type == 'GROUP_INPUT' and socket and parent: + # Exiting a custom group, has some code duplication with entering + # Acceding node tree, pop origin off the parent stack (using a copy to deal with branching) + gparent = parent.copy() + gout = gparent.pop() + gsoc = 0 + # First add a new RGBA input to the parent and connect it to the previous thing (should be 2nd last on inside) + if bake_settings['bbbk']: + gout.node_tree.inputs.new('NodeSocketColor', 'BW_BBIn') + links.new(bake_settings['bbbk_sock'], node.outputs[-2]) + gout.node_tree.inputs.new('NodeSocketColor', 'BW_In') + links.new(link_socket, node.outputs[-2]) + # Set links to the external tree + links.set_links(gout.id_data.links) + # Get index of the socket as names may not be unique + for soc in node.outputs: + if socket == soc: break + else: gsoc += 1 + # Quick sanity check + if not gout or not gout.inputs[gsoc].is_linked: + if debug: _print("> Error: Group node input socket not linked", tag=True) + return True + # Return the next node to be considered, using the added RGBA external socket as the link_sock (should be last) + next = follow_input_link(gout.inputs[gsoc].links[0]) + if bake_settings['bbbk']: bake_settings['bbbk_sock'] = gout.inputs[-2] + return prep_material_rec(next.from_node, next.from_socket, gout.inputs[-1], bake_type, bake_settings, links, gparent) + + # All of the shader types that aren't really supported. Instead of failing, they will be converted to Principled using some assumptions. + if node.type in ['BSDF_DIFFUSE', 'BSDF_GLOSSY', 'BSDF_GLASS', 'BSDF_REFRACTION', 'BSDF_TRANSLUCENT', 'BSDF_TRANSPARENT', 'BSDF_ANISOTROPIC', 'SUBSURFACE_SCATTERING', 'EMISSION', 'HOLDOUT', 'BSDF_HAIR', 'BSDF_HAIR_PRINCIPLED', 'PRINCIPLED_VOLUME', 'BSDF_TOON', 'BSDF_VELVET', 'VOLUME_ABSORPTION', 'VOLUME_SCATTER']: + proxy = nodes.new('ShaderNodeBsdfPrincipled') + proxy.inputs['Base Color'].default_value = [0,0,0,0] + #links.new(proxy.outputs[0], link_socket) + # Almost all of these have a color input that can be mapped + if node.type != 'HOLDOUT': + if node.type == 'SUBSURFACE_SCATTERING': + map_input_or_value(proxy, 'Subsurface Color', node.inputs['Color']) + elif node.type == 'EMISSION': + map_input_or_value(proxy, 'Emission', node.inputs['Color']) + else: + map_input_or_value(proxy, 'Base Color', node.inputs['Color']) + # Many also have a roughness value + if node.type in ['BSDF_GLOSSY', 'BSDF_DIFFUSE', 'BSDF_GLASS', 'BSDF_REFRACTION', 'BSDF_ANISOTROPIC', 'BSDF_HAIR_PRINCIPLED']: + map_input_or_value(proxy, 'Roughness', node.inputs['Roughness']) + # A bunch can have a normal input mapped + if node.type in ['BSDF_GLOSSY', 'BSDF_DIFFUSE', 'BSDF_GLASS', 'BSDF_REFRACTION', 'SUBSURFACE_SCATTERING', 'BSDF_ANISOTROPIC', 'BSDF_TRANSLUCENT', 'BSDF_TOON', 'BSDF_VELVET']: + map_input_or_value(proxy, 'Normal', node.inputs['Normal']) + # Not really any more common properties, but still a few that can be mapped + if node.type in ['BSDF_GLASS', 'BSDF_REFRACTION', 'BSDF_HAIR_PRINCIPLED']: + map_input_or_value(proxy, 'IOR', node.inputs['IOR']) + if node.type == 'EMISSION': + map_input_or_value(proxy, 'Emission Strength', node.inputs['Strength']) + if node.type == 'SUBSURFACE_SCATTERING': + map_input_or_value(proxy, 'Subsurface Radius', node.inputs['Radius']) + if node.type in ['BSDF_ANISOTROPIC', 'PRINCIPLED_VOLUME', 'VOLUME_SCATTER']: + map_input_or_value(proxy, 'Anisotropic', node.inputs['Anisotropy']) + if node.type == 'BSDF_ANISOTROPIC': + map_input_or_value(proxy, 'Anisotropic Rotation', node.inputs['Rotation']) + if node.type in ['BSDF_HAIR', 'BSDF_ANISOTROPIC']: + map_input_or_value(proxy, 'Tangent', node.inputs['Tangent']) + # Work out a metalness based on some nodes + if node.type in ['BSDF_DIFFUSE', 'BSDF_GLOSSY', 'BSDF_TOON', 'BSDF_ANISOTROPIC']: + if node.type == 'BSDF_TOON': + if node.component == 'DIFFUSE': proxy.inputs['Metallic'].default_value = 0.0 + else: proxy.inputs['Metallic'].default_value = 1.0 + elif node.type == 'BSDF_DIFFUSE': proxy.inputs['Metallic'].default_value = 0.0 + elif node.type == 'BSDF_GLOSSY': proxy.inputs['Metallic'].default_value = 1.0 + elif node.type == 'BSDF_ANISOTROPIC': proxy.inputs['Metallic'].default_value = 1.0 + # Clear coat + if node.type == 'BSDF_HAIR_PRINCIPLED': + map_input_or_value(proxy, 'Clearcoat', node.inputs['Coat']) + if node.type == 'PRINCIPLED_VOLUME': + map_input_or_value(proxy, 'Emission', node.inputs['Emission Color']) + map_input_or_value(proxy, 'Emission Strength', node.inputs['Emission Strength']) + # Alpha + if node.type == 'BSDF_TRANSPARENT': + proxy.inputs['Alpha'].default_value = 0.0 + return prep_material_rec(proxy, proxy.outputs[0], link_socket, bake_type, bake_settings, links, parent) + + if node.type == 'BSDF_PRINCIPLED': + # End of a branch as far as the prep is concerned. Either link the desired bake value or set the + # previous socket to the value if it isn't linked + if bake_type == 'ALBEDO': + bake_input = node.inputs['Base Color'] + elif bake_type == 'SUBSURFACE': + bake_input = node.inputs['Subsurface'] + elif bake_type == 'SUBRADIUS': + bake_input = node.inputs['Subsurface Radius'] + elif bake_type == 'SUBCOLOR': + bake_input = node.inputs['Subsurface Color'] + elif bake_type == 'METALLIC': + bake_input = node.inputs['Metallic'] + elif bake_type == 'SPECULAR': + bake_input = node.inputs['Specular'] + elif bake_type == 'SPECTINT': + bake_input = node.inputs['Specular Tint'] + elif bake_type in ['ROUGHNESS' ,'SMOOTHNESS']: + bake_input = node.inputs['Roughness'] + elif bake_type == 'ANISOTROPIC': + bake_input = node.inputs['Anisotropic'] + elif bake_type == 'ANISOROTATION': + bake_input = node.inputs['Anisotropic Rotation'] + elif bake_type == 'SHEEN': + bake_input = node.inputs['Sheen'] + elif bake_type == 'SHEENTINT': + bake_input = node.inputs['Sheen Tint'] + elif bake_type == 'CLEARCOAT': + bake_input = node.inputs['Clearcoat'] + elif bake_type == 'CLEARROUGH': + bake_input = node.inputs['Clearcoat Roughness'] + elif bake_type == 'TRANSIOR': + bake_input = node.inputs['IOR'] + elif bake_type == 'TRANSMISSION': + bake_input = node.inputs['Transmission'] + elif bake_type == 'TRANSROUGH': + bake_input = node.inputs['Transmission Roughness'] + elif bake_type == 'EMIT': + bake_input = node.inputs['Emission'] + elif bake_type == 'ALPHA': + bake_input = node.inputs['Alpha'] + elif bake_type in ['TEXNORM', 'OBJNORM', 'BBNORM', 'OSL_BENTNORM']: + bake_input = node.inputs['Normal'] + elif bake_type == 'CLEARNORM': + bake_input = node.inputs['Clearcoat Normal'] + else: + bake_input = None + + if debug: _print("> Reached branch end, ", tag=True, wrap=False) + + if bake_input: + if bake_settings['bbbk']: + # Connect alpha to billboard bake alpha mix nodes + bbbk_input = node.inputs['Alpha'] + if bbbk_input.is_linked: + links.new(bbbk_input.links[0].from_socket, bake_settings['bbbk_sock']) + else: + # Create a color node to use as input + bbcolornode = nodes.new('ShaderNodeRGB') + bbcolorsoct = bbcolornode.outputs["Color"] + links.new(bake_settings['bbbk_sock'], bbcolorsoct) + bbcolorsoct.default_value[0] = bbbk_input.default_value + bbcolorsoct.default_value[1] = bbbk_input.default_value + bbcolorsoct.default_value[2] = bbbk_input.default_value + bbcolorsoct.default_value[3] = 1.0 + if bake_type in ['TEXNORM', 'CLEARNORM', 'BBNORM']: + # Normal map types need an extra step, plus configuration of space and swizzle + # Add normal mapping node group + normgrp = nodes.new('ShaderNodeGroup') + if bake_type == 'BBNORM': + normgrp.node_tree = billboard_norm.copy() + normgrp.inputs['Rotation'].default_value = bake_settings['bb_rot'] + else: + normgrp.node_tree = normals_group.copy() + normnod = normgrp.node_tree.nodes + normlnk = normgrp.node_tree.links + # Link it + links.new(normgrp.outputs["Color"], link_socket) + link_socket = normgrp.inputs["Normal"] + # Configure it + if bake_type == 'BBNORM': + spacenod = normnod["bw_norm"] + else: + normlnk.new(normnod["bw_inputnorm"].outputs["Normal"], normnod["bw_normal_input"].inputs[0]) + spacenod = normnod[bake_settings["norm_s"]] + swizzleR = spacenod.outputs[bake_settings["norm_r"]] + swizzleG = spacenod.outputs[bake_settings["norm_g"]] + swizzleB = spacenod.outputs[bake_settings["norm_b"]] + normoutp = normnod["bw_normal_xyz"] + normlnk.new(swizzleR, normoutp.inputs["X"]) + normlnk.new(swizzleG, normoutp.inputs["Y"]) + normlnk.new(swizzleB, normoutp.inputs["Z"]) + if bake_type in ['OSL_BENTNORM']: + # Add bent normals group to material and set values + bentnormgrp = nodes.new('ShaderNodeGroup') + bentnormgrp.node_tree = bent_norm_group + bentnormgrp.inputs['Distance'].default_value = bake_settings['osl_bentnorm_dist'] + bentnormgrp.inputs['Samples'].default_value = bake_settings['osl_bentnorm_samp'] + # Link it + if bake_input.is_linked: + if debug: _print("Normals Link found, [%s] will be connected" % (bake_input.links[0].from_socket.name), tag=True) + tree.links.new(bake_input.links[0].from_socket, bentnormgrp.inputs['Normal']) + else: # For Bent normals just connect object normals instead + if debug: _print("Nomrals not linked, objects normals used instead", tag=True) + objgeo = nodes.new('ShaderNodeNewGeometry') + tree.links.new(objgeo.outputs['Normal'], bentnormgrp.inputs['Normal']) + tree.links.new(bentnormgrp.outputs["Bent Normal"], bake_input) + return True + if bake_input.is_linked: + if debug: _print("Link found, [%s] will be connected" % (bake_input.links[0].from_socket.name), tag=True) + if bake_type == 'OBJNORM': + # Remove any texture influence to normals + links.remove(bake_input.links[0]) + else: + # Connect the linked node up to the emit shader + links.new(bake_input.links[0].from_socket, link_socket) + else: + if debug: _print("Not linked, value will be copied", tag=True) + if link_socket.is_linked: + links.remove(link_socket.links[0]) + # Copy the value into the socket instead + if bake_input.type == 'RGBA': + link_socket.default_value = bake_input.default_value + elif bake_input.type == 'VECTOR': + link_socket.default_value[0] = bake_input.default_value[0] + link_socket.default_value[1] = bake_input.default_value[1] + link_socket.default_value[2] = bake_input.default_value[2] + #link_socket.default_value[3] = 1.0 + else: + # Create a color node to use as input + colornode = nodes.new('ShaderNodeRGB') + colorsoct = colornode.outputs["Color"] + links.new(link_socket, colorsoct) + colorsoct.default_value[0] = bake_input.default_value + colorsoct.default_value[1] = bake_input.default_value + colorsoct.default_value[2] = bake_input.default_value + colorsoct.default_value[3] = 1.0 + # Branch completed + return True + + # Something went wrong + if debug: _print("> Error: Reached unsupported node type", tag=True) + return False + + + +# Make sure object has at least a generic material +def check_has_material(object, materials, mat): + # A material is generally required to direct baking output, so add a generic one if none is present + add_mat = True + if len(object.material_slots): + for slot in object.material_slots: + if slot.material: + add_mat = False + break + if add_mat: + if materials and mat.name not in materials: + materials[mat.name] = mat + object.data.materials.append(mat) + + + +# Replace all materials in scene with a shader for bake type +def replace_materials_for_shader_bake(scene, bake_settings, mesh_settings): + bake_type = bake_settings['bake_type'] + bake_cat = bake_settings['bake_cat'] + override = mesh_settings['material_replace'] + override_mat = mesh_settings['material_override'] + for obj in scene.objects: + # First strip existing materials unless they are needed + if bake_type not in ['MATID', 'OSL_BENTNORM']: + obj.data.materials.clear() + obj.data.polygons.foreach_set('material_index', [0] * len(obj.data.polygons)) + obj.data.update() + # Do material override if it makes sense + if bake_cat != 'WRANG' and override and override_mat: + obj.data.materials.append(override_mat) + # Add correct shader for bake type + elif bake_type == 'BEVMASK': + obj.data.materials.append(bevmask_shader) + elif bake_type == 'BEVNORMEMIT': + obj.data.materials.append(bevnormemit_shader) + elif bake_type == 'BEVNORMNORM': + obj.data.materials.append(bevnormnorm_shader) + elif bake_type == 'CAVITY': + obj.data.materials.append(cavity_shader) + elif bake_type == 'CURVATURE': + obj.data.materials.append(curvature_shader) + elif bake_type == 'ISLANDID': + obj.data.materials.append(islandid_shader) + elif bake_type == 'OBJCOL': + obj.data.materials.append(objcol_shader) + elif bake_type == 'WORLDPOS': + obj.data.materials.append(worldpos_shader) + elif bake_type == 'THICKNESS': + obj.data.materials.append(thickness_shader) + elif bake_type == 'VERTCOL': + obj.data.materials.append(vertcol_shader) + elif bake_type == 'OSL_CURV': + obj.data.materials.append(osl_curvature) + elif bake_type == 'MASKPASS': + obj.data.materials.append(mask_pass_shader) + + + +# Consider all materials in scene and create scene only copies +def make_materials_unique_to_scene(scene, suffix, bake_settings): + # Go through all the materials on every object + materials = {} + bake_type = bake_settings['bake_type'] + bake_cat = bake_settings['bake_cat'] + + for obj in scene.objects: + if len(obj.material_slots): + for slot in obj.material_slots: + if slot.material: + # If its a new material, create a copy (adding suffix) and add the pair to the list + if slot.material.name not in materials: + copy = slot.material.copy() + copy.name = slot.material.name + suffix + materials[slot.material.name] = copy + replace = copy + else: + replace = materials[slot.material.name] + # Replace with copy + slot.material = replace + + # For shader bakes there should now be only one material and it will be configured now + if bake_cat == 'WRANG': + mat_key = materials.keys() + if len(mat_key) > 1 and bake_type != 'MATID': _print("> Sanity Error: Doing shader bake, but objects had multiple materials", tag=True) + for key in mat_key: + nodes = materials[key].node_tree.nodes + + if bake_type in ['BEVMASK', 'BEVNORMEMIT', 'BEVNORMNORM']: + bevel = nodes["bw_bevel"] + bevel.inputs["Radius"].default_value = bake_settings["bev_rad"] + bevel.samples = bake_settings["bev_samp"] + if bake_type == 'BEVNORMEMIT': + # Configure normal settings + bevel_norm = nodes["BW_Normals.Bevel"] + normnod = bevel_norm.node_tree.nodes + normlnk = bevel_norm.node_tree.links + spacenod = normnod[bake_settings["norm_s"]] + swizzleR = spacenod.outputs[bake_settings["norm_r"]] + swizzleG = spacenod.outputs[bake_settings["norm_g"]] + swizzleB = spacenod.outputs[bake_settings["norm_b"]] + normoutp = normnod["bw_normal_xyz"] + normlnk.new(swizzleR, normoutp.inputs["X"]) + normlnk.new(swizzleG, normoutp.inputs["Y"]) + normlnk.new(swizzleB, normoutp.inputs["Z"]) + elif bake_type in ['CAVITY', 'THICKNESS']: + node_ao = nodes["bw_ao"] + node_ao.inputs["Distance"].default_value = bake_settings["cavity_dist"] + node_ao.samples = bake_settings["cavity_samp"] + if bake_type == 'CAVITY': + node_gamma = nodes["bw_ao_cavity_gamma"] + node_ao.inside = bake_settings["cavity_edges"] + node_gamma.inputs["Gamma"].default_value = bake_settings["cavity_gamma"] + elif bake_type == 'CURVATURE': + con_vex = nodes["bw_convex_range"] + con_cav = nodes["bw_concave_range"] + midv = bake_settings["curv_mid"] + cavv = bake_settings["curv_cav"] + vexv = bake_settings["curv_vex"] + con_vex.color_ramp.elements[0].color = [midv, midv, midv, 1.0] + con_vex.color_ramp.elements[1].color = [vexv, vexv, vexv, 1.0] + con_vex.color_ramp.elements[1].position = bake_settings["curv_vex_max"] + con_cav.color_ramp.elements[1].color = [midv, midv, midv, 1.0] + con_cav.color_ramp.elements[0].color = [cavv, cavv, cavv, 1.0] + con_cav.color_ramp.elements[0].position = bake_settings["curv_cav_min"] + elif bake_type == 'OSL_CURV': + osl_scr = nodes["bw_osl_script"] + osl_scr.inputs["Distance"].default_value = bake_settings["osl_curv_dist"] + osl_scr.inputs["Samples"].default_value = bake_settings["osl_curv_samp"] + osl_scr.inputs["contrast"].default_value = bake_settings["osl_curv_cont"] + if bake_settings["osl_curv_srgb"]: + osl_scr.inputs["srgb"].default_value = 1 + else: + osl_scr.inputs["srgb"].default_value = 0 + elif bake_type == 'OSL_HEIGHT': + osl_scr = nodes["bw_osl_script"] + osl_scr.inputs["Distance"].default_value = bake_settings["osl_height_dist"] + osl_scr.inputs["Midlevel"].default_value = bake_settings["osl_height_midl"] + osl_scr.inputs["iterations"].default_value = bake_settings["osl_height_samp"] + osl_scr.inputs["Voidcolor"].default_value = bake_settings["osl_height_void"] + elif bake_type == 'VERTCOL': + verts = nodes["bw_vertex_col"] + verts.layer_name = bake_settings["vert_col"] + elif bake_type == 'MATID': + # This one is different to the rest as it must replace all the existing materials with a unique per material color output + col_used = [] + for name, mat in materials.items(): + ntree = materials[name].node_tree + ntree.nodes.clear() + outp = ntree.nodes.new('ShaderNodeOutputMaterial') + if bake_settings["use_material_vpcolor"]: + # Use materials viewport color + colg = ntree.nodes.new('ShaderNodeEmission') + colg.inputs["Color"].default_value = materials[name].diffuse_color + else: + # Generate a random color using name as seed for repeatability + random.seed(a=name) + col = round(random.random(), 2) + while col in col_used: + round(random.random(), 2) + col_used.append(col) + # Clear all nodes and place the custom group connected to a output with the above value set + colg = ntree.nodes.new('ShaderNodeGroup') + colg.node_tree = materialid_group.copy() + colg.inputs["Random"].default_value = col + ntree.links.new(colg.outputs["Emission"], outp.inputs["Surface"]) + + # Return the dict + return materials + + +# Free up memory by removing created data +def free_data(): + has_refs = [] + if debug: _print("> Clearing data created for previous bake(s)", tag=True) + # Scenes first + while len(bw_solution_data['scenes']): + obj = bw_solution_data['scenes'].pop() + # Unlink any objects in the base collection + for obx in obj.collection.objects: + obj.collection.objects.unlink(obx) + # Remove the scene + if obj.name in bpy.data.scenes.keys(): bpy.data.scenes.remove(obj) + # Collections + while len(bw_solution_data['collections']): + obj = bw_solution_data['collections'].pop() + # Unlink any objects in the base collection + for obx in obj.objects: + obj.objects.unlink(obx) + # Remove collection + if obj.name in bpy.data.collections.keys() and not obj.users: bpy.data.collections.remove(obj) + else: + has_refs.append(['COL', obj]) + if debug: _print("> Collection: %s removal postponed with %s users" % (obj.name, obj.users), tag=True) + # Objects + while len(bw_solution_data['objects']): + obj = bw_solution_data['objects'].pop() + # Unlink object from any collections + for col in bpy.data.collections: + if obj.name in col.objects.keys(): + col.objects.unlink(obj) + # Unlink object from any scenes + for scn in bpy.data.scenes: + if obj.name in scn.collection.objects.keys(): + scn.collection.objects.unlink(obj) + # Clear all materials from object + obj.data.materials.clear() + # Remove object + if obj.name in bpy.data.objects.keys() and not obj.users: bpy.data.objects.remove(obj) + else: + has_refs.append(['OBJ', obj]) + if debug: _print("> Object: %s removal postponed with %s users" % (obj.name, obj.users), tag=True) + # Materials + while len(bw_solution_data['materials']): + obj = bw_solution_data['materials'].pop() + if obj.name in bpy.data.materials.keys() and not obj.users: bpy.data.materials.remove(obj) + else: + has_refs.append(['MAT', obj]) + if debug: _print("> Material: %s removal postponed with %s users" % (obj.name, obj.users), tag=True) + # Images + while len(bw_solution_data['images']): + obj = bw_solution_data['images'].pop() + if obj.name in bpy.data.images.keys() and not obj.users: bpy.data.images.remove(obj) + else: + has_refs.append(['IMG', obj]) + if debug: _print("> Image: %s removal postponed with %s users" % (obj.name, obj.users), tag=True) + # Data postponed due to having references last run + while len(bw_solution_data['had_refs']): + obj = bw_solution_data['had_refs'].pop() + if obj[0] == 'COL': dataBlk = bpy.data.collections + elif obj[0] == 'OBJ': dataBlk = bpy.data.objects + elif obj[0] == 'MAT': dataBlk = bpy.data.materials + elif obj[0] == 'IMG': dataBlk = bpy.data.images + else: + if debug: _print("> Unknown data type in postponed data blocks: %s for %s" % (obj[0], obj[1].name), tag=True) + continue + # Try to remove it again + if obj[1].name in dataBlk.keys() and not obj[1].users: dataBlk.remove(obj[1]) + else: + has_refs.append([obj[0], obj[1]]) + if debug: _print("> Postponed data block: %s removal postponed AGAIN with %s users" % (obj[1].name, obj[1].users), tag=True) + # Clear any zero ref count data blocks + #bpy.ops.outliner.orphans_purge(do_recursive=True) + # Add postponed data into data list + bw_solution_data['had_refs'] = has_refs + + +# Pack all textures +def pack_data(): + if debug: _print("> Packing all textures into temporary blend file", tag=True) + for img in bpy.data.images: + if img.source != 'GENERATED' and not img.packed_file and len(img.packed_files.values()) == 0: + try: + img.pack() + except: + _print("> Unable to pack %s" % (img.name), tag=True) + + + +# It's a me, main +def main(): + import sys # to get command line args + import argparse # to parse options for us and print a nice help message + + # get the args passed to blender after "--", all of which are ignored by + # blender so scripts may receive their own arguments + argv = sys.argv + + if "--" not in argv: + argv = [] # as if no args are passed + else: + argv = argv[argv.index("--") + 1:] # get all args after "--" + + # When --help or no args are given, print this help + usage_text = ( + "This script is used internally by Bake Wrangler add-on." + ) + + parser = argparse.ArgumentParser(description=usage_text) + + # Possible types are: string, int, long, choice, float and complex. + parser.add_argument( + "-t", "--tree", dest="tree", type=str, required=True, + help="Name of bakery tree where the starting node is", + ) + parser.add_argument( + "-n", "--node", dest="node", type=str, required=True, + help="Name of bakery node to start process from", + ) + parser.add_argument( + "-o", "--sock", dest="sock", type=int, required=False, + help="Socket for single suffix output", + ) + parser.add_argument( + "-v", "--ignorevis", dest="ignorevis", type=int, required=False, + help="Treat all selected objects as visibile", + ) + parser.add_argument( + "-d", "--debug", dest="debug", type=int, required=False, + help="Enable debug messages", + ) + parser.add_argument( + "-r", "--rend_dev", dest="rend_dev", type=str, required=False, + help="Cycles render device type", + ) + parser.add_argument( + "-u", "--rend_use", dest="rend_use", type=str, required=False, + help="Cycles enabled render devices", + ) + parser.add_argument( + "--solitr", dest="solution_restart", type=str, required=False, + help="Iterations of solutions before retry", + ) + parser.add_argument( + "--frameitr", dest="frames_restart", type=str, required=False, + help="Iterations of frames before retry", + ) + parser.add_argument( + "--batchitr", dest="batch_restart", type=str, required=False, + help="Iterations of batches before retry", + ) + + args = parser.parse_args(argv) + + if not argv: + parser.print_help() + return + + if not args.tree or not args.node: + print("Error: Bake Wrangler baker required arguments not found") + return + + global ignorevis + if args.ignorevis: + ignorevis = bool(args.ignorevis) + else: + ignorevis = False + + global debug + if args.debug: + debug = bool(args.debug) + else: + debug = False + + global images_saved + images_saved = [] + + global pickled_verts + pickled_verts = [] + + global solution_restart + solution_restart = 0 + if args.solution_restart: + solution_restart = int(args.solution_restart) + global frames_restart + frames_restart = 0 + if args.frames_restart: + frames_restart = int(args.frames_restart) + global batch_restart + batch_restart = 0 + if args.batch_restart: + batch_restart = int(args.batch_restart) + + # Track created data + global bw_solution_data + bw_solution_data = {'scenes': [], + 'collections': [], + 'objects': [], + 'materials': [], + 'images': [], + 'had_refs': [], + } + + # Reconfigure cycles render devices if args supplied + if args.rend_dev and args.rend_use: + bpy.context.preferences.addons["cycles"].preferences.get_devices() + bpy.context.preferences.addons["cycles"].preferences.compute_device_type = args.rend_dev + itr = 0 + for char in args.rend_use: + bpy.context.preferences.addons["cycles"].preferences.devices[itr].use = int(char) + + # Make sure the node classes are registered + try: + node_tree.register() + except: + print("Info: Bake Wrangler nodes already registered") + else: + print("Info: Bake Wrangler nodes registered") + + # Make sure to be in object mode before doing anything + if bpy.context.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Load shaders and scenes + bake_scene_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources", "BakeWrangler_Scene.blend") + with bpy.data.libraries.load(bake_scene_path, link=False, relative=False) as (file_from, file_to): + file_to.materials.append("BW_Bevel_Mask") + file_to.materials.append("BW_Bevel_Normals_Emit") + file_to.materials.append("BW_Bevel_Normals_Norm") + file_to.materials.append("BW_Cavity_Map") + file_to.materials.append("BW_Curvature_Map") + file_to.materials.append("BW_Island_ID") + file_to.materials.append("BW_Object_Color") + file_to.materials.append("BW_Thickness_Map") + file_to.materials.append("BW_Vertex_Color") + file_to.materials.append("BW_World_Pos") + file_to.materials.append("BW_OSL_Curvature") + file_to.materials.append("BW_OSL_Height") + file_to.materials.append("BW_Post_") + file_to.materials.append("BW_Post_Col_") + file_to.materials.append("BW_Billboard") + file_to.materials.append("BW_Mask_Pass") + file_to.node_groups.append("BW_Material_ID") + file_to.node_groups.append("BW_Normals") + file_to.node_groups.append("BW_Masked_Bake") + file_to.node_groups.append("BW_Channel_Map") + file_to.node_groups.append("bw_add_mask") + file_to.node_groups.append("BW_Billboard_Norm") + file_to.node_groups.append("BW_Bent_Norm") + file_to.scenes.append("BakeWrangler_Post") + file_to.scenes.append("BakeWrangler_Output") + file_to.objects.append("BW_MatPlane") + global bevmask_shader + global bevnormemit_shader + global bevnormnorm_shader + global cavity_shader + global curvature_shader + global islandid_shader + global objcol_shader + global thickness_shader + global vertcol_shader + global worldpos_shader + global osl_curvature + global osl_height + global post_proc_mat + global post_proc_col + global billboard_mat + global mask_pass_shader + bevmask_shader = file_to.materials[0] + bevnormemit_shader = file_to.materials[1] + bevnormnorm_shader = file_to.materials[2] + cavity_shader = file_to.materials[3] + curvature_shader = file_to.materials[4] + islandid_shader = file_to.materials[5] + objcol_shader = file_to.materials[6] + thickness_shader = file_to.materials[7] + vertcol_shader = file_to.materials[8] + worldpos_shader = file_to.materials[9] + osl_curvature = file_to.materials[10] + osl_height = file_to.materials[11] + post_proc_mat = file_to.materials[12] + post_proc_col = file_to.materials[13] + billboard_mat = file_to.materials[14] + mask_pass_shader = file_to.materials[15] + global materialid_group + global normals_group + global post_masked_bake + global post_chan_map + global internal_add_mask + global billboard_norm + global bent_norm_group + materialid_group = file_to.node_groups[0] + normals_group = file_to.node_groups[1] + post_masked_bake = file_to.node_groups[2] + post_chan_map = file_to.node_groups[3] + internal_add_mask = file_to.node_groups[4] + billboard_norm = file_to.node_groups[5] + bent_norm_group = file_to.node_groups[6] + global post_scene + global output_scene + post_scene = file_to.scenes[0] + output_scene = file_to.scenes[1] + global material_plane + material_plane = file_to.objects[0] + + # Start processing bakery node tree + err = process_tree(args.tree, args.node, args.sock) + + # Send comma separated list of files written + file_list_str = "" + for file in images_saved: + file_list_str += file + if file != images_saved[-1]: + file_list_str += "," + file_list_str += "" + _print(file_list_str) + + # Send comma separated list of pickled vertex colors + pickle_list_str = "" + for file in pickled_verts: + pickle_list_str += file + if file != pickled_verts[-1]: + pickle_list_str += "," + pickle_list_str += "" + _print(pickle_list_str) + + # Send end tag + if err: + _print("", tag=True) + else: + _print("", tag=True) + + # Save changes to the file for debugging and exit + if debug: + bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath, exit=True) + + return 0 + + +if __name__ == "__main__": + import os.path + import math + import random + import bpy + from datetime import datetime + '''try: + from BakeWrangler.nodes import node_tree + from BakeWrangler.nodes.node_tree import _print + from BakeWrangler.nodes.node_tree import material_recursor + from BakeWrangler.nodes.node_tree import get_input + from BakeWrangler.nodes.node_tree import follow_input_link + from BakeWrangler.nodes.node_tree import gather_output_links + import BakeWrangler.marginer as marginer + except:''' + # Need to import this stuff without referencing the BW module so marginer doesn't try to import the module + import sys + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + from nodes import node_tree + from nodes.node_tree import _print + from nodes.node_tree import material_recursor + from nodes.node_tree import get_input + from nodes.node_tree import follow_input_link + from nodes.node_tree import gather_output_links + from vert import ipc + import marginer + main() diff --git a/cg/blender/scripts/addons/BakeWrangler/marginer.py b/cg/blender/scripts/addons/BakeWrangler/marginer.py new file mode 100644 index 0000000..58d5746 --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/marginer.py @@ -0,0 +1,162 @@ +import numpy as np + +# Main loop over image broken into segments. Will try to calculate pixel values outside of +# the masked area using the preconfigured weighting system. +def worker(hunk, shm_pixels, shm_mask, shm_bools, shm_margin, margin, limit, hit_target): + np_pixels = np.ndarray(shm_pixels[1], dtype=shm_pixels[2], buffer=shm_pixels[0].buf) + np_mask = np.ndarray(shm_mask[1], dtype=shm_mask[2], buffer=shm_mask[0].buf) + np_bools = np.ndarray(shm_bools[1], dtype=shm_bools[2], buffer=shm_bools[0].buf) + margins_bool = np.ndarray(shm_margin[1], dtype=shm_margin[2], buffer=shm_margin[0].buf) + hit_stub = np.zeros((0,3)) + lim = len(margins_bool) if not limit else limit + for multi_index in hunk: + # Index ranges to create local view of arrays centred on pixel + view_idx = [multi_index[0], + multi_index[0]+(margin*2)+1, + multi_index[1], + multi_index[1]+(margin*2)+1] + bool_view = np_bools[view_idx[0]:view_idx[1],view_idx[2]:view_idx[3]] + hit_max = np.count_nonzero(bool_view) # Count number of non alpha pixels in view + if hit_max: + pixel_view = np_pixels[view_idx[0]:view_idx[1],view_idx[2]:view_idx[3]] # Get view of pixel data + hit_targ = hit_max if hit_max < hit_target else hit_target + hits = hit_stub + iteration = 0 + # Majority of time cost is here due to arrays being copied in every case + while hits.shape[0] < hit_targ and iteration < lim: + sub_bool = bool_view[margins_bool[iteration]] + if np.count_nonzero(sub_bool): + sub_pixel = pixel_view[margins_bool[iteration]] + hits = np.append(hits, sub_pixel[sub_bool,:3], axis=0) if hits.shape[0] else sub_pixel[sub_bool,:3] + iteration += 1 + # Get average of selected pixels colour and write value + if hits.shape[0] >= hit_target: + np_pixels[multi_index[0]+margin, multi_index[1]+margin,:3] = hits.sum(0) / hits.shape[0] + np_mask[multi_index[0]+margin, multi_index[1]+margin] = 1.0 + + +# Simply writes pixels to a bpy.image. This is to keep bpy outside of the main working loop +def write_back(image, pixels): + import bpy + image.pixels.foreach_set(pixels.ravel()) + image.update() + + +# Create numpy arrays of the image and mask as well as set up a weighting system for sampling +# pixels within the margin step area +def set_up(image, mask, margin): + import bpy + # Load numpy array from input image and mask + w, h = image.size + np_pixels = np.zeros((w, h, 4), 'f') + np_mask = np.zeros((w, h, 4), 'f') + image.pixels.foreach_get(np_pixels.ravel()) + mask.pixels.foreach_get(np_mask.ravel()) + + # Create a weighting system for pixel samples within the margin area + px_offsets = np.array(np.meshgrid(np.arange(0,margin*2+1), np.arange(0,margin*2+1))) + px_offsets = np.moveaxis(px_offsets, 0, -1) # Change to X by Y by 2 + px_offsets = np.absolute(px_offsets - [margin,margin]) + # Manhattan distance array + #px_manhat = px_offsets.sum(2) + # Euclid distances + px_euclid = np.sqrt(np.power(px_offsets[:,:,0],2) + np.power(px_offsets[:,:,1],2)) + px_euclid_c = np.int_(np.ceil(px_euclid)) + px_euclid_r = np.int_(np.round(px_euclid)) + # Bool arrays for each weight level starting at 1 + margins_bool = [] + for i in range(1,margin+1): + margins_bool.append(px_euclid_r == i) + + # Expand pixel data by margin size by copying the start onto the end to hopefully make iteration faster + # (negative array indexes work, but you can't exceed array bounds) + np_pixels = np.vstack((np_pixels, np_pixels[0:margin,:,:])) # Add rows from the bottom to the top + np_pixels = np.vstack((np_pixels[h-margin:h,:,:], np_pixels)) # Add rows from old top to the new top + np_pixels = np.hstack((np_pixels, np_pixels[:,0:margin,:])) # Add cols from left to right + np_pixels = np.hstack((np_pixels[:,w-margin:w,:], np_pixels)) # Add cols from old right to new right + # Do same for mask + np_mask = np.vstack((np_mask, np_mask[0:margin,:,:])) # Add rows from the bottom to the top + np_mask = np.vstack((np_mask[h-margin:h,:,:], np_mask)) # Add rows from old top to the new top + np_mask = np.hstack((np_mask, np_mask[:,0:margin,:])) # Add cols from left to right + np_mask = np.hstack((np_mask[:,w-margin:w,:], np_mask)) # Add cols from old right to new right + # Reduce mask values to just reds to save space + np_mask = np_mask[...,0].copy() + np_bool = np_mask > 0.9 + + return np_pixels, np_mask, np_bool, np.asarray(margins_bool), w, h, margin + + +# Takes all the outputs from the setup routine (not called from within to avoid interacting with +# bpy in the subprocesses). Creates shared memory versions of the data and spawns a bunch of +# processes to work on smaller hunks of pixels in parallel. +def add_margin(pixels, mask, bools, margins, w, h, margin_step, margin, hit_target): + import concurrent.futures + from multiprocessing.managers import SharedMemoryManager + m_step = margin_step if margin >= margin_step or margin == -1 else margin + with SharedMemoryManager() as smm: + # Create shared memory versions of these arrays for the processes to share + shm_pixels = smm.SharedMemory(size=pixels.nbytes) + shm_mask = smm.SharedMemory(size=mask.nbytes) + shm_bools = smm.SharedMemory(size=bools.nbytes) + shm_margin = smm.SharedMemory(size=margins.nbytes) + np_pixels = np.ndarray(pixels.shape, dtype=pixels.dtype, buffer=shm_pixels.buf) + np_pixels[:] = pixels[:] + del pixels + np_mask = np.ndarray(mask.shape, dtype=mask.dtype, buffer=shm_mask.buf) + np_mask[:] = mask[:] + del mask + np_bools = np.ndarray(bools.shape, dtype=bools.dtype, buffer=shm_bools.buf) + np_bools[:] = bools[:] + del bools + margins_bool = np.ndarray(margins.shape, dtype=margins.dtype, buffer=shm_margin.buf) + margins_bool[:] = margins + del margins + + # Split work into smaller hunks to split between cpu cores + import os + cpus = os.cpu_count() * 2 + mask_where = np.argwhere(np_bools[margin_step:w+margin_step,margin_step:h+margin_step] == False) + hunks = np.array_split(mask_where, cpus) + + # Do the processing in parallel + with concurrent.futures.ProcessPoolExecutor() as executor: + futures = [] + limit = 0 + # Negative margin indicates complete fill is wanted + if margin == -1: + # Simply keep processing hunks until they come back empty + while len(hunks[0]) > 0: + for i in hunks: + futures.append(executor.submit(worker, i, [shm_pixels, np_pixels.shape, np_pixels.dtype], [shm_mask, np_mask.shape, np_mask.dtype], [shm_bools, np_bools.shape, np_bools.dtype], [shm_margin, margins_bool.shape, margins_bool.dtype], m_step, limit, hit_target)) + # Wait for this steps hunks to finish, then calculate the next set + concurrent.futures.wait(futures) + np_bools[:] = np_mask > 0.9 + mask_where = np.argwhere(np_bools[margin_step:w+margin_step,margin_step:h+margin_step] == False) + hunks = np.array_split(mask_where, cpus) + # Check the margin actually has a size before doing anything + elif margin > 0: + # Work out how many steps are needed and if a last sub step size pass will be needed at the end + steps = int(margin / m_step) + lasts = margin % m_step + if lasts: steps += 1 + # Process all hunks for each step in parallel + for step in range(steps): + # If the margin step didn't fit evenly a last sub sized step will be done to fill it + if lasts and step == steps-1: + limit = lasts + for i in hunks: + futures.append(executor.submit(worker, i, [shm_pixels, np_pixels.shape, np_pixels.dtype], [shm_mask, np_mask.shape, np_mask.dtype], [shm_bools, np_bools.shape, np_bools.dtype], [shm_margin, margins_bool.shape, margins_bool.dtype], m_step, limit, hit_target)) + # Wait for this steps hunks to finish, then calculate the next set if there are more steps + concurrent.futures.wait(futures) + if step < steps-1: + np_bools[:] = np_mask > 0.9 + mask_where = np.argwhere(np_bools[margin_step:w+margin_step,margin_step:h+margin_step] == False) + hunks = np.array_split(mask_where, cpus) + + # Copy pixels from shared memory before the smm exits + output_px = np_pixels[margin_step:w+margin_step,margin_step:h+margin_step].copy() + return output_px + + +if __name__ == '__main__': + pass \ No newline at end of file diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/__init__.py b/cg/blender/scripts/addons/BakeWrangler/nodes/__init__.py new file mode 100644 index 0000000..d09ed78 --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/nodes/__init__.py @@ -0,0 +1,17 @@ +from . import node_tree +from . import node_msgbus +from . import node_panel +from . import node_update + +def register(): + node_tree.register() + node_msgbus.register() + node_panel.register() + node_update.register() + + +def unregister(): + node_tree.unregister() + node_msgbus.unregister() + node_panel.unregister() + node_update.unregister() diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/node_mexport.py b/cg/blender/scripts/addons/BakeWrangler/nodes/node_mexport.py new file mode 100644 index 0000000..33ca16f --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/nodes/node_mexport.py @@ -0,0 +1,370 @@ +from bl_operators.presets import AddPresetBase +from bl_ui.utils import PresetPanel +from bpy.types import Panel, Menu, Operator +import bpy + + +# Helper functions and data for exporting meshes + + +# Classes to manage FBX preset panel/menu +class BW_PT_PresetsFBX(PresetPanel, Panel): + bl_label = 'FBX Presets' + preset_subdir = 'bake_wrangler\export.fbx' + preset_operator = 'script.execute_preset' + preset_add_operator = 'bake_wrangler.add_preset_fbx' + +class BW_MT_PresetsFBX(Menu): + bl_label = 'FBX Presets' + preset_subdir = 'bake_wrangler\export.fbx' + preset_operator = 'script.execute_preset' + draw = Menu.draw_preset + +class BW_OT_AddPresetFBX(AddPresetBase, Operator): + '''Add new FBX preset''' + bl_idname = 'bake_wrangler.add_preset_fbx' + bl_label = 'Add FBX preset' + preset_menu = 'BW_MT_PresetsFBX' + + # Common variable used for all preset values + preset_defines = [ + 'node = bpy.context.active_node.FBX', + ] + + # Properties to store in the preset + preset_values = [] + for key in bpy.ops.export_scene.fbx.get_rna_type().properties.keys()[2:]: + preset_values.append("node." + key) + + # Directory to store the presets + preset_subdir = 'bake_wrangler\export.fbx' + + +#Helper functions and data +export_supported = { + 'FBX': [BW_PT_PresetsFBX, 'export_scene.fbx', None], + } +exporters = {} + + +def get_exporters(): + presets_enum = [] + for key, val in exporters.items(): + if key == 'FBX': + presets_enum.append(('FBX', "FBX", "Export to FBX")) + return tuple(presets_enum) + + +def draw_presets(preset, layout): + exporters[preset][0].draw_menu(layout) + + +def draw_properties(node, preset, layout): + props = getattr(node, preset) + #for prop in props.rna_type.properties.keys(): + # if prop not in ["rna_type", "name"]: + # layout.prop(props, prop) + # Go the road to hell and have custom layouts for each format mostly stolen from their panels + # instead of just displaying all the properties and letting god sort them out + if preset == 'FBX': + # Main section + layout.use_property_decorate = False + row = layout.row(align=True) + row.prop(props, "path_mode") + sub = row.row(align=True) + sub.enabled = (props.path_mode == 'COPY') + sub.prop(props, "embed_textures", text="", icon='PACKAGE' if props.embed_textures else 'UGLYPACKAGE') + box = layout.box() + if not node.show_pt_1: + box.prop(node, "show_pt_1", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Include") + else: + box.prop(node, "show_pt_1", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Include") + box.use_property_split = True + box.column().prop(props, "object_types") + box.prop(props, "use_custom_props") + box = layout.box() + if not node.show_pt_2: + box.prop(node, "show_pt_2", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Transform") + else: + box.prop(node, "show_pt_2", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Transform") + box.use_property_split = True + box.prop(props, "global_scale") + box.prop(props, "apply_scale_options") + + box.prop(props, "axis_forward") + box.prop(props, "axis_up") + + box.prop(props, "apply_unit_scale") + box.prop(props, "use_space_transform") + row = box.row() + row.prop(props, "bake_space_transform") + row.label(text="", icon='ERROR') + box = layout.box() + if not node.show_pt_3: + box.prop(node, "show_pt_3", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Geometry") + else: + box.prop(node, "show_pt_3", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Geometry") + box.use_property_split = True + box.prop(props, "mesh_smooth_type") + box.prop(props, "use_subsurf") + box.prop(props, "use_mesh_modifiers") + box.prop(props, "use_mesh_edges") + sub = box.row() + sub.prop(props, "use_tspace") + box = layout.box() + if not node.show_pt_4: + box.prop(node, "show_pt_4", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Armature") + else: + box.prop(node, "show_pt_4", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Armature") + box.use_property_split = True + box.prop(props, "primary_bone_axis") + box.prop(props, "secondary_bone_axis") + box.prop(props, "armature_nodetype") + box.prop(props, "use_armature_deform_only") + box.prop(props, "add_leaf_bones") + box = layout.box() + hed = box.row() + if not node.show_pt_5: + hed.prop(node, "show_pt_5", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="") + hed.prop(props, "bake_anim", text="") + hed.prop(node, "show_pt_5", icon="NONE", emboss=False, text="Bake Animation") + else: + hed.prop(node, "show_pt_5", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="") + hed.prop(props, "bake_anim", text="") + hed.prop(node, "show_pt_5", icon="NONE", emboss=False, text="Bake Animation") + box.use_property_split = True + col = box.column() + col.enabled = props.bake_anim + col.prop(props, "bake_anim_use_all_bones") + col.prop(props, "bake_anim_use_nla_strips") + col.prop(props, "bake_anim_use_all_actions") + col.prop(props, "bake_anim_force_startend_keying") + col.prop(props, "bake_anim_step") + col.prop(props, "bake_anim_simplify_factor") + + +# Creates a property group from an operators properties +def prop_grp_from_op(opName, grpName): + oppath, opnm = opName.split(".") + op = getattr(bpy.ops, oppath, None) + if op is None: + return op + op = getattr(op, opnm, None) + if op is None: + return op + props = op.get_rna_type() + props = props.properties + grp_props = {'__annotations__' : {}} + for prop in props: + if prop.identifier in ["rna_type", "filepath"]: + continue + if prop.type == 'BOOLEAN': + grp_props['__annotations__'][prop.identifier] = bpy.props.BoolProperty( + name=prop.name, + description=prop.description, + default=prop.default, + subtype=prop.subtype) + elif prop.type == 'ENUM': + eitems = [] + eopts = set() + ende =prop.default + if prop.is_enum_flag: + eopts = set({'ENUM_FLAG'}) + ende = prop.default_flag + for key in prop.enum_items.keys(): + eitems.append((key, prop.enum_items[key].name, prop.enum_items[key].description)) + grp_props['__annotations__'][prop.identifier] = bpy.props.EnumProperty( + items=tuple(eitems), + name=prop.name, + description=prop.description, + options=eopts, + default=ende) + elif prop.type == 'STRING': + grp_props['__annotations__'][prop.identifier] = bpy.props.StringProperty( + name=prop.name, + description=prop.description, + default=prop.default, + maxlen=prop.length_max, + subtype=prop.subtype) + elif prop.type == 'POINTER': + grp_props['__annotations__'][prop.identifier] = bpy.props.PointerProperty( + type=getattr(bpy.types, prop.fixed_type.name), + name=prop.name, + description=prop.description) + elif prop.type == 'FLOAT': + grp_props['__annotations__'][prop.identifier] = bpy.props.FloatProperty( + name=prop.name, + description=prop.description, + default=prop.default, + min=prop.hard_min, + max=prop.hard_max, + soft_min=prop.soft_min, + soft_max=prop.soft_max, + step=prop.step, + precision=prop.precision, + subtype=prop.subtype, + unit=prop.unit) + else: + print("Unknown type: %s on %s" % (prop.type, prop.identifier)) + # Create and return prop group class from the props + return type(grpName, tuple([bpy.types.PropertyGroup]), grp_props) + + +# Node to export baked models in some format +class BakeWrangler_Output_Export_Mesh(Node, BakeWrangler_Tree_Node): + '''Node to export baked models to the selected format''' + bl_label = 'Output Export Mesh' + + # Makes sure there is always one empty input socket at the bottom by adding and removing sockets + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Mesh', "Mesh") + + # Check node settings are valid to bake. Returns true/false, plus error message(s). + def validate(self, is_primary=False): + valid = [True] + # Validate inputs + has_valid_input = False + for input in self.inputs: + if not is_primary: + has_valid_input = True + break + else: + input_valid = get_input(input).validate() + valid[0] = input_valid.pop(0) + if valid[0]: + has_valid_input = True + valid += input_valid + errs = len(valid) + if not has_valid_input and errs < 2: + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"]) + # Validate file path + self.get_full_path(bpy.context) + if not os.path.isdir(os.path.abspath(self.out_path)): + # Try creating the path if enabled in prefs + if _prefs("make_dirs") and not os.path.exists(os.path.abspath(self.out_path)): + try: + os.makedirs(os.path.abspath(self.out_path)) + except OSError as err: + valid[0] = False + valid.append([_print("Path error", node=self, ret=True), ": Trying to create path at '%s'" % (err.strerror)]) + return valid + else: + valid[0] = False + valid.append([_print("Path error", node=self, ret=True), ": Invalid path '%s'" % (os.path.abspath(self.out_path))]) + return valid + # Check if there is read/write access to the file/directory + file_path = os.path.join(os.path.abspath(self.out_path), self.name_with_ext()) + if os.path.exists(file_path): + if os.path.isfile(file_path): + # It exists so try to open it r/w + try: + file = open(file_path, "a") + except OSError as err: + valid[0] = False + valid.append([_print("File error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)]) + else: + # It exists but isn't a file + valid[0] = False + valid.append([_print("File error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)]) + else: + # See if it can be created + try: + file = open(file_path, "a") + except OSError as err: + valid[0] = False + valid.append([_print("File error", node=self, ret=True), ": %s trying to create file at '%s'" % (err.strerror, file_path)]) + else: + file.close() + os.remove(file_path) + # Validated + return valid + + # Get full path, removing any relative references + def get_full_path(self, context): + cwd = os.path.dirname(bpy.data.filepath) + self.out_path = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.disp_path))) + + # Deal with any path components that may be in the filename + def update_filename(self, context): + fullpath = os.path.normpath(bpy.path.abspath(self.out_name)) + path, name = os.path.split(fullpath) + if path: + self.disp_path = self.out_name[:-len(name)] + if name and self.out_name != name: + self.out_name = name + + # Return the file name with the correct extension and suffix + def name_with_ext(self, suffix=""): + return self.out_name + suffix + self.exporter.lower() + + def get_exporters(self, context): + return node_mexport.get_exporters() + + # Core settings + disp_path: bpy.props.StringProperty(name="Output Path", description="Path to save mesh in", default="", subtype='DIR_PATH', update=get_full_path) + out_path: bpy.props.StringProperty(name="Output Path", description="Path to save mesh in", default="", subtype='DIR_PATH') + out_name: bpy.props.StringProperty(name="Output File", description="File prefix to save mesh as", default="Mesh", subtype='FILE_PATH', update=update_filename) + exporter: bpy.props.EnumProperty(name="Format", description="Export file format", items=get_exporters) + + show_pt_1: bpy.props.BoolProperty(default=True) + show_pt_2: bpy.props.BoolProperty(default=False) + show_pt_3: bpy.props.BoolProperty(default=False) + show_pt_4: bpy.props.BoolProperty(default=False) + show_pt_5: bpy.props.BoolProperty(default=False) + + def init(self, context): + super().init(context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Mesh', "Mesh") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Bake', "Bake") + # Prefs + self.disp_path = _prefs("def_meshpath") + self.out_name = _prefs("def_meshname") + + def draw_buttons(self, context, layout): + colnode = layout.column(align=False) + colpath = colnode.column(align=True) + colpath.prop(self, "disp_path", text="") + colpath.prop(self, "out_name", text="") + colpath.prop(self, "exporter") + + def draw_buttons_ext(self, context, layout): + node_mexport.draw_presets(self.exporter, layout.row()) + col = layout.column() + node_mexport.draw_properties(self, self.exporter, col) + + +# Classes to register +classes = ( + BW_PT_PresetsFBX, + BW_MT_PresetsFBX, + BW_OT_AddPresetFBX, +) + + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + for exp in export_supported.keys(): + if getattr(bpy.ops, exp[1], None) is not None: + prop_grp = prop_grp_from_op(export_supported[exp][1], "BW_PropGrp" + exp) + exporters[exp] = [export_supported[exp][0], export_supported[exp][1], prop_grp] + register_class(prop_grp) + from .node_tree import BakeWrangler_Output_Export_Mesh + setattr(BakeWrangler_Output_Export_Mesh, exp, bpy.props.PointerProperty(type=prop_grp)) + + +def unregister(): + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + for exp in exporters.keys(): + unregister_class(exporters[exp][2]) + exporters = {} + + +if __name__ == "__main__": + register() diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/node_msgbus.py b/cg/blender/scripts/addons/BakeWrangler/nodes/node_msgbus.py new file mode 100644 index 0000000..94220fb --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/nodes/node_msgbus.py @@ -0,0 +1,114 @@ +import bpy +from .node_tree import _prefs, _print, BW_TREE_VERSION + + +# Msgbus will call this when the loaded node tree changes. Checks on tree version etc can be done +def BakeWrangler_Msgbus_NodeTreeChange(*args): + debug = _prefs('debug') + if debug: _print("Node Tree Changed") + wm = bpy.context.window_manager + ar = bpy.context.area + if debug: _print("Context Area: %s" % (ar)) + # First find all the open node editors that belong to BW + spaces = [] + for window in wm.windows: + for area in window.screen.areas: + if area.ui_type == 'BakeWrangler_Tree': + if len(area.spaces) > 0: + for spc in area.spaces: + if spc.type == 'NODE_EDITOR' and hasattr(spc, 'node_tree'): + if debug: _print("Node editor found: %s" % (spc)) + spaces.append(spc) + break + for space in spaces: + tree = space.node_tree + # Init a new tree + if tree and not tree.initialised: + if debug: _print("New/Uninitialized node tree active") + tree.use_fake_user = True + # Give tree a nice name + '''if tree.name.startswith("NodeTree"): + num = 0 + for nodes in bpy.data.node_groups: + if nodes.name.startswith("Bake Recipe"): + if num == 0: + num = 1 + splt = nodes.name.split('.') + if len(splt) > 1 and splt[1].isnumeric: + num = int(splt[1]) + 1 + if num == 0: + name = "Bake Recipe" + else: + name = "Bake Recipe.%03d" % (num) + tree.name = tree.name.replace("NodeTree", name, 1)''' + # Add initial basic node set up + if len(tree.nodes) == 0: + bake_mesh = tree.nodes.new('BakeWrangler_Bake_Mesh') + bake_pass = tree.nodes.new('BakeWrangler_Bake_Pass') + output_img = tree.nodes.new('BakeWrangler_Output_Image_Path') + global_mesh_set = tree.nodes.new('BakeWrangler_MeshSettings') + global_mesh_set.pinned = True + global_pass_set = tree.nodes.new('BakeWrangler_PassSettings') + global_pass_set.pinned = True + global_outp_set = tree.nodes.new('BakeWrangler_OutputSettings') + global_outp_set.pinned = True + global_samp_set = tree.nodes.new('BakeWrangler_SampleSettings') + global_samp_set.pinned = True + + bake_mesh.location[0] -= 300 + output_img.location[0] += 200 + global_mesh_set.location[0] -= 300 + global_mesh_set.location[1] += 210 + global_pass_set.location[0] += 100 + global_pass_set.location[1] += 210 + global_outp_set.location[0] += 280 + global_outp_set.location[1] += 210 + global_samp_set.location[0] -= 80 + global_samp_set.location[1] += 210 + + tree.links.new(bake_pass.inputs[1], bake_mesh.outputs[0]) + tree.links.new(output_img.inputs[2], bake_pass.outputs[0]) + output_img.inputs[2].valid = True + + tree.tree_version = BW_TREE_VERSION + tree.initialised = True + if debug: _print("Tree initialized") + + +# Reregister message bus subscription +bw_subscriber = object() +from bpy.app.handlers import persistent +@persistent +def BakeWrangler_Hook_Post_Load(dummy): + BakeWrangler_Msgbus_Subscribe(bw_subscriber) + + +# Subscribe to message bus +def BakeWrangler_Msgbus_Subscribe(owner, sub=True): + if owner is not None: + bpy.msgbus.clear_by_owner(owner) + if sub: + subscribe_to = bpy.types.SpaceNodeEditor, "node_tree" + bpy.msgbus.subscribe_rna(key=subscribe_to, + owner=owner, + args=(1,2), + notify=BakeWrangler_Msgbus_NodeTreeChange) + + +def register(): + BakeWrangler_Msgbus_Subscribe(bw_subscriber) + bpy.app.handlers.load_post.append(BakeWrangler_Hook_Post_Load) + + +def unregister(): + hook_index = None + for idx in range(len(bpy.app.handlers.load_post)): + if bpy.app.handlers.load_post[idx] == BakeWrangler_Hook_Post_Load: + hook_index = idx + if hook_index != None: + bpy.app.handlers.load_post.pop(hook_index) + BakeWrangler_Msgbus_Subscribe(bw_subscriber, False) + + +if __name__ == "__main__": + register() diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/node_panel.py b/cg/blender/scripts/addons/BakeWrangler/nodes/node_panel.py new file mode 100644 index 0000000..67841b9 --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/nodes/node_panel.py @@ -0,0 +1,299 @@ +import bpy +from .node_tree import _prefs, _print, BW_TREE_VERSION, BakeWrangler_Operator + + +# Panel displaying info about recipe version and containing update button +class BakeWrangler_RecipeInfo(bpy.types.Panel): + '''Panel in node editor to show recipe information''' + bl_label = "Recipe Info" + bl_idname = "OBJECT_PT_BW_RecipeInfo" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = "area" + bl_category = "Bake Wrangler" + + @classmethod + def poll(cls, context): + # Only display if the edited tree is of the correct type + return (context.area and context.area.ui_type == 'BakeWrangler_Tree') + + def draw(self, context): + tree = context.space_data.node_tree + layout = self.layout + if tree is None: + layout.label(text="No recipe loaded") + return + tree_ver = getattr(tree, "tree_version", 0) + curr_ver = BW_TREE_VERSION + nodes = len(tree.nodes) + + col = layout.column() + op = col.operator("bake_wrangler.show_log", icon='TEXT') + op.tree = tree.name + col.label(text="Recipe version: " + str(tree_ver)) + col.label(text="Add-on version: " + str(curr_ver)) + col.label(text="Nodes: " + str(nodes)) + + if tree_ver != curr_ver: + row = col.row() + if tree_ver > curr_ver: + row.label(text="Status: Add-on requires update") + else: + row.label(text="Status: Recipe requires update") + op_row = col.row() + if tree_ver >= 5: + op = op_row.operator("bake_wrangler_op.update_recipe", icon='FILE_REFRESH', text="Update Recipe") + op.tree = tree.name + else: + op_row.operator("bake_wrangler_op.update_recipe", icon='CANCEL', text="Update Unavailable") + op_row.enabled = False + + +# Panel for automatic cage management tasks +class BakeWrangler_AutoCages(bpy.types.Panel): + '''Panel in node editor to manage automatic cages''' + bl_label = "Auto Cages" + bl_idname = "OBJECT_PT_BW_AutoCages" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = "area" + bl_category = "Bake Wrangler" + + @classmethod + def poll(cls, context): + # Only display if the edited tree is of the correct type + return (context.area and context.area.ui_type == 'BakeWrangler_Tree') + + def draw(self, context): + tree = context.space_data.node_tree + layout = self.layout + if tree is None: + layout.label(text="No recipe loaded") + return + col = layout.column() + op = col.operator("bake_wrangler.auto_cage_create") + op.tree = tree.name + op = col.operator("bake_wrangler.auto_cage_update") + op.tree = tree.name + op = col.operator("bake_wrangler.auto_cage_remove") + op.tree = tree.name + + +# Show log file +class BakeWrangler_Operator_ShowLog(BakeWrangler_Operator, bpy.types.Operator): + '''Show last log created by this recipe''' + bl_idname = "bake_wrangler.show_log" + bl_label = "Show Log" + bl_options = {"REGISTER"} + + # Called either after invoke from UI or directly from script + def execute(self, context): + return {'FINISHED'} + + # Called from button press, set modifier key states + def invoke(self, context, event): + tree = bpy.data.node_groups[self.tree] + if tree.last_log: + bpy.ops.screen.area_dupli('INVOKE_DEFAULT') + open_ed = bpy.context.window_manager.windows[len(bpy.context.window_manager.windows) - 1].screen.areas[0] + open_ed.type = 'TEXT_EDITOR' + log = bpy.data.texts.load(tree.last_log) + open_ed.spaces[0].text = log + open_ed.spaces[0].show_line_numbers = False + open_ed.spaces[0].show_syntax_highlight = False + return {'FINISHED'} + else: + self.report({'WARNING'}, "No log file set") + return {'CANCELLED'} + + +# Generate auto cages +class BakeWrangler_Operator_AutoCageCreate(BakeWrangler_Operator, bpy.types.Operator): + '''Create cages in current scene for objects in recipe that don't have a cage set.\nShift-Click to exclude hidden objects''' + bl_idname = "bake_wrangler.auto_cage_create" + bl_label = "Generate Cages" + bl_options = {"REGISTER", "UNDO"} + + # Called either after invoke from UI or directly from script + def execute(self, context): + return {'FINISHED'} + + # Called from button press, set modifier key states + def invoke(self, context, event): + mod_shift = event.shift + objs = get_auto_caged(bpy.data.node_groups[self.tree], mod_shift, context) + if len(objs): + # Check if cage collection exists and create it if needed + if 'BW Cages' not in bpy.data.collections.keys(): + bpy.data.collections.new('BW Cages') + # Check if cage collection is in current scene and link if needed + if 'BW Cages' not in context.scene.collection.children.keys(): + context.scene.collection.children.link(bpy.data.collections['BW Cages']) + bw_cages = bpy.data.collections['BW Cages'].objects + # Create and link cages to the collection for all objects + for obj in objs: + if not obj[0].bw_auto_cage: + generate_auto_cage(obj[0], obj[1], obj[2], context) + if obj[0].bw_auto_cage not in bw_cages.values(): + bw_cages.link(obj[0].bw_auto_cage) + return {'FINISHED'} + else: + self.report({'WARNING'}, "No objects with auto cages found") + return {'CANCELLED'} + + +# Update auto cages +class BakeWrangler_Operator_AutoCageUpdate(BakeWrangler_Operator, bpy.types.Operator): + '''Update cages in current scene for objects in recipe. Overwrites user changes if 'bw_cage' modifier has been removed.\nShift-Click to exclude hidden objects''' + bl_idname = "bake_wrangler.auto_cage_update" + bl_label = "Update Cages" + bl_options = {"REGISTER", "UNDO"} + + # Called either after invoke from UI or directly from script + def execute(self, context): + return {'FINISHED'} + + # Called from button press, set modifier key states + def invoke(self, context, event): + mod_shift = event.shift + objs = get_auto_caged(bpy.data.node_groups[self.tree], mod_shift, context) + if len(objs): + for obj in objs: + if obj[0].bw_auto_cage: + cage = obj[0].bw_auto_cage + # If the modifier is still on the object just change it instead of making a new object + if "bw_cage" in cage.modifiers: + cage.modifiers["bw_cage"].strength = obj[1] + cage.data.auto_smooth_angle = obj[2] + elif 'BW Cages' in bpy.data.collections.keys(): + bpy.data.collections['BW Cages'].objects.unlink(cage) + generate_auto_cage(obj[0], obj[1], obj[2], context) + return {'FINISHED'} + else: + self.report({'WARNING'}, "No objects with auto cages found") + return {'CANCELLED'} + + +# Remove auto cages +class BakeWrangler_Operator_AutoCageRemove(BakeWrangler_Operator, bpy.types.Operator): + '''Remove cages in current scene for objects in recipe.\nShift-Click to exclude hidden objects''' + bl_idname = "bake_wrangler.auto_cage_remove" + bl_label = "Remove Cages" + bl_options = {"REGISTER", "UNDO"} + + # Called either after invoke from UI or directly from script + def execute(self, context): + return {'FINISHED'} + + # Called from button press, set modifier key states + def invoke(self, context, event): + mod_shift = event.shift + if 'BW Cages' in bpy.data.collections.keys(): + bw_cages = bpy.data.collections['BW Cages'].objects + objs = context.scene.collection.all_objects + for obj in objs: + if obj.bw_auto_cage and (not mod_shift or obj.visible_get()): + bw_cages.unlink(obj.bw_auto_cage) + obj.bw_auto_cage = None + if 'BW Cages' in context.scene.collection.children: + context.scene.collection.children.unlink(bw_cages.id_data) + return {'FINISHED'} + else: + self.report({'WARNING'}, "No objects with auto cages found") + return {'CANCELLED'} + + +# Return a list of objects that would get a cage auto generated +def get_auto_caged(tree, vis, context): + nodes = tree.nodes + objs = [] + for node in nodes: + if node.bl_idname == 'BakeWrangler_Output_Image_Path': + objs += node.get_unique_objects('TARGET', for_auto_cage=True) + # Get a list of all objects in the scene and cull it down to only visible ones + vl_objs = context.scene.collection.all_objects.values() + if vis: + vl_vis = [] + for obj in vl_objs: + if obj.visible_get() and obj not in vl_vis: + vl_vis.append(obj) + vl_objs = vl_vis + # Return a list of unique objects that are in the scene and visible and would have a cage + objs_prune = [] + for obj in objs: + if obj not in objs_prune and obj[0] in vl_objs: + objs_prune.append(obj) + return objs_prune + + +# Create an auto cage for the given mesh +def generate_auto_cage(mesh, cage_exp, smooth, context): + # Create a copy of the base mesh with modifiers applied to use a the base cage + cage = mesh.copy() + cage.data = mesh.data.copy() + cage.name = mesh.name + '.cage' + cage.name = mesh.name + '.cage' + cage.data.materials.clear() + cage.data.polygons.foreach_set('material_index', [0] * len(cage.data.polygons)) + cage.display_type = 'WIRE' + if cage not in bpy.data.collections['BW Cages'].objects.values(): + bpy.data.collections['BW Cages'].objects.link(cage) + if len(cage.modifiers): + prev_active = bpy.context.view_layer.objects.active + bpy.context.view_layer.objects.active = cage + for mod in cage.modifiers: + if mod.show_render: + try: + bpy.ops.object.modifier_apply(modifier=mod.name) + except: + _print("Error applying modifier '%s' to object '%s'" % (mod.name, mesh.name)) + bpy.ops.object.modifier_remove(modifier=mod.name) + else: + bpy.ops.object.modifier_remove(modifier=mod.name) + bpy.context.view_layer.objects.active = prev_active + # Expand cage on normals + cage_disp = cage.modifiers.new("bw_cage", 'DISPLACE') + cage_disp.strength = cage_exp + cage_disp.direction = 'NORMAL' + cage_disp.mid_level = 0.0 + cage_disp.show_in_editmode = True + cage_disp.show_on_cage = True + cage_disp.show_expanded = False + # Smooth normals and clear sharps + cage.data.use_auto_smooth = True + cage.data.auto_smooth_angle = smooth + for poly in cage.data.polygons: + poly.use_smooth = True + for edge in cage.data.edges: + edge.use_edge_sharp = False + # Link cage via property on mesh + mesh.bw_auto_cage = cage + + +# Classes to register +classes = ( + BakeWrangler_RecipeInfo, + BakeWrangler_AutoCages, + BakeWrangler_Operator_ShowLog, + BakeWrangler_Operator_AutoCageCreate, + BakeWrangler_Operator_AutoCageUpdate, + BakeWrangler_Operator_AutoCageRemove, +) + + +def register(): + # Add pointer to generated cage + bpy.types.Object.bw_auto_cage = bpy.props.PointerProperty(name="Cage", description="Bake Wrangler auto generated cage", type=bpy.types.Object) + from bpy.utils import register_class + for cls in classes: + register_class(cls) + + +def unregister(): + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/node_tree.py b/cg/blender/scripts/addons/BakeWrangler/nodes/node_tree.py new file mode 100644 index 0000000..2cf9707 --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/nodes/node_tree.py @@ -0,0 +1,5218 @@ +import os +import sys +import threading, queue +import subprocess +from datetime import datetime, timedelta +import bpy +from bpy.types import NodeTree, Node, NodeSocket, NodeSocketColor, NodeSocketFloat, NodeFrame, NodeReroute +try: + from BakeWrangler.status_bar.status_bar_icon import ensure_bw_icon as update_status_bar +except: + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + from status_bar.status_bar_icon import ensure_bw_icon as update_status_bar + +BW_TREE_VERSION = 9 +BW_VERSION_STRING = '1.5.b11' + + +# Message formatter +def _print(str, node=None, ret=False, tag=False, wrap=True, enque=None, textdata="BakeWrangler"): + output = "%s" % (str) + endl = '' + flsh = False + + if node: + output = "[%s]: %s" % (node.get_name(), output) + + if tag: + output.replace("<", "<_") + output = "<%s>%s" % ("PBAKE", output) + flsh = True + if wrap: + output = "%s" % (output, "PWRAP") + else: + output = "%s" % (output, "PBAKE") + + if wrap: + endl = '\n' + + if enque != None: + eout = "%s%s" % (output, endl) + enque.put(eout) + return None + + if ret: + return output + + if textdata != None and _prefs('text_msgs'): + if not textdata in bpy.data.texts.keys(): + bpy.data.texts.new(textdata) + text = bpy.data.texts[textdata] + end = len(text.lines[len(text.lines) - 1].body) - 1 + text.cursor_set(len(text.lines) - 1, character=end) + tout = "%s%s" % (output, endl) + text.write(tout) + + print(output, end=endl, flush=flsh) + + + +# Preference reader +default_true = ["text_msgs", "clear_msgs", "def_filter_mesh", "def_filter_curve", "def_filter_surface", + "def_filter_meta", "def_filter_font", "def_filter_light", "auto_open", "fact_start", "show_icon"] +default_false = ["def_filter_collection", "def_show_adv", "ignore_vis", "make_dirs", "wind_close", + "invert_bakemod", "wind_msgs", "save_packed", "save_images",] +default_res = ["def_xres", "def_yres", "def_xout", "def_yout",] +default_zero = ["def_margin", "def_mask_margin", "def_max_ray_dist", "retrys",] +def _prefs(key): + try: + name = __package__.split('.') + prefs = bpy.context.preferences.addons[name[0]].preferences + except: + pref = False + else: + pref = True + + if pref and key in prefs: + return prefs[key] + else: + # Default values to fall back on + if key == 'debug': + if pref: + return False + else: + #return False + return True + elif key in default_true: + return True + elif key in default_false: + return False + elif key in default_res: + return 1024 + elif key in default_zero: + return 0 + elif key == 'def_device': + return 0 # CPU + elif key == 'def_samples': + return 1 + elif key == 'def_format': + return 2 # PNG + elif key == 'def_raydist': + return 0.01 + elif key == 'def_outpath': + return "" + elif key == 'def_outname': + return "Image" + elif key == 'img_non_color': + return None + else: + return None + + + +# Material validation recursor (takes a shader node and descends the tree via recursion) +def material_recursor(node, link=None, parent=None): + # Accepted node types are OUTPUT_MATERIAL, BSDF_PRINCIPLED, MIX/ADD_SHADER and GROUP (plus REROUTE) + if node.type == 'BSDF_PRINCIPLED': + return True + if node.type == 'OUTPUT_MATERIAL' and node.inputs['Surface'].is_linked: + return material_recursor(node.inputs['Surface'].links[0].from_node, node.inputs['Surface'].links[0], parent) + if node.type in ['MIX_SHADER', 'ADD_SHADER']: + if node.type == 'MIX_SHADER': + input1 = 1 + input2 = 2 + else: + input1 = 0 + input2 = 1 + inputA = False + if node.inputs[input1].is_linked: + inputA = material_recursor(node.inputs[input1].links[0].from_node, node.inputs[input1].links[0], parent) + inputB = False + if node.inputs[input2].is_linked: + inputB = material_recursor(node.inputs[input2].links[0].from_node, node.inputs[input2].links[0], parent) + return inputA and inputB + if node.type == 'REROUTE' and node.inputs[0].is_linked: + return material_recursor(node.inputs[0].links[0].from_node, node.inputs[0].links[0], parent) + if node.type == 'GROUP' and link: + # Entering a group, requires similar steps to exiting, but will duplicate code for now + if parent: + # Parent modification is always performed on a copy due to branching of recursion + gparent = parent.copy() + gparent.append(node) + else: + gparent = [node] + gout = None + gsoc = 0 + # Find active group socket to begin from. Names may not be unique, so get index + for soc in node.outputs: + if link.from_socket == soc: break + else: gsoc += 1 + for gnode in node.node_tree.nodes: + if gnode.type == 'GROUP_OUTPUT' and gnode.is_active_output: + gout = gnode + break + if gout and gout.inputs[gsoc].is_linked: + return material_recursor(gout.inputs[gsoc].links[0].from_node, gout.inputs[gsoc].links[0], gparent) + if node.type == 'GROUP_INPUT' and link and parent: + # Exiting a group, requires similar steps to entering, but will duplicate code for now + # Parent modification is always performed on a copy due to branching of recursion + gparent = parent.copy() + gout = gparent.pop() + gsoc = 0 + for soc in node.outputs: + if link.from_socket == soc: break + else: gsoc += 1 + if gout and gout.inputs[gsoc].is_linked: + return material_recursor(gout.inputs[gsoc].links[0].from_node, gout.inputs[gsoc].links[0], gparent) + return False + + + +# Return the node connected to an input, dealing with re-routes +def get_input(input): + if not input.is_output and input.islinked() and input.valid: + link = follow_input_link(input.links[0]) + return link.from_node + return None + + + +# Prune error messages to remove duplicates +def prune_messages(messages): + unique = [] + for msg in messages: + if not unique.count(msg): + unique.append(msg) + return unique + + + +# Prune a list of objects to remove duplicate references +def prune_objects(objs, allow_dups=False): + count = [] + dups = [] + # First remove duplicates + for obj in objs: + if objs.count(obj) > 1: + objs.remove(obj) + # Then remove non duplicate entries that reference the same object where appropriate + for obj in objs: + # Get a list of just the referenced objects to count them + count.append(obj[0]) + for obj in count: + # Create a list of objects with multiple refs and count how many + if count.count(obj) > 1: + found = False + for dup in dups: + if dup[0] == obj: + found = True + dup[1] += 1 + break + if not found: + dups.append([obj, 1]) + for obj in dups: + # Go over all the duplicate entries and prune appropriately + num = obj[1] + for dup in objs: + if dup[0] == obj[0]: + # For target set, remove only dups that came from a group (the user may + # want the same object with different settings) + if allow_dups: + if len(dup) == 1: + objs.remove(dup) + num -= 1 + # For other sets just reduce to one reference + else: + objs.remove(dup) + num -= 1 + # Break out when/if one dup remains + if num == 1: + break + + # Return pruned object list + return objs + + + +# Follow an input link through any reroutes +def follow_input_link(link): + if link.from_node.type == 'REROUTE': + if link.from_node.inputs[0].is_linked: + try: # During link insertion this can have weird states + return follow_input_link(link.from_node.inputs[0].links[0]) + except: + pass + return link + + + +# Gather all links from an output, going past any reroutes +def gather_output_links(output): + links = [] + for link in output.links: + if link.is_valid: + if link.to_node.type == 'REROUTE': + if link.to_node.outputs[0].is_linked: + links += gather_output_links(link.to_node.outputs[0]) + else: + links.append(link) + return links + + +# Switch mode to object/back again - Returns the mode being switched from unless no swich is needed +def switch_mode(mode='OBJECT'): + curr_mode = bpy.context.mode + # Convert mode to mode set enum (why are these different?!) + if curr_mode in ['OBJECT', 'SCULPT']: + enum_mode = curr_mode + elif curr_mode.startswith('PAINT_'): + enum_mode = curr_mode[6:] + "_PAINT" + else: + enum_mode = 'EDIT' + + if enum_mode != mode: + bpy.ops.object.mode_set(mode=mode) + return enum_mode + else: + return None + + +# +# Bake Wrangler Operators +# + +# Base class for all bakery operators, provides data to find owning node, etc. +class BakeWrangler_Operator: + # Use strings to store their names, since Node isn't a subclass of ID it can't be stored as a pointer + tree: bpy.props.StringProperty() + node: bpy.props.StringProperty() + sock: bpy.props.IntProperty(default=-1) + + @classmethod + def poll(type, context): + return True + if context.area is not None: + return True + #return context.area.type == "NODE_EDITOR" and context.space_data.tree_type == "BakeWrangler_Tree" + else: + return False + + +# Dummy operator to draw when no vertex colors are in list +class BakeWrangler_Operator_Dummy_VCol(BakeWrangler_Operator, bpy.types.Operator): + '''No vertex data currently in cache''' + bl_idname = "bake_wrangler.dummy_vcol" + bl_label = "" + + @classmethod + def poll(type, context): + # This operator is always supposed to be disabled + return False + + + +# Dummy operator to draw when a bake is in progress +class BakeWrangler_Operator_Dummy(BakeWrangler_Operator, bpy.types.Operator): + '''Bake currently in progress, either cancel the current bake or wait for it to finish''' + bl_idname = "bake_wrangler.dummy" + bl_label = "" + + @classmethod + def poll(type, context): + # This operator is always supposed to be disabled + return False + + + +# Contol filter selection by allowing modifier keys +class BakeWrangler_Operator_FilterToggle(BakeWrangler_Operator, bpy.types.Operator): + '''Ctrl-Click to deselect others, Shift-Click to select all others''' + bl_idname = "bake_wrangler.filter_toggle" + bl_label = "" + bl_options = {"REGISTER", "UNDO"} + + filters = ('filter_mesh', + 'filter_curve', + 'filter_surface', + 'filter_meta', + 'filter_font', + 'filter_light') + + filter: bpy.props.StringProperty() + + @classmethod + def description(self, context, properties): + return properties.filter.split("_")[1].title() + " filter toggle. Ctrl-Click to deselect others, Shift-Click to select all others" + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + mod_shift = event.shift + mod_ctrl = event.ctrl + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + if node and node.bl_idname == 'BakeWrangler_Input_ObjectList': + # Toggle + if not mod_ctrl and not mod_shift: + setattr(node, self.filter, not getattr(node, self.filter)) + # Enable self and disable others + elif mod_ctrl and not mod_shift: + for fltr in self.filters: + setattr(node, fltr, False) + setattr(node, self.filter, True) + # Disable self and enable others + elif not mod_ctrl and mod_shift: + for fltr in self.filters: + setattr(node, fltr, True) + setattr(node, self.filter, False) + # Invert current states + elif mod_ctrl and mod_shift: + for fltr in self.filters: + setattr(node, fltr, not getattr(node, fltr)) + return {'FINISHED'} + + + +# Double/Halve value +class BakeWrangler_Operator_DoubleVal(BakeWrangler_Operator, bpy.types.Operator): + '''Description set by function''' + bl_idname = "bake_wrangler.double_val" + bl_label = "" + bl_options = {"REGISTER", "UNDO"} + + val: bpy.props.StringProperty() + half: bpy.props.BoolProperty() + + @classmethod + def set_props(self, inst, node, tree, value, half=False): + inst.tree = tree + inst.node = node + inst.val = value + inst.half = half + + @classmethod + def description(self, context, properties): + if properties.half: return "Halve value" + else: return "Double value" + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + value = getattr(node, self.val) + if self.half: value /= 2 + else: value *= 2 + setattr(node, self.val, int(value)) + return {'FINISHED'} + + + +# Pick an enum from a menu +class BakeWrangler_Operator_PickMenuEnum(BakeWrangler_Operator, bpy.types.Operator): + '''Description set by function''' + bl_idname = "bake_wrangler.pick_menu_enum" + bl_label = "" + bl_options = {"REGISTER", "UNDO"} + + enum_id: bpy.props.StringProperty() + enum_desc: bpy.props.StringProperty() + enum_prop: bpy.props.StringProperty() + + @classmethod + def set_props(self, inst, e_id, e_desc, e_prop, node, tree): + inst.tree = tree + inst.node = node + inst.enum_prop = e_prop + inst.enum_desc = e_desc + inst.enum_id = e_id + + @classmethod + def description(self, context, properties): + return properties.enum_desc + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + setattr(node, self.enum_prop, self.enum_id) + return {'FINISHED'} + + + +# Add selected objects to an ObjectList node (ignoring duplicates unless Shift held) +class BakeWrangler_Operator_AddSelected(BakeWrangler_Operator, bpy.types.Operator): + '''Adds selected objects to the node, respecting filter and ignoring duplicates\nShift-Click: Adds items even if they are duplicates''' + bl_idname = "bake_wrangler.add_selected" + bl_label = "Add Selected" + bl_options = {"REGISTER", "UNDO"} + + mod_shift = False + + # Called either after invoke from UI or directly from script + def execute(self, context): + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + + existing_objs = [] + # Get all the objects in the current node (ignoring connected nodes and groups) + for input in node.inputs: + if not input.is_linked and input.value: + existing_objs.append(input.value) + + selected_objs = [] + # Get a list of all selected objects that also match current filter + for obj in context.selected_objects: + if node.input_filter(obj.name, obj): + selected_objs.append(obj) + + # Add non duplicate objects to the end of the node (includes duplicates if Shift) + for obj in selected_objs: + if self.mod_shift or obj not in existing_objs: + node.inputs[-1].value = obj + + return {'FINISHED'} + + # Called from button press, set modifier key states + def invoke(self, context, event): + self.mod_shift = event.shift + return self.execute(context) + + +# Read vertex color data from temp files and apply to current file +class BakeWrangler_Operator_DiscardBakedVertCols(BakeWrangler_Operator, bpy.types.Operator): + '''Discard baked vertex color data''' + bl_idname = "bake_wrangler.discard_vertcols" + bl_label = "Discard Data" + + # Read and apply vertex colors + def execute(self, context): + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + if node.bl_idname not in ['BakeWrangler_Output_Vertex_Cols', 'BakeWrangler_Output_Batch_Bake']: + return {'CANCELLED'} + files = node.vert_files + while len(node.vert_files): + jar = node.vert_files.pop() + try: + os.remove(jar) + except: + pass + + self.report({'INFO'}, "Data Removed") + return {'FINISHED'} + + # Confirm the action + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + +# Read vertex color data from temp files and apply to current file +class BakeWrangler_Operator_ApplyBakedVertCols(BakeWrangler_Operator, bpy.types.Operator): + '''Apply baked vertex color data to current blend file objects''' + bl_idname = "bake_wrangler.apply_vertcols" + bl_label = "Apply Data" + + # Read and apply vertex colors + def execute(self, context): + try: + from BakeWrangler.vert import ipc + except: + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + from vert import ipc + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + if node.bl_idname not in ['BakeWrangler_Output_Vertex_Cols', 'BakeWrangler_Output_Batch_Bake']: + return {'CANCELLED'} + files = node.vert_files + if not len(files): + return {'CANCELLED'} + + # Open each file and apply the data one at a time + oerr = 0 + for jar in files: + fd = ipc.open_pickle_jar(file=jar) + data = ipc.depickle_verts(file=fd) + err, str = ipc.import_verts(cols=data) + if err: + _print("Error applying vertex data: %s" % (str), node=node) + oerr += 1 + else: + _print("Applied %s" % (str), node=node) + if fd: fd.close() + + if oerr: + self.report({'ERROR'}, "Apply Failed") + return {'CANCELLED'} + + # Remove temp files if no errors + while len(node.vert_files): + jar = node.vert_files.pop() + try: + os.remove(jar) + except: + pass + + self.report({'INFO'}, "Data Applied") + return {'FINISHED'} + + # Confirm the action + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + +# Kill switch to stop a bake in progress +class BakeWrangler_Operator_BakeStop(BakeWrangler_Operator, bpy.types.Operator): + '''Cancel currently running bake''' + bl_idname = "bake_wrangler.bake_stop" + bl_label = "Cancel Bake" + + # Stop the currently running bake + def execute(self, context): + tree = bpy.data.node_groups[self.tree] + if tree.baking != None: + tree.baking.stop() + tree.interface_update(context) + return {'FINISHED'} + + # Ask the user if they really want to cancel bake + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + +# Operator for bake pass node +class BakeWrangler_Operator_BakePass(BakeWrangler_Operator, bpy.types.Operator): + '''Perform requested bake action(s)''' + bl_idname = "bake_wrangler.bake_pass" + bl_label = "Bake Pass" + + _timer = None + + _thread = None + _kill = False + _success = False + _finish = False + _lock = threading.Lock() + _queue = queue.SimpleQueue() + _ifileq = queue.SimpleQueue() + _vfileq = queue.SimpleQueue() + stopping = False + + open_win = None + open_ed = None + node_ed = None + + start = None + valid = None + blend_copy = None + blend_log = None + bake_proc = None + was_dirty = False + img_list = [] + vert_list = [] + shutdown = False + + # Stop this bake if it's currently running + def stop(self, kill=True): + if self._thread and self._thread.is_alive() and kill: + with self._lock: + self.stopping = self._kill = True + return self.stopping + + # Runs a blender subprocess + def thread(self, node_name, tree_name, file_name, exec_name, script_name): + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + sock = self.sock + debug = _prefs('debug') + ignorevis = _prefs('ignore_vis') + factory = "" + rend_dev_str = "" + rend_dev_val = "" + rend_use_str = "" + rend_use_val = "" + solution_itr = 0 + frames_itr = 0 + batch_itr = 0 + retry = _prefs('retrys') + 2 + if _prefs('fact_start'): + factory = "--factory-startup" + # Need to reselect gpu render some how when fact starting + rend_type = bpy.context.preferences.addons['cycles'].preferences.compute_device_type + rend_use = "" + for dev in bpy.context.preferences.addons['cycles'].preferences.devices: + if dev.use: rend_use = "%s1" % rend_use + else: rend_use = "%s0" % rend_use + rend_dev_str = "--rend_dev" + rend_dev_val = str(rend_type) + rend_use_str = "--rend_use" + rend_use_val = str(rend_use) + + _print("Launching background process:", node=node, enque=self._queue) + _print("================================================================================", enque=self._queue) + while not self._finish and retry > 0: + sub = subprocess.Popen([ + 'blender', + file_name, + "--background", + "--python", script_name, + factory, + "--", + "--tree", tree_name, + "--node", node_name, + "--sock", str(int(sock)), + "--debug", str(int(debug)), + "--ignorevis", str(int(ignorevis)), + "--solitr", str(solution_itr), + "--frameitr", str(frames_itr), + "--batchitr", str(batch_itr), + rend_dev_str, rend_dev_val, + rend_use_str, rend_use_val, + ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace") + + # Read output from subprocess and print tagged lines + out = "" + kill = False + while sub.poll() == None: + # Check for kill flag + if self._lock.acquire(blocking=False): + if self._kill: + _print("Bake canceled, terminating process...", enque=self._queue) + sub.kill() + out, err = sub.communicate() + kill = True + self._lock.release() + + if not kill: + out = sub.stdout.read(1) + # Collect tagged lines and display them in console + if out == '<': + out += sub.stdout.read(6) + if out == "": + tag_end = False + tag_line = "" + out = "" + tag_out = "" + # Read until end tag is found + while not tag_end: + tag = sub.stdout.read(1) + + if tag == '<': + tag_line = sub.stdout.read(1) + if tag_line != '_': + tag_line = tag + tag_line + sub.stdout.read(6) + if tag_line == "": + tag_end = True + out += '\n' + elif tag_line == "": + tag_end = True + tag_out += '\n' + #sys.stdout.write('\n') + #sys.stdout.flush() + elif tag_line == "": + tag_line += sub.stdout.read(8) + tag_end = True + self._success = True + self._finish = True + elif tag_line == "": + tag_line += sub.stdout.read(8) + tag_end = True + self._success = False + self._finish = True + if tag != '' and not tag_end: + tag_out += tag + #sys.stdout.write(tag_line) + #sys.stdout.flush() + out += tag + _print(tag_out, enque=self._queue, wrap=False) + if out == "" or out == "": + tag_end = False + tag_line = "" + files = "" + # Set output queue + if out == "": + que = self._ifileq + else: + que = self._vfileq + while not tag_end: + tag = sub.stdout.read(1) + if tag == '<': + tag_line = sub.stdout.read(1) + if tag_line != '_': + tag_line = tag + tag_line + sub.stdout.read(6) + if tag_line == "" or tag_line == "": + tag_end = True + _print(files, enque=que, wrap=False) + if tag != '' and not tag_end: + files += tag + out = '' + if out == "" or out == "" or out == "": + tag_end = False + tag_line = "" + num = "" + while not tag_end: + tag = sub.stdout.read(1) + if tag == '<': + tag_line = sub.stdout.read(1) + if tag_line != '_': + tag_line = tag + tag_line + sub.stdout.read(6) + if tag_line == "": + tag_end = True + frames_itr = int(num) + elif tag_line == "": + tag_end = True + solution_itr = int(num) + elif tag_line == "": + tag_end = True + batch_itr = int(num) + if tag != '' and not tag_end: + num += tag + out = '' + # Write to log + if out != '' and self.blend_log: + self.blend_log.write(out) + self.blend_log.flush() + _print("================================================================================", enque=self._queue) + _print("Background process ended", node=node, enque=self._queue) + retry -= 1 + if not self._finish and retry > 0: + _print("Process did not complete, retry from last known success (%s tries remain)" % (retry - 1), node=node, enque=self._queue) + + # Event handler + def modal(self, context, event): + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + + # Check if the bake thread has ended every timer event + if event.type == 'TIMER': + self.print_queue(context) + # Reapply dirt by pushing something to undo stack (not ideal) + if self.was_dirty and not bpy.data.is_dirty and bpy.ops.node.select_all.poll(): + bpy.ops.node.select_all(action='INVERT') + bpy.ops.node.select_all(True, action='INVERT') + self.was_dirty = False + if not self._thread.is_alive(): + self.cancel(context) + if self._kill: + _print("Bake canceled after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue) + _print("Canceled\n", node=node, enque=self._queue) + self.report({'WARNING'}, "Bake Canceled") + self.update_images() + self.print_queue(context) + if self.blend_log: + context.window_manager.bw_lastlog = self.blend_copy + ".log" + context.window_manager.bw_lastfile = self.blend_copy + return {'CANCELLED'} + else: + if self._success and self._finish: + _print("Bake finished in %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue) + _print("Success\n", node=node, enque=self._queue) + self.report({'INFO'}, "Bake Completed") + context.window_manager.bw_status = 0 # Bake finished / idle status + elif self._finish: + _print("Bake finished with errors after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue) + _print("Errors\n", node=node, enque=self._queue) + self.report({'WARNING'}, "Bake Finished with Errors") + context.window_manager.bw_status = 2 # Bake error status + else: + _print("Bake failed after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue) + _print("Failed\n", node=node, enque=self._queue) + self.report({'ERROR'}, "Bake Failed") + context.window_manager.bw_status = 2 # Bake error status + self.print_queue(context) + if self.blend_log: + context.window_manager.bw_lastlog = self.blend_copy + ".log" + context.window_manager.bw_lastfile = self.blend_copy + # Update images + self.dequeue_files(context, self._ifileq, self.img_list) + if _prefs('debug'): _print("Img list: %s" % self.img_list) + self.update_images() + # Send vertex file names to node + if hasattr(node, 'vert_files'): + self.dequeue_files(context, self._vfileq, self.vert_list) + if _prefs('debug'): _print("Vert list: %s" % self.vert_list) + for vfile in self.vert_list: + node.vert_files.append(vfile) + # Check if a post-bake user script should be run + if self._finish and node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.loc_post != 'NON': + _print("Running user created post-bake script: ", node=node, wrap=False) + if node.loc_post == 'INT': + post_scr = node.script_post_int.as_string() + elif node.loc_post == 'EXT': + with open(node.script_post_can, "r") as scr: + post_scr = scr.read() + try: + exec(post_scr, {'BW_TARGETS': node.get_unique_objects('TARGET'), 'BW_SOURCES': node.get_unique_objects('SOURCE')}) + except Exception as err: + _print(" Failed - %s" % (str(err))) + else: + _print(" Done") + if _prefs("wind_msgs") and self.open_win: + if _prefs("wind_close"): + bpy.ops.wm.window_close({"window": self.open_win}) + # Do batch shutdown + if self.shutdown: + if sys.platform == 'win32': + os.system('shutdown /s /t 60') + else: + os.system('sudo shutdown +1') + if _prefs("show_icon"): update_status_bar() + return {'FINISHED'} + return {'PASS_THROUGH'} + + # Get queued file list + def dequeue_files(self, context, queue, list): + fstr = "" + try: + # An Empty exception will be raised when nothing is in the queue + while True: + fstr += queue.get_nowait() + except: + list += fstr.split(",") + return + + # Print queued messages + def print_queue(self, context): + try: + # An Empty exception will be raised when nothing is in the queue + while True: + msg = self._queue.get_nowait() + _print(msg, wrap=False) + except: + return + + # Display log file if debugging is enabled and the bake failed or had errors + def show_log(self): + if _prefs('debug') and self.blend_log and self.node_ed: + bpy.ops.screen.area_dupli({'area': self.node_ed}, 'INVOKE_DEFAULT') + open_ed = bpy.context.window_manager.windows[len(bpy.context.window_manager.windows) - 1].screen.areas[0] + open_ed.type = 'TEXT_EDITOR' + log = bpy.data.texts.load(self.blend_copy + ".log") + open_ed.spaces[0].text = log + open_ed.spaces[0].show_line_numbers = False + open_ed.spaces[0].show_syntax_highlight = False + + # Update any loaded images that might be changed by the bake + def update_images(self): + if len(self.img_list): + cwd = os.path.dirname(bpy.data.filepath) + open_imgs = {} + for img in bpy.data.images: + open_imgs[os.path.normpath(os.path.join(cwd, bpy.path.abspath(img.filepath_raw)))] = img + for img in self.img_list: + if img in open_imgs.keys(): + open_imgs[img].reload() + elif _prefs("auto_open"): + try: + bpy.data.images.load(img) + except: + pass + + # Called after invoke to perform the bake if everything passed validation + def execute(self, context): + # If called from script, do prepare now + if self.valid == None: + if self.tree and self.node: + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + self.prepare(context, tree, node) + else: + self.valid = [False] + self.valid.append([_print("Bake Tree/Node missing", ret=True), ": Bake Tree or Node was not set for operator"]) + self.report({'ERROR'}, "Operator required arguments missing") + return {'CANCELLED'} + # Do any interactive actions if called from invoke + else: + # If message log in new window is enabled + if _prefs("text_msgs") and _prefs("wind_msgs"): + bpy.ops.screen.area_dupli('INVOKE_DEFAULT') + self.open_win = context.window_manager.windows[len(context.window_manager.windows) - 1] + self.open_ed = self.open_win.screen.areas[0] + self.open_ed.type = 'TEXT_EDITOR' + self.open_ed.spaces[0].text = bpy.data.texts["BakeWrangler"] + self.open_ed.spaces[0].show_line_numbers = False + self.open_ed.spaces[0].show_syntax_highlight = False + + if not self.valid[0]: + self.cancel(context) + self.report({'ERROR'}, "Validation failed") + return {'CANCELLED'} + + self.start = datetime.now() + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + + # Check for batch shutdown + if node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.shutdown_after: + self.shutdown = True + + # Save a temporary copy of the blend file and store the path. Make sure the path doesn't exist first. + # All baking will be done using this copy so the user can continue working in this session. + blend_name = bpy.path.clean_name(bpy.path.display_name_from_filepath(bpy.data.filepath)) + blend_temp = bpy.path.abspath(bpy.app.tempdir) + node_cname = bpy.path.clean_name(node.get_name()) + blend_copy = os.path.join(blend_temp, "BW_" + blend_name) + + # Increment file name until it doesn't exist + if os.path.exists(blend_copy + ".blend"): + fno = 1 + while os.path.exists(blend_copy + "_%03i.blend" % (fno)): + fno = fno + 1 + blend_copy = blend_copy + "_%03i.blend" % (fno) + else: + blend_copy = blend_copy + ".blend" + + # Check if a pre-bake user script should be run + if node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.loc_pre != 'NON': + _print("Running user created pre-bake script: ", node=node, wrap=False) + if node.loc_pre == 'INT': + pre_scr = node.script_pre_int.as_string() + elif node.loc_pre == 'EXT': + with open(node.script_pre_can, "r") as scr: + pre_scr = scr.read() + try: + exec(pre_scr, {'BW_TARGETS': node.get_unique_objects('TARGET'), 'BW_SOURCES': node.get_unique_objects('SOURCE')}) + except Exception as err: + _print(" Failed - %s" % (str(err))) + return {'CANCELLED'} + else: + _print(" Done") + + # Print out start message and temp path + _print("") + _print("=== Bake starts ===", node=node) + _print("Creating temporary files in %s" % (blend_temp), node=node) + + # Maintain dirt + if bpy.data.is_dirty: + self.was_dirty = True + + # Save dirty images based on preferences as unsaved changes will not effect bake + if _prefs("save_packed") or _prefs("save_images"): + for img in bpy.data.images: + if img.is_dirty: + if (img.packed_file is not None and _prefs("save_packed")) or (img.packed_file is None and _prefs("save_images")): + bpy.ops.image.save({'edit_image': img}) + + try: + bpy.ops.wm.save_as_mainfile(filepath=blend_copy, copy=True) + except RuntimeError as err: + _print("Temporary file creation failed: %s" % (str(err)), node=node) + self.report({'ERROR'}, "Blend file copy failed") + return {'CANCELLED'} + else: + # Check copy exists + if not os.path.exists(blend_copy): + _print("Temporary file creation failed", node=node) + self.report({'ERROR'}, "Blend file copy failed") + return {'CANCELLED'} + else: + self.blend_copy = blend_copy + + # Open a log file at the same location with a .log appended to the name + log_err = None + blend_log = None + try: + blend_log = open(blend_copy + ".log", "w", encoding="utf-8", errors="replace") + except OSError as err: + self.report({'WARNING'}, "Couldn't create log file") + log_err = err.strerror + else: + self.blend_log = blend_log + tree.last_log = blend_copy + ".log" + + # Print out blend copy and log names + _print(" - %s" % (os.path.basename(self.blend_copy)), node=node) + if self.blend_log and not log_err: + _print(" - %s" % (os.path.basename(self.blend_copy + ".log")), node=node) + else: + _print(" - Log file creation failed: %s" % (log_err), node=node) + _print("Blender: %s Addon: %s" % (bpy.app.version_string, BW_VERSION_STRING), node=node) + + # Create a thread which will launch a background instance of blender running a script that does all the work. + # Process is complete when thread exits. Will need full path to blender, node, temp file and proc script. + blend_exec = bpy.path.abspath(bpy.app.binary_path) + self._thread = threading.Thread(target=self.thread, args=(self.node, self.tree, self.blend_copy, blend_exec, self.bake_proc,)) + + # Get a list of image file names that will be updated by the bake so they can be reloaded on success + self.img_list = [] + '''if node.bl_idname == 'BakeWrangler_Output_Batch_Bake': + for input in node.inputs: + outnode = get_input(input) + if outnode and outnode.bl_idname == 'BakeWrangler_Output_Image_Path': + files = outnode.get_output_files() + for name in files.keys(): + img_name = os.path.join(outnode.img_path, name) + if not self.img_list.count(img_name): + self.img_list.append(img_name) + elif node.bl_idname == 'BakeWrangler_Output_Image_Path': + files = node.get_output_files() + for name in files.keys(): + img_name = os.path.join(node.img_path, name) + if not self.img_list.count(img_name): + self.img_list.append(img_name)''' + + # Init vert file list + self.vert_list = [] + bpy.ops.bake_wrangler.discard_vertcols(node=self.node, tree=self.tree) + + # Add a timer to periodically check if the bake has finished + wm = context.window_manager + self._timer = wm.event_timer_add(0.5, window=context.window) + wm.modal_handler_add(self) + + self._thread.start() + + # Go modal + context.window_manager.bw_status = 1 # Baking status + if _prefs("show_icon"): update_status_bar() + return {'RUNNING_MODAL'} + + # Called by UI when the button is clicked. Will validate settings and prepare files for execute + def invoke(self, context, event): + # Prep for bake + self.node_ed = context.area + if self.tree and self.node: + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + self.prepare(context, tree, node) + else: + self.valid = [False] + self.valid.append([_print("Bake Tree/Node missing", ret=True), ": Bake Tree or Node was not set for operator"]) + self.report({'ERROR'}, "Operator properties not set") + return {'CANCELLED'} + + if not self.valid[0] or len(self.valid) > 1: + # Draw pop-up that will use custom draw function to display any validation errors + return context.window_manager.invoke_props_dialog(self, width=400) + else: + return self.execute(context) + + # Prepare for bake, do all validation tasks and set properties + def prepare(self, context, tree, node): + # Init variables + self.valid = [False] + + # Are text editor messages enabled? + if _prefs("text_msgs"): + # Make sure the text block exists + if not "BakeWrangler" in bpy.data.texts.keys(): + bpy.data.texts.new("BakeWrangler") + # Clear the block if option set + if _prefs("clear_msgs"): + bpy.data.texts["BakeWrangler"].clear() + + # Do full validation of bake so it can be reported + tree.baking = self + tree.interface_update(context) + self.valid = node.validate(is_primary=True) + + # Remove UV errors if node is a vertex color output, kinda dumb, but... Least changes required this way + if node.bl_idname == 'BakeWrangler_Output_Vertex_Cols' and not self.valid[0]: + idx = 0 + for msg in self.valid: + if idx == 0: + idx += 1 + continue + if msg[0].endswith("UV error"): + self.valid.pop(idx) + idx += 1 + if len(self.valid) == 1: + self.valid[0] = True + + # Check tree is of the current version + if tree.tree_version != BW_TREE_VERSION: + self.valid[0] = False + if tree.tree_version < BW_TREE_VERSION: + self.valid.append([_print("Bake Recipe for older version of Bake Wrangler", node=node, ret=True), ": Recipe is version %s, but version %s is requied. Please use the auto-update function if available, or create a new recipe" % (tree.tree_version, BW_TREE_VERSION)]) + else: + self.valid.append([_print("Bake Recipe for newer version of Bake Wrangler", node=node, ret=True), ": Recipe is version %s, but version %s is requied. You need to update Bake Wrangler to a version that supports this recipe, or create a new recipe" % (tree.tree_version, BW_TREE_VERSION)]) + # Check processing script exists + bake_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + bake_proc = bpy.path.abspath(os.path.join(bake_path, "baker.py")) + if not os.path.exists(bake_proc): + self.valid[0] = False + self.valid.append([_print("File missing", node=node, ret=True), ": Bake processing script wasn't found at '%s'" % (bake_proc)]) + else: + self.bake_proc = bake_proc + + # Check baking scene file exists + scene_file = bpy.path.abspath(os.path.join(bake_path, "resources", "BakeWrangler_Scene.blend")) + if not os.path.exists(scene_file): + self.valid[0] = False + self.valid.append([_print("File missing", node=node, ret=True), ": Bake scene library wasn't found at '%s'" % (scene_file)]) + self.valid = prune_messages(self.valid) + + # Cancel the bake + def cancel(self, context): + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + if self._timer: + wm = context.window_manager + wm.event_timer_remove(self._timer) + if self.blend_log: + self.blend_log.close() + if tree.baking != None: + tree.baking = None + tree.interface_update(context) + if self.blend_copy and os.path.exists(self.blend_copy): + if not _prefs("debug"): + try: + os.remove(self.blend_copy) + except OSError as err: + _print("Temporary file removal failed: %s\n" % (err.strerror), node=node, enque=self._queue) + + # Draw custom pop-up + def draw(self, context): + tree = bpy.data.node_groups[self.tree] + node = tree.nodes[self.node] + layout = self.layout.column(align=True) + if not self.valid[0]: + layout.label(text="!!! Validation FAILED:") + _print("") + _print("!!! Validation FAILED:", node=node) + col = layout.column() + for i in range(len(self.valid) - 1): + col.label(text=self.valid[i + 1][0]) + _print(self.valid[i + 1][0] + self.valid[i + 1][1]) + layout.label(text="See console for details") + _print("") + elif len(self.valid) > 1: + layout.label(text="") + layout.label(text="!!! Material Warnings:") + _print("") + _print("!!! Material Warnings:") + col = layout.column() + for i in range(len(self.valid) - 1): + col.label(text=self.valid[i + 1][0]) + _print(self.valid[i + 1][0] + self.valid[i + 1][1]) + layout.label(text="See console for details") + _print("") + + + +# +# Bake Wrangler nodes system +# + + +# Node tree definition that shows up in the editor type list. Sets the name, icon and description. +class BakeWrangler_Tree(NodeTree): + '''Improved baking system to extend and replace existing internal bake system''' + bl_label = 'Bake Recipe' + bl_icon = 'NODETREE' + + def __init__(self): + pass + + # Get pinned mesh settings if exists + def get_pinned_settings(self, setting): + for node in self.nodes: + if node.bl_idname == 'BakeWrangler_' + setting and node.pinned: + return node + return None + + # Get the active global resolution node in a tree + def get_active_res(self): + for node in self.nodes: + if node.bl_idname == 'BakeWrangler_Global_Resolution' and node.is_active: + return node + return None + + # Does this need a lock for modal event access? + baking = None + + # Do some initial set up when a new tree is created + initialised: bpy.props.BoolProperty(name="Initialized", default=False) + tree_version: bpy.props.IntProperty(name="Tree Version", default=0) + last_log: bpy.props.StringProperty(name="Last Log", default="") + + + +# Custom Sockets: + +# Base class for all bakery sockets +class BakeWrangler_Tree_Socket: + # Workaround for link.is_valid being un-usable + valid: bpy.props.BoolProperty() + + def socket_label(self, text): + if self.is_output or (self.is_linked and self.valid) or (not self.is_output and not self.is_linked): + return text + else: + return text + " [invalid]" + + def socket_color(self, color): + if not self.is_output and self.is_linked and not self.valid: + return (1.0, 0.0, 0.0, 1.0) + else: + return color + + # Returns a list of valid bl_idnames that can connect + def valid_inputs(self): + return [self.bl_idname] + + # Follows through reroutes + def islinked(self): + if self.is_linked and not self.is_output: + try: # During link removal this can be in a weird state + node = self.links[0].from_node + while node.type == "REROUTE": + if node.inputs[0].is_linked and node.inputs[0].links[0].is_valid: + node = node.inputs[0].links[0].from_node + else: + return False + return True + except: + pass + return False + + + +# Socket for an object or list of objects to be used in a bake pass in some way +class BakeWrangler_Socket_Object(BakeWrangler_Tree_Socket, NodeSocket): + '''Socket for baking relevant objects''' + bl_label = 'Object' + + object_types = ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT', 'LIGHT'] + + # Called to filter objects listed in the value search field + def value_prop_filter(self, object): + return self.node.input_filter(self.name, object) + + def cage_prop_filter(self, cage): + return cage.type == 'MESH' + + # Try to auto locate the cage by name when enabled + def use_cage_update(self, context): + if self.use_cage and not self.cage and self.value: + for obj in bpy.data.objects: + if obj.name.startswith(self.value.name) and obj.name.lower().startswith("cage", len(self.value.name) + 1): + self.cage = obj + break + + # Called when the value property changes + def value_prop_update(self, context): + self.type = 'NONE' + if self.value: + if self.value.rna_type.identifier == 'Collection': + self.type = 'GROUP' + elif self.value.rna_type.identifier == 'Object': + if self.value.type in self.object_types: + self.type = '%s_DATA' % (self.value.type) + if self.node: + self.node.update_inputs() + + # Get own objects or the full linked tree + def get_objects(self, only_mesh=False, no_lights=False, only_groups=False): + objects = [] + # Follow links + if self.islinked() and self.valid: + return follow_input_link(self.links[0]).from_node.get_objects(only_mesh, no_lights, only_groups) + # Otherwise return self values + if self.value and self.type and self.type != 'NONE' and not self.is_linked: + # Only interested in mesh types? + if self.type not in ['MESH_DATA', 'GROUP'] and only_mesh: + return [] + if self.type == 'LIGHT_DATA' and no_lights: + return [] + if only_groups and self.type != 'GROUP': + return [] + # Need to get all the grouped objects + if self.type == 'GROUP': + filter = list(self.object_types) + if no_lights: + filter.remove('LIGHT') + if only_mesh: + filter = ['MESH'] + # Iterate over the objects applying the type filter + for obj in self.get_grouped(): + if obj.type in filter: + objects.append([obj]) + if only_groups: + return [[self.value, objects]] + # Mesh data can have a few extra properties + elif self.type == 'MESH_DATA': + uv_map = "" + if self.pick_uv and self.uv_map: + uv_map = self.uv_map + cage = None + if self.use_cage and self.cage: + cage = self.cage + objects.append([self.value, uv_map, cage]) + else: + objects.append([self.value]) + return objects + + # Return objects contained in a group + def get_grouped(self): + if self.recursive: + return self.value.all_objects + else: + return self.value.objects + + # Validate value(s) + def validate(self, check_materials=False, check_as_active=False, check_multi=False): + valid = [True] + # Follow links + if self.islinked() and self.valid: + return follow_input_link(self.links[0]).from_node.validate(check_materials, check_as_active, check_multi) + # Has a value and isn't linked + if self.value and self.type and not self.islinked(): + objs = [self.value] + if self.type == 'GROUP': + objs = self.get_grouped() + + # Iterate over objs, it will just be one object unless the type is group (but maintains a single algo for both) + for obj in objs: + # Perform checks needed for an active bake target + if check_as_active: + # Only a mesh type object can be a valid target, it will just be silently ignored + if obj.type != 'MESH': + return valid + # Any UV map? + if len(obj.data.uv_layers) < 1: + valid[0] = False + valid.append([_print("UV error", node=self.node, ret=True), ": No UV Maps found on object <%s>." % (obj.name)]) + # Custom UV map still exists? (can't be done for grouped objects) + if self.type != 'GROUP' and self.pick_uv and self.uv_map not in obj.data.uv_layers and self.uv_map != "": + valid[0] = False + valid.append([_print("UV error", node=self.node, ret=True), ": Selected UV map <%s> not present on object <%s> (it could have been deleted or renamed)" % (self.uv_map, obj.name)]) + # Check for a valid multi-res mod if enabled + if check_multi: + has_multi_mod = False + if len(obj.modifiers): + for mod in obj.modifiers: + if mod.type == 'MULTIRES' and mod.total_levels > 0: + has_multi_mod = True + break + if not has_multi_mod: + valid[0] = False + valid.append([_print("Multires error", node=self.node, ret=True), ": No multires data on object <%s>." % (obj.name)]) + # Check that materials can be converted to enable PBR data bakes + if check_materials and obj.type in self.object_types: + mats = [] + if len(obj.material_slots): + for slot in obj.material_slots: + mat = slot.material + if mat != None and not mat in mats: + mats.append(mat) + # Is node based? + if not mat.node_tree or not mat.node_tree.nodes: + valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> not a node based material" % (mat.name)]) + continue + # Is a 'principled' material? + passed = False + for node in mat.node_tree.nodes: + if node.type == 'OUTPUT_MATERIAL' and node.target in ['CYCLES', 'ALL']: + if material_recursor(node): + passed = True + break + if not passed: + valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> Output doesn't appear to be a valid combination of Principled and Mix/Add shaders. Baked values may not be correct for this material." % (mat.name)]) + return valid + + # Blender Properties + value: bpy.props.PointerProperty(name="Object", description="Object to be used in some way in a bake pass", type=bpy.types.ID, poll=value_prop_filter, update=value_prop_update) + type: bpy.props.StringProperty(name="Type", description="ID String of value type", default="NONE") + recursive: bpy.props.BoolProperty(name="Recursive Selection", description="When enabled all collections within the selected collection will be used", default=False) + pick_uv: bpy.props.BoolProperty(name="Pick UV Map", description="Enables selecting which UV map to use instead of the active one", default=False) + uv_map: bpy.props.StringProperty(name="UV Map", description="UV Map to use instead of active if value is a mesh", default="") + use_cage: bpy.props.BoolProperty(name="Use Cage", description="Enables cage usage and selection of cage mesh", default=False, update=use_cage_update) + cage: bpy.props.PointerProperty(name="Cage", description="Mesh to use a cage", type=bpy.types.Object, poll=cage_prop_filter) + + def draw(self, context, layout, node, text): + if not self.is_output and not self.islinked() and not self.node.bl_idname == 'BakeWrangler_Bake_Material': + row = layout.row(align=True) + label = "" + if self.name in ['Target', 'Source', 'Scene']: + split_fac = 44 / self.node.width + split = row.split(factor=split_fac) + rowl = split.row(align=True) + rowl.label(text=self.name) + row = split.row(align=True) + if self.name in ['Target', 'Source'] or (hasattr(node, "filter_collection") and not node.filter_collection): + row.prop_search(self, "value", bpy.data, "objects", text=label, icon=self.type) + else: + row.prop_search(self, "value", bpy.data, "collections", text=label, icon=self.type) + if self.value and self.type: + if self.type == 'GROUP': + row.prop(self, "recursive", icon='OUTLINER', text="") + if self.type == 'MESH_DATA': + row.prop(self, "pick_uv", icon='UV', text="") + if self.pick_uv: + row.prop_search(self, "uv_map", self.value.data, "uv_layers", text="", icon='UV_DATA') + row.prop(self, "use_cage", icon='FILE_VOLUME', text="") + if self.use_cage: + row.prop_search(self, "cage", bpy.data, "objects", text="", icon='MESH_DATA') + elif self.is_output: + row = layout.row(align=False) + row0 = row.row() + row0.ui_units_x = 50 + op = row0.operator("bake_wrangler.add_selected") + op.tree = self.node.id_data.name + op.node = self.node.name + row2 = row.row(align=False) + row2.alignment = 'RIGHT' + row2.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + else: + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.2, 1.0, 1.0)) + + +# Socket for materials baking +class BakeWrangler_Socket_Material(BakeWrangler_Tree_Socket, NodeSocket): + '''Socket for specifying a material to bake''' + bl_label = 'Material' + + # Called when the value property changes + def value_prop_update(self, context): + if self.node: + self.node.update_inputs() + + value: bpy.props.PointerProperty(name="Material", description="Material to be used in a bake pass", type=bpy.types.Material, update=value_prop_update) + + def draw(self, context, layout, node, text): + if not self.is_output: + row = layout.row(align=True) + split_fac = 52 / self.node.width + split = row.split(factor=split_fac) + rowl = split.row(align=True) + rowl.label(text=self.name) + rowr = split.row(align=True) + rowr.prop_search(self, "value", bpy.data, "materials", icon='MATERIAL_DATA', text="") + else: + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + if self.is_output: + return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0)) + return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.0, 0.0, 0.0)) + + +# Socket for sharing a target mesh +class BakeWrangler_Socket_Mesh(BakeWrangler_Tree_Socket, NodeSocket): + '''Socket for connecting a mesh node''' + bl_label = 'Mesh' + + # Returns a list of valid bl_idnames that can connect + def valid_inputs(self): + return [self.bl_idname, 'BakeWrangler_Socket_Material'] + + def draw(self, context, layout, node, text): + if node.bl_idname == 'BakeWrangler_Bake_Pass': + if self.islinked() and self.valid: + if get_input(self).bl_idname == 'BakeWrangler_Bake_Material': + label = "Material" + else: + label = "Mesh" + else: + label = "Mesh / Material" + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, label)) + else: + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0)) + + +# Socket for RGB(A) data, extends the base color node +class BakeWrangler_Socket_Color(NodeSocketColor, BakeWrangler_Tree_Socket): + '''Socket for RGB(A) data''' + bl_label = 'Color' + + # Returns a list of valid bl_idnames that can connect + def valid_inputs(self): + return [self.bl_idname, 'BakeWrangler_Socket_Float'] + + #Props + suffix: bpy.props.StringProperty(name="Suffix", description="Suffix appended to filename for this output") + value_rgb: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.5,0.5,0.5,1.0], size=4) + + def draw(self, context, layout, node, text): + if self.is_linked and self.valid and node.bl_idname in ['BakeWrangler_Output_Image_Path', 'BakeWrangler_Output_Vertex_Cols']: + row = layout.row(align=True) + sfac = 40 / node.width + split = row.split(factor=sfac) + if node.bl_idname == 'BakeWrangler_Output_Image_Path': + split.label(text="Suffix") + else: + split.label(text="Name") + srow = split.row(align=True) + srow.prop(self, "suffix", text="") + idx = 0 + for input in node.inputs: + if input == self: + break + idx += 1 + BakeWrangler_Tree_Node.draw_bake_button(node, srow, 'IMAGE', "", True, idx) + elif node.bl_idname == 'BakeWrangler_Post_MixRGB' and not self.is_linked and not self.is_output: + layout.prop(self, "value_rgb", text=self.name) + else: + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.78, 0.78, 0.16, 1.0)) + + +# Socket to map input value to output channel. Works like a separator/combiner node pair +class BakeWrangler_Socket_ChanMap(NodeSocketColor, BakeWrangler_Tree_Socket): + '''Socket for splitting and joining color channels''' + bl_label = 'Channel Map' + + # Returns a list of valid bl_idnames that can connect + def valid_inputs(self): + return ['BakeWrangler_Socket_Color'] + + channels = ( + ('Red', "Red", "Red"), + ('Green', "Green", "Green"), + ('Blue', "Blue", "Blue"), + ('Value', "Value", "Value"), + ) + + # Props + input_channel: bpy.props.EnumProperty(name="Input Channel", description="Channel of input color data to take values from", items=channels, default='Value') + + def draw(self, context, layout, node, text): + row = layout.row(align=True) + label = row.row() + if not self.is_output and self.is_linked and self.valid: + chan = row.row() + chan.prop(self, "input_channel", text="From") + label.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + if self.name == 'Alpha': + return BakeWrangler_Tree_Socket.socket_color(self, (0.631, 0.631, 0.631, 1.0)) + else: + return BakeWrangler_Tree_Socket.socket_color(self, (0.78, 0.78, 0.16, 1.0)) + + +# Socket for Float data, extends the base float node +class BakeWrangler_Socket_Float(NodeSocketFloat, BakeWrangler_Tree_Socket): + '''Socket for Float data''' + bl_label = 'Float' + + # Returns a list of valid bl_idnames that can connect + def valid_inputs(self): + return [self.bl_idname, 'BakeWrangler_Socket_Color'] + + value_fac: bpy.props.FloatProperty(name="Fac", default=0.5, subtype='FACTOR', min=0.0, max=1.0, precision=3, step=10) + value_col: bpy.props.FloatProperty(name="Fac", soft_min=0.0, soft_max=1.0, precision=3, step=10) + value_gam: bpy.props.FloatProperty(name="Fac", default=1.0, min=0.0, soft_min=0.001, soft_max=10.0, precision=3, step=1) + value: bpy.props.FloatProperty(name="Value", precision=3, step=10) + + def draw(self, context, layout, node, text): + if node.bl_idname == 'BakeWrangler_Post_MixRGB' and not self.islinked() and not self.is_output: + layout.prop(self, "value_fac") + elif node.bl_idname == 'BakeWrangler_Post_JoinRGB' and not self.islinked() and not self.is_output: + layout.prop(self, "value_col", text=self.name) + elif node.bl_idname == 'BakeWrangler_Post_Math' and not self.islinked() and not self.is_output: + if node.op == 'POWER': + if self.identifier == '0': + layout.prop(self, "value", text="Base") + elif self.identifier == '1': + layout.prop(self, "value", text="Exponent") + elif node.op == 'LOGARITHM' and self.identifier == '1': + layout.prop(self, "value", text="Base") + else: + layout.prop(self, "value", text=self.name) + elif node.bl_idname == 'BakeWrangler_Post_Gamma' and not self.islinked() and not self.is_output: + layout.prop(self, "value_gam", text=self.name) + else: + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.631, 0.631, 0.631, 1.0)) + + +# Socket for connecting an output image to a batch job node +class BakeWrangler_Socket_Bake(BakeWrangler_Tree_Socket, NodeSocket): + '''Socket for connecting an output image node to a batch node''' + bl_label = 'Bake' + + def draw(self, context, layout, node, text): + row = layout.row(align=True) + if self.is_output: + row0 = row.row() + row0.ui_units_x = 50 + row1 = row.row(align=False) + row1.alignment = 'RIGHT' + if self.node.bl_idname == 'BakeWrangler_Output_Image_Path': + BakeWrangler_Tree_Node.draw_bake_button(self.node, row0, 'IMAGE', "Bake Image") + elif self.node.bl_idname == 'BakeWrangler_Output_Vertex_Cols': + BakeWrangler_Tree_Node.draw_bake_button(self.node, row0, 'IMAGE', "Bake Colors") + row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + else: + row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (1.0, 0.5, 1.0, 1.0)) + + +# Socket for connecting a mesh settings node to a mesh +class BakeWrangler_Socket_MeshSetting(BakeWrangler_Tree_Socket, NodeSocket): + '''Socket for connecting a mesh settings node to a mesh node''' + bl_label = 'Mesh Settings' + + def draw(self, context, layout, node, text): + row = layout.row(align=False) + if self.is_output: + row0 = row.row() + row1 = row.row(align=False) + row1.alignment = 'RIGHT' + row1.ui_units_x = 100 + if self.node.pinned: + row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="") + else: + row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="") + row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + else: + row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (1.0, 0.3, 0.0, 1.0)) + + +# Socket for connecting a pass settings node to a pass +class BakeWrangler_Socket_PassSetting(BakeWrangler_Tree_Socket, NodeSocket): + '''Socket for connecting a pass settings node to a pass node''' + bl_label = 'Pass Settings' + + # Returns a list of valid bl_idnames that can connect + def valid_inputs(self): + return [self.bl_idname, 'BakeWrangler_Socket_SampleSetting'] + + def draw(self, context, layout, node, text): + row = layout.row(align=False) + if self.is_output: + row0 = row.row() + row1 = row.row(align=False) + row1.alignment = 'RIGHT' + row1.ui_units_x = 100 + if self.node.pinned: + row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="") + else: + row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="") + row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + elif not self.is_linked: + split = row.split(factor=0.35) + split.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + split.prop(self.node, "bake_samples", text="Samples") + else: + row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.37, 0.08, 1.0, 1.0)) + + +# Socket for connecting a pass settings node to a pass +class BakeWrangler_Socket_SampleSetting(BakeWrangler_Tree_Socket, NodeSocket): + '''Socket for connecting a pass settings node to a pass node''' + bl_label = 'Sample Settings' + + def draw(self, context, layout, node, text): + row = layout.row(align=False) + if self.is_output: + row0 = row.row() + row1 = row.row(align=False) + row1.alignment = 'RIGHT' + row1.ui_units_x = 100 + if self.node.pinned: + row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="") + else: + row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="") + row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + else: + row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.4, 0.08, 1.0, 1.0)) + + +# Socket for connecting an output settings node to an output +class BakeWrangler_Socket_OutputSetting(BakeWrangler_Tree_Socket, NodeSocket): + '''Socket for connecting an output settings node to an output node''' + bl_label = 'Output Settings' + + def draw(self, context, layout, node, text): + row = layout.row(align=False) + if self.is_output: + row0 = row.row() + row1 = row.row(align=False) + row1.alignment = 'RIGHT' + row1.ui_units_x = 100 + if self.node.pinned: + row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="") + else: + row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="") + row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + else: + row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.14, 1.0, 1.0, 1.0)) + + +# Socket that takes the names of all objects input to use for filename outputs +class BakeWrangler_Socket_ObjectNames(BakeWrangler_Tree_Socket, NodeSocket): + '''Take the names of input objects''' + bl_label = 'Object Names' + + # Returns a list of valid bl_idnames that can connect + def valid_inputs(self): + return ['BakeWrangler_Socket_Mesh', 'BakeWrangler_Socket_Object', 'BakeWrangler_Socket_Material'] + + def draw(self, context, layout, node, text): + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0)) + + +# Socket to take objects to use for splitting output into separate files +class BakeWrangler_Socket_SplitOutput(BakeWrangler_Tree_Socket, NodeSocket): + '''Split output into files based on input''' + bl_label = 'Split Output' + + # Returns a list of valid bl_idnames that can connect + def valid_inputs(self): + if not self.name == "Path/Filename": + return [self.bl_idname, 'BakeWrangler_Socket_Mesh', 'BakeWrangler_Socket_Object', 'BakeWrangler_Socket_Material'] + return [None] + + # Get what ever the split list input node is, if there is one + def get_split(self): + linked = get_input(self) + if linked: + if linked.bl_idname == 'BakeWrangler_Input_Filenames': + return linked.get_names() + return [linked] + return None + + # Get full path, removing any relative references + def get_full_path(self): + linked = get_input(self) + if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames': + return linked.inputs["Path/Filename"].get_full_path() + cwd = os.path.dirname(bpy.data.filepath) + self.img_path = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.disp_path))) + return self.img_path + + # Deal with any path components that may be in the filename and remove recognised extensions + def update_filename(self, context): + if self.img_name == "": + return + fullpath = os.path.normpath(bpy.path.abspath(self.img_name)) + path, name = os.path.split(fullpath) + if path: + self.disp_path = self.img_name[:-len(name)] + if name: + # Remove file extension if recognised + nname, ext = os.path.splitext(name) + if ext not in [".", "", None]: + for enum, iext in self.img_ext: + if ext.lower() == iext: + name = nname + break + if self.img_name != name: + self.img_name = name + + # Return the file name with the correct image type extension (unless it has an existing unknown extension) + def name_with_ext(self, suffix="", type=""): + linked = get_input(self) + if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames': + return linked.inputs["Path/Filename"].name_with_ext(suffix, type) + for enum, iext in self.img_ext: + if type == enum: + return (self.img_name + suffix + iext) + + def frame_range(self, padding=False, animated=False): + linked = get_input(self) + if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames': + return linked.get_frames(padding, animated) + if padding: return None + elif animated: return False + else: return [] + + def get_path(self): + linked = get_input(self) + if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames': + return linked.inputs["Path/Filename"].get_path() + return self.disp_path + + def get_name(self): + linked = get_input(self) + if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames': + return linked.inputs["Path/Filename"].get_name() + return self.img_name + + # Properties + disp_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH') + img_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH') + img_name: bpy.props.StringProperty(name="Output File", description="File prefix to save image as", default="Image", subtype='FILE_PATH', update=update_filename) + + img_ext = ( + ('BMP', ".bmp"), + ('IRIS', ".rgb"), + ('PNG', ".png"), + ('JPEG', ".jpg"), + ('JPEG2000', ".jp2"), + ('TARGA', ".tga"), + ('TARGA_RAW', ".tga"), + ('CINEON', ".cin"), + ('DPX', ".dpx"), + ('OPEN_EXR_MULTILAYER', ".exr"), + ('OPEN_EXR', ".exr"), + ('HDR', ".hdr"), + ('TIFF', ".tif"), + ) + + def draw(self, context, layout, node, text): + colpath = layout.column(align=True) + linked = get_input(self) + if node.bl_idname == 'BakeWrangler_Output_Image_Path' and linked and linked.bl_idname == 'BakeWrangler_Input_Filenames': + colpath.label(text="Path: " + linked.inputs["Path/Filename"].get_path()) + colpath.label(text="Name: " + linked.inputs["Path/Filename"].get_name()) + elif not self.is_output: + colpath.prop(self, "disp_path", text="") + colpath.prop(self, "img_name", text="") + else: + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + if self.node.bl_idname == 'BakeWrangler_Input_Filenames' and not self.is_output: + return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.0, 0.0, 0.0)) + return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.35, 1.0, 1.0)) + + +# Custom Nodes: + +# Base class for all bakery nodes. Identifies that they belong in the bakery tree. +class BakeWrangler_Tree_Node: + # Tree version that created the node + tree_version: bpy.props.IntProperty(name="Tree Version", default=0) + def init(self, context): + self.tree_version = BW_TREE_VERSION + + @classmethod + def poll(cls, ntree): + return ntree.bl_idname == 'BakeWrangler_Tree' + + def get_name(self): + name = self.name + #if self.label: + # name += ".%s" % (self.label) + return name + + def validate(self, inputs=False, endl=False): + if not inputs: + return [True] + valid = [True] + # Validate inputs + has_valid_input = False + for input in self.inputs: + if input.islinked() and input.valid: + input_valid = get_input(input).validate() + valid[0] = input_valid.pop(0) + if valid[0]: + has_valid_input = True + valid += input_valid + errs = len(valid) + if not has_valid_input and errs < 2: + if endl: return valid + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"]) + return valid + + # Makes sure there is always one empty input socket at the bottom by adding and removing sockets + def update_inputs(self, socket_type=None, socket_name=None, sub_socket_dict=None): + if socket_type is None or self.tree_version != BW_TREE_VERSION: + return + idx = 0 + sub = 0 + if sub_socket_dict: + sub = len(sub_socket_dict.keys()) + for socket in self.inputs: + if socket.bl_idname != socket_type: + idx = idx + 1 + continue + if socket.is_linked or (hasattr(socket, 'value') and socket.value): + if len(self.inputs) == idx + 1 + sub: + self.inputs.new(socket_type, socket_name) + if sub_socket_dict: + for key in sub_socket_dict.keys(): + self.inputs.new(sub_socket_dict[key], key) + else: + if len(self.inputs) > idx + 1 + sub: + self.inputs.remove(socket) + rem = idx + idx = idx - 1 + if sub_socket_dict: + for key in sub_socket_dict.keys(): + self.inputs.remove(self.inputs[rem]) + idx = idx - 1 + idx = idx + 1 + + # Update inputs and links on updates + def update(self): + if self.tree_version != BW_TREE_VERSION: + return + self.update_inputs() + # Links can get inserted without calling insert_link, but update is called. + for socket in self.inputs: + if socket.islinked(): + self.insert_link(socket.links[0]) + + # Validate incoming links + def insert_link(self, link): + if link.to_node == self: + if follow_input_link(link).from_socket.bl_idname in link.to_socket.valid_inputs() and link.is_valid: + link.to_socket.valid = True + else: + link.to_socket.valid = False + + # Draw a double/halve button set + def draw_double_halve(self, layout, value): + op = layout.operator("bake_wrangler.double_val", icon='SORT_DESC', text="") + BakeWrangler_Operator_DoubleVal.set_props(op, self.name, self.id_data.name, value) + op = layout.operator("bake_wrangler.double_val", icon='SORT_ASC', text="") + BakeWrangler_Operator_DoubleVal.set_props(op, self.name, self.id_data.name, value, True) + + # Draw bake button in correct state + def draw_bake_button(self, layout, icon, label, icon_only=False, socket=-1): + is_self = False + baking_valid = False + try: + if self.id_data.baking: + baking_valid = True + if self.id_data.baking.node == self.name: + is_self = True + except ReferenceError: + is_self = False + baking_valid = False + + if baking_valid: + if is_self: + if self.id_data.baking.stop(kill=False): + if icon_only: + stext = "" + else: + stext = "Stopping..." + layout.operator("bake_wrangler.dummy", icon='CANCEL', text=stext) + else: + if icon_only: + stext = "" + else: + stext = "Cancel Bake" + op = layout.operator("bake_wrangler.bake_stop", icon='CANCEL', text=stext) + op.tree = self.id_data.name + op.node = self.name + op.sock = socket + else: + layout.operator("bake_wrangler.dummy", icon=icon, text=label) + else: + op = layout.operator("bake_wrangler.bake_pass", icon=icon, text=label) + op.tree = self.id_data.name + op.node = self.name + op.sock = socket + + +# All settings which are not pass specific +class BakeWrangler_Settings(BakeWrangler_Tree_Node, Node): + '''Settings node''' + bl_label = 'Settings' + bl_width_default = 173 + + # Inputs are static (none) + def update_inputs(self): + pass + + # Only one of this node can be pinned at a time + def pin_node(self, context): + if self.pinned: + tree = self.id_data + for node in tree.nodes: + if node != self and node.bl_idname == 'BakeWrangler_Settings': + node.pinned = False + + # Props + pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node) + ray_dist: bpy.props.FloatProperty(name="Ray Distance", description="Distance to use for inward ray cast when using a selected to active bake", default=0.01, step=1, min=0.0, unit='LENGTH') + max_ray_dist: bpy.props.FloatProperty(name="Max Ray Dist", description="The maximum ray distance for matching points between the active and selected objects. If zero, there is no limit", default=0.0, step=1, min=0.0, unit='LENGTH') + margin: bpy.props.IntProperty(name="Margin", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL') + margin_extend: bpy.props.BoolProperty(name="Extend", description="Margin extends border pixels outward (instead of taking values from adjacent faces)", default=True) + #mask_margin: bpy.props.IntProperty(name="Mask Margin", description="Adds extra padding to the mask bake. Use if edge details are being cut off when masking is enabled", default=0, min=0, subtype='PIXEL') + auto_cage: bpy.props.BoolProperty(name="Auto Cage", description="Automatically generate a cage for objects that don't have one set", default=False) + acage_expansion: bpy.props.FloatProperty(name="Cage Expansion", description="Distance to expand automatically generated cage geometry from original object", default=0.02, step=0.01, precision=3, unit='LENGTH') + acage_smooth: bpy.props.IntProperty(name="Cage Smoothing Angle", description="Angle range that automatic normal smoothing will be applied to", default=179) + material_replace: bpy.props.BoolProperty(name="Material Override", description="Replace all materials on selected objects with the specified material (objects without a material will have it added)", default=False) + material_override: bpy.props.PointerProperty(name="Override Material", description="Material that will be used in place of all other materials", type=bpy.types.Material) + material_osl: bpy.props.BoolProperty(name="OSL", description="Material uses an OSL shader node", default=False) + bake_mods: bpy.props.BoolProperty(name="Bake Mods to Unmodded", description="Modifiers with viewport visibility enabled will be stripped from Target objects and a copy with those modifiers applied will be created and used as the Source object (Disable viewport visibility on a modifier to exclude it - this setting can be inverted in the add-on preferences if you prefer the visibilty setting to work the other way)", default=False) + + cycles_devices = ( + ('CPU', "CPU", "Use CPU for baking"), + ('GPU', "GPU", "Use GPU for baking"), + ) + + tile_sizes = ( + ('DEF', "Default", "Use Bake Wrangler default"), + ('IMG', "Bake Size", "Use size of bake as tile size"), + ('CUST', "Custom", "Enter your own custom tile size"), + ) + + # Props + pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node) + res_bake_x: bpy.props.IntProperty(name="Bake X resolution ", description="Width (X) to bake maps at", default=2048, min=1, subtype='PIXEL') + res_bake_y: bpy.props.IntProperty(name="Bake Y resolution ", description="Height (Y) to bake maps at", default=2048, min=1, subtype='PIXEL') + bake_device: bpy.props.EnumProperty(name="Device", description="Bake device", items=cycles_devices, default='CPU') + interpolate: bpy.props.BoolProperty(name="Interpolate", description="Use cubic interpolation between baked pixel and output pixel, creating a soft anti-aliasing effect", default=False) + + adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False) + use_world: bpy.props.BoolProperty(name="Use World", description="Enabled to pick a world to use (empty to use active), instead of Bake Wranglers default", default=False) + the_world: bpy.props.PointerProperty(name="World", description="World to use instead of Bake Wranglers default (empty to use active)", type=bpy.types.World) + cpy_render: bpy.props.BoolProperty(name="Copy Settings", description="Copy render settings from selected scene (empty to use active), instead of using defaults", default=False) + cpy_from: bpy.props.PointerProperty(name="Render Scene", description="Scene to copy render settings from (empty to use active)", type=bpy.types.Scene) + render_tile: bpy.props.IntProperty(name="Tiles", description="Render tile size", default=2048, min=8, subtype='PIXEL') + use_tiles: bpy.props.EnumProperty(name="Tiles", description="Render tile size", items=tile_sizes, default='DEF') + render_threads: bpy.props.IntProperty(name="Threads", description="Maximum number of CPU cores to use simultaneously (set to zero for automatic)", default=0, min=0, max=1024) + use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False) + bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4) + + bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 1 for all PBR passes and Normal maps. Values over 50 generally won't improve results.\nQuality is gained by increasing resolution rather than samples past that point", default=1, min=1) + bake_threshold: bpy.props.FloatProperty(name="Noise Threshold", description="Noise level to stop sampling at if reached before sample count", default=0.01, min=0.001, max=1.0) + bake_usethresh: bpy.props.BoolProperty(name="Use Threshold", description="Enables use of noise level threshold", default=False) + bake_timelimit: bpy.props.FloatProperty(name="Time Limit", description="Maximum time to spend on a single bake. Zero to disable", default=0.0, min=0.0, subtype='TIME_ABSOLUTE', unit='TIME_ABSOLUTE', step=100) + + # Update output nodes to display alpha input or not depending on setting + def check_alpha(self, context): + tree = self.id_data + for node in tree.nodes: + if node.bl_idname == 'BakeWrangler_Output_Image_Path': + node.update_inputs() + + # Recreate image format drop down as the built in one doesn't seem usable? Also most of the settings + # for the built in image settings selector don't seem applicable to saving from script... + img_format = ( + ('BMP', "BMP", "Output image in bitmap format."), + ('IRIS', "Iris", "Output image in (old!) SGI IRIS format."), + ('PNG', "PNG", "Output image in PNG format."), + ('JPEG', "JPEG", "Output image in JPEG format."), + ('JPEG2000', "JPEG 2000", "Output image in JPEG 2000 format."), + ('TARGA', "Targa", "Output image in Targa format."), + ('TARGA_RAW', "Targa Raw", "Output image in uncompressed Targa format."), + ('CINEON', "Cineon", "Output image in Cineon format."), + ('DPX', "DPX", "Output image in DPX format."), + ('OPEN_EXR_MULTILAYER', "OpenEXR MultiLayer", "Output image in multilayer OpenEXR format."), + ('OPEN_EXR', "OpenEXR", "Output image in OpenEXR format."), + ('HDR', "Radiance HDR", "Output image in Radiance HDR format."), + ('TIFF', "TIFF", "Output image in TIFF format."), + ) + + img_color_modes = ( + ('BW', "BW", "Image saved in 8 bit grayscale"), + ('RGB', "RGB", "Image saved with RGB (color) data"), + ('RGBA', "RGBA", "Image saved with RGB and Alpha data"), + ) + + img_color_modes_noalpha = ( + ('BW', "BW", "Image saved in 8 bit grayscale"), + ('RGB', "RGB", "Image saved with RGB (color) data"), + ) + + img_color_depths_8_16 = ( + ('8', "8", "8 bit color channels"), + ('16', "16", "16 bit color channels"), + ) + + img_color_depths_8_12_16 = ( + ('8', "8", "8 bit color channels"), + ('12', "12", "12 bit color channels"), + ('16', "16", "16 bit color channels"), + ) + + img_color_depths_8_10_12_16 = ( + ('8', "8", "8 bit color channels"), + ('10', "10", "10 bit color channels"), + ('12', "12", "12 bit color channels"), + ('16', "16", "16 bit color channels"), + ) + + img_color_depths_16_32 = ( + ('16', "Float (Half)", "16 bit color channels"), + ('32', "Float (Full)", "32 bit color channels"), + ) + + img_codecs_jpeg2k = ( + ('JP2', "JP2", ""), + ('J2K', "J2K", ""), + ) + + img_codecs_openexr = ( + ('DWAA', "DWAA (lossy)", ""), + ('B44A', "B44A (lossy)", ""), + ('ZIPS', "ZIPS (lossless)", ""), + ('RLE', "RLE (lossless)", ""), + ('RLE', "RLE (lossless)", ""), + ('PIZ', "PIZ (lossless)", ""), + ('ZIP', "ZIP (lossless)", ""), + ('PXR24', "Pxr24 (lossy)", ""), + ('NONE', "None", ""), + ) + + img_codecs_tiff = ( + ('PACKBITS', "Pack Bits", ""), + ('LZW', "LZW", ""), + ('DEFLATE', "Deflate", ""), + ('NONE', "None", ""), + ) + + img_color_spaces = [] + for space in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys(): + img_color_spaces.append((space, space, space)) + img_color_spaces = tuple(img_color_spaces) + + # Props + pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node) + img_xres: bpy.props.IntProperty(name="Image X resolution", description="Number of horizontal pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL') + img_yres: bpy.props.IntProperty(name="Image Y resolution", description="Number of vertical pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL') + img_clear: bpy.props.BoolProperty(name="Clear Image", description="Clear image before writing bake data", default=False) + img_udim: bpy.props.BoolProperty(name="UDIM", description="Treat UV map as UDIM space and append standard number system to file name", default=False) + img_type: bpy.props.EnumProperty(name="Image Format", description="File format to save bake as", items=img_format, default='PNG') + fast_aa: bpy.props.BoolProperty(name="Fast Anti-Alias", description="Fast Anti-Aliasing. For more control use down or up sampling of bake to output by using different resolutions", default=False) + fast_aa_lvl: bpy.props.IntProperty(name="Fast AA Level", description="Level of fast AA to apply from 1 to 9", default=3, min=1, max=9) + + marginer: bpy.props.BoolProperty(name="Marginer", description="Use alternative margin generator (slower)", default=False) + marginer_size: bpy.props.IntProperty(name="Marginer Size", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL') + marginer_fill: bpy.props.BoolProperty(name="Marginer Fill", description="Fill all gaps with margin instead of using a fixed width", default=False) + + adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Display or hide advanced settings", default=False) + + # Core settings + img_color_space: bpy.props.EnumProperty(name="Color Space", description="Color space to use when saving the image", items=img_color_spaces) + img_use_float: bpy.props.BoolProperty(name="Use 32 Bit Float", description="Generate all input passes using 32 bit floating point color (128 bits per pixel). Note this isn't very useful if your image format isn't set to a high bit depth", default=False) + img_color_mode: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels, and RGBA for saving red, green, blue and alpha channels", items=img_color_modes, default='RGB', update=check_alpha) + img_color_mode_noalpha: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels", items=img_color_modes_noalpha, default='RGB') + + # Color Depths + img_color_depth_8_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_16, default='8') + img_color_depth_8_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_12_16, default='8') + img_color_depth_8_10_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_10_12_16, default='8') + img_color_depth_16_32: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_16_32, default='16') + + # Compression / Quality + img_compression: bpy.props.IntProperty(name="Compression", description="Amount of time to determine best compression: 0 = no compression, 100 = maximum lossless compression", default=15, min=0, max=100, subtype='PERCENTAGE') + img_quality: bpy.props.IntProperty(name="Quality", description="Quality for image formats that support lossy compression", default=90, min=0, max=100, subtype='PERCENTAGE') + + # Codecs + img_codec_jpeg2k: bpy.props.EnumProperty(name="Codec", description="Codec settings for jpeg2000", items=img_codecs_jpeg2k, default='JP2') + img_codec_openexr: bpy.props.EnumProperty(name="Codec", description="Codec settings for OpenEXR", items=img_codecs_openexr, default='ZIP') + img_codec_tiff: bpy.props.EnumProperty(name="Compression", description="Compression mode for TIFF", items=img_codecs_tiff, default='DEFLATE') + + # Other random image format settings + img_jpeg2k_cinema: bpy.props.BoolProperty(name="Cinema", description="Use Openjpeg Cinema Preset", default=True) + img_jpeg2k_cinema48: bpy.props.BoolProperty(name="Cinema (48)", description="Use Openjpeg Cinema Preset (48 fps)", default=False) + img_jpeg2k_ycc: bpy.props.BoolProperty(name="YCC", description="Save luminance-chrominance-chrominance channels instead of RGB colors", default=False) + img_dpx_log: bpy.props.BoolProperty(name="Log", description="Convert to logarithmic color space", default=False) + img_openexr_zbuff: bpy.props.BoolProperty(name="Z Buffer", description="Save the z-depth per pixel (32 bit unsigned int z-buffer)", default=True) + + def copy(self, node): + self.pinned = False + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN (none) + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_MeshSetting', "Mesh Settings") + # Prefs + self.ray_dist = _prefs("def_raydist") + self.max_ray_dist = _prefs("def_max_ray_dist") + self.margin = _prefs("def_margin") + #self.mask_margin = _prefs("def_mask_margin") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_PassSetting', "Pass Settings") + # Prefs + self.res_bake_x = _prefs("def_xres") + self.res_bake_y = _prefs("def_yres") + self.bake_samples = _prefs("def_samples") + self.bake_device = self.cycles_devices[int(_prefs("def_device"))][0] + self.adv_settings = _prefs("def_show_adv") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_SampleSetting', "Sample Settings") + # Prefs + self.bake_samples = _prefs("def_samples") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_OutputSetting', "Output Settings") + # Prefs + self.img_type = self.img_format[_prefs("def_format")][0] + self.img_xres = _prefs("def_xout") + self.img_yres = _prefs("def_yout") + self.adv_settings = _prefs("def_show_adv") + self.img_color_space = bpy.data.scenes[0].sequencer_colorspace_settings.name + + def draw_buttons(self, context, layout): + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, "margin") + row.prop(self, "margin_extend", toggle=True, icon_only=True, icon='IMAGE_PLANE') + #col.prop(self, "mask_margin") + col.prop(self, "ray_dist") + col.prop(self, "max_ray_dist") + if not self.auto_cage: + col.prop(self, "auto_cage", toggle=True) + else: + row = col.row(align=True) + row.prop(self, "auto_cage", toggle=True) + row.prop(self, "acage_expansion", text="") + row.prop(self, "acage_smooth", text="") + if not self.material_replace: + col.prop(self, "material_replace", toggle=True) + else: + row = col.row(align=True) + row.prop(self, "material_replace", toggle=True) + row.prop_search(self, "material_override", bpy.data, "materials", text="") + row.prop(self, "material_osl", toggle=True, icon_only=True, icon='SCRIPT') + col.prop(self, "bake_mods", toggle=True) + + colnode = layout.column(align=False) + colbasic = colnode.column(align=True) + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "res_bake_x", text="X") + BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_x") + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "res_bake_y", text="Y") + BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_y") + #colbasic.prop(self, "bake_samples", text="Samples") + colbasic.prop(self, "interpolate") + + split = colnode.split(factor=0.35) + split.label(text="Device:") + split.prop(self, "bake_device", text="") + + advrow = colnode.row() + advrow.alignment = 'LEFT' + if not self.adv_settings: + advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:") + advrow.separator() + else: + advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:") + advrow.separator() + advcol = colnode.column(align=True) + + row = advcol.row(align=True) + row.prop(self, "use_world", text="Use My World", toggle=True) + if self.use_world: + row.prop_search(self, "the_world", bpy.data, "worlds", text="") + + row = advcol.row(align=True) + row.prop(self, "cpy_render", text="Use My Settings", toggle=True) + if self.cpy_render: + row.prop_search(self, "cpy_from", bpy.data, "scenes", text="") + + row = advcol.row(align=True) + row.prop(self, "use_tiles") + if self.use_tiles == 'CUST': + row.prop(self, "render_tile", text="") + + row = advcol.row(align=True) + row.prop(self, "render_threads") + + row = advcol.row(align=True) + row.prop(self, "use_bg_col", toggle=True) + if self.use_bg_col: + row.prop(self, "bg_color", text="") + + colnode = layout.column(align=False) + colbasic = colnode.column(align=True) + rowbasic = colbasic.row(align=True) + rowbasic.label(text="Noise") + rowbasic.prop(self, "bake_usethresh", text="") + rowbasic.prop(self, "bake_threshold", text="") + colbasic.prop(self, "bake_samples", text="Samples") + colbasic.prop(self, "bake_timelimit") + + colnode = layout.column(align=False) + colbasic = colnode.column(align=True) + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "img_xres", text="X") + BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_xres") + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "img_yres", text="Y") + BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_yres") + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "img_clear", text="Clear", toggle=True) + rowbasic.prop(self, "img_udim", toggle=True) + rowbasic = colbasic.row(align=True) + if not self.fast_aa: + rowbasic.prop(self, "fast_aa", toggle=True) + if self.fast_aa: + rowbasic.prop(self, "fast_aa", toggle=True, text="Fast AA:") + rowbasic.prop(self, "fast_aa_lvl", text="") + + split = colnode.split(factor=0.35) + split.label(text="Format:") + split.prop(self, "img_type", text="") + + advrow = colnode.row() + advrow.alignment = 'LEFT' + if not self.adv_settings: + advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:") + advrow.separator() + else: + advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:") + advrow.separator() + coladv = colnode.column(align=True) + + row = coladv.row(align=True) + if self.marginer: + row.prop(self, "marginer", toggle=True, icon_only=True, icon='NODE_INSERT_OFF') + else: + row.prop(self, "marginer", toggle=True, icon='NODE_INSERT_OFF') + if self.marginer: + row_size = row.row(align=True) + row_size.prop(self, "marginer_size", text="") + if self.marginer_fill: + row_size.enabled = False + row.prop(self, "marginer_fill", toggle=True, icon_only=True, icon='TPAINT_HLT') + + coladv.prop(self, "img_use_float", toggle=True) + + splitadv = coladv.split(factor=0.4) + coladvtxt = splitadv.column(align=True) + coladvopt = splitadv.column(align=True) + + # Color Spaces + if self.img_type != 'CINEON': + coladvtxt.label(text="Space:") + coladvopt.prop(self, "img_color_space", text="") + # Color Modes + if self.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']: + coladvtxt.label(text="Color:") + coladvopt.prop(self, "img_color_mode_noalpha", text="") + if self.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']: + coladvtxt.label(text="Color:") + coladvopt.prop(self, "img_color_mode", text="") + # Color Depths + if self.img_type in ['PNG', 'TIFF']: + coladvtxt.label(text="Depth:") + coladvopt.prop(self, "img_color_depth_8_16", text="") + if self.img_type == 'JPEG2000': + coladvtxt.label(text="Depth:") + coladvopt.prop(self, "img_color_depth_8_12_16", text="") + if self.img_type == 'DPX': + coladvtxt.label(text="Depth:") + coladvopt.prop(self, "img_color_depth_8_10_12_16", text="") + if self.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']: + coladvtxt.label(text="Depth:") + coladvopt.prop(self, "img_color_depth_16_32", text="") + # Compression / Quality + if self.img_type == 'PNG': + coladvtxt.label(text="Compression:") + coladvopt.prop(self, "img_compression", text="") + if self.img_type in ['JPEG', 'JPEG2000']: + coladvtxt.label(text="Quality:") + coladvopt.prop(self, "img_quality", text="") + # Codecs + if self.img_type == 'JPEG2000': + coladvtxt.label(text="Codec:") + coladvopt.prop(self, "img_codec_jpeg2k", text="") + if self.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']: + coladvtxt.label(text="Codec:") + coladvopt.prop(self, "img_codec_openexr", text="") + if self.img_type == 'TIFF': + coladvtxt.label(text="Compression:") + coladvopt.prop(self, "img_codec_tiff", text="") + # Other random image settings + if self.img_type == 'JPEG2000': + coladv.prop(self, "img_jpeg2k_cinema") + coladv.prop(self, "img_jpeg2k_cinema48") + coladv.prop(self, "img_jpeg2k_ycc") + if self.img_type == 'DPX': + coladv.prop(self, "img_dpx_log") + if self.img_type == 'OPEN_EXR': + coladv.prop(self, "img_openexr_zbuff") + + +# Node to configure pass settings, which can be pinned as global +class BakeWrangler_MeshSettings(BakeWrangler_Tree_Node, Node): + '''Mesh settings node''' + bl_label = 'Mesh Settings' + bl_width_default = 173 + + # Inputs are static (none) + def update_inputs(self): + pass + + # Only one of this node can be pinned at a time + def pin_node(self, context): + if self.pinned: + tree = self.id_data + for node in tree.nodes: + if node != self and node.bl_idname == 'BakeWrangler_MeshSettings': + node.pinned = False + + # Props + pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node) + ray_dist: bpy.props.FloatProperty(name="Ray Distance", description="Distance to use for inward ray cast when using a selected to active bake", default=0.01, step=1, min=0.0, unit='LENGTH') + max_ray_dist: bpy.props.FloatProperty(name="Max Ray Dist", description="The maximum ray distance for matching points between the active and selected objects. If zero, there is no limit", default=0.0, step=1, min=0.0, unit='LENGTH') + margin: bpy.props.IntProperty(name="Margin", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL') + margin_extend: bpy.props.BoolProperty(name="Extend", description="Margin extends border pixels outward (instead of taking values from adjacent faces)", default=True) + margin_auto: bpy.props.BoolProperty(name="Auto Margin", description="Automatically set margin size based on smallest dimension of texture", default=True) + #mask_margin: bpy.props.IntProperty(name="Mask Margin", description="Adds extra padding to the mask bake. Use if edge details are being cut off when masking is enabled", default=0, min=0, subtype='PIXEL') + auto_cage: bpy.props.BoolProperty(name="Auto Cage", description="Automatically generate a cage for objects that don't have one set", default=False) + acage_expansion: bpy.props.FloatProperty(name="Cage Expansion", description="Distance to expand automatically generated cage geometry from original object", default=0.02, step=0.01, precision=3, unit='LENGTH') + acage_smooth: bpy.props.IntProperty(name="Cage Smoothing Angle", description="Angle range that automatic normal smoothing will be applied to", default=179) + material_replace: bpy.props.BoolProperty(name="Material Override", description="Replace all materials on selected objects with the specified material (objects without a material will have it added)", default=False) + material_override: bpy.props.PointerProperty(name="Override Material", description="Material that will be used in place of all other materials", type=bpy.types.Material) + material_osl: bpy.props.BoolProperty(name="OSL", description="Material uses an OSL shader node", default=False) + bake_mods: bpy.props.BoolProperty(name="Bake Mods to Unmodded", description="Modifiers with viewport visibility enabled will be stripped from Target objects and a copy with those modifiers applied will be created and used as the Source object (Disable viewport visibility on a modifier to exclude it - this setting can be inverted in the add-on preferences if you prefer the visibilty setting to work the other way)", default=False) + + def copy(self, node): + self.pinned = False + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN (none) + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_MeshSetting', "Mesh Settings") + # Prefs + self.ray_dist = _prefs("def_raydist") + self.max_ray_dist = _prefs("def_max_ray_dist") + self.margin = _prefs("def_margin") + #self.mask_margin = _prefs("def_mask_margin") + + def draw_buttons(self, context, layout): + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, "margin_auto", toggle=True, icon_only=True, icon='MOD_MESHDEFORM') + mrg = row.row(align=True) + mrg.prop(self, "margin") + if self.margin_auto: + mrg.enabled = False + row.prop(self, "margin_extend", toggle=True, icon_only=True, icon='IMAGE_PLANE') + #col.prop(self, "mask_margin") + col.prop(self, "ray_dist") + col.prop(self, "max_ray_dist") + if not self.auto_cage: + col.prop(self, "auto_cage", toggle=True) + else: + row = col.row(align=True) + row.prop(self, "auto_cage", toggle=True) + row.prop(self, "acage_expansion", text="") + row.prop(self, "acage_smooth", text="") + if not self.material_replace: + col.prop(self, "material_replace", toggle=True) + else: + row = col.row(align=True) + row.prop(self, "material_replace", toggle=True) + row.prop_search(self, "material_override", bpy.data, "materials", text="") + row.prop(self, "material_osl", toggle=True, icon_only=True, icon='SCRIPT') + col.prop(self, "bake_mods", toggle=True) + + +# Node to configure pass settings, which can be pinned as global +class BakeWrangler_PassSettings(BakeWrangler_Tree_Node, Node): + '''Pass settings node''' + bl_label = 'Pass Settings' + bl_width_default = 144 + + # Inputs are static (none) + def update_inputs(self): + pass + + # Only one of this node can be pinned at a time + def pin_node(self, context): + if self.pinned: + tree = self.id_data + for node in tree.nodes: + if node != self and node.bl_idname == 'BakeWrangler_PassSettings': + node.pinned = False + + cycles_devices = ( + ('CPU', "CPU", "Use CPU for baking"), + ('GPU', "GPU", "Use GPU for baking"), + ) + + tile_sizes = ( + ('DEF', "Default", "Use Bake Wrangler default"), + ('IMG', "Bake Size", "Use size of bake as tile size"), + ('CUST', "Custom", "Enter your own custom tile size"), + ) + + # Props + pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node) + res_bake_x: bpy.props.IntProperty(name="Bake X resolution ", description="Width (X) to bake maps at", default=2048, min=1, subtype='PIXEL') + res_bake_y: bpy.props.IntProperty(name="Bake Y resolution ", description="Height (Y) to bake maps at", default=2048, min=1, subtype='PIXEL') + bake_device: bpy.props.EnumProperty(name="Device", description="Bake device", items=cycles_devices, default='CPU') + interpolate: bpy.props.BoolProperty(name="Interpolate", description="Use cubic interpolation between baked pixel and output pixel, creating a soft anti-aliasing effect", default=False) + + adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False) + use_world: bpy.props.BoolProperty(name="Use World", description="Enabled to pick a world to use (empty to use active), instead of Bake Wranglers default", default=False) + the_world: bpy.props.PointerProperty(name="World", description="World to use instead of Bake Wranglers default (empty to use active)", type=bpy.types.World) + cpy_render: bpy.props.BoolProperty(name="Copy Settings", description="Copy render settings from selected scene (empty to use active), instead of using defaults", default=False) + cpy_from: bpy.props.PointerProperty(name="Render Scene", description="Scene to copy render settings from (empty to use active)", type=bpy.types.Scene) + render_tile: bpy.props.IntProperty(name="Tiles", description="Render tile size", default=2048, min=8, subtype='PIXEL') + use_tiles: bpy.props.EnumProperty(name="Tiles", description="Render tile size", items=tile_sizes, default='DEF') + render_threads: bpy.props.IntProperty(name="Threads", description="Maximum number of CPU cores to use simultaneously (set to zero for automatic)", default=0, min=0, max=1024) + use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False) + bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4) + + def copy(self, node): + self.pinned = False + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN (none) + self.inputs.new('BakeWrangler_Socket_SampleSetting', "Samples") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_PassSetting', "Pass Settings") + # Prefs + self.res_bake_x = _prefs("def_xres") + self.res_bake_y = _prefs("def_yres") + self.bake_samples = _prefs("def_samples") + self.bake_device = self.cycles_devices[int(_prefs("def_device"))][0] + self.adv_settings = _prefs("def_show_adv") + + def draw_buttons(self, context, layout): + colnode = layout.column(align=False) + + colbasic = colnode.column(align=True) + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "res_bake_x", text="X") + BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_x") + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "res_bake_y", text="Y") + BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_y") + #colbasic.prop(self, "bake_samples", text="Samples") + colbasic.prop(self, "interpolate") + + split = colnode.split(factor=0.35) + split.label(text="Device:") + split.prop(self, "bake_device", text="") + + advrow = colnode.row() + advrow.alignment = 'LEFT' + if not self.adv_settings: + advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:") + advrow.separator() + else: + advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:") + advrow.separator() + advcol = colnode.column(align=True) + + row = advcol.row(align=True) + row.prop(self, "use_world", text="Use My World", toggle=True) + if self.use_world: + row.prop_search(self, "the_world", bpy.data, "worlds", text="") + + row = advcol.row(align=True) + row.prop(self, "cpy_render", text="Use My Settings", toggle=True) + if self.cpy_render: + row.prop_search(self, "cpy_from", bpy.data, "scenes", text="") + + row = advcol.row(align=True) + row.prop(self, "use_tiles") + if self.use_tiles == 'CUST': + row.prop(self, "render_tile", text="") + + row = advcol.row(align=True) + row.prop(self, "render_threads") + + row = advcol.row(align=True) + row.prop(self, "use_bg_col", toggle=True) + if self.use_bg_col: + row.prop(self, "bg_color", text="") + + + +# Node to configure pass settings, which can be pinned as global +class BakeWrangler_SampleSettings(BakeWrangler_Tree_Node, Node): + '''Pass settings node''' + bl_label = 'Sample Settings' + bl_width_default = 144 + + # Inputs are static (none) + def update_inputs(self): + pass + + # Only one of this node can be pinned at a time + def pin_node(self, context): + if self.pinned: + tree = self.id_data + for node in tree.nodes: + if node != self and node.bl_idname == 'BakeWrangler_SampleSettings': + node.pinned = False + + # Props + pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node) + bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 1 for all PBR passes and Normal maps. Values over 50 generally won't improve results.\nQuality is gained by increasing resolution rather than samples past that point", default=1, min=1) + bake_threshold: bpy.props.FloatProperty(name="Noise Threshold", description="Noise level to stop sampling at if reached before sample count", default=0.01, min=0.001, max=1.0) + bake_usethresh: bpy.props.BoolProperty(name="Use Threshold", description="Enables use of noise level threshold", default=False) + bake_timelimit: bpy.props.FloatProperty(name="Time Limit", description="Maximum time to spend on a single bake. Zero to disable", default=0.0, min=0.0, subtype='TIME_ABSOLUTE', unit='TIME_ABSOLUTE', step=100) + + def copy(self, node): + self.pinned = False + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN (none) + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_SampleSetting', "Sample Settings") + # Prefs + self.bake_samples = _prefs("def_samples") + + def draw_buttons(self, context, layout): + colnode = layout.column(align=False) + + colbasic = colnode.column(align=True) + rowbasic = colbasic.row(align=True) + rowbasic.label(text="Noise") + rowbasic.prop(self, "bake_usethresh", text="") + rowbasic.prop(self, "bake_threshold", text="") + colbasic.prop(self, "bake_samples", text="Samples") + colbasic.prop(self, "bake_timelimit") + + +# Node to configure output settings, which can be pinned as global +class BakeWrangler_OutputSettings(BakeWrangler_Tree_Node, Node): + '''Output settings node''' + bl_label = 'Output Settings' + bl_width_default = 152 + + # Inputs are static (none) + def update_inputs(self): + pass + + # Only one of this node can be pinned at a time + def pin_node(self, context): + if self.pinned: + tree = self.id_data + for node in tree.nodes: + if node != self and node.bl_idname == 'BakeWrangler_OutputSettings': + node.pinned = False + + # Update output nodes to display alpha input or not depending on setting + def check_alpha(self, context): + tree = self.id_data + for node in tree.nodes: + if node.bl_idname == 'BakeWrangler_Output_Image_Path': + node.update_inputs() + + # Recreate image format drop down as the built in one doesn't seem usable? Also most of the settings + # for the built in image settings selector don't seem applicable to saving from script... + img_format = ( + ('BMP', "BMP", "Output image in bitmap format."), + ('IRIS', "Iris", "Output image in (old!) SGI IRIS format."), + ('PNG', "PNG", "Output image in PNG format."), + ('JPEG', "JPEG", "Output image in JPEG format."), + ('JPEG2000', "JPEG 2000", "Output image in JPEG 2000 format."), + ('TARGA', "Targa", "Output image in Targa format."), + ('TARGA_RAW', "Targa Raw", "Output image in uncompressed Targa format."), + ('CINEON', "Cineon", "Output image in Cineon format."), + ('DPX', "DPX", "Output image in DPX format."), + ('OPEN_EXR_MULTILAYER', "OpenEXR MultiLayer", "Output image in multilayer OpenEXR format."), + ('OPEN_EXR', "OpenEXR", "Output image in OpenEXR format."), + ('HDR', "Radiance HDR", "Output image in Radiance HDR format."), + ('TIFF', "TIFF", "Output image in TIFF format."), + ) + + img_color_modes = ( + ('BW', "BW", "Image saved in 8 bit grayscale"), + ('RGB', "RGB", "Image saved with RGB (color) data"), + ('RGBA', "RGBA", "Image saved with RGB and Alpha data"), + ) + + img_color_modes_noalpha = ( + ('BW', "BW", "Image saved in 8 bit grayscale"), + ('RGB', "RGB", "Image saved with RGB (color) data"), + ) + + img_color_depths_8_16 = ( + ('8', "8", "8 bit color channels"), + ('16', "16", "16 bit color channels"), + ) + + img_color_depths_8_12_16 = ( + ('8', "8", "8 bit color channels"), + ('12', "12", "12 bit color channels"), + ('16', "16", "16 bit color channels"), + ) + + img_color_depths_8_10_12_16 = ( + ('8', "8", "8 bit color channels"), + ('10', "10", "10 bit color channels"), + ('12', "12", "12 bit color channels"), + ('16', "16", "16 bit color channels"), + ) + + img_color_depths_16_32 = ( + ('16', "Float (Half)", "16 bit color channels"), + ('32', "Float (Full)", "32 bit color channels"), + ) + + img_codecs_jpeg2k = ( + ('JP2', "JP2", ""), + ('J2K', "J2K", ""), + ) + + img_codecs_openexr = ( + ('DWAA', "DWAA (lossy)", ""), + ('B44A', "B44A (lossy)", ""), + ('ZIPS', "ZIPS (lossless)", ""), + ('RLE', "RLE (lossless)", ""), + ('RLE', "RLE (lossless)", ""), + ('PIZ', "PIZ (lossless)", ""), + ('ZIP', "ZIP (lossless)", ""), + ('PXR24', "Pxr24 (lossy)", ""), + ('NONE', "None", ""), + ) + + img_codecs_tiff = ( + ('PACKBITS', "Pack Bits", ""), + ('LZW', "LZW", ""), + ('DEFLATE', "Deflate", ""), + ('NONE', "None", ""), + ) + + img_color_spaces = [] + for space in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys(): + img_color_spaces.append((space, space, space)) + img_color_spaces = tuple(img_color_spaces) + + # Props + pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node) + img_xres: bpy.props.IntProperty(name="Image X resolution", description="Number of horizontal pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL') + img_yres: bpy.props.IntProperty(name="Image Y resolution", description="Number of vertical pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL') + img_clear: bpy.props.BoolProperty(name="Clear Image", description="Clear image before writing bake data", default=False) + img_udim: bpy.props.BoolProperty(name="UDIM", description="Treat UV map as UDIM space and append standard number system to file name", default=False) + img_type: bpy.props.EnumProperty(name="Image Format", description="File format to save bake as", items=img_format, default='PNG') + fast_aa: bpy.props.BoolProperty(name="Fast Anti-Alias", description="Fast Anti-Aliasing. For more control use down or up sampling of bake to output by using different resolutions", default=False) + fast_aa_lvl: bpy.props.IntProperty(name="Fast AA Level", description="Level of fast AA to apply from 1 to 9", default=3, min=1, max=9) + + marginer: bpy.props.BoolProperty(name="Marginer", description="Use alternative margin generator (slower)", default=False) + marginer_size: bpy.props.IntProperty(name="Marginer Size", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL') + marginer_fill: bpy.props.BoolProperty(name="Marginer Fill", description="Fill all gaps with margin instead of using a fixed width", default=False) + + adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Display or hide advanced settings", default=False) + + # Core settings + img_color_space: bpy.props.EnumProperty(name="Color Space", description="Color space to use when saving the image", items=img_color_spaces) + img_use_float: bpy.props.BoolProperty(name="Use 32 Bit Float", description="Generate all input passes using 32 bit floating point color (128 bits per pixel). Note this isn't very useful if your image format isn't set to a high bit depth", default=False) + img_color_mode: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels, and RGBA for saving red, green, blue and alpha channels", items=img_color_modes, default='RGB', update=check_alpha) + img_color_mode_noalpha: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels", items=img_color_modes_noalpha, default='RGB') + img_non_color: bpy.props.StringProperty(name="Non Color Space", default="NONE") + + # Color Depths + img_color_depth_8_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_16, default='8') + img_color_depth_8_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_12_16, default='8') + img_color_depth_8_10_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_10_12_16, default='8') + img_color_depth_16_32: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_16_32, default='16') + + # Compression / Quality + img_compression: bpy.props.IntProperty(name="Compression", description="Amount of time to determine best compression: 0 = no compression, 100 = maximum lossless compression", default=15, min=0, max=100, subtype='PERCENTAGE') + img_quality: bpy.props.IntProperty(name="Quality", description="Quality for image formats that support lossy compression", default=90, min=0, max=100, subtype='PERCENTAGE') + + # Codecs + img_codec_jpeg2k: bpy.props.EnumProperty(name="Codec", description="Codec settings for jpeg2000", items=img_codecs_jpeg2k, default='JP2') + img_codec_openexr: bpy.props.EnumProperty(name="Codec", description="Codec settings for OpenEXR", items=img_codecs_openexr, default='ZIP') + img_codec_tiff: bpy.props.EnumProperty(name="Compression", description="Compression mode for TIFF", items=img_codecs_tiff, default='DEFLATE') + + # Other random image format settings + img_jpeg2k_cinema: bpy.props.BoolProperty(name="Cinema", description="Use Openjpeg Cinema Preset", default=True) + img_jpeg2k_cinema48: bpy.props.BoolProperty(name="Cinema (48)", description="Use Openjpeg Cinema Preset (48 fps)", default=False) + img_jpeg2k_ycc: bpy.props.BoolProperty(name="YCC", description="Save luminance-chrominance-chrominance channels instead of RGB colors", default=False) + img_dpx_log: bpy.props.BoolProperty(name="Log", description="Convert to logarithmic color space", default=False) + img_openexr_zbuff: bpy.props.BoolProperty(name="Z Buffer", description="Save the z-depth per pixel (32 bit unsigned int z-buffer)", default=True) + + def copy(self, node): + self.pinned = False + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN (none) + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_OutputSetting', "Output Settings") + # Prefs + self.img_type = self.img_format[_prefs("def_format")][0] + self.img_xres = _prefs("def_xout") + self.img_yres = _prefs("def_yout") + self.adv_settings = _prefs("def_show_adv") + self.img_color_space = bpy.data.scenes[0].sequencer_colorspace_settings.name + + def draw_buttons(self, context, layout): + colnode = layout.column(align=False) + + colbasic = colnode.column(align=True) + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "img_xres", text="X") + BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_xres") + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "img_yres", text="Y") + BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_yres") + rowbasic = colbasic.row(align=True) + rowbasic.prop(self, "img_clear", text="Clear", toggle=True) + rowbasic.prop(self, "img_udim", toggle=True) + rowbasic = colbasic.row(align=True) + if not self.fast_aa: + rowbasic.prop(self, "fast_aa", toggle=True) + if self.fast_aa: + rowbasic.prop(self, "fast_aa", toggle=True, text="Fast AA:") + rowbasic.prop(self, "fast_aa_lvl", text="") + + split = colnode.split(factor=0.35) + split.label(text="Format:") + split.prop(self, "img_type", text="") + + advrow = colnode.row() + advrow.alignment = 'LEFT' + if not self.adv_settings: + advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:") + advrow.separator() + else: + advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:") + advrow.separator() + coladv = colnode.column(align=True) + + row = coladv.row(align=True) + if self.marginer: + row.prop(self, "marginer", toggle=True, icon_only=True, icon='NODE_INSERT_OFF') + else: + row.prop(self, "marginer", toggle=True, icon='NODE_INSERT_OFF') + if self.marginer: + row_size = row.row(align=True) + row_size.prop(self, "marginer_size", text="") + if self.marginer_fill: + row_size.enabled = False + row.prop(self, "marginer_fill", toggle=True, icon_only=True, icon='TPAINT_HLT') + + coladv.prop(self, "img_use_float", toggle=True) + + splitadv = coladv.split(factor=0.4) + coladvtxt = splitadv.column(align=True) + coladvopt = splitadv.column(align=True) + + # Color Spaces + if self.img_type != 'CINEON': + coladvtxt.label(text="Space:") + coladvopt.prop(self, "img_color_space", text="") + # Color Modes + if self.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']: + coladvtxt.label(text="Color:") + coladvopt.prop(self, "img_color_mode_noalpha", text="") + if self.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']: + coladvtxt.label(text="Color:") + coladvopt.prop(self, "img_color_mode", text="") + # Color Depths + if self.img_type in ['PNG', 'TIFF']: + coladvtxt.label(text="Depth:") + coladvopt.prop(self, "img_color_depth_8_16", text="") + if self.img_type == 'JPEG2000': + coladvtxt.label(text="Depth:") + coladvopt.prop(self, "img_color_depth_8_12_16", text="") + if self.img_type == 'DPX': + coladvtxt.label(text="Depth:") + coladvopt.prop(self, "img_color_depth_8_10_12_16", text="") + if self.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']: + coladvtxt.label(text="Depth:") + coladvopt.prop(self, "img_color_depth_16_32", text="") + # Compression / Quality + if self.img_type == 'PNG': + coladvtxt.label(text="Compression:") + coladvopt.prop(self, "img_compression", text="") + if self.img_type in ['JPEG', 'JPEG2000']: + coladvtxt.label(text="Quality:") + coladvopt.prop(self, "img_quality", text="") + # Codecs + if self.img_type == 'JPEG2000': + coladvtxt.label(text="Codec:") + coladvopt.prop(self, "img_codec_jpeg2k", text="") + if self.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']: + coladvtxt.label(text="Codec:") + coladvopt.prop(self, "img_codec_openexr", text="") + if self.img_type == 'TIFF': + coladvtxt.label(text="Compression:") + coladvopt.prop(self, "img_codec_tiff", text="") + # Other random image settings + if self.img_type == 'JPEG2000': + coladv.prop(self, "img_jpeg2k_cinema") + coladv.prop(self, "img_jpeg2k_cinema48") + coladv.prop(self, "img_jpeg2k_ycc") + if self.img_type == 'DPX': + coladv.prop(self, "img_dpx_log") + if self.img_type == 'OPEN_EXR': + coladv.prop(self, "img_openexr_zbuff") + + + +# File names input to allow attaching prefixes to outputs and make object->filename system more intuitive +class BakeWrangler_Input_Filenames(BakeWrangler_Tree_Node, Node): + '''File Names node''' + bl_label = 'File Names' + bl_width_default = 198 + + def get_names(self): + names = [] + for input in self.inputs: + if input.bl_idname == 'BakeWrangler_Socket_ObjectNames' and get_input(input): + names.append(get_input(input)) + return names + + def get_frames(self, padding=False, animated=False): + if animated: return self.use_rnd_seed + frames = [] + pad = None + # Parse string + ranges = self.frame_ranges.split(sep=",") + import re + extract = re.compile(r'(\D*#([0-9]*)\D*)|(\D*([0-9]*)-?([0-9]*):?([0-9]*)\D*)') + for arange in ranges: + match = extract.match(arange) + if match.group(1) and padding: + pad = int(match.group(2)) + elif match.group(3) and not padding: + start = int(match.group(4)) + end = int(match.group(5)) if match.group(5) else None + step = int(match.group(6)) if match.group(6) else 1 + if end: + if end < start: + step *= -1 + end -= 1 + else: end +=1 + for f in range(start, end, step): frames.append(f) + else: + frames.append(start) + else: continue + if padding: + return pad + return set(frames) + + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_ObjectNames', "Object Names") + + # Props + frame_ranges: bpy.props.StringProperty(name="Frame Ranges", description="Comma separated list of frame ranges to bake (eg: 1,3,4-12). For non-default zero padding include #number_of_zeros as one of your ranges (eg: #3,1-3,10 to pad all numbers to 3). Frame numbers are added to the end of file names", default="") + use_rnd_seed: bpy.props.BoolProperty(name="Use Animated Seed", description="Use different seed values (and hence noise patterns) at different frames", default=True) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_SplitOutput', "Path/Filename") + self.inputs.new('BakeWrangler_Socket_ObjectNames', "Object Names") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_SplitOutput', "Path / Filenames / Frames") + + def draw_buttons(self, context, layout): + col = layout.column(align=True) + row0 = col.row() + row0.label(text="Frame Ranges:") + row1 = col.row(align=True) + row1.prop(self, "frame_ranges", text="") + row1.prop(self, "use_rnd_seed", text="", icon='TIME') + + + +# Input node that contains a list of objects relevant to baking +class BakeWrangler_Input_ObjectList(BakeWrangler_Tree_Node, Node): + '''Object list node''' + bl_label = 'Objects' + bl_width_default = 198 + + # Makes sure there is always one empty input socket at the bottom by adding and removing sockets + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Object', "Object") + + # Determine if object meets current input filter + def input_filter(self, input_name, object): + if self.filter_collection: + if object.rna_type.identifier == 'Collection': + return True + elif object.rna_type.identifier == 'Object': + if (self.filter_mesh and object.type == 'MESH') or \ + (self.filter_curve and object.type == 'CURVE') or \ + (self.filter_surface and object.type == 'SURFACE') or \ + (self.filter_meta and object.type == 'META') or \ + (self.filter_font and object.type == 'FONT') or \ + (self.filter_light and object.type == 'LIGHT'): + return True + return False + + # Get all objects in tree from this node (mostly just uses the sockets methods) + def get_objects(self, only_mesh=False, no_lights=False, only_groups=False): + objects = [] + for input in self.inputs: + in_objs = input.get_objects(only_mesh, no_lights, only_groups) + if len(in_objs): + objects += in_objs + return objects + + # Validate all objects in tree from this node (mostly just uses the sockets methods) + def validate(self, check_materials=False, check_as_active=False, check_multi=False): + valid = [True] + for input in self.inputs: + valid_input = input.validate(check_materials, check_as_active, check_multi) + if not valid_input.pop(0): + valid[0] = False + if len(valid_input): + valid += valid_input + return valid + + filter_mesh: bpy.props.BoolProperty(name="Meshes", description="Show mesh type objects", default=True) + filter_curve: bpy.props.BoolProperty(name="Curves", description="Show curve type objects", default=True) + filter_surface: bpy.props.BoolProperty(name="Surfaces", description="Show surface type objects", default=True) + filter_meta: bpy.props.BoolProperty(name="Metas", description="Show meta type objects", default=True) + filter_font: bpy.props.BoolProperty(name="Fonts", description="Show font type objects", default=True) + filter_light: bpy.props.BoolProperty(name="Lights", description="Show light type objects", default=True) + filter_collection: bpy.props.BoolProperty(name="Collections", description="Toggle only collections", default=False) + + def copy(self, node): + self.inputs.clear() + for sok in node.inputs: + csok = self.inputs.new('BakeWrangler_Socket_Object', "Object") + csok.value = sok.value + csok.type = sok.type + csok.recursive = sok.recursive + csok.pick_uv = sok.pick_uv + csok.uv_map = sok.uv_map + csok.use_cage = sok.use_cage + csok.cage = sok.cage + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Object', "Object") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Object', "Objects") + # Prefs + self.filter_mesh = _prefs("def_filter_mesh") + self.filter_curve = _prefs("def_filter_curve") + self.filter_surface = _prefs("def_filter_surface") + self.filter_meta = _prefs("def_filter_meta") + self.filter_font = _prefs("def_filter_font") + self.filter_light = _prefs("def_filter_light") + self.filter_collection = _prefs("def_filter_collection") + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + row0 = row.row() + row0.label(text="Filter:") + + row1 = row.row(align=True) + row1.alignment = 'RIGHT' + for fltr in BakeWrangler_Operator_FilterToggle.filters: + icn = fltr.split("_")[1].upper() + "_DATA" + op = row1.operator("bake_wrangler.filter_toggle", icon=icn, text="", depress=getattr(self, fltr)) + op.tree = self.id_data.name + op.node = self.name + op.filter = fltr + if self.filter_collection: + row1.enabled = False + + row2 = row.row(align=False) + row2.alignment = 'RIGHT' + row2.prop(self, "filter_collection", text="", icon='GROUP') + + + +# Automatic sorting of meshes into groups for baking +class BakeWrangler_Sort_Meshes(BakeWrangler_Tree_Node, Node): + '''Sorting and grouping of meshes by name''' + bl_label = 'Auto Sort Meshes' + bl_width_default = 240 + + # Inputs are static on this node + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Mesh', "Mesh") + + # Try to get nodes settings from local or global node + def get_settings(self, validating=False, input=None): + if input: + settings = get_input(input) + if settings: + return settings.get_settings() + return {} + + # Check node settings are valid to bake. Returns true/false, plus error message. + def validate(self, check_materials=False, multires=False): + valid = [True] + has_valid_input = False + for inpt in self.inputs: + if inpt.islinked() and inpt.valid: + mesh = get_input(inpt) + if mesh: + input_valid = mesh.validate(check_materials, multires) + if not input_valid.pop(0): + valid[0] = False + else: + has_valid_input = True + if len(input_valid): + valid += input_valid + if not has_valid_input and len(valid) < 2: + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"]) + return valid + + # Return name with ident removed or empty string if ident not in name + def find_ident(self, type, name, ident, case_sens=True): + if type == 'PASS' or not ident: + return name + ident_idx = 0 + ident_len = len(ident) + cname = name + cident = ident + if not case_sens: + cname = name.lower() + cident = ident.lower() + if type == 'STARTS': + if not cname.startswith(cident): + return '' + else: return name[ident_len:] + elif type == 'ENDS': + if not cname.endswith(cident): + return '' + else: return name[:-ident_len] + elif type in ['SCONTAINS', 'ECONTAINS']: + if type == 'SCONTAINS': + ident_idx = cname.find(cident) + if type == 'ECONTAINS': + ident_idx = cname.rfind(cident) + if ident_idx == -1: + return '' + else: return name[:ident_idx] + name[ident_idx + ident_len:] + + # Return a list of object pairings + def get_objects(self, set, input): + objs = [] + mesh = get_input(input) + if not mesh: + return [] + if set == 'TARGET' and 'Target' in mesh.inputs.keys(): + targets = prune_objects(mesh.inputs["Target"].get_objects(only_mesh=True), True) + sources = prune_objects(mesh.inputs["Source"].get_objects(no_lights=True)) + sourgrp = prune_objects(mesh.inputs["Source"].get_objects(no_lights=True, only_groups=True)) + scenes = prune_objects(mesh.inputs["Scene"].get_objects()) + scengrp = prune_objects(mesh.inputs["Scene"].get_objects(only_groups=True)) + if not len(sources) and self.high_search != 'PASS': + sources = prune_objects(mesh.inputs["Target"].get_objects(no_lights=True)) + sourgrp = prune_objects(mesh.inputs["Target"].get_objects(no_lights=True, only_groups=True)) + + # Create pairings of high to low, first ident possible low polys + for obj in targets: + low_name = self.find_ident(self.low_search, obj[0].name, self.low_string, self.low_case) + high_obj = [] + scen_obj = [] + if not low_name: continue + # Create the high poly name string based on the possible low objects name + if self.high_search == 'PASS': + high_obj = sources + else: + if self.high_collect: + for high in sourgrp: + high_name = self.find_ident(self.high_search, high[0].name, self.high_string, self.high_case) + if high_name == low_name: + high_obj += high[1] + if not len(high_obj): + for high in sources: + high_name = self.find_ident(self.high_search, high[0].name, self.high_string, self.high_case) + if high_name == low_name: + high_obj.append(high) + # Check scene objects (if no scene string is set, scene is included as is) + if self.scene_search == 'PASS': + scen_obj = scenes + else: + if self.scene_collect: + for scen in scengrp: + scen_name = self.find_ident(self.scene_search, scen[0].name, self.scene_string, self.scene_case) + if scen_name == low_name: + scen_obj += scen[1] + if not len(scen_obj): + for scen in scenes: + scen_name = self.find_ident(self.scene_search, scen[0].name, self.scene_string, self.scene_case) + if scen_name == low_name: + scen_obj.append(scen) + # Only paired objects will be added + if (len(high_obj) and self.high_search != 'PASS') or (len(scen_obj) and self.scene_search != 'PASS') or self.low_search == 'PASS': + objs.append([obj, high_obj, scen_obj, low_name]) + else: pass + + # Return pruned object list + return objs + + # Get a list of unique objects used as either source or target + def get_unique_objects(self, type, for_auto_cage=False, input=None): + if type not in ['TARGET', 'SOURCE']: + return [] + objs_set = [] + objs = self.get_objects('TARGET', input) + if len(objs): + if for_auto_cage: + settings = self.get_settings(input=input) + if not settings['auto_cage']: + return [] + for obj in objs: + if type == 'TARGET': + if for_auto_cage: + if len(obj[0]) > 2 and obj[0][2]: + continue + objs_set.append([obj[0][0], settings['acage_expansion'], settings['acage_smooth']]) + else: + objs_set.append(obj[0]) + elif type == 'SOURCE': + objs_set.append(obj[1]) + else: + return [] + return objs_set + + search_type = ( + ('PASS', "Pass Through", "Pass all items through without doing any matching", 'ANIM', 0), + ('STARTS', "Starts", "Starts with ID", 'TRIA_RIGHT', 1), + ('ENDS', "Ends", "Ends with ID", 'TRIA_LEFT', 2), + ('SCONTAINS', "Contains ->", "Contains ID, searching from start", 'NEXT_KEYFRAME', 3), + ('ECONTAINS', "Contains <-", "Contains ID, searching from end", 'PREV_KEYFRAME', 4), + ) + + low_search: bpy.props.EnumProperty(name="Target Search", description="How to search for target identifier", items=search_type, default='PASS') + low_string: bpy.props.StringProperty(name="Target Ident", description="Target identifier") + low_case: bpy.props.BoolProperty(name="Target case sensitivity", description="Use case sensitive matching", default=True) + high_string: bpy.props.StringProperty(name="Source Ident", description="Source identifier") + high_search: bpy.props.EnumProperty(name="Source Search", description="How to search for source identifier", items=search_type, default='PASS') + high_collect: bpy.props.BoolProperty(name="Match Collections", description="Try to match a source collection before trying the items inside it", default=True) + high_case: bpy.props.BoolProperty(name="Source case sensitivity", description="Use case sensitive matching", default=True) + scene_string: bpy.props.StringProperty(name="Scene Ident", description="Scene identifier") + scene_search: bpy.props.EnumProperty(name="Scene Search", description="How to search for scene identifier", items=search_type, default='PASS') + scene_collect: bpy.props.BoolProperty(name="Match Collections", description="Try to match a scene collection before trying the items inside it", default=True) + scene_case: bpy.props.BoolProperty(name="Scene case sensitivity", description="Use case sensitive matching", default=True) + + show_groupings: bpy.props.BoolProperty(name="Show Groupings", default=False) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Mesh', "Mesh") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh") + + def draw_buttons(self, context, layout): + colnode = layout.column(align=True) + split_fac = 60 / self.width + split = colnode.split(factor=split_fac) + split.label(text="Target ID:") + row = split.row(align=True) + row1 = row.row(align=True) + row2 = row.row(align=True) + row1.prop(self, "low_string", text="") + row1.prop(self, "low_case", text="", icon_only=True, icon='SMALL_CAPS') + row2.prop(self, "low_search", text="", icon_only=True) + if self.low_search == 'PASS': + row1.enabled = False + + split = colnode.split(factor=split_fac) + split.label(text="Source ID:") + row = split.row(align=True) + row1 = row.row(align=True) + row2 = row.row(align=True) + row1.prop(self, "high_string", text="") + row1.prop(self, "high_collect", text="", icon_only=True, icon='OUTLINER_COLLECTION') + row1.prop(self, "high_case", text="", icon_only=True, icon='SMALL_CAPS') + row2.prop(self, "high_search", text="", icon_only=True) + if self.high_search == 'PASS': + row1.enabled = False + + split = colnode.split(factor=split_fac) + split.label(text="Scene ID:") + row = split.row(align=True) + row1 = row.row(align=True) + row2 = row.row(align=True) + row1.prop(self, "scene_string", text="") + row1.prop(self, "scene_collect", text="", icon_only=True, icon='OUTLINER_COLLECTION') + row1.prop(self, "scene_case", text="", icon_only=True, icon='SMALL_CAPS') + row2.prop(self, "scene_search", text="", icon_only=True) + if self.scene_search == 'PASS': + row1.enabled = False + + def draw_buttons_ext(self, context, layout): + layout.prop(self, 'show_groupings') + if self.show_groupings: + for input in self.inputs: + mesh = get_input(input) + if mesh: + layout.label(text=mesh.get_name()) + box = layout.box() + for obj_grp in self.get_objects('TARGET', input): + boxin = box.box() + col = boxin.column(align=True) + row = col.row() + row.label(text=obj_grp[0][0].name) + row.label(text="", icon=obj_grp[0][0].type + '_DATA') + col.label(text="-Source:") + for hi in obj_grp[1]: + row = col.row() + row.label(text=" " + hi[0].name) + row.label(text="", icon=hi[0].type + '_DATA') + col.label(text="-Scene:") + for scen in obj_grp[2]: + row = col.row() + row.label(text=" " + scen[0].name) + row.label(text="", icon=scen[0].type + '_DATA') + + + +# Settings to be used when baking a billboard +class BakeWrangler_Bake_Billboard(BakeWrangler_Tree_Node, Node): + '''Mesh input node''' + bl_label = 'Input Billboard' + bl_width_default = 240 + + # Inputs are static on this node + def update_inputs(self): + pass + + # Determine if object meets current input filter + def input_filter(self, input_name, object): + if input_name == "Target": + if object.type == 'MESH': + return True + elif input_name == "Source": + if object.type in ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT']: + return True + return False + + # Try to get nodes settings from local or global node + def get_settings(self, validating=False): + settings = get_input(self.inputs["Settings"]) + if not settings: + settings = self.id_data.get_pinned_settings("MeshSettings") + if validating: + return settings + mesh_settings = {} + mesh_settings['ray_dist'] = 0 + mesh_settings['max_ray_dist'] = 0 + mesh_settings['margin'] = settings.margin + mesh_settings['margin_extend'] = settings.margin_extend + mesh_settings['margin_auto'] = settings.margin_auto + #mesh_settings['marginer'] = settings.marginer + #mesh_settings['marginer_fill'] = settings.marginer_fill + #mesh_settings['mask_margin'] = settings.mask_margin + mesh_settings['auto_cage'] = False + mesh_settings['acage_expansion'] = 0 + mesh_settings['acage_smooth'] = 0 + mesh_settings['material_replace'] = False + mesh_settings['material_override'] = None + mesh_settings['material_osl'] = False + mesh_settings['bake_mods'] = False + mesh_settings['bake_mods_invert'] = False + mesh_settings['alpha_bounce'] = self.alpha_bounce + return mesh_settings + + # Check node settings are valid to bake. Returns true/false, plus error message. + def validate(self, check_materials=False, multires=False): + valid = [True] + # Check settings are set somewhere + if not self.get_settings(validating=True): + valid[0] = False + valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected mesh settings found"]) + # Check source objects + has_selected = False + if not multires: + has_selected = len(self.inputs["Source"].get_objects()) > 0 + if has_selected and check_materials: + valid_selected = self.inputs["Source"].validate(check_materials) + # Add any generated messages to the stack. Material errors wont stop bake + if len(valid_selected) > 1: + valid_selected.pop(0) + valid += valid_selected + # Check target meshes + has_active = len(self.inputs["Target"].get_objects(True)) > 0 + if has_active: + valid_active = self.inputs["Target"].validate(check_materials and not has_selected, True, multires) + valid[0] = valid_active.pop(0) + # Add any generated messages to the stack. Errors here will stop bake + if len(valid_active): + valid += valid_active + else: + valid[0] = False + valid.append([_print("Target error", node=self, ret=True), ": No valid target objects selected"]) + return valid + + # Return the requested set of objects from the appropriate input socket + def get_objects(self, set): + #if _prefs("debug"): _print("Getting objects in %s" % (set)) + if set == 'TARGET': + objs = prune_objects(self.inputs["Target"].get_objects(only_mesh=True), True) + elif set == 'SOURCE': + objs = prune_objects(self.inputs["Source"].get_objects(no_lights=True)) + elif set == 'SCENE': + objs = [] + # Return pruned object list + return objs + + # Get a list of unique objects used as either source or target + def get_unique_objects(self, type, for_auto_cage=False): + if for_auto_cage: + return [] + objs = self.get_objects(type) + return objs + + alpha_bounce: bpy.props.IntProperty(name="Alpha Bounces", description="Number of times a ray can pass through transparent surfaces", default=3, min=1) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_MeshSetting', "Settings") + self.inputs.new('BakeWrangler_Socket_Object', "Target") + self.inputs.new('BakeWrangler_Socket_Object', "Source") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh") + + def draw_buttons(self, context, layout): + layout.prop(self, "alpha_bounce") + + +# Bake materials as a texture by projecting them on a plane +class BakeWrangler_Bake_Material(BakeWrangler_Tree_Node, Node): + '''Material input node''' + bl_label = 'Input Material' + bl_width_default = 240 + + # Makes sure there is always one empty input socket at the bottom by adding and removing sockets + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Material', "Material") + + # Hard coded settings as the values are either not used or have only one meaningful possible value for this node + def get_settings(self, validating=False): + mesh_settings = {} + mesh_settings['ray_dist'] = 0 + mesh_settings['max_ray_dist'] = 0 + mesh_settings['margin'] = 0 + mesh_settings['margin_extend'] = False + mesh_settings['margin_auto'] = False + #mesh_settings['marginer'] = False + #mesh_settings['mask_margin'] = 0 + mesh_settings['auto_cage'] = False + mesh_settings['acage_expansion'] = 0 + mesh_settings['acage_smooth'] = 0 + mesh_settings['material_replace'] = False + mesh_settings['material_override'] = None + mesh_settings['material_osl'] = False + mesh_settings['bake_mods'] = False + mesh_settings['bake_mods_invert'] = False + mesh_settings['matbk_width'] = self.mat_width + mesh_settings['matbk_height'] = self.mat_height + return mesh_settings + + # Check node settings are valid to bake. Returns true/false, plus error message. + def validate(self, check_materials=False, multires=False): + valid = [True] + mats = self.get_materials() + # Check is has some materials + if not len(mats): + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": No valid inputs"]) + return valid + # Check the materials can be baked if required + if check_materials: + for mat in mats: + # Is node based? + if not mat[0].node_tree or not mat[0].node_tree.nodes: + valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> not a node based material" % (mat.name)]) + continue + # Is a 'principled' material? + passed = False + for node in mat[0].node_tree.nodes: + if node.type == 'OUTPUT_MATERIAL' and node.target in ['CYCLES', 'ALL']: + if material_recursor(node): + passed = True + break + if not passed: + valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> Output doesn't appear to be a valid combination of Principled and Mix shaders. Baked values will not be correct for this material." % (mat.name)]) + return valid + + # Create a list of unique materials from node inputs + def get_materials(self): + materials = [] + # First collect any objects in the object input + objs = self.inputs[0].get_objects(no_lights=True) + # If there are any objects, collect their materials + for obj in objs: + for mat in obj[0].data.materials: + materials.append([mat]) + # Next just add materials from the remaining inputs + for input in self.inputs: + if input.bl_idname == 'BakeWrangler_Socket_Material': + if input.value is not None: + materials.append([input.value]) + # Now prune out any duplicates + return prune_objects(materials) + + mat_width: bpy.props.FloatProperty(name="Width", description="Width of plane to project material on", precision=3, unit='LENGTH', default=1, min=0) + mat_height: bpy.props.FloatProperty(name="Height", description="Height of plane to project material on", precision=3, unit='LENGTH', default=1, min=0) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Object', "Materials from Objects") + self.inputs.new('BakeWrangler_Socket_Material', "Material") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Material', "Material") + + def draw_buttons(self, context, layout): + col = layout.column(align=True) + col.prop(self, "mat_width") + col.prop(self, "mat_height") + + +# Mesh settings to be used when baking attached objects +class BakeWrangler_Bake_Mesh(BakeWrangler_Tree_Node, Node): + '''Mesh input node''' + bl_label = 'Input Mesh' + bl_width_default = 240 + + # Inputs are static on this node + def update_inputs(self): + pass + + # Determine if object meets current input filter + def input_filter(self, input_name, object): + if input_name == "Target": + if object.type == 'MESH': + return True + elif input_name == "Source": + if object.type in ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT']: + return True + elif input_name == "Scene": + if object.rna_type.identifier == 'Collection': + return True + return False + + # Try to get nodes settings from local or global node + def get_settings(self, validating=False): + settings = get_input(self.inputs["Settings"]) + if not settings: + settings = self.id_data.get_pinned_settings("MeshSettings") + if validating: + return settings + mesh_settings = {} + mesh_settings['ray_dist'] = settings.ray_dist + mesh_settings['max_ray_dist'] = settings.max_ray_dist + mesh_settings['margin'] = settings.margin + mesh_settings['margin_extend'] = settings.margin_extend + mesh_settings['margin_auto'] = settings.margin_auto + #mesh_settings['marginer'] = settings.marginer + #mesh_settings['marginer_fill'] = settings.marginer_fill + #mesh_settings['mask_margin'] = settings.mask_margin + mesh_settings['auto_cage'] = settings.auto_cage + mesh_settings['acage_expansion'] = settings.acage_expansion + mesh_settings['acage_smooth'] = settings.acage_smooth + mesh_settings['material_replace'] = settings.material_replace + mesh_settings['material_override'] = settings.material_override + mesh_settings['material_osl'] = settings.material_osl + mesh_settings['bake_mods'] = settings.bake_mods + mesh_settings['bake_mods_invert'] = _prefs("invert_bakemod") + if self.view_from == 'CAM' and self.view_cam: + mesh_settings['view_from'] = self.view_cam + return mesh_settings + + # Check node settings are valid to bake. Returns true/false, plus error message. + def validate(self, check_materials=False, multires=False): + valid = [True] + # Check settings are set somewhere + if not self.get_settings(validating=True): + valid[0] = False + valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected mesh settings found"]) + # Check source objects + has_selected = False + if not multires: + has_selected = len(self.inputs["Source"].get_objects()) > 0 + if has_selected and check_materials: + valid_selected = self.inputs["Source"].validate(check_materials) + # Add any generated messages to the stack. Material errors wont stop bake + if len(valid_selected) > 1: + valid_selected.pop(0) + valid += valid_selected + # Check target meshes + has_active = len(self.inputs["Target"].get_objects(True)) > 0 + if has_active: + valid_active = self.inputs["Target"].validate(check_materials and not has_selected, True, multires) + valid[0] = valid_active.pop(0) + # Add any generated messages to the stack. Errors here will stop bake + if len(valid_active): + valid += valid_active + else: + valid[0] = False + valid.append([_print("Target error", node=self, ret=True), ": No valid target objects selected"]) + return valid + + # Return the requested set of objects from the appropriate input socket + def get_objects(self, set): + #if _prefs("debug"): _print("Getting objects in %s" % (set)) + if set == 'TARGET': + objs = prune_objects(self.inputs["Target"].get_objects(only_mesh=True), True) + elif set == 'SOURCE': + objs = prune_objects(self.inputs["Source"].get_objects(no_lights=True)) + elif set == 'SCENE': + objs = prune_objects(self.inputs["Scene"].get_objects()) + # Return pruned object list + return objs + + # Get a list of unique objects used as either source or target + def get_unique_objects(self, type, for_auto_cage=False): + if for_auto_cage: + settings = self.get_settings() + if not settings['auto_cage']: + return [] + objs = self.get_objects(type) + if for_auto_cage: + objs_cage = [] + for obj in objs: + if len(obj) > 2 and obj[2]: + continue + else: + objs_cage.append([obj[0], settings['acage_expansion'], settings['acage_smooth']]) + return objs_cage + return objs + + view_orig = ( + ('ABV', "Above Surface", "Cast rays from above surface"), + ('CAM', "Camera", "Cast rays from camera location"), + ) + + view_from: bpy.props.EnumProperty(name="View from", description="Ray casting origin (where applicable)", items=view_orig, default='ABV') + view_cam: bpy.props.PointerProperty(name="View camera", description="Camera to use for ray origins", type=bpy.types.Object) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_MeshSetting', "Settings") + self.inputs.new('BakeWrangler_Socket_Object', "Target") + self.inputs.new('BakeWrangler_Socket_Object', "Source") + self.inputs.new('BakeWrangler_Socket_Object', "Scene") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh") + + def draw_buttons(self, context, layout): + row0 = layout.row(align=True) + row1 = row0.row(align=True) + row2 = row0.row(align=True) + + row1.prop(self, "view_from", text="View") + row2.prop_search(self, "view_cam", bpy.data, "objects", text="", icon='CAMERA_DATA') + + if self.view_from == 'CAM': + row2.enabled = True + else: + row2.enabled = False + + +# Baking node that holds all the settings for a type of bake 'pass'. Takes one or more mesh input nodes as input. +class BakeWrangler_Bake_Pass(BakeWrangler_Tree_Node, Node): + '''Baking pass node''' + bl_label = 'Bake Pass' + bl_width_default = 160 + + # Returns the most identifing string for the node + def get_name(self): + name = BakeWrangler_Tree_Node.get_name(self) + if self.bake_picked: + name += " (%s)" % (self.bake_picked) + return name + + # Makes sure there is always one empty input socket at the bottom by adding and removing sockets + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Mesh', "Mesh") + + # Update node label based on selected pass + def update_pass(self, context): + if self.bake_cat == 'PBR': + pass_enum = self.passes_pbr + pass_bake = self.bake_pbr + elif self.bake_cat == 'CORE': + pass_enum = self.passes_core + pass_bake = self.bake_core + else: + pass_enum = self.passes_wrang + pass_bake = self.bake_wrang + + # Update picked value + self.bake_picked = pass_bake + + if self.label == "": + pass_label = "Pass: " + elif ":" in self.label: + start, sep, end = self.label.rpartition(":") + pass_label = start + ": " + for pas in pass_enum: + if pas[0] == pass_bake: + self.label = pass_label + pas[1] + break + + # Get Mesh node inputs + def get_inputs(self): + meshes = [] + for input in self.inputs: + if input.bl_idname == 'BakeWrangler_Socket_Mesh': + mesh = get_input(input) + if mesh: + if mesh.bl_idname == 'BakeWrangler_Sort_Meshes': + for inpt in mesh.inputs: + if inpt.islinked() and inpt.valid: + meshes.append([mesh, inpt]) + else: + meshes.append([mesh]) + return meshes + + # Try to get nodes settings from local or global node + def get_settings(self, validating=False): + pass_settings = {} + settings = sampsets = get_input(self.inputs["Settings"]) + if not settings or settings.bl_idname == 'BakeWrangler_SampleSettings': + settings = self.id_data.get_pinned_settings("PassSettings") + # Handle sample settings if pass settings found + if settings: + if not sampsets or sampsets.bl_idname != 'BakeWrangler_SampleSettings': + sampsets = None + # See if the settings has a connected samples settings + if "Samples" in settings.inputs.keys(): + sampsets = get_input(settings.inputs["Samples"]) + if not sampsets: + # See if there is a pinned samples settings + sampsets = self.id_data.get_pinned_settings("SampleSettings") + if validating: + return settings + # Make it so having no sample settings node still works + if not sampsets: + pass_settings['bake_samples'] = self.bake_samples if self.bake_samples != 0 else 1 + pass_settings['bake_threshold'] = 0.0 + pass_settings['bake_usethresh'] = False + pass_settings['bake_timelimit'] = 0.0 + else: + if not get_input(self.inputs["Settings"]) and self.bake_samples != 0: + pass_settings['bake_samples'] = self.bake_samples + pass_settings['bake_samples'] = sampsets.bake_samples if 'bake_samples' not in pass_settings else pass_settings['bake_samples'] + pass_settings['bake_threshold'] = sampsets.bake_threshold + pass_settings['bake_usethresh'] = sampsets.bake_usethresh + pass_settings['bake_timelimit'] = sampsets.bake_timelimit + pass_settings['x_res'] = settings.res_bake_x + pass_settings['y_res'] = settings.res_bake_y + pass_settings['bake_device'] = settings.bake_device + pass_settings['interpolate'] = settings.interpolate + pass_settings['use_world'] = settings.use_world + pass_settings['the_world'] = settings.the_world + pass_settings['cpy_render'] = settings.cpy_render + pass_settings['cpy_from'] = settings.cpy_from + pass_settings['tiles'] = settings.use_tiles + pass_settings['tile_size'] = settings.render_tile + pass_settings['threads'] = settings.render_threads + pass_settings['use_bg_col'] = settings.use_bg_col + pass_settings['bg_color'] = settings.bg_color + pass_settings['bake_cat'] = self.bake_cat + pass_settings['bake_type'] = self.bake_picked + pass_settings['use_mask'] = self.use_mask + pass_settings['node_name'] = self.get_name() + pass_settings['norm_s'] = self.norm_space + pass_settings['norm_r'] = self.norm_R + pass_settings['norm_g'] = self.norm_G + pass_settings['norm_b'] = self.norm_B + pass_settings['multi_pass'] = self.multi_pass + pass_settings['multi_samp'] = self.multi_samp + pass_settings['multi_targ'] = self.multi_targ + pass_settings['multi_sorc'] = self.multi_sorc + pass_settings['bev_rad'] = self.bev_rad + pass_settings['bev_samp'] = self.bev_samp + pass_settings['cavity_samp'] = self.cavity_samp + pass_settings['cavity_dist'] = self.cavity_dist + pass_settings['cavity_gamma'] = self.cavity_gamma + pass_settings['cavity_edges'] = self.cavity_edges + pass_settings['curv_mid'] = self.curv_mid + pass_settings['curv_vex'] = self.curv_vex + pass_settings['curv_cav'] = self.curv_cav + pass_settings['curv_vex_max'] = self.curv_vex_max + pass_settings['curv_cav_min'] = self.curv_cav_min + pass_settings['osl_curv_dist'] = self.osl_curv_dist + pass_settings['osl_curv_samp'] = self.osl_curv_samp + pass_settings['osl_curv_cont'] = self.osl_curv_cont + pass_settings['osl_curv_srgb'] = False + pass_settings['osl_height_dist'] = self.osl_height_dist + pass_settings['osl_height_samp'] = self.osl_height_samp + pass_settings['osl_height_midl'] = self.osl_height_midl + pass_settings['osl_height_void'] = self.osl_height_void + pass_settings['vert_col'] = self.vert_col + pass_settings['influences'] = set() + pass_settings['aov_name'] = self.aov_name + pass_settings['aov_input'] = self.aov_input + pass_settings['use_material_vpcolor'] = self.use_material_vpcolor + pass_settings['osl_bentnorm_dist'] = self.osl_bentnorm_dist + pass_settings['osl_bentnorm_samp'] = self.osl_bentnorm_samp + + if self.use_direct: + pass_settings['influences'].add('DIRECT') + if self.use_indirect: + pass_settings['influences'].add('INDIRECT') + if self.use_color: + pass_settings['influences'].add('COLOR') + if self.bake_picked == 'COMBINED': + if self.use_diffuse: + pass_settings['influences'].add('DIFFUSE') + if self.use_glossy: + pass_settings['influences'].add('GLOSSY') + if self.use_transmission: + pass_settings['influences'].add('TRANSMISSION') + #if self.use_ao: + # pass_settings['influences'].add('AO') + if self.use_emit: + pass_settings['influences'].add('EMIT') + if self.use_bg_col: + pass_settings['use_bg_col'] = self.use_bg_col + pass_settings['bg_color'] = self.bg_color + return pass_settings + + # Check node settings are valid to bake. Returns true/false, plus error message(s). + def validate(self, is_primary=False): + valid = [True] + # Validate has Settings + self.update_pass(None) + if not self.get_settings(validating=True): + valid[0] = False + valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected pass settings found"]) + # Validate inputs + has_valid_input = False + is_multires = (self.bake_cat == 'CORE' and self.bake_core == 'MULTIRES') + for input in self.inputs: + if input.bl_idname == 'BakeWrangler_Socket_Mesh' and input.islinked() and input.valid: + if self.bake_cat == 'PBR': + input_valid = get_input(input).validate(check_materials=True) + else: + input_valid = get_input(input).validate(multires=is_multires) + if not input_valid.pop(0): + valid[0] = False + else: + has_valid_input = True + if len(input_valid): + valid += input_valid + errs = len(valid) + if not has_valid_input and errs < 2: + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"]) + # Validate outputs + if is_primary: + has_valid_output = False + for output in self.outputs: + if output.is_linked: + for link in gather_output_links(output): + if link.is_valid and link.to_socket.valid: + output_valid = link.to_node.validate() + if not output_valid.pop(0): + valid[0] = False + else: + has_valid_output = True + if len(output_valid): + valid += output_valid + if not has_valid_output and errs == len(valid): + valid[0] = False + valid.append([_print("Output error", node=self, ret=True), ": No valid outputs connected"]) + # Validated + return valid + + pass_cats = ( + ('PBR', "PBR", "Bake passes for PBR materials. Most require the material to use a Principled BSDF"), + ('CORE', "Blender", "Standard bake passes normally found in blender. Passes with the same name in a different category will have different behavior"), + ('WRANG', "Wrangler", "Additional passes provided in Bake Wrangler that aren't PBR specific"), + ) + + passes_pbr = ( + ('ALBEDO', "Albedo", "Surface color without lighting (Principled shader only)"), + ('SUBSURFACE', "Subsurface", "Multiplies with the subsurface radius to determine distance light travels below the surface (Principled shader only)"), + ('SUBRADIUS', "Subsurf Radius", "Distance light scatters below the surface, each channel is for that R/G/B color of light (a higher R value will mean red light travels deeper) (Principled shader only)"), + ('SUBCOLOR', "Subsurf Color", "Base subsurface scattering color (Principled shader only)"), + ('METALLIC', "Metallic", "Surface 'metalness' values (Principled shader only)"), + ('SPECULAR', "Specular", "Surface dielectric specular reflection values (Princpled shader only)"), + ('SPECTINT', "Specular Tint", "Tint of direct facing reflections (Note: dielectrics have colorless reflections, but this allows faking some effects) (Principled shader only)"), + ('ROUGHNESS', "Roughness", "Surface roughness values with no other influences (Principled shader only)"), + ('SMOOTHNESS', "Smoothness", "Surface inverted roughness values with no other influences (Principled shader only)"), + ('ANISOTROPIC', "Anisotropic", "Amount of anisotropy for specular reflections (Principled shader only)"), + ('ANISOROTATION', "Aniso Rotation", "Rotation direction of anisotropy (Principled shader only)"), + ('SHEEN', "Sheen", "Amount of soft velvet like reflections near edges for cloth like materials (Principled shader only)"), + ('SHEENTINT', "Sheen Tint", "Color mixed with white for sheen reflections (Principled shader only)"), + ('CLEARCOAT', "Clearcoat", "Extra white specular layer on surface of material (Principled shader only)"), + ('CLEARROUGH', "Clear Roughness", "Roughness values of the clearcoat (Principled shader only)"), + ('TRANSIOR', "IOR", "Index of refraction used for transmission values (Principled shader only)"), + ('TRANSMISSION', "Transmission", "Opacity values of surface with no other influences (Principled shader only)"), + ('TRANSROUGH', "Trans Roughness", "Roughness values for light transmitted completely through the surface (Principled shader only)"), + ('EMIT', "Emission", "Surface self emission color values with no other influences (Principled shader only)"), + ('ALPHA', "Alpha", "Surface transparency values (Principled shader only)"), + ('TEXNORM', "Texture Normals", "Surface normals influenced only by texture values (no geometry)"), + ('CLEARNORM', "Clearcoat Normals", "Surface normals influenced only by the clearcoat values"), + ('OBJNORM', "Geometry Normals", "Surface normals ignoring any influence from textures"), + ('AOV', "AOV Node", "The color or value channel of a named AOV node in the materials"), + ('BBNORM', "Billboard Normals", "Normals using target billboards rotation as tangent space"), + ) + + passes_core = ( + ('COMBINED', "Combined", "Combine multiple passes into a single bake"), + ('AO', "Ambient Occlusion", "Surface self occlusion values"), + ('SHADOW', "Shadow", "Shadow map"), + ('NORMAL', "Normal", "Surface normals"), + ('UV', "UV", "UV Layout"), + ('ROUGHNESS', "Roughness", "Surface roughness values"), + ('SMOOTHNESS', "Smoothness", "Surface inverted roughness values"), + ('EMIT', "Emit", "Surface self emission color values"), + ('ENVIRONMENT', "Environment", "Colors coming from the environment"), + ('DIFFUSE', "Diffuse", "Colors of a surface generated by a diffuse shader"), + ('GLOSSY', "Glossy", "Colors of a surface generated by a glossy shader"), + ('TRANSMISSION', "Transmission", "Colors of light passing through a material"), + ('MULTIRES', "Multiresolution", "Data from a multiresolution modifier"), + ) + + passes_wrang = ( + ('BEVMASK', "Bevel Mask", "Map of bevels where beveled areas will be baked in white"), + ('BEVNORMEMIT', "Bevel Normals (Emit)", "Normal map with only bevel influences (can bake from one object to another, but inverted faces will be backwards)"), + ('BEVNORMNORM', "Bevel Normals (Norm)", "Normal map with only bevel influences (does not work for baking from one object to another, but handles inverted faces)"), + ('CAVITY', "Cavity/Edges", "Surface cavity occlusion or edges map"), + ('CURVATURE', "Curvature", "Surface curvature map"), + #('OSL_CURV', "Curvature (OSL)", "OSL implementation of surface curvature map (CPU only)"), + ('OSL_HEIGHT', "Height (OSL)", "Height map created by the distance between two surfaces (OSL shader only supports CPU)"), + ('ISLANDID', "Island ID", "Map where each island of faces are baked in a different color"), + ('MATID', "Material ID", "Map where each material is baked in a random solid color (based on their name)"), + ('OBJCOL', "Object Color", "Each object is baked using its assigned viewport color"), + ('WORLDPOS', "Position", "Areas of the object are baked with colors representing their position in the world"), + ('THICKNESS', "Thickness", "Mesh thickness is baked from white (thin) to black (thick)"), + ('VERTCOL', "Vertex Color", "Selected vertex colors are baked as the surface color"), + ('OSL_BENTNORM', "Bent Normals (OSL)", "Bends normals based on ambient occlusion giving a directional bias"), + ('MASKPASS', "UV Mask", "Black and white mask of pixels covered by UV islands"), + ) + + passes_all = passes_pbr + passes_core + passes_wrang + + bake_has_influence = ['SUBSURFACE', 'TRANSMISSION', 'GLOSSY', 'DIFFUSE', 'COMBINED'] + + normal_spaces = ( + ('TANGENT', "Tangent", "Bake the normals in tangent space"), + ('OBJECT', "Object", "Bake the normals in object space"), + ) + + normal_swizzle = ( + ('POS_X', "+X", ""), + ('POS_Y', "+Y", ""), + ('POS_Z', "+Z", ""), + ('NEG_X', "-X", ""), + ('NEG_Y', "-Y", ""), + ('NEG_Z', "-Z", ""), + ) + + multires_subpasses = ( + ('NORMALS', "Normals", "Bake Normals"), + ('DISPLACEMENT', "Displacement", "Bake Displacement"), + ) + + multires_sampling = ( + ('MAXIMUM', "Max to Min", "Bake the highest resolution down to the lowest"), + ('FROMMOD', "Modifier Values", "Bake from the current render resolution to the current preview resolution"), + ('CUSTOM', "Custom Values", "Choose custom values for the target and source resolutions"), + ) + + aov_inputs = ( + ('COL', "Color", "Use color data"), + ('VAL', "Value", "Use value data"), + ) + + bake_result: bpy.props.PointerProperty(name="bake_result", description="Used internally by BW", type=bpy.types.Image) + mask_result: bpy.props.PointerProperty(name="mask_result", description="Used internally by BW", type=bpy.types.Image) + sbake_result: bpy.props.PointerProperty(name="sbake_result", description="Used internally by BW", type=bpy.types.Image) + smask_result: bpy.props.PointerProperty(name="smask_result", description="Used internally by BW", type=bpy.types.Image) + + bake_cat: bpy.props.EnumProperty(name="Group", description="Category to select bake passes from", items=pass_cats, default='PBR', update=update_pass) + bake_pbr: bpy.props.EnumProperty(name="Pass", description="Type of PBR pass to bake", items=passes_pbr, default='ALBEDO', update=update_pass) + bake_core: bpy.props.EnumProperty(name="Pass", description="Type of Blender standard pass to bake", items=passes_core, default='COMBINED', update=update_pass) + bake_wrang: bpy.props.EnumProperty(name="Pass", description="Type of Wrangler pass to bake", items=passes_wrang, default='BEVMASK', update=update_pass) + bake_picked: bpy.props.EnumProperty(name="Picked", description="Selected bake type", items=passes_all) + bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 25 to 50 samples for most bake types (AO may look better with more).\nQuality is gained by increaseing resolution rather than samples past that point", default=0, min=0) + + adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False) + + use_mask: bpy.props.BoolProperty(name="Use Masking", description="Generate a map of changed UV islands to use as a mask when updating pixel values. Allows layering of multiple passes onto a single image so long as they don't overlap", default=False) + use_direct: bpy.props.BoolProperty(name="Direct", description="Add direct lighting contribution", default=True) + use_indirect: bpy.props.BoolProperty(name="Indirect", description="Add indirect lighting contribution", default=True) + use_color: bpy.props.BoolProperty(name="Color", description="Color the pass", default=True) + use_diffuse: bpy.props.BoolProperty(name="Diffuse", description="Add diffuse contribution", default=True) + use_glossy: bpy.props.BoolProperty(name="Glossy", description="Add glossy contribution", default=True) + use_transmission: bpy.props.BoolProperty(name="Transmission", description="Add transmission contribution", default=True) + #use_subsurface: bpy.props.BoolProperty(name="Subsurface", description="Add subsurface contribution", default=True) + #use_ao: bpy.props.BoolProperty(name="Ambient Occlusion", description="Add ambient occlusion contribution", default=True) + use_emit: bpy.props.BoolProperty(name="Emit", description="Add emission contribution", default=True) + + norm_space: bpy.props.EnumProperty(name="Space", description="Space to bake the normals in", items=normal_spaces, default='TANGENT') + norm_R: bpy.props.EnumProperty(name="R", description="Axis to bake in Red channel", items=normal_swizzle, default='POS_X') + norm_G: bpy.props.EnumProperty(name="G", description="Axis to bake in Green channel", items=normal_swizzle, default='POS_Y') + norm_B: bpy.props.EnumProperty(name="B", description="Axis to bake in Blue channel", items=normal_swizzle, default='POS_Z') + multi_pass: bpy.props.EnumProperty(name="Multires Type", description="Type of multiresolution pass to bake", items=multires_subpasses, default='NORMALS') + multi_samp: bpy.props.EnumProperty(name="Multires Method", description="Method to pick multiresolution source and target", items=multires_sampling, default='MAXIMUM') + multi_targ: bpy.props.IntProperty(name="Multires Target", description="Subdivision level for target of bake", default=0, min=0, soft_max=16) + multi_sorc: bpy.props.IntProperty(name="Multires Source", description="Subdivision level for source of bake", default=8, min=0, soft_max=16) + + bev_rad: bpy.props.FloatProperty(name="Bevel Radius", description="Width of bevel on edges", default=0.05, min=0) + bev_samp: bpy.props.IntProperty(name="Bevel Samples", description="Number of samples to take (more gives greater accuracy at cost of time. A value of 4 works well in most cases and noise can be resolved by using more bake pass samples instead of increasing this value)", min=2, max=16, default=4) + cavity_samp: bpy.props.IntProperty(name="Cavity Over Samples", description="Number of samples to take (more gives a more accurate result but takes longer, increase bake pass samples to reduce noise before increasing this value)", default=16, min=1, max=128) + cavity_dist: bpy.props.FloatProperty(name="Cavity Sample Distance", description="How far away a face can be to contribute to the calculation (may need larger distances for larger objects)", default=0.4, step=1, min=0.0, unit='LENGTH') + cavity_gamma: bpy.props.FloatProperty(name="Cavity Gamma", description="Gamma transform to be performed on cavity values", default=1.0, step=1) + cavity_edges: bpy.props.BoolProperty(name="Edge Mode", description="Inverts the cavity map normals to find edges. If it's too dark, lower distance value", default=False) + curv_mid: bpy.props.FloatProperty(name="Curvature Flat", description="Value to assign to the mid-point between curves (flat area)", default=0.5, min=0.0, max=1.0) + curv_vex: bpy.props.FloatProperty(name="Curvature Convex", description="Value to assign to the sharpest (maximum) point of a convex curve", default=1.0, min=0.0, max=1.0) + curv_cav: bpy.props.FloatProperty(name="Curvature Concave", description="Value to assign to the sharpest (minimum) point of a concave curve", default=0.0, min=0.0, max=1.0) + curv_vex_max: bpy.props.FloatProperty(name="Curvature Convex Max", description="Convex value to consider the maximum curvature (higher values create a greater range)", default=0.2, min=0.0001, max=1.0) + curv_cav_min: bpy.props.FloatProperty(name="Curvature Concave Min", description="Concave value to consider the minimum curvature (lower values create a greater range)", default=0.8, min=0.0, max=0.9999) + osl_curv_dist: bpy.props.FloatProperty(name="Curvature Distance (OSL)", description="Distance to search for a neighboring surface", default=0.1, min=0.0) + osl_curv_samp: bpy.props.IntProperty(name="Curvature Samples (OSL)", description="Number of attempts to try finding a neighboring surface within the distance value", default=16, min=1) + osl_curv_cont: bpy.props.FloatProperty(name="Curvature Contrast (OSL)", description="Contrast level applied to curvature value", default=0.0, min=0.0) + osl_height_dist: bpy.props.FloatProperty(name="Height Distance", description="Maximum distance from source to look for a surface", default=0.1, min=0.0) + osl_height_samp: bpy.props.IntProperty(name="Height Samples", description="How many surfaces to consider for each point. Must be at least two but complex objects with faces close together may need more for correct results", default=2, min=2) + osl_height_midl: bpy.props.FloatProperty(name="Height Mid Level", description="Value to use as the middel level (neither high or low)", default=0.5, min=0.0, max=1.0) + osl_height_void: bpy.props.FloatVectorProperty(name="Height Void Color", description="Color to fill in areas where no height value could be found within the search distance", default=[0.5,0.5,0.5,1.0], size=4, subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10) + vert_col: bpy.props.StringProperty(name="Vertex Color Layer", description="Vertex color layer to bake from (leaving blank will use active)", default="") + use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False) + bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4) + aov_name: bpy.props.StringProperty(name="AOV Name", description="Name assigned to AOV node you want to bake from", default="") + aov_input: bpy.props.EnumProperty(name="AOV Source", description="Take data from either color or value input of AOV node", items=aov_inputs, default='COL') + use_material_vpcolor: bpy.props.BoolProperty(name="Use Viewport Color", description="Use the materials assigned viewport color instead of generating a random one based on its name", default=False) + use_subtraction: bpy.props.BoolProperty(name="Use Subtraction", description="Subtract the object normals from the material normals to isolate them. This results in correct tangent normal rotations but may not be as clean a result", default=True) + osl_bentnorm_dist: bpy.props.FloatProperty(name="Bent Normals Distance", description="Maximum distance for a surface to contribute to ambient occlusion", default=1.0, min=0.001) + osl_bentnorm_samp: bpy.props.IntProperty(name="Height Samples", description="Number of ambient light samples to take for each surface point", default=8, min=1) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Set label to pass + self.update_pass(context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_PassSetting', "Settings") + self.inputs.new('BakeWrangler_Socket_Mesh', "Mesh") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Color', "Color") + # Prefs + self.adv_settings = _prefs("def_show_adv") + + def draw_buttons(self, context, layout): + colnode = layout.column(align=False) + + colpass = colnode.column(align=True) + colpass.prop(self, "bake_cat") + if self.bake_cat == 'PBR': + colpass.prop(self, "bake_pbr") + elif self.bake_cat == 'CORE': + colpass.prop(self, "bake_core") + else: + colpass.prop(self, "bake_wrang") + + advrow = colnode.row() + advrow.alignment = 'LEFT' + + if not self.adv_settings: + adv = advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:") + advrow.separator() + else: + adv = advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:") + advrow.separator() + + col = colnode.column(align=True) + col.prop(self, "use_mask", toggle=True) + bg_col = col.row(align=True) + bg_col.prop(self, "use_bg_col", toggle=True) + if self.use_bg_col: + bg_col.prop(self, "bg_color", toggle=True, text="") + + splitopt = colnode.split(factor=0.5) + colopttxt = splitopt.column(align=True) + colopttxt.alignment = 'RIGHT' + coloptval = splitopt.column(align=True) + + # Additional options for PBR passes + if self.bake_cat == 'PBR': + # AOV ouput + if self.bake_pbr == 'AOV': + colopttxt.label(text="Name:") + colopttxt.label(text="Input:") + + coloptval.prop(self, "aov_name", text="") + coloptval.prop(self, "aov_input", text="") + # Additional options for 'Wrangler' passes + if self.bake_cat == 'WRANG': + # Bevel mask + if self.bake_wrang in ['BEVMASK', 'BEVNORMEMIT', 'BEVNORMNORM']: + colopttxt.label(text="Samples:") + colopttxt.label(text="Radius:") + + coloptval.prop(self, "bev_samp", text="") + coloptval.prop(self, "bev_rad", text="") + elif self.bake_wrang == 'CAVITY': + colopttxt.label(text="Edge Mode:") + colopttxt.label(text="Over Samples:") + colopttxt.label(text="Distance:") + colopttxt.label(text="Gamma:") + + coloptval.prop(self, "cavity_edges", text="") + coloptval.prop(self, "cavity_samp", text="") + coloptval.prop(self, "cavity_dist", text="") + coloptval.prop(self, "cavity_gamma", text="") + elif self.bake_wrang == 'THICKNESS': + colopttxt.label(text="Over Samples:") + colopttxt.label(text="Distance:") + + coloptval.prop(self, "cavity_samp", text="") + coloptval.prop(self, "cavity_dist", text="") + elif self.bake_wrang == 'CURVATURE': + colopttxt.label(text="Flat:") + colopttxt.label(text="Convex:") + colopttxt.label(text="Convex Max:") + colopttxt.label(text="Concave:") + colopttxt.label(text="Concave Min:") + + coloptval.prop(self, "curv_mid", text="") + coloptval.prop(self, "curv_vex", text="") + coloptval.prop(self, "curv_vex_max", text="") + coloptval.prop(self, "curv_cav", text="") + coloptval.prop(self, "curv_cav_min", text="") + elif self.bake_wrang == 'OSL_CURV': + colopttxt.label(text="Distance:") + colopttxt.label(text="Samples:") + colopttxt.label(text="Contrast:") + + coloptval.prop(self, "osl_curv_dist", text="") + coloptval.prop(self, "osl_curv_samp", text="") + coloptval.prop(self, "osl_curv_cont", text="") + elif self.bake_wrang == 'OSL_HEIGHT': + colopttxt.label(text="Distance:") + colopttxt.label(text="Samples:") + colopttxt.label(text="Mid Level:") + colopttxt.label(text="Void Color:") + + coloptval.prop(self, "osl_height_dist", text="") + coloptval.prop(self, "osl_height_samp", text="") + coloptval.prop(self, "osl_height_midl", text="") + coloptval.prop(self, "osl_height_void", text="") + elif self.bake_wrang == 'VERTCOL': + colopttxt.label(text="Layer:") + + coloptval.prop(self, "vert_col", text="") + elif self.bake_wrang == 'MATID': + colnode.prop(self, "use_material_vpcolor") + elif self.bake_wrang == 'OSL_BENTNORM': + colopttxt.label(text="Distance:") + colopttxt.label(text="Samples:") + + coloptval.prop(self, "osl_bentnorm_dist", text="") + coloptval.prop(self, "osl_bentnorm_samp", text="") + + # Additional options for 'core' passes + if self.bake_cat == 'CORE': + # Multires + if self.bake_core == 'MULTIRES': + colopttxt.label(text="Type:") + colopttxt.label(text="Method:") + + coloptval.prop(self, "multi_pass", text="") + coloptval.prop(self, "multi_samp", text="") + if self.multi_samp == 'CUSTOM': + colopttxt.label(text="Target Divs:") + colopttxt.label(text="Source Divs:") + + coloptval.prop(self, "multi_targ", text="") + coloptval.prop(self, "multi_sorc", text="") + elif self.bake_core in self.bake_has_influence: + row = colnode.row(align=True) + row.use_property_split = False + row.prop(self, "use_direct", toggle=True) + row.prop(self, "use_indirect", toggle=True) + if self.bake_core != 'COMBINED': + row.prop(self, "use_color", toggle=True) + else: + col = colnode.column(align=True) + col.prop(self, "use_diffuse") + col.prop(self, "use_glossy") + col.prop(self, "use_transmission") + #col.prop(self, "use_subsurface") + #col.prop(self, "use_ao") + col.prop(self, "use_emit") + # Any normal map passes + if (self.bake_cat == 'PBR' and self.bake_pbr in ['TEXNORM', 'CLEARNORM', 'OBJNORM', 'BBNORM']) \ + or (self.bake_cat == 'CORE' and self.bake_core in ['NORMAL']) \ + or (self.bake_cat == 'WRANG' and self.bake_wrang in ['BEVNORMEMIT', 'BEVNORMNORM', 'OSL_BENTNORM']): + if self.bake_picked == 'TEXNORM': + colnode.prop(self, "use_subtraction") + colopttxt.label(text="Space:") + colopttxt.label(text="R:") + colopttxt.label(text="G:") + colopttxt.label(text="B:") + + coloptval.prop(self, "norm_space", text="") + coloptval.prop(self, "norm_R", text="") + coloptval.prop(self, "norm_G", text="") + coloptval.prop(self, "norm_B", text="") + + + +# The channel map combines inputs by mapping them to RGBA channels of the output and sits between passes and output +# images +class BakeWrangler_Channel_Map(BakeWrangler_Tree_Node, Node): + '''Channel map node''' + bl_label = 'Channel Map' + + def update_inputs(self): + pass + + def validate(self): + return BakeWrangler_Tree_Node.validate(self, True) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Color', "Color") + self.inputs.new('BakeWrangler_Socket_ChanMap', "Red") + self.inputs.new('BakeWrangler_Socket_ChanMap', "Green") + self.inputs.new('BakeWrangler_Socket_ChanMap', "Blue") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Color', "Color") + + + +# Mix RGB via a factor and operation +class BakeWrangler_Post_MixRGB(BakeWrangler_Tree_Node, Node): + '''Mix RGB node''' + bl_label = 'Mix RGB' + + def update_inputs(self): + pass + + def validate(self): + return BakeWrangler_Tree_Node.validate(self, True, True) + + ops = ( + ('MIX', "Mix", ""), + ('ADD', "Add", ""), + ('SUBTRACT', "Subtract", ""), + ('MULTIPLY', "Multiply", ""), + ('DIVIDE', "Divide", ""), + ('OVERLAY', "Overlay", ""), + ) + + op: bpy.props.EnumProperty(name="Operator", description="Mathematical operation to perform", items=ops, default='MIX') + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Float', "Fac") + self.inputs.new('BakeWrangler_Socket_Color', "Color1") + self.inputs.new('BakeWrangler_Socket_Color', "Color2") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Color', "Color") + + def draw_buttons(self, context, layout): + layout.prop(self, "op", text="") + + + +# Split RGB ouput into channels +class BakeWrangler_Post_SplitRGB(BakeWrangler_Tree_Node, Node): + '''Split RGB node''' + bl_label = 'Split RGB' + + def update_inputs(self): + pass + + def validate(self): + return BakeWrangler_Tree_Node.validate(self, True) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Color', "Color") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Float', "Red") + self.outputs.new('BakeWrangler_Socket_Float', "Green") + self.outputs.new('BakeWrangler_Socket_Float', "Blue") + + + +# Join channels into single RGB color +class BakeWrangler_Post_JoinRGB(BakeWrangler_Tree_Node, Node): + '''Join RGB node''' + bl_label = 'Join RGB' + + def update_inputs(self): + pass + + def validate(self): + return BakeWrangler_Tree_Node.validate(self, True, True) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Float', "Red") + self.inputs.new('BakeWrangler_Socket_Float', "Green") + self.inputs.new('BakeWrangler_Socket_Float', "Blue") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Color', "Color") + + + +# Perform math functions on image data +class BakeWrangler_Post_Math(BakeWrangler_Tree_Node, Node): + '''Math node''' + bl_label = 'Math' + + def update_inputs(self): + pass + + def validate(self): + return BakeWrangler_Tree_Node.validate(self, True, True) + + # Disable inputs based on function + def chg_op(self, context): + if self.op in ['FLOOR', 'CEIL']: + self.inputs[1].enabled = False + else: + self.inputs[1].enabled = True + + ops = ( + ('ADD', "Add", "A + B"), + ('SUBTRACT', "Subtract", "A - B"), + ('MULTIPLY', "Multiply", "A * B"), + ('DIVIDE', "Divide", "A / B"), + + ('POWER', "Power", "A^B"), + ('LOGARITHM', "Logarithm", "Log A base B"), + + ('FLOOR', "Floor", "Largest integer <= A"), + ('CEIL', "Ceil", "Smallest integer >= A"), + ('MODULO', "Modulo", "Mod A / B"), + ) + + op: bpy.props.EnumProperty(name="Operator", description="Mathematical operation to perform", items=ops, update=chg_op, default='ADD') + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Float', "Value", identifier="0") + self.inputs.new('BakeWrangler_Socket_Float', "Value", identifier="1") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Float', "Value") + + def draw_buttons(self, context, layout): + layout.menu("BW_MT_math_ops", text=layout.enum_item_name(self, "op", self.op)) + +# Menu displayed in the math node to select function +# +# Found out headers can be created in enums by setting all but name to empty string +# eg ("", "ColumnName", "") - So this could probably be replaced with that, but w/e +# +class BakeWrangler_Post_Math_OpMenu(bpy.types.Menu): + bl_idname = "BW_MT_math_ops" + bl_label = "Ops" + + def draw(self, context): + layout = self.layout.row() + layout.alignment = 'LEFT' + col1 = layout.column() + col1.alignment = 'LEFT' + col1.label(text="Functions") + col1.separator() + for op in context.node.ops: + if op[0] in ['ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE']: + itm = col1.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1]) + BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name) + col1.separator() + for op in context.node.ops: + if op[0] in ['POWER', 'LOGARITHM']: + itm = col1.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1]) + BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name) + col2 = layout.column() + col2.alignment = 'LEFT' + col2.label(text="Rounding") + col2.separator() + for op in context.node.ops: + if op[0] in ['FLOOR', 'CEIL', 'MODULO']: + itm = col2.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1]) + BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name) + + + +# Apply gamma transform to color +class BakeWrangler_Post_Gamma(BakeWrangler_Tree_Node, Node): + '''Gamma node''' + bl_label = 'Gamma' + + def update_inputs(self): + pass + + def validate(self): + return BakeWrangler_Tree_Node.validate(self, True) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Color', "Color") + self.inputs.new('BakeWrangler_Socket_Float', "Gamma") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Color', "Color") + + + +# Output node that specifies the path to a file where a bake should be saved along with size and format information. +# Takes input from the outputs of a bake pass node. Connecting multiple inputs will cause higher position inputs to +# be over written by lower ones. Eg: Having a color input and an R input would cause the R channel of the color data +# to be overwritten by the data connected tot he R input. +class BakeWrangler_Output_Image_Path(BakeWrangler_Tree_Node, Node): + '''Output image path node''' + bl_label = 'Output Image Path' + bl_width_default = 160 + + # Returns the most identifying string for the node + def get_name(self): + name = BakeWrangler_Tree_Node.get_name(self) + if self.inputs['Split Output'].get_name(): + name += " (%s)" % (self.inputs['Split Output'].get_name()) + return name + + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Color', "Color", {'Alpha':'BakeWrangler_Socket_ChanMap'}) + settings = self.get_settings() + if settings: + if settings['img_color_mode'] != 'RGBA': + for input in self.inputs: + if input.name == 'Alpha' and input.enabled: + input.enabled = False + else: + idx = 0 + for input in self.inputs: + if input.name == 'Color': + if input.is_linked: + self.inputs[idx+1].enabled = True + else: + self.inputs[idx+1].enabled = False + idx += 1 + + # Try to get nodes settings from local or global node + def get_settings(self, validating=False): + settings = None + if 'Settings' in self.inputs: + settings = get_input(self.inputs["Settings"]) + if not settings: + settings = self.id_data.get_pinned_settings("OutputSettings") + if validating: + img_non_color = None + if 'Non-Color' in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys(): + img_non_color = 'Non-Color' + elif _prefs('img_non_color') is not None and (len(bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys()) - 1) >= int(_prefs('img_non_color')): + img_non_color = bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys()[_prefs('img_non_color')] + settings.img_non_color = img_non_color + return settings, img_non_color + if not settings: + # No local or pinned settings + return None + output_settings = {} + output_settings['img_non_color'] = settings.img_non_color + output_settings['img_xres'] = settings.img_xres + output_settings['img_yres'] = settings.img_yres + output_settings['img_clear'] = settings.img_clear + output_settings['fast_aa'] = settings.fast_aa + output_settings['fast_aa_lvl'] = settings.fast_aa_lvl + output_settings['img_type'] = settings.img_type + output_settings['img_udim'] = settings.img_udim + output_settings['img_color_space'] = settings.img_color_space + output_settings['img_use_float'] = settings.img_use_float + output_settings['img_jpeg2k_cinema'] = settings.img_jpeg2k_cinema + output_settings['img_jpeg2k_cinema48'] = settings.img_jpeg2k_cinema48 + output_settings['img_jpeg2k_ycc'] = settings.img_jpeg2k_ycc + output_settings['img_dpx_log'] = settings.img_dpx_log + output_settings['img_openexr_zbuff'] = settings.img_openexr_zbuff + output_settings['marginer'] = settings.marginer + output_settings['marginer_size'] = settings.marginer_size + output_settings['marginer_fill'] = settings.marginer_fill + + output_settings['img_color_mode'] = None + if settings.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']: + output_settings['img_color_mode'] = settings.img_color_mode_noalpha + elif settings.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']: + output_settings['img_color_mode'] = settings.img_color_mode + + output_settings['img_color_depth'] = None + if settings.img_type in ['PNG', 'TIFF']: + output_settings['img_color_depth'] = settings.img_color_depth_8_16 + elif settings.img_type == 'JPEG2000': + output_settings['img_color_depth'] = settings.img_color_depth_8_12_16 + elif settings.img_type == 'DPX': + output_settings['img_color_depth'] = settings.img_color_depth_8_10_12_16 + elif settings.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']: + output_settings['img_color_depth'] = settings.img_color_depth_16_32 + + output_settings['img_quality'] = None + if settings.img_type == 'PNG': + output_settings['img_quality'] = settings.img_compression + elif settings.img_type in ['JPEG', 'JPEG2000']: + output_settings['img_quality'] = settings.img_quality + + output_settings['img_codec'] = None + if settings.img_type == 'JPEG2000': + output_settings['img_codec'] = settings.img_codec_jpeg2k + elif settings.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']: + output_settings['img_codec'] = settings.img_codec_openexr + elif settings.img_type == 'TIFF': + output_settings['img_codec'] = settings.img_codec_tiff + + output_settings['img_path'] = self.get_full_path() + + return output_settings + + # Check node settings are valid to bake. Returns true/false, plus error message(s). + def validate(self, is_primary=False): + valid = [True] + # Validate has Settings + settings, none_color = self.get_settings(True) + if not settings: + valid[0] = False + valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected output settings found"]) + return valid + if not none_color: + valid[0] = False + valid.append([_print("Non-standard color spaces", node=self, ret=True), ": Please set up your color space in addon preferences"]) + return valid + # Validate inputs + has_valid_input = False + for input in self.inputs: + if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid: + if not is_primary: + has_valid_input = True + break + else: + input_valid = get_input(input).validate() + valid[0] = input_valid.pop(0) + if valid[0]: + has_valid_input = True + valid += input_valid + errs = len(valid) + if not has_valid_input and errs < 2: + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"]) + # Validate file path + self.img_path = self.get_full_path() + if not os.path.isdir(os.path.abspath(self.img_path)): + # Try creating the path if enabled in prefs + if _prefs("make_dirs") and not os.path.exists(os.path.abspath(self.img_path)): + try: + os.makedirs(os.path.abspath(self.img_path)) + except OSError as err: + valid[0] = False + valid.append([_print("Path error", node=self, ret=True), ": Trying to create path at '%s'" % (err.strerror)]) + return valid + else: + valid[0] = False + valid.append([_print("Path error", node=self, ret=True), ": Invalid path '%s'" % (os.path.abspath(self.img_path))]) + return valid + # Check if there is read/write access to the file/directory + settings = self.get_settings() + file_path = os.path.join(os.path.abspath(self.img_path), self.name_with_ext(settings['img_type'])) + if os.path.exists(file_path): + if os.path.isfile(file_path): + # It exists so try to open it r/w + try: + file = open(file_path, "a") + except OSError as err: + valid[0] = False + valid.append([_print("File error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)]) + else: + # See if it can be read as an image + file.close() + file_img = bpy.data.images.load(file_path) + if not len(file_img.pixels): + valid[0] = False + valid.append([_print("File error", node=self, ret=True), ": File exists but doesn't seem to be a known image format"]) + bpy.data.images.remove(file_img) + else: + # It exists but isn't a file + valid[0] = False + valid.append([_print("File error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)]) + else: + # See if it can be created + try: + file = open(file_path, "a") + except OSError as err: + valid[0] = False + valid.append([_print("File error", node=self, ret=True), ": %s trying to create file at '%s'" % (err.strerror, file_path)]) + else: + file.close() + os.remove(file_path) + # Validated + return valid + + # Get full path, removing any relative references + def get_full_path(self): + return self.inputs["Split Output"].get_full_path() + + # Return the file name with the correct image type extension (unless it has an existing unknown extension) + def name_with_ext(self, suffix=""): + settings = self.get_settings() + return self.inputs["Split Output"].name_with_ext(suffix, settings['img_type']) + + # Return frame ranges or padding or animated seed if set, otherwise empty list, None or False + def frame_range(self, padding=False, animated=False): + return self.inputs["Split Output"].frame_range(padding, animated) + + # Return a dict of format settings + def get_format(self): + format = {} + for prop in self.rna_type.properties.keys(): + format[prop] = getattr(self, prop) + return format + + # Return a dict of output files with their connected input + def get_output_files(self): + output_files = {} + for input in self.inputs: + if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid: + output_files[self.name_with_ext(suffix=input.suffix)] = input + return output_files + + # Get list of mesh objects from the split output socket + def get_split_objects(self): + objs = [] + split = self.inputs["Split Output"].get_split() + if split and len(split): + for splt in split: + if splt.bl_idname == 'BakeWrangler_Bake_Mesh': + objs += prune_objects(splt.inputs["Target"].get_objects(only_mesh=True)) + elif splt.bl_idname == 'BakeWrangler_Input_ObjectList': + objs += prune_objects(splt.get_objects(only_mesh=True)) + elif splt.bl_idname == 'BakeWrangler_Bake_Material': + objs += splt.get_materials() + elif splt.bl_idname == 'BakeWrangler_Sort_Meshes': + sobj = [] + for inpt in splt.inputs: + if inpt.islinked() and inpt.valid: + if get_input(inpt).bl_idname == 'BakeWrangler_Bake_Material': + for mat in get_input(inpt).get_materials(): + sobj += [[mat]] + sobj += splt.get_objects('TARGET', inpt) + for grp in sobj: + grp[0].append(grp[3]) + objs.append(grp[0]) + objs = prune_objects(objs) + if len(objs): return objs + else: return None + + # Get a list of unique objects used as either source or target + def get_unique_objects(self, type, for_auto_cage=False): + def get_meshes(socket, meshes): + pas = get_input(socket) + if pas: + if pas.bl_idname == 'BakeWrangler_Bake_Pass': + meshes += pas.get_inputs() + else: + for input in pas.inputs: + get_meshes(input, meshes) + meshes = [] + for input in self.inputs: + if input.name in ["Color", "Alpha"]: + get_meshes(input, meshes) + objs = [] + for mesh in meshes: + if len(mesh) > 1: + objs += mesh[0].get_unique_objects(type, for_auto_cage, mesh[1]) + else: + objs += mesh[0].get_unique_objects(type, for_auto_cage) + objs = prune_objects(objs) + if for_auto_cage: + return objs + return objs + + img_ext = ( + ('BMP', ".bmp"), + ('IRIS', ".rgb"), + ('PNG', ".png"), + ('JPEG', ".jpg"), + ('JPEG2000', ".jp2"), + ('TARGA', ".tga"), + ('TARGA_RAW', ".tga"), + ('CINEON', ".cin"), + ('DPX', ".dpx"), + ('OPEN_EXR_MULTILAYER', ".exr"), + ('OPEN_EXR', ".exr"), + ('HDR', ".hdr"), + ('TIFF', ".tif"), + ) + + # Core settings + img_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH') + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_SplitOutput', "Split Output") + self.inputs.new('BakeWrangler_Socket_OutputSetting', "Settings") + self.inputs.new('BakeWrangler_Socket_Color', "Color") + self.inputs.new('BakeWrangler_Socket_ChanMap', "Alpha") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Bake', "Bake") + # Prefs + self.inputs['Split Output'].disp_path = _prefs("def_outpath") + self.inputs['Split Output'].img_name = _prefs("def_outname") + + def draw_buttons(self, context, layout): + pass + #colnode = layout.column(align=False) + #colpath = colnode.column(align=True) + #colpath.prop(self, "disp_path", text="") + #colpath.prop(self, "img_name", text="") + + +class BakeWrangler_Output_Vertex_Cols(BakeWrangler_Tree_Node, Node): + '''Output vertex colors node''' + bl_label = 'Output Vertex Colors' + bl_width_default = 160 + + vert_files = [] + + # Returns the most identifying string for the node + def get_name(self): + name = BakeWrangler_Tree_Node.get_name(self) + return name + + # Make sure an empty input is always at the bottom + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Color', "Color") + + # All objects are split objects? We don't use this + def get_split_objects(self): + return None + + # Image settings are mostly irrelevant here, mostly static values will be set + def get_settings(self, validating=False): + output_settings = {} + output_settings['vcol'] = True + output_settings['vcol_type'] = self.vcol_type + output_settings['vcol_domain'] = self.vcol_domain + output_settings['img_udim'] = False + output_settings['marginer'] = False + output_settings['marginer_size'] = 0 + output_settings['marginer_fill'] = False + output_settings['img_color_mode'] = None + output_settings['img_color_depth'] = None + output_settings['img_path'] = None + return output_settings + + # Check node settings are valid to bake. Returns true/false, plus error message(s). + def validate(self, is_primary=False): + valid = [True] + # Validate inputs + has_valid_input = False + for input in self.inputs: + if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid: + if not input.suffix or not len(input.suffix): + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": Connected vertex output requires valid name for data"]) + if not is_primary: + has_valid_input = True + break + else: + input_valid = get_input(input).validate() + valid[0] = input_valid.pop(0) + if valid[0]: + has_valid_input = True + valid += input_valid + errs = len(valid) + if not has_valid_input and errs < 2: + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"]) + return valid + + vcol_domains = ( + ('POINT', "Vertex", "Vertex"), + ('CORNER', "Face Corner", "Face Corner"), + ) + + vcol_types = ( + ('FLOAT_COLOR', "Color", "32-bit floating point values"), + ('BYTE_COLOR', "Byte Color", "8-bit integer values"), + ) + + # Core settings + vcol_domain: bpy.props.EnumProperty(name="Domain", description="Type of element the data is stored on", items=vcol_domains, default='POINT') + vcol_type: bpy.props.EnumProperty(name="Data Type", description="Type of data stored in element", items=vcol_types, default='FLOAT_COLOR') + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + self.inputs.new('BakeWrangler_Socket_Color', "Color") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Bake', "Bake") + # Prefs + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + col = layout.column(align=True) + if not len(self.vert_files): + row.operator("bake_wrangler.dummy_vcol", icon='CHECKMARK', text="Apply") + row.operator("bake_wrangler.dummy_vcol", icon='PANEL_CLOSE', text="Discard") + else: + op1 = row.operator("bake_wrangler.apply_vertcols", icon='CHECKMARK', text="Apply") + op1.tree = self.id_data.name + op1.node = self.name + op2 = row.operator("bake_wrangler.discard_vertcols", icon='PANEL_CLOSE', text="Discard") + op2.tree = self.id_data.name + op2.node = self.name + + col.prop(self, "vcol_domain", text="") + col.prop(self, "vcol_type", text="") + + +# Output controller node provides batch execution of multiple connected bake passes. +class BakeWrangler_Output_Batch_Bake(BakeWrangler_Tree_Node, Node): + '''Output controller oven node''' + bl_label = 'Batch Bake' + + vert_files = [] + + # Makes sure there is always one empty input socket at the bottom by adding and removing sockets + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Bake', "Bake") + + # Check node settings are valid to bake. Returns true/false, plus error message(s). + def validate(self, is_primary=True): + valid = [True] + # Batch mode needs to avoid validating the same things more than once. Collect a + # unique list of the passes before validating them. + img_node_list = [] + pass_node_list = [] + for input in self.inputs: + if input.islinked() and input.valid: + img_node = follow_input_link(input.links[0]).from_node + if not img_node_list.count(img_node): + img_node_list.append(img_node) + for img_node_input in img_node.inputs: + if img_node_input.islinked() and img_node_input.valid: + pass_node = follow_input_link(img_node_input.links[0]).from_node + if not pass_node_list.count(pass_node): + pass_node_list.append(pass_node) + # Validate all the listed nodes + has_valid_input = False + for node in img_node_list: + img_node_valid = node.validate() + if not img_node_valid.pop(0): + valid[0] = False + if valid[0]: + has_valid_input = True + valid += img_node_valid + for node in pass_node_list: + pass_node_valid = node.validate() + if not pass_node_valid.pop(0): + valid[0] = False + valid += pass_node_valid + errs = len(valid) + if not has_valid_input and errs < 2: + valid[0] = False + valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"]) + # Validate pre and post scripts if set to external file + if self.loc_pre == 'EXT': + file_path = self.script_pre_can + # Validate file path + if os.path.exists(file_path): + if os.path.isfile(file_path): + # It exists so try to open it for read + try: + file = open(file_path, "r") + except OSError as err: + valid[0] = False + valid.append([_print("Pre-Script file error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)]) + else: + # It exists but isn't a file + valid[0] = False + valid.append([_print("Pre-Script file error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)]) + # See if the pre script compiles if set + if self.loc_pre != 'NON': + if self.loc_pre == 'INT': + pre_scr = self.script_pre_int.as_string() + elif self.loc_pre == 'EXT': + with open(self.script_pre_can, "r") as scr: + pre_scr = scr.read() + try: + compile(pre_scr, '', 'exec') + except SyntaxError or ValueError as err: + valid[0] = False + valid.append([_print("Pre-Script compile error", node=self, ret=True), ": %s" % (str(err))]) + if self.loc_post == 'EXT': + file_path = self.script_post_can + # Validate file path + if os.path.exists(file_path): + if os.path.isfile(file_path): + # It exists so try to open it for read + try: + file = open(file_path, "r") + except OSError as err: + valid[0] = False + valid.append([_print("Post-Script file error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)]) + else: + # It exists but isn't a file + valid[0] = False + valid.append([_print("Post-Script file error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)]) + # See if the pre script compiles if set + if self.loc_post != 'NON': + if self.loc_post == 'INT': + post_scr = self.script_post_int.as_string() + elif self.loc_post == 'EXT': + with open(self.script_post_can, "r") as scr: + post_scr = scr.read() + try: + compile(post_scr, '', 'exec') + except SyntaxError or ValueError as err: + valid[0] = False + valid.append([_print("Post-Script compile error", node=self, ret=True), ": %s" % (str(err))]) + # Everything validated + return valid + + # Get a list of unique objects used as either source or target + def get_unique_objects(self, type): + objs = [] + for input in self.inputs: + input = get_input(input) + if input and input.bl_idname == 'BakeWrangler_Output_Image_Path': + objs += input.get_unique_objects(type) + objs = prune_objects(objs) + objs_single = [] + for obj in objs: + objs_single.append(obj[0]) + return objs_single + + # Generate enum of base collection types + def get_collection_types(self, context): + col_types = [] + for prop in bpy.data.bl_rna.properties: + if prop.type == 'COLLECTION': + col_types.append((prop.identifier, prop.name, prop.description)) + return col_types + + # Generate enum of props + def get_user_props(self, context): + props = [] + if self.user_prop_objt: + for prop in self.user_prop_objt.keys(): + props.append((prop, prop, prop + " custom property")) + return props + + # Set the nodes background color to red when shutdown is enabled + def shutdown_update(self, context): + if self.shutdown_after: + self.use_custom_color = True + self.color = [0.9, 0, 0] + else: + self.use_custom_color = False + self.color = [0.608, 0.608, 0.608] + + # Get full path, removing any relative references + def update_pre_script(self, context): + cwd = os.path.dirname(bpy.data.filepath) + self.script_pre_can = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.script_pre))) + + # Get full path, removing any relative references + def update_post_script(self, context): + cwd = os.path.dirname(bpy.data.filepath) + self.script_post_can = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.script_post))) + + loc = ( + ('EXT', "External", ""), + ('INT', "Internal", ""), + ('NON', "None", ""), + ) + + loc_pre: bpy.props.EnumProperty(name="Script Location", description="Either internal or external file", items=loc, default='NON') + script_pre: bpy.props.StringProperty(name="Script path", description="Path to python script to run", default="", subtype='FILE_PATH', update=update_pre_script) + script_pre_can: bpy.props.StringProperty(name="Canical Script File", description="Canicial path to script", default="", subtype='FILE_PATH') + script_pre_int: bpy.props.PointerProperty(name="Script", description="Python script to run", type=bpy.types.Text) + + loc_post: bpy.props.EnumProperty(name="Script Location", description="Either internal or external file", items=loc, default='NON') + script_post: bpy.props.StringProperty(name="Script path", description="Path to python script to run", default="", subtype='FILE_PATH', update=update_post_script) + script_post_can: bpy.props.StringProperty(name="Canical Script File", description="Canicial path to script", default="", subtype='FILE_PATH') + script_post_int: bpy.props.PointerProperty(name="Script", description="Python script to run", type=bpy.types.Text) + + user_prop: bpy.props.BoolProperty(name="User Property", description="Enable custom user property incrementer", default=False) + user_prop_type: bpy.props.EnumProperty(name="Type", description="Type of data property is on", items=get_collection_types) + user_prop_objt: bpy.props.PointerProperty(name="Data", description="Data property is on", type=bpy.types.ID) + user_prop_name: bpy.props.EnumProperty(name="Name", description="Name of the property", items=get_user_props) + user_prop_zero: bpy.props.BoolProperty(name="Zero on Bake", description="Resets property to zero when bake starts", default=True) + shutdown_after: bpy.props.BoolProperty(name="Shutdown on Completion", description="Attempt to shutdown system after batch bake completes", default=False, update=shutdown_update) + + def init(self, context): + BakeWrangler_Tree_Node.init(self, context) + self.inputs.new('BakeWrangler_Socket_Bake', "Bake") + + def draw_buttons(self, context, layout): + BakeWrangler_Tree_Node.draw_bake_button(self, layout, 'OUTLINER', "Bake All") + row = layout.row(align=True) + if not len(self.vert_files): + row.operator("bake_wrangler.dummy_vcol", icon='CHECKMARK', text="Apply") + row.operator("bake_wrangler.dummy_vcol", icon='PANEL_CLOSE', text="Discard") + else: + op1 = row.operator("bake_wrangler.apply_vertcols", icon='CHECKMARK', text="Apply") + op1.tree = self.id_data.name + op1.node = self.name + op2 = row.operator("bake_wrangler.discard_vertcols", icon='PANEL_CLOSE', text="Discard") + op2.tree = self.id_data.name + op2.node = self.name + layout.label(text="Bake Images:") + + def draw_buttons_ext(self, context, layout): + col = layout.column(align=False) + + col.label(text="Pre-Bake Script:") + row = col.row(align=True) + row.prop_enum(self, "loc_pre", "NON") + row.prop_enum(self, "loc_pre", "INT") + row.prop_enum(self, "loc_pre", "EXT") + if self.loc_pre == 'EXT': + col.prop(self, "script_pre", text="") + elif self.loc_pre == 'INT': + col.prop_search(self, "script_pre_int", bpy.data, "texts", text="") + + col.label(text="Post-Bake Script:") + row = col.row(align=True) + row.prop_enum(self, "loc_post", "NON") + row.prop_enum(self, "loc_post", "INT") + row.prop_enum(self, "loc_post", "EXT") + if self.loc_post == 'EXT': + col.prop(self, "script_post", text="") + elif self.loc_post == 'INT': + col.prop_search(self, "script_post_int", bpy.data, "texts", text="") + + row = col.row(align=True) + row.prop(self, "user_prop", text="Increment Property") + if self.user_prop: + col.prop(self, "user_prop_type") + col.prop_search(self, "user_prop_objt", bpy.data, self.user_prop_type) + col.prop(self, "user_prop_name") + col.prop(self, "user_prop_zero") + col.prop(self, "shutdown_after") + + + +# +# Node Categories +# + +import nodeitems_utils +from nodeitems_utils import NodeCategory, NodeItem + +# Base class for the node category menu system +class BakeWrangler_Node_Category(NodeCategory): + @classmethod + def poll(cls, context): + tree_type = getattr(context.space_data, "tree_type", None) + return tree_type == 'BakeWrangler_Tree' + +# List of all bakery nodes put into categories with identifier, name +BakeWrangler_Node_Categories = [ + BakeWrangler_Node_Category('BakeWrangler_Settings', "Settings", items=[ + NodeItem("BakeWrangler_MeshSettings"), + NodeItem("BakeWrangler_SampleSettings"), + NodeItem("BakeWrangler_PassSettings"), + NodeItem("BakeWrangler_OutputSettings"), + ]), + BakeWrangler_Node_Category('BakeWrangler_Nodes', "Bake", items=[ + NodeItem("BakeWrangler_Input_Filenames"), + NodeItem("BakeWrangler_Input_ObjectList"), + NodeItem("BakeWrangler_Bake_Billboard"), + NodeItem("BakeWrangler_Bake_Material"), + NodeItem("BakeWrangler_Bake_Mesh"), + NodeItem("BakeWrangler_Sort_Meshes"), + NodeItem("BakeWrangler_Bake_Pass"), + NodeItem("BakeWrangler_Output_Image_Path"), + NodeItem("BakeWrangler_Output_Vertex_Cols"), + NodeItem("BakeWrangler_Output_Batch_Bake"), + ]), + BakeWrangler_Node_Category('BakeWrangler_Post', "Post", items=[ + NodeItem("BakeWrangler_Channel_Map"), + NodeItem("BakeWrangler_Post_MixRGB"), + NodeItem("BakeWrangler_Post_SplitRGB"), + NodeItem("BakeWrangler_Post_JoinRGB"), + NodeItem("BakeWrangler_Post_Math"), + NodeItem("BakeWrangler_Post_Gamma"), + ]), + BakeWrangler_Node_Category('BakeWrangler_Layout', "Layout", items=[ + NodeItem("NodeFrame"), + NodeItem("NodeReroute"), + ]), +] + + +# +# Registration +# + +# All bakery classes that need to be registered +classes = ( + BakeWrangler_Operator_Dummy, + BakeWrangler_Operator_Dummy_VCol, + BakeWrangler_Operator_FilterToggle, + BakeWrangler_Operator_DoubleVal, + BakeWrangler_Operator_PickMenuEnum, + BakeWrangler_Operator_AddSelected, + BakeWrangler_Operator_DiscardBakedVertCols, + BakeWrangler_Operator_ApplyBakedVertCols, + BakeWrangler_Operator_BakeStop, + BakeWrangler_Operator_BakePass, + BakeWrangler_Tree, + BakeWrangler_Socket_Object, + BakeWrangler_Socket_Material, + BakeWrangler_Socket_Mesh, + BakeWrangler_Socket_Color, + BakeWrangler_Socket_ChanMap, + BakeWrangler_Socket_Float, + BakeWrangler_Socket_Bake, + BakeWrangler_Socket_MeshSetting, + BakeWrangler_Socket_PassSetting, + BakeWrangler_Socket_SampleSetting, + BakeWrangler_Socket_OutputSetting, + BakeWrangler_Socket_ObjectNames, + BakeWrangler_Socket_SplitOutput, + BakeWrangler_MeshSettings, + BakeWrangler_SampleSettings, + BakeWrangler_PassSettings, + BakeWrangler_OutputSettings, + BakeWrangler_Input_ObjectList, + BakeWrangler_Input_Filenames, + BakeWrangler_Bake_Billboard, + BakeWrangler_Bake_Material, + BakeWrangler_Bake_Mesh, + BakeWrangler_Sort_Meshes, + BakeWrangler_Bake_Pass, + BakeWrangler_Channel_Map, + BakeWrangler_Post_MixRGB, + BakeWrangler_Post_SplitRGB, + BakeWrangler_Post_JoinRGB, + BakeWrangler_Post_Math, + BakeWrangler_Post_Math_OpMenu, + BakeWrangler_Post_Gamma, + BakeWrangler_Output_Image_Path, + BakeWrangler_Output_Vertex_Cols, + BakeWrangler_Output_Batch_Bake, +) + + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + nodeitems_utils.register_node_categories('BakeWrangler_Nodes', BakeWrangler_Node_Categories) + + +def unregister(): + nodeitems_utils.unregister_node_categories('BakeWrangler_Nodes') + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + + + +if __name__ == "__main__": + register() diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/node_update.py b/cg/blender/scripts/addons/BakeWrangler/nodes/node_update.py new file mode 100644 index 0000000..5fe674a --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/nodes/node_update.py @@ -0,0 +1,329 @@ +import bpy +from .node_tree import _prefs, _print, BW_TREE_VERSION, get_input, follow_input_link + + +class BakeWrangler_Operator_UpdateRecipe(bpy.types.Operator): + '''Update older recipe version to current version''' + bl_idname = "bake_wrangler_op.update_recipe" + bl_label = "Update Recipe" + bl_options = {"REGISTER", "UNDO"} + + tree: bpy.props.StringProperty() + + @classmethod + def poll(type, context): + return context.area.type == "NODE_EDITOR" and context.space_data.tree_type == "BakeWrangler_Tree" + + def execute(self, context): + tree = bpy.data.node_groups[self.tree] + nodes = tree.nodes + links = tree.links + fail = False + RGB = ['R', 'G', 'B'] + # Helper to get lists of desired nodes + def get_bw_nodes(nodes, before_version=-1, idnames=[]): + bw_nodes = [] + for node in nodes: + if node.bl_idname in ['NodeFrame', 'NodeReroute']: + continue + if before_version > -1 and getattr(node, "tree_version", before_version) >= before_version: + continue + if len(idnames) and node.bl_idname not in idnames: + continue + bw_nodes.append(node) + return bw_nodes + def link_route(input, node, socket, links): + # If the input is linked via reroutes, this will get the tail reroutes input + if len(input.links): + input = follow_input_link(input.links[0]).to_socket + # If the specified node is a reroute, follow to the source, and change the link at that end + if node.bl_idname == 'NodeReroute': + tail = follow_input_link(node.inputs[0].links[0]) + tail_node = tail.from_node + tail_sock = tail.to_socket + links.new(tail_node.outputs[socket], tail_sock) + if input == tail_sock: return + socket = 0 + links.new(input, node.outputs[socket]) + # 5 -> 6 + if getattr(tree, "tree_version", 0) == 5: + # Load out dated nodes + from .prev_trees import node_tree_v5 as node_tree_v5 + node_tree_v5.register() + try: + glob_bake = [] + glob_outp = [] + # First the active global res node should be found (if exists) and the values stored + for node in tree.nodes: + if node.bl_idname == 'BakeWrangler_Global_Resolution' and node.is_active: + glob_bake.append(node.res_bake_x) + glob_bake.append(node.res_bake_y) + glob_outp.append(node.res_outp_x) + glob_outp.append(node.res_outp_y) + break + # Go through all the nodes and update them as needed based on idname + for node in get_bw_nodes(nodes, 6, ['BakeWrangler_Global_Resolution', 'BakeWrangler_Bake_Mesh', 'BakeWrangler_Bake_Pass', 'BakeWrangler_Output_Image_Path']): + if node.bl_idname == 'BakeWrangler_Global_Resolution': + nodes.remove(node) # No longer used + continue + socket_type = None + node_type = None + if node.bl_idname == 'BakeWrangler_Bake_Mesh': + socket_type = 'BakeWrangler_Socket_MeshSetting' + node_type = 'BakeWrangler_MeshSettings' + elif node.bl_idname == 'BakeWrangler_Bake_Pass': + socket_type = 'BakeWrangler_Socket_PassSetting' + node_type = 'BakeWrangler_PassSettings' + elif node.bl_idname == 'BakeWrangler_Output_Image_Path': + socket_type = 'BakeWrangler_Socket_OutputSetting' + node_type = 'BakeWrangler_OutputSettings' + # Setting input needs to be added and settings placed in the correct settings node + if 'Settings' not in node.inputs.keys(): + sset = node.inputs.new(socket_type, "Settings") + node.inputs.move(len(node.inputs)-1, 0) + mset = nodes.new(node_type) + mset.location = [node.location[0] - 10, node.location[1] + 10] + links.new(sset, mset.outputs[0]) + for key in node.keys(): + if key == 'pause_update': continue + mset[key] = node[key] + # Finished with Mesh node + if node.bl_idname == 'BakeWrangler_Bake_Mesh': + node.tree_version = 6 + # Extra work for bake pass + elif node.bl_idname == 'BakeWrangler_Bake_Pass': + xres = getattr(node, 'bake_xres', 0) + usex = getattr(node, 'bake_usex', False) + if not usex: + if len(glob_bake): + mset.res_bake_x = glob_bake[0] + else: + mset.res_bake_x = xres + yres = getattr(node, 'bake_yres', 0) + usey = getattr(node, 'bake_usey', False) + if not usey: + if len(glob_bake): + mset.res_bake_y = glob_bake[1] + else: + mset.res_bake_y = yres + # A pass was added to 'WRANG' cat at pos 3, so selected pass needs to move down one + if node.bake_cat == 'WRANG': + # It's an enum and stored as an int, but when addon loads becomes a string.. + enum = 0 + enum2str = {} + str2enum = {} + for wpass in node.passes_wrang: + str2enum[wpass[0]] = enum + enum2str[enum] = wpass[0] + enum += 1 + if str2enum[node.bake_wrang] >= 3: + enum = str2enum[node.bake_wrang] + 1 + node.bake_wrang = enum2str[enum] + # Version will get set when working back from an Output + # Extra work for output + elif node.bl_idname == 'BakeWrangler_Output_Image_Path': + node.inputs.new('BakeWrangler_Socket_ChanMap', "Alpha") + xres = getattr(node, 'img_xres', 0) + usex = getattr(node, 'img_usex', False) + if not usex: + if len(glob_bake): + mset.img_xres = glob_outp[0] + else: + mset.img_xres = xres + yres = getattr(node, 'img_yres', 0) + usey = getattr(node, 'img_usey', False) + if not usey: + if len(glob_bake): + mset.img_yres = glob_outp[1] + else: + mset.img_yres = yres + # Loop over all the nodes again, just back tracking from outputs this time + for node in get_bw_nodes(nodes, 6, ['BakeWrangler_Output_Image_Path']): + # Unless the only input is color and alpha, a mapping node needs to be set up + map = None + for input in node.inputs: + if input.name in ['R', 'G', 'B', 'A'] and input.islinked() and input.valid: + if map is None and not input.name == 'A': + # Create map node and connect the color socket + map = nodes.new('BakeWrangler_Channel_Map') + map.location = [node.location[0] - 20, node.location[1] - 20] + if node.inputs['Color'].islinked() and node.inputs['Color'].valid: + link_route(map.inputs['Color'], node.inputs['Color'].links[0].from_node, 'Color', links) + # Connect up this input and set mapping + if input.name == 'A': + sock = node.inputs['Alpha'] + else: + sock = map.inputs[input.name] + chan = follow_input_link(input.links[0]).from_socket.name[:1] + link_route(sock, input.links[0].from_node, 'Color', links) + _print("Input:" + str(input.name)) + if chan == 'C': + if input.name == 'A': + chan = 'V' + else: + chan = input.name + sock.input_channel = chan + # Remove the old link + links.remove(input.links[0]) + # If map wasn't created then just link the color input, else link the map + if map is None: + link_route(node.inputs['Color'], node.inputs['Color'].links[0].from_node, 'Color', links) + else: + link_route(node.inputs['Color'], map, 'Color', links) + # Remove the now unused sockets + for input in node.inputs: + if input.name in ['R', 'G', 'B', 'A']: + node.inputs.remove(input) + node.tree_version = 6 + # Go through all the pass nodes, only their color outputs should be linked now + for node in get_bw_nodes(nodes, 6): + if node.bl_idname == 'BakeWrangler_Bake_Pass': + for output in node.outputs: + if output.name != 'Color': + node.outputs.remove(output) + node.tree_version = 6 + # Helper fn to compare and group settings + def consolidate_settings(settings): + def group_settings(settings, groups={}, idx=0): + def compare_settings(this, that): + for key in this.keys(): + if key == 'pause_update': continue + if getattr(that, key, None) == getattr(this, key, None): continue + return False + return True + excluded = [] + if len(settings): + comp = settings[0] + groups[idx] = [comp] + for sett in settings[1:]: + if compare_settings(comp, sett): + groups[idx].append(sett) + else: + excluded.append(sett) + return group_settings(excluded, groups, idx+1) + return groups + if len(settings) > 1: + # Create groups of same + grps = group_settings(settings) + for key in grps.keys(): + merge = grps[key][0].outputs[0] + mlinks = [] + # Collect all the link dests + for sett in grps[key][1:]: + for link in sett.outputs[0].links: + mlinks.append(link.to_socket) + # Link all the dests to the first setting node + for link in mlinks: + links.new(merge, link) + # Delete extra nodes + for sett in grps[key][1:]: + nodes.remove(sett) + # Consolidate duplicate settings nodes + consolidate_settings(get_bw_nodes(nodes, -1, ['BakeWrangler_MeshSettings'])) + consolidate_settings(get_bw_nodes(nodes, -1, ['BakeWrangler_PassSettings'])) + consolidate_settings(get_bw_nodes(nodes, -1, ['BakeWrangler_OutputSettings'])) + # Everything should be updated to version 6 + tree.tree_version = 6 + except Exception as err: + _print("Updating recipe from v5 to v6 failed: %s" % (str(err))) + self.report({'ERROR'}, "Updating recipe from v5 to v6 failed: %s" % (str(err))) + fail = True + else: + _print("Recipe updated from v5 to v6") + node_tree_v5.unregister() + if fail: return {'CANCELLED'} + # 6 -> 7 + if getattr(tree, "tree_version", 0) == 6: + try: + # Output path nodes are changed up a bit + for node in get_bw_nodes(nodes, 7): + if node.bl_idname == 'BakeWrangler_Output_Image_Path': + node.inputs.new('BakeWrangler_Socket_SplitOutput', "Split Output") + node.inputs.move(len(node.inputs)-1, 0) + node.update_inputs() + node.tree_version = 7 + tree.tree_version = 7 + except Exception as err: + _print("Updating recipe from v6 to v7 failed: %s" % (str(err))) + self.report({'ERROR'}, "Updating recipe from v6 to v7 failed: %s" % (str(err))) + fail = True + else: + _print("Recipe updated from v6 to v7") + if fail: return {'CANCELLED'} + # 7 -> 8 + if getattr(tree, "tree_version", 0) == 7: + try: + # Output paths moved to socket properties + for node in get_bw_nodes(nodes, 8): + if node.bl_idname == 'BakeWrangler_Output_Image_Path': + socket = node.inputs["Split Output"] + for key in node.keys(): + if key == 'disp_path' and node[key]: socket.disp_path = node[key] + if key == 'img_path' and node[key]: socket.img_path = node[key] + if key == 'img_name' and node[key]: socket.img_name = node[key] + node.tree_version = 8 + tree.tree_version = 8 + except Exception as err: + _print("Updating recipe from v7 to v8 failed: %s" % (str(err))) + self.report({'ERROR'}, "Updating recipe from v7 to v8 failed: %s" % (str(err))) + fail = True + else: + _print("Recipe updated from v7 to v8") + if fail: return {'CANCELLED'} + # 8 -> 9 + if getattr(tree, "tree_version", 0) == 8: + try: + # RGB sockets have changed to Red, Green, Blue and PassSettings gains a samples socket + for node in get_bw_nodes(nodes, 9): + if node.bl_idname in ['BakeWrangler_Channel_Map', 'BakeWrangler_Post_SplitRGB', 'BakeWrangler_Post_JoinRGB']: + soks = node.inputs + if node.bl_idname == 'BakeWrangler_Post_SplitRGB': soks = node.outputs + for sok in soks: + if sok.name == 'R': sok.name = 'Red' + elif sok.name == 'G': sok.name = 'Green' + elif sok.name == 'B': sok.name = 'Blue' + soks = node.outputs + if node.bl_idname == 'BakeWrangler_Post_SplitRGB': soks = node.inputs + for sok in soks: + if sok.name == 'Image': sok.name = 'Color' + elif node.bl_idname == 'BakeWrangler_PassSettings': + node.inputs.new('BakeWrangler_Socket_SampleSetting', "Samples") + elif node.bl_idname == 'BakeWrangler_OutputSettings': + if not hasattr(node, 'img_color_space'): + node.img_color_space = bpy.data.scenes[0].sequencer_colorspace_settings.name + node.tree_version = 9 + tree.tree_version = 9 + except Exception as err: + _print("Updating recipe from v8 to v9 failed: %s" % (str(err))) + self.report({'ERROR'}, "Updating recipe from v8 to v9 failed: %s" % (str(err))) + fail = True + else: + _print("Recipe updated from v8 to v9") + if fail: return {'CANCELLED'} + return {'FINISHED'} + + # Ask the user if they really want to update + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + +# Classes to register +classes = ( + BakeWrangler_Operator_UpdateRecipe, +) + + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + + +def unregister(): + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/prev_trees/__init__.py b/cg/blender/scripts/addons/BakeWrangler/nodes/prev_trees/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/prev_trees/node_tree_v1.py b/cg/blender/scripts/addons/BakeWrangler/nodes/prev_trees/node_tree_v1.py new file mode 100644 index 0000000..91826fb --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/nodes/prev_trees/node_tree_v1.py @@ -0,0 +1,305 @@ +import bpy +from bpy.types import NodeTree, Node, NodeSocket +from .node_tree import BakeWrangler_Tree_Socket, BakeWrangler_Tree_Node + +#icons = ['NONE', 'QUESTION', 'ERROR', 'CANCEL', 'TRIA_RIGHT', 'TRIA_DOWN', 'TRIA_LEFT', 'TRIA_UP', 'ARROW_LEFTRIGHT', 'PLUS', 'DISCLOSURE_TRI_RIGHT', 'DISCLOSURE_TRI_DOWN', 'RADIOBUT_OFF', 'RADIOBUT_ON', 'MENU_PANEL', 'BLENDER', 'GRIP', 'DOT', 'COLLAPSEMENU', 'X', 'DUPLICATE', 'TRASH', 'COLLECTION_NEW', 'NODE', 'NODE_SEL', 'WINDOW', 'WORKSPACE', 'RIGHTARROW_THIN', 'BORDERMOVE', 'VIEWZOOM', 'ADD', 'REMOVE', 'PANEL_CLOSE', 'COPY_ID', 'EYEDROPPER', 'CHECKMARK', 'AUTO', 'CHECKBOX_DEHLT', 'CHECKBOX_HLT', 'UNLOCKED', 'LOCKED', 'UNPINNED', 'PINNED', 'SCREEN_BACK', 'RIGHTARROW', 'DOWNARROW_HLT', 'FCURVE_SNAPSHOT', 'OBJECT_HIDDEN', 'PLUGIN', 'HELP', 'GHOST_ENABLED', 'COLOR', 'UNLINKED', 'LINKED', 'HAND', 'ZOOM_ALL', 'ZOOM_SELECTED', 'ZOOM_PREVIOUS', 'ZOOM_IN', 'ZOOM_OUT', 'DRIVER_DISTANCE', 'DRIVER_ROTATIONAL_DIFFERENCE', 'DRIVER_TRANSFORM', 'FREEZE', 'STYLUS_PRESSURE', 'GHOST_DISABLED', 'FILE_NEW', 'FILE_TICK', 'QUIT', 'URL', 'RECOVER_LAST', 'THREE_DOTS', 'FULLSCREEN_ENTER', 'FULLSCREEN_EXIT', 'LIGHT', 'MATERIAL', 'TEXTURE', 'ANIM', 'WORLD', 'SCENE', 'OUTPUT', 'SCRIPT', 'PARTICLES', 'PHYSICS', 'SPEAKER', 'TOOL_SETTINGS', 'SHADERFX', 'MODIFIER', 'BLANK1', 'FAKE_USER_OFF', 'FAKE_USER_ON', 'VIEW3D', 'GRAPH', 'OUTLINER', 'PROPERTIES', 'FILEBROWSER', 'IMAGE', 'INFO', 'SEQUENCE', 'TEXT', 'SOUND', 'ACTION', 'NLA', 'PREFERENCES', 'TIME', 'NODETREE', 'CONSOLE', 'TRACKER', 'ASSET_MANAGER', 'NODE_COMPOSITING', 'NODE_TEXTURE', 'NODE_MATERIAL', 'UV', 'OBJECT_DATAMODE', 'EDITMODE_HLT', 'UV_DATA', 'VPAINT_HLT', 'TPAINT_HLT', 'WPAINT_HLT', 'SCULPTMODE_HLT', 'POSE_HLT', 'PARTICLEMODE', 'TRACKING', 'TRACKING_BACKWARDS', 'TRACKING_FORWARDS', 'TRACKING_BACKWARDS_SINGLE', 'TRACKING_FORWARDS_SINGLE', 'TRACKING_CLEAR_BACKWARDS', 'TRACKING_CLEAR_FORWARDS', 'TRACKING_REFINE_BACKWARDS', 'TRACKING_REFINE_FORWARDS', 'SCENE_DATA', 'RENDERLAYERS', 'WORLD_DATA', 'OBJECT_DATA', 'MESH_DATA', 'CURVE_DATA', 'META_DATA', 'LATTICE_DATA', 'LIGHT_DATA', 'MATERIAL_DATA', 'TEXTURE_DATA', 'ANIM_DATA', 'CAMERA_DATA', 'PARTICLE_DATA', 'LIBRARY_DATA_DIRECT', 'GROUP', 'ARMATURE_DATA', 'COMMUNITY', 'BONE_DATA', 'CONSTRAINT', 'SHAPEKEY_DATA', 'CONSTRAINT_BONE', 'CAMERA_STEREO', 'PACKAGE', 'UGLYPACKAGE', 'EXPERIMENTAL', 'BRUSH_DATA', 'IMAGE_DATA', 'FILE', 'FCURVE', 'FONT_DATA', 'RENDER_RESULT', 'SURFACE_DATA', 'EMPTY_DATA', 'PRESET', 'RENDER_ANIMATION', 'RENDER_STILL', 'LIBRARY_DATA_BROKEN', 'BOIDS', 'STRANDS', 'LIBRARY_DATA_INDIRECT', 'GREASEPENCIL', 'LINE_DATA', 'LIBRARY_DATA_OVERRIDE', 'GROUP_BONE', 'GROUP_VERTEX', 'GROUP_VCOL', 'GROUP_UVS', 'FACE_MAPS', 'RNA', 'RNA_ADD', 'MOUSE_LMB', 'MOUSE_MMB', 'MOUSE_RMB', 'MOUSE_MOVE', 'MOUSE_LMB_DRAG', 'MOUSE_MMB_DRAG', 'MOUSE_RMB_DRAG', 'PRESET_NEW', 'DECORATE', 'DECORATE_KEYFRAME', 'DECORATE_ANIMATE', 'DECORATE_DRIVER', 'DECORATE_LINKED', 'DECORATE_LIBRARY_OVERRIDE', 'DECORATE_UNLOCKED', 'DECORATE_LOCKED', 'DECORATE_OVERRIDE', 'FUND', 'TRACKER_DATA', 'HEART', 'ORPHAN_DATA', 'USER', 'SYSTEM', 'SETTINGS', 'OUTLINER_OB_EMPTY', 'OUTLINER_OB_MESH', 'OUTLINER_OB_CURVE', 'OUTLINER_OB_LATTICE', 'OUTLINER_OB_META', 'OUTLINER_OB_LIGHT', 'OUTLINER_OB_CAMERA', 'OUTLINER_OB_ARMATURE', 'OUTLINER_OB_FONT', 'OUTLINER_OB_SURFACE', 'OUTLINER_OB_SPEAKER', 'OUTLINER_OB_FORCE_FIELD', 'OUTLINER_OB_GROUP_INSTANCE', 'OUTLINER_OB_GREASEPENCIL', 'OUTLINER_OB_LIGHTPROBE', 'OUTLINER_OB_IMAGE', 'RESTRICT_COLOR_OFF', 'RESTRICT_COLOR_ON', 'HIDE_ON', 'HIDE_OFF', 'RESTRICT_SELECT_ON', 'RESTRICT_SELECT_OFF', 'RESTRICT_RENDER_ON', 'RESTRICT_RENDER_OFF', 'RESTRICT_INSTANCED_OFF', 'OUTLINER_DATA_EMPTY', 'OUTLINER_DATA_MESH', 'OUTLINER_DATA_CURVE', 'OUTLINER_DATA_LATTICE', 'OUTLINER_DATA_META', 'OUTLINER_DATA_LIGHT', 'OUTLINER_DATA_CAMERA', 'OUTLINER_DATA_ARMATURE', 'OUTLINER_DATA_FONT', 'OUTLINER_DATA_SURFACE', 'OUTLINER_DATA_SPEAKER', 'OUTLINER_DATA_LIGHTPROBE', 'OUTLINER_DATA_GP_LAYER', 'OUTLINER_DATA_GREASEPENCIL', 'GP_SELECT_POINTS', 'GP_SELECT_STROKES', 'GP_MULTIFRAME_EDITING', 'GP_ONLY_SELECTED', 'GP_SELECT_BETWEEN_STROKES', 'MODIFIER_OFF', 'MODIFIER_ON', 'ONIONSKIN_OFF', 'ONIONSKIN_ON', 'RESTRICT_VIEW_ON', 'RESTRICT_VIEW_OFF', 'RESTRICT_INSTANCED_ON', 'MESH_PLANE', 'MESH_CUBE', 'MESH_CIRCLE', 'MESH_UVSPHERE', 'MESH_ICOSPHERE', 'MESH_GRID', 'MESH_MONKEY', 'MESH_CYLINDER', 'MESH_TORUS', 'MESH_CONE', 'MESH_CAPSULE', 'EMPTY_SINGLE_ARROW', 'LIGHT_POINT', 'LIGHT_SUN', 'LIGHT_SPOT', 'LIGHT_HEMI', 'LIGHT_AREA', 'CUBE', 'SPHERE', 'CONE', 'META_PLANE', 'META_CUBE', 'META_BALL', 'META_ELLIPSOID', 'META_CAPSULE', 'SURFACE_NCURVE', 'SURFACE_NCIRCLE', 'SURFACE_NSURFACE', 'SURFACE_NCYLINDER', 'SURFACE_NSPHERE', 'SURFACE_NTORUS', 'EMPTY_AXIS', 'STROKE', 'EMPTY_ARROWS', 'CURVE_BEZCURVE', 'CURVE_BEZCIRCLE', 'CURVE_NCURVE', 'CURVE_NCIRCLE', 'CURVE_PATH', 'LIGHTPROBE_CUBEMAP', 'LIGHTPROBE_PLANAR', 'LIGHTPROBE_GRID', 'COLOR_RED', 'COLOR_GREEN', 'COLOR_BLUE', 'TRIA_RIGHT_BAR', 'TRIA_DOWN_BAR', 'TRIA_LEFT_BAR', 'TRIA_UP_BAR', 'FORCE_FORCE', 'FORCE_WIND', 'FORCE_VORTEX', 'FORCE_MAGNETIC', 'FORCE_HARMONIC', 'FORCE_CHARGE', 'FORCE_LENNARDJONES', 'FORCE_TEXTURE', 'FORCE_CURVE', 'FORCE_BOID', 'FORCE_TURBULENCE', 'FORCE_DRAG', 'FORCE_SMOKEFLOW', 'RIGID_BODY', 'RIGID_BODY_CONSTRAINT', 'IMAGE_PLANE', 'IMAGE_BACKGROUND', 'IMAGE_REFERENCE', 'NODE_INSERT_ON', 'NODE_INSERT_OFF', 'NODE_TOP', 'NODE_SIDE', 'NODE_CORNER', 'SELECT_SET', 'SELECT_EXTEND', 'SELECT_SUBTRACT', 'SELECT_INTERSECT', 'SELECT_DIFFERENCE', 'ALIGN_LEFT', 'ALIGN_CENTER', 'ALIGN_RIGHT', 'ALIGN_JUSTIFY', 'ALIGN_FLUSH', 'ALIGN_TOP', 'ALIGN_MIDDLE', 'ALIGN_BOTTOM', 'BOLD', 'ITALIC', 'UNDERLINE', 'SMALL_CAPS', 'CON_ACTION', 'HOLDOUT_OFF', 'HOLDOUT_ON', 'INDIRECT_ONLY_OFF', 'INDIRECT_ONLY_ON', 'CON_CAMERASOLVER', 'CON_FOLLOWTRACK', 'CON_OBJECTSOLVER', 'CON_LOCLIKE', 'CON_ROTLIKE', 'CON_SIZELIKE', 'CON_TRANSLIKE', 'CON_DISTLIMIT', 'CON_LOCLIMIT', 'CON_ROTLIMIT', 'CON_SIZELIMIT', 'CON_SAMEVOL', 'CON_TRANSFORM', 'CON_TRANSFORM_CACHE', 'CON_CLAMPTO', 'CON_KINEMATIC', 'CON_LOCKTRACK', 'CON_SPLINEIK', 'CON_STRETCHTO', 'CON_TRACKTO', 'CON_ARMATURE', 'CON_CHILDOF', 'CON_FLOOR', 'CON_FOLLOWPATH', 'CON_PIVOT', 'CON_SHRINKWRAP', 'MODIFIER_DATA', 'MOD_WAVE', 'MOD_BUILD', 'MOD_DECIM', 'MOD_MIRROR', 'MOD_SOFT', 'MOD_SUBSURF', 'HOOK', 'MOD_PHYSICS', 'MOD_PARTICLES', 'MOD_BOOLEAN', 'MOD_EDGESPLIT', 'MOD_ARRAY', 'MOD_UVPROJECT', 'MOD_DISPLACE', 'MOD_CURVE', 'MOD_LATTICE', 'MOD_TINT', 'MOD_ARMATURE', 'MOD_SHRINKWRAP', 'MOD_CAST', 'MOD_MESHDEFORM', 'MOD_BEVEL', 'MOD_SMOOTH', 'MOD_SIMPLEDEFORM', 'MOD_MASK', 'MOD_CLOTH', 'MOD_EXPLODE', 'MOD_FLUIDSIM', 'MOD_MULTIRES', 'MOD_SMOKE', 'MOD_SOLIDIFY', 'MOD_SCREW', 'MOD_VERTEX_WEIGHT', 'MOD_DYNAMICPAINT', 'MOD_REMESH', 'MOD_OCEAN', 'MOD_WARP', 'MOD_SKIN', 'MOD_TRIANGULATE', 'MOD_WIREFRAME', 'MOD_DATA_TRANSFER', 'MOD_NORMALEDIT', 'MOD_PARTICLE_INSTANCE', 'MOD_HUE_SATURATION', 'MOD_NOISE', 'MOD_OFFSET', 'MOD_SIMPLIFY', 'MOD_THICKNESS', 'MOD_INSTANCE', 'MOD_TIME', 'MOD_OPACITY', 'REC', 'PLAY', 'FF', 'REW', 'PAUSE', 'PREV_KEYFRAME', 'NEXT_KEYFRAME', 'PLAY_SOUND', 'PLAY_REVERSE', 'PREVIEW_RANGE', 'ACTION_TWEAK', 'PMARKER_ACT', 'PMARKER_SEL', 'PMARKER', 'MARKER_HLT', 'MARKER', 'KEYFRAME_HLT', 'KEYFRAME', 'KEYINGSET', 'KEY_DEHLT', 'KEY_HLT', 'MUTE_IPO_OFF', 'MUTE_IPO_ON', 'DRIVER', 'SOLO_OFF', 'SOLO_ON', 'FRAME_PREV', 'FRAME_NEXT', 'NLA_PUSHDOWN', 'IPO_CONSTANT', 'IPO_LINEAR', 'IPO_BEZIER', 'IPO_SINE', 'IPO_QUAD', 'IPO_CUBIC', 'IPO_QUART', 'IPO_QUINT', 'IPO_EXPO', 'IPO_CIRC', 'IPO_BOUNCE', 'IPO_ELASTIC', 'IPO_BACK', 'IPO_EASE_IN', 'IPO_EASE_OUT', 'IPO_EASE_IN_OUT', 'NORMALIZE_FCURVES', 'VERTEXSEL', 'EDGESEL', 'FACESEL', 'CURSOR', 'PIVOT_BOUNDBOX', 'PIVOT_CURSOR', 'PIVOT_INDIVIDUAL', 'PIVOT_MEDIAN', 'PIVOT_ACTIVE', 'CENTER_ONLY', 'ROOTCURVE', 'SMOOTHCURVE', 'SPHERECURVE', 'INVERSESQUARECURVE', 'SHARPCURVE', 'LINCURVE', 'NOCURVE', 'RNDCURVE', 'PROP_OFF', 'PROP_ON', 'PROP_CON', 'PROP_PROJECTED', 'PARTICLE_POINT', 'PARTICLE_TIP', 'PARTICLE_PATH', 'SNAP_FACE_CENTER', 'SNAP_PERPENDICULAR', 'SNAP_MIDPOINT', 'SNAP_OFF', 'SNAP_ON', 'SNAP_NORMAL', 'SNAP_GRID', 'SNAP_VERTEX', 'SNAP_EDGE', 'SNAP_FACE', 'SNAP_VOLUME', 'SNAP_INCREMENT', 'STICKY_UVS_LOC', 'STICKY_UVS_DISABLE', 'STICKY_UVS_VERT', 'CLIPUV_DEHLT', 'CLIPUV_HLT', 'SNAP_PEEL_OBJECT', 'GRID', 'OBJECT_ORIGIN', 'ORIENTATION_GLOBAL', 'ORIENTATION_GIMBAL', 'ORIENTATION_LOCAL', 'ORIENTATION_NORMAL', 'ORIENTATION_VIEW', 'COPYDOWN', 'PASTEDOWN', 'PASTEFLIPUP', 'PASTEFLIPDOWN', 'VIS_SEL_11', 'VIS_SEL_10', 'VIS_SEL_01', 'VIS_SEL_00', 'AUTOMERGE_OFF', 'AUTOMERGE_ON', 'UV_VERTEXSEL', 'UV_EDGESEL', 'UV_FACESEL', 'UV_ISLANDSEL', 'UV_SYNC_SELECT', 'TRANSFORM_ORIGINS', 'GIZMO', 'ORIENTATION_CURSOR', 'NORMALS_VERTEX', 'NORMALS_FACE', 'NORMALS_VERTEX_FACE', 'SHADING_BBOX', 'SHADING_WIRE', 'SHADING_SOLID', 'SHADING_RENDERED', 'SHADING_TEXTURE', 'OVERLAY', 'XRAY', 'LOCKVIEW_OFF', 'LOCKVIEW_ON', 'AXIS_SIDE', 'AXIS_FRONT', 'AXIS_TOP', 'NDOF_DOM', 'NDOF_TURN', 'NDOF_FLY', 'NDOF_TRANS', 'LAYER_USED', 'LAYER_ACTIVE', 'SORTALPHA', 'SORTBYEXT', 'SORTTIME', 'SORTSIZE', 'SHORTDISPLAY', 'LONGDISPLAY', 'IMGDISPLAY', 'BOOKMARKS', 'FONTPREVIEW', 'FILTER', 'NEWFOLDER', 'FILE_PARENT', 'FILE_REFRESH', 'FILE_FOLDER', 'FILE_BLANK', 'FILE_BLEND', 'FILE_IMAGE', 'FILE_MOVIE', 'FILE_SCRIPT', 'FILE_SOUND', 'FILE_FONT', 'FILE_TEXT', 'SORT_DESC', 'SORT_ASC', 'LINK_BLEND', 'APPEND_BLEND', 'IMPORT', 'EXPORT', 'LOOP_BACK', 'LOOP_FORWARDS', 'BACK', 'FORWARD', 'FILE_ARCHIVE', 'FILE_CACHE', 'FILE_VOLUME', 'FILE_3D', 'FILE_HIDDEN', 'FILE_BACKUP', 'DISK_DRIVE', 'MATPLANE', 'MATSPHERE', 'MATCUBE', 'MONKEY', 'HAIR', 'ALIASED', 'ANTIALIASED', 'MAT_SPHERE_SKY', 'MATSHADERBALL', 'MATCLOTH', 'MATFLUID', 'WORDWRAP_OFF', 'WORDWRAP_ON', 'SYNTAX_OFF', 'SYNTAX_ON', 'LINENUMBERS_OFF', 'LINENUMBERS_ON', 'SCRIPTPLUGINS', 'DESKTOP', 'EXTERNAL_DRIVE', 'NETWORK_DRIVE', 'SEQ_SEQUENCER', 'SEQ_PREVIEW', 'SEQ_LUMA_WAVEFORM', 'SEQ_CHROMA_SCOPE', 'SEQ_HISTOGRAM', 'SEQ_SPLITVIEW', 'SEQ_STRIP_META', 'SEQ_STRIP_DUPLICATE', 'IMAGE_RGB', 'IMAGE_RGB_ALPHA', 'IMAGE_ALPHA', 'IMAGE_ZDEPTH', 'VIEW_PERSPECTIVE', 'VIEW_ORTHO', 'VIEW_CAMERA', 'VIEW_PAN', 'VIEW_ZOOM', 'BRUSH_BLOB', 'BRUSH_BLUR', 'BRUSH_CLAY', 'BRUSH_CLAY_STRIPS', 'BRUSH_CLONE', 'BRUSH_CREASE', 'BRUSH_FILL', 'BRUSH_FLATTEN', 'BRUSH_GRAB', 'BRUSH_INFLATE', 'BRUSH_LAYER', 'BRUSH_MASK', 'BRUSH_MIX', 'BRUSH_NUDGE', 'BRUSH_PINCH', 'BRUSH_SCRAPE', 'BRUSH_SCULPT_DRAW', 'BRUSH_SMEAR', 'BRUSH_SMOOTH', 'BRUSH_SNAKE_HOOK', 'BRUSH_SOFTEN', 'BRUSH_TEXDRAW', 'BRUSH_TEXFILL', 'BRUSH_TEXMASK', 'BRUSH_THUMB', 'BRUSH_ROTATE', 'GPBRUSH_SMOOTH', 'GPBRUSH_THICKNESS', 'GPBRUSH_STRENGTH', 'GPBRUSH_GRAB', 'GPBRUSH_PUSH', 'GPBRUSH_TWIST', 'GPBRUSH_PINCH', 'GPBRUSH_RANDOMIZE', 'GPBRUSH_CLONE', 'GPBRUSH_WEIGHT', 'GPBRUSH_PENCIL', 'GPBRUSH_PEN', 'GPBRUSH_INK', 'GPBRUSH_INKNOISE', 'GPBRUSH_BLOCK', 'GPBRUSH_MARKER', 'GPBRUSH_FILL', 'GPBRUSH_AIRBRUSH', 'GPBRUSH_CHISEL', 'GPBRUSH_ERASE_SOFT', 'GPBRUSH_ERASE_HARD', 'GPBRUSH_ERASE_STROKE', 'SMALL_TRI_RIGHT_VEC', 'KEYTYPE_KEYFRAME_VEC', 'KEYTYPE_BREAKDOWN_VEC', 'KEYTYPE_EXTREME_VEC', 'KEYTYPE_JITTER_VEC', 'KEYTYPE_MOVING_HOLD_VEC', 'HANDLETYPE_FREE_VEC', 'HANDLETYPE_ALIGNED_VEC', 'HANDLETYPE_VECTOR_VEC', 'HANDLETYPE_AUTO_VEC', 'HANDLETYPE_AUTO_CLAMP_VEC', 'COLORSET_01_VEC', 'COLORSET_02_VEC', 'COLORSET_03_VEC', 'COLORSET_04_VEC', 'COLORSET_05_VEC', 'COLORSET_06_VEC', 'COLORSET_07_VEC', 'COLORSET_08_VEC', 'COLORSET_09_VEC', 'COLORSET_10_VEC', 'COLORSET_11_VEC', 'COLORSET_12_VEC', 'COLORSET_13_VEC', 'COLORSET_14_VEC', 'COLORSET_15_VEC', 'COLORSET_16_VEC', 'COLORSET_17_VEC', 'COLORSET_18_VEC', 'COLORSET_19_VEC', 'COLORSET_20_VEC', 'EVENT_A', 'EVENT_B', 'EVENT_C', 'EVENT_D', 'EVENT_E', 'EVENT_F', 'EVENT_G', 'EVENT_H', 'EVENT_I', 'EVENT_J', 'EVENT_K', 'EVENT_L', 'EVENT_M', 'EVENT_N', 'EVENT_O', 'EVENT_P', 'EVENT_Q', 'EVENT_R', 'EVENT_S', 'EVENT_T', 'EVENT_U', 'EVENT_V', 'EVENT_W', 'EVENT_X', 'EVENT_Y', 'EVENT_Z', 'EVENT_SHIFT', 'EVENT_CTRL', 'EVENT_ALT', 'EVENT_OS', 'EVENT_F1', 'EVENT_F2', 'EVENT_F3', 'EVENT_F4', 'EVENT_F5', 'EVENT_F6', 'EVENT_F7', 'EVENT_F8', 'EVENT_F9', 'EVENT_F10', 'EVENT_F11', 'EVENT_F12', 'EVENT_ESC', 'EVENT_TAB', 'EVENT_PAGEUP', 'EVENT_PAGEDOWN', 'EVENT_RETURN', 'EVENT_SPACEKEY'] + +# +# Bake Wrangler nodes system +# + +BW_TREE_VERSION = 1 + +# Socket for sharing high poly mesh, or really any mesh data that should be in the bake but isn't the target +class BakeWrangler_Socket_HighPolyMesh(NodeSocket, BakeWrangler_Tree_Socket): + '''Socket for connecting a high poly mesh node''' + bl_label = 'Bake From' + + # Called to filter objects listed in the value search field. Only objects of type 'MESH' are shown. + def value_prop_filter(self, object): + if self.collection: + return len(object.all_objects) + else: + return object.type in ['MESH', 'CURVE'] + + # Called when the value property changes. + def value_prop_update(self, context): + if self.node and self.node.bl_idname == 'BakeWrangler_Input_HighPolyMesh': + self.node.update_inputs() + + # Called when the collection property changes + def collection_prop_update(self, context): + if self.collection == False and self.value: + self.value = None + + value: bpy.props.PointerProperty(name="Bake From Object(s)", description="Geometry to be part of selection when doing a 'selected to active' type bake", type=bpy.types.ID, poll=value_prop_filter, update=value_prop_update) + collection: bpy.props.BoolProperty(name="Collection", description="When enabled whole collections will be selected instead of individual objects", update=collection_prop_update, default=False) + recursive: bpy.props.BoolProperty(name="Recursive Selection", description="When enabled all collections within the selected collection will be used", default=False) + + def draw(self, context, layout, node, text): + if node.bl_idname == 'BakeWrangler_Input_Mesh' and node.multi_res: + layout.label(text=text + " [ignored]") + elif not self.is_output and not self.is_linked and node.bl_idname != 'BakeWrangler_Input_Mesh': + row = layout.row() + if self.collection: + if self.value: + row.prop_search(self, "value", context.scene.collection, "children", text="", icon='GROUP') + else: + row.prop(self, "collection", icon='GROUP', text="") + row.prop_search(self, "value", context.scene.collection, "children", text="", icon='NONE') + row.prop(self, "recursive", icon='OUTLINER', text="") + else: + ico = 'NONE' + if self.value: + obj = bpy.types.Object(self.value) + if obj.type == 'MESH': + ico = 'MESH_DATA' + elif obj.type == 'CURVE': + ico = 'CURVE_DATA' + else: + row.prop(self, "collection", icon='GROUP', text="") + row.prop_search(self, "value", context.scene, "objects", text="", icon=ico) + else: + layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text)) + + def draw_color(self, context, node): + return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.2, 1.0, 1.0)) + + +# Input node that takes any number of objects that should be selected during a bake +class BakeWrangler_Input_HighPolyMesh(Node, BakeWrangler_Tree_Node): + '''High poly mesh data node''' + bl_label = 'Bake From' + + # Makes sure there is always one empty input socket at the bottom by adding and removing sockets + def update_inputs(self): + BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_HighPolyMesh', "Bake From") + + # Returns a list of all chosen mesh objects. May recurse through multiple connected nodes. + def get_objects(self): + objects = [] + for input in self.inputs: + if not input.is_linked: + if input.value: + if input.collection: + col_objects = [] + if input.recursive: + col_objects = input.value.all_objects + else: + col_objects = input.value.objects + visible_objects = [ob for ob in col_objects if ob.type in ['MESH', 'CURVE']] + for ob in visible_objects: + objects.append(ob) + else: + objects.append(input.value) + else: + linked_objects = [] + if input.links[0].is_valid and input.valid: + linked_objects = input.links[0].from_node.get_objects() + if len(linked_objects): + objects.extend(linked_objects) + return objects + + def init(self, context): + # Sockets IN + self.inputs.new('BakeWrangler_Socket_HighPolyMesh', "Bake From") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_HighPolyMesh', "Bake From") + + def draw_buttons(self, context, layout): + layout.label(text="Objects:") + + +# Input node that takes a single target mesh and its bake settings. High poly mesh nodes can be added as input. +class BakeWrangler_Input_Mesh(Node, BakeWrangler_Tree_Node): + '''Mesh data and settings node''' + bl_label = 'Mesh' + bl_width_default = 146 + + # Returns the most identifing string for the node + def get_name(self): + name = BakeWrangler_Tree_Node.get_name(self) + if self.mesh_object: + name += " (%s)" % (self.mesh_object.name) + return name + + def update_inputs(self): + pass + + # Check node settings are valid to bake. Returns true/false, plus error message. + def validate(self, check_materials=False): + valid = [True] + # Is a mesh selected? + if not self.mesh_object: + valid[0] = False + valid.append(_print("No valid mesh object selected", node=self, ret=True)) + # Check for multires modifier if multires is enabled + if self.multi_res and self.mesh_object: + has_multi_mod = False + if len(self.mesh_object.modifiers): + for mod in self.mesh_object.modifiers: + if mod.type == 'MULTIRES' and mod.total_levels > 0: + has_multi_mod = True + break + if not has_multi_mod: + valid[0] = False + valid.append(_print("Multires enabled but no multires data on selected mesh object", node=self, ret=True)) + # Check cage if enabled + if self.cage: + if not self.cage_obj: + valid[0] = False + valid.append(_print("Cage enabled but no cage object selected", node=self, ret=True)) + if self.mesh_object and self.cage_obj and len(self.mesh_object.data.polygons) != len(self.cage_obj.data.polygons): + valid[0] = False + valid.append(_print("Cage object face count does not match mesh object", node=self, ret=True)) + if self.mesh_object and len(self.get_objects()) < 2: + valid[0] = False + valid.append(_print("Cage enabled but no high poly objects selected", node=self, ret=True)) + # Check valid UV Map + if self.mesh_object and len(self.mesh_object.data.uv_layers) < 1: + valid[0] = False + valid.append(_print("Mesh object has no UV map(s)", node=self, ret=True)) + if self.mesh_object and len(self.mesh_object.data.uv_layers) > 1 and self.uv_map and self.uv_map not in self.mesh_object.data.uv_layers: + valid[0] = False + valid.append(_print("Selected UV map not present on object (it could have been deleted or renamed)", node=self, ret=True)) + # Validated? + if not valid[0]: + return valid + + # Valid, should materials also be checked? + if check_materials: + # Some bake types need to modify the materials, check if this can be done. A failure wont invalidate + # but warnings will be issued about the materails that fail. + mats = [] + others = self.get_objects() + if self.multi_res or len(others) < 2: + # Just check self materials + if len(self.mesh_object.data.materials): + for mat in self.mesh_object.data.materials: + if mats.count(mat) == 0: + mats.append(mat) + else: + # Just check not self materials + others.pop(0) + for obj in others: + if len(obj.data.materials): + for mat in obj.data.materials: + if mats.count(mat) == 0: + mats.append(mat) + + # Go through the list of materials and see if they will pass the prep phase + for mat in mats: + nodes = mat.node_tree.nodes + node_outputs = [] + passed = False + + # Not a node based material or not enough nodes to be valid + if not nodes or len(nodes) < 2: + valid.append(_print("'%s' not node based or too few nodes" % (mat.name), node=self, ret=True)) + continue + + # Collect all outputs + for node in nodes: + if node.type == 'OUTPUT_MATERIAL': + if node.target == 'CYCLES' or node.target == 'ALL': + node_outputs.append(node) + + # Try to find at least one usable node pair from the outputs + for node in node_outputs: + passed = material_recursor(node) + if passed: + break + + # Didn't find any usable node pairs + if not passed: + valid.append(_print("'%s' Output doesn't appear to be a valid combination of Principled and Mix shaders. Baked values will not be correct for this material." % (mat.name), node=self, ret=True)) + + return valid + + # Returns a list of all chosen mesh objects. The bake target will be at index 0, extra objects indicate + # a 'selected to active' type bake should be performed. May recurse through multiple prior nodes. If no + # mesh_object is set an empty list will be returned instead. Only unique objects will be returned. + def get_objects(self): + objects = [] + if self.mesh_object: + objects.append(self.mesh_object) + if not self.inputs[0].is_linked: + if self.inputs[0].value and objects.count(self.inputs[0].value) == 0: + objects.append(self.inputs[0].value) + else: + linked_objects = [] + if self.inputs[0].links[0].is_valid and self.inputs[0].valid: + linked_objects = self.inputs[0].links[0].from_node.get_objects() + if len(linked_objects): + for obj in linked_objects: + if objects.count(obj) == 0: + objects.append(obj) + return objects + + # Filter for prop_search field used to select mesh_object + def mesh_object_filter(self, object): + return object.type == 'MESH' + + multi_res_passes = ( + ('NORMALS', "Normals", "Bake normals"), + ('DISPLACEMENT', "Displacment", "Bake displacement"), + ) + + mesh_object: bpy.props.PointerProperty(name="Bake Target", description="Mesh that will be the active object during the bake", type=bpy.types.Object, poll=mesh_object_filter) + ray_dist: bpy.props.FloatProperty(name="Ray Distance", description="Distance to use for inward ray cast when using a selected to active bake", default=0.01, step=1, min=0.0, unit='LENGTH') + margin: bpy.props.IntProperty(name="Margin", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL') + mask_margin: bpy.props.IntProperty(name="Mask Margin", description="Adds extra padding to the mask bake. Use if edge details are being cut off", default=0, min=0, subtype='PIXEL') + multi_res: bpy.props.BoolProperty(name="Multires", description="Bake directly from multires object. This will disable or ignore the other bake settings.\nOnly Normals and Displacment can be baked") + multi_res_pass: bpy.props.EnumProperty(name="Pass", description="Choose shading information to bake into the image.\nMultires pass will override any connected bake pass", items=multi_res_passes, default='NORMALS') + cage: bpy.props.BoolProperty(name="Cage", description="Cast rays to active object from a cage. The cage must have the same number of faces") + cage_obj: bpy.props.PointerProperty(name="Cage Object", description="Object to use as a cage instead of calculating the cage from the active object", type=bpy.types.Object, poll=mesh_object_filter) + uv_map: bpy.props.StringProperty(name="UV Map", description="Pick map to bake if object has multiple layers. Leave blank to use active layer") + + def init(self, context): + # Sockets IN + self.inputs.new('BakeWrangler_Socket_HighPolyMesh', "Bake From") + # Sockets OUT + self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh") + + def draw_buttons(self, context, layout): + layout.label(text="Mesh Object:") + layout.prop_search(self, "mesh_object", context.scene, "objects", text="") + layout.prop(self, "margin", text="Margin") + layout.prop(self, "mask_margin", text="Padding") + if self.mesh_object and len(self.mesh_object.data.uv_layers) > 1: + split = layout.split(factor=0.21) + split.label(text="UV:") + split.prop_search(self, "uv_map", self.mesh_object.data, "uv_layers", text="") + layout.prop(self, "multi_res", text="From Multires") + if not self.multi_res: + if not self.cage: + layout.prop(self, "cage", text="Cage") + else: + layout.prop(self, "cage", text="Cage:") + layout.prop_search(self, "cage_obj", context.scene, "objects", text="") + layout.label(text="Bake From:") + layout.prop(self, "ray_dist", text="Ray Dist") + else: + layout.prop(self, "multi_res_pass") + +# All bakery classes that need to be registered +classes = ( + BakeWrangler_Socket_HighPolyMesh, + BakeWrangler_Input_HighPolyMesh, + BakeWrangler_Input_Mesh, +) + + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + +def unregister(): + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/cg/blender/scripts/addons/BakeWrangler/nodes/prev_trees/node_tree_v5.py b/cg/blender/scripts/addons/BakeWrangler/nodes/prev_trees/node_tree_v5.py new file mode 100644 index 0000000..e473e01 --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/nodes/prev_trees/node_tree_v5.py @@ -0,0 +1,66 @@ +import bpy +from bpy.types import NodeTree, Node, NodeSocket +from ..node_tree import BakeWrangler_Tree_Socket, BakeWrangler_Tree_Node + +# Node to globally set bake and output resolutions +class BakeWrangler_Global_Resolution(Node, BakeWrangler_Tree_Node): + '''Global resolution settings node''' + bl_label = 'Resolutions' + + # Inputs are static on this node + def update_inputs(self): + pass + + # When toggled as active, any other nodes of the same type need to be deactivated + def toggle_active(self, context): + pass + + is_active: bpy.props.BoolProperty(name="Active", description="Causes this nodes settings to be used globally (only one can be active at a time)", default=False, update=toggle_active) + res_bake_x: bpy.props.IntProperty(name="Bake X resolution ", description="Width (X) to bake maps at", default=2048, min=1, subtype='PIXEL') + res_bake_y: bpy.props.IntProperty(name="Bake Y resolution ", description="Height (Y) to bake maps at", default=2048, min=1, subtype='PIXEL') + res_outp_x: bpy.props.IntProperty(name="Image X resolution ", description="Width (X) to output images at (bake will be scaled up or down to match, use this to smooth maps by down sampling)", default=2048, min=1, subtype='PIXEL') + res_outp_y: bpy.props.IntProperty(name="Image Y resolution ", description="Height (Y) to output images at (bake will be scaled up or down to match, use this to smooth maps by down sampling)", default=2048, min=1, subtype='PIXEL') + + def copy(self, node): + self.is_active = False + + def init(self, context): + pass + #BakeWrangler_Tree_Node.init(self, context) + # Sockets IN + # Sockets OUT + # Prefs + #self.res_bake_x = _prefs("def_glob_bakx") + #self.res_bake_y = _prefs("def_glob_baky") + #self.res_outp_x = _prefs("def_glob_outx") + #self.res_outp_y = _prefs("def_glob_outy") + + def draw_buttons(self, context, layout): + colres = layout.column(align=True) + colres.prop(self, "is_active", toggle=True) + colres.label(text="Bake Resolution:") + colres.prop(self, "res_bake_x", text="X") + colres.prop(self, "res_bake_y", text="Y") + colres.label(text="Output Resolution:") + colres.prop(self, "res_outp_x", text="X") + colres.prop(self, "res_outp_y", text="Y") + + +classes = ( + BakeWrangler_Global_Resolution, +) + + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + +def unregister(): + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/cg/blender/scripts/addons/BakeWrangler/resources/BakeWrangler_Scene.blend b/cg/blender/scripts/addons/BakeWrangler/resources/BakeWrangler_Scene.blend new file mode 100644 index 0000000..9d3cdcc Binary files /dev/null and b/cg/blender/scripts/addons/BakeWrangler/resources/BakeWrangler_Scene.blend differ diff --git a/cg/blender/scripts/addons/BakeWrangler/status_bar/__init__.py b/cg/blender/scripts/addons/BakeWrangler/status_bar/__init__.py new file mode 100644 index 0000000..f4b9f82 --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/status_bar/__init__.py @@ -0,0 +1,8 @@ +from . import status_bar_icon + +def register(): + status_bar_icon.register() + + +def unregister(): + status_bar_icon.unregister() diff --git a/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_error.png b/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_error.png new file mode 100644 index 0000000..6fca879 Binary files /dev/null and b/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_error.png differ diff --git a/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_good.png b/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_good.png new file mode 100644 index 0000000..e147bff Binary files /dev/null and b/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_good.png differ diff --git a/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_working.png b/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_working.png new file mode 100644 index 0000000..e299491 Binary files /dev/null and b/cg/blender/scripts/addons/BakeWrangler/status_bar/icons/bw_working.png differ diff --git a/cg/blender/scripts/addons/BakeWrangler/status_bar/status_bar_icon.py b/cg/blender/scripts/addons/BakeWrangler/status_bar/status_bar_icon.py new file mode 100644 index 0000000..b520433 --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/status_bar/status_bar_icon.py @@ -0,0 +1,106 @@ +import bpy + + +# An operator to open a new window and display the text log when you click on the icon +class click_bw_icon(bpy.types.Operator): + '''Open baking log''' + bl_idname = "bake_wrangler.open_log" + bl_label = "" + + @classmethod + def poll(type, context): + return getattr(bpy.context.window_manager, 'bw_status', -1) > -1 + + def invoke(self, context, event): + text = None + if event.alt and event.ctrl: + file = getattr(bpy.context.window_manager, 'bw_lastfile', "") + if file: + import subprocess + sub = subprocess.Popen([bpy.path.abspath(bpy.app.binary_path), file]) + elif event.ctrl: + log = getattr(bpy.context.window_manager, 'bw_lastlog', "") + if log: text = bpy.data.texts.load(log) + else: + if "BakeWrangler" in bpy.data.texts: text = bpy.data.texts["BakeWrangler"] + if text: + bpy.ops.wm.window_new() + log_win = context.window_manager.windows[-1] + log_ed = log_win.screen.areas[0] + log_ed.type = 'TEXT_EDITOR' + log_ed.spaces[0].text = text + log_ed.spaces[0].show_line_numbers = False + log_ed.spaces[0].show_syntax_highlight = False + bpy.ops.text.move(type='FILE_TOP') + return {'FINISHED'} + + +# Draw a different icon depending on the current bake state +def draw_bw_icon(self, context): + row = self.layout.row(align=True) + bake_status = getattr(bpy.context.window_manager, 'bw_status', -1) + if bake_status == 0: #Good (green) + row.operator("bake_wrangler.open_log", text="", icon_value=status_icons['main']['bw_good'].icon_id) + elif bake_status == 1: #Baking (blue) + row.operator("bake_wrangler.open_log", text="", icon_value=status_icons['main']['bw_working'].icon_id) + elif bake_status == 2: #Error (red) + row.operator("bake_wrangler.open_log", text="", icon_value=status_icons['main']['bw_error'].icon_id) + + +# Make the status bar redraw itself +def redraw_status_bar(context): + # This causes the status bar to redraw without deleting any custom drawing fns appended to it + context.workspace.status_text_set_internal(None) + + +# Remove and then re-add icon draw function to the status bar to make sure it's there +def ensure_bw_icon(): + bpy.types.STATUSBAR_HT_header.remove(draw_fn) + bpy.types.STATUSBAR_HT_header.append(draw_fn) + + +# Remove the icon if it is turned off +def disable_bw_icon(): + bpy.types.STATUSBAR_HT_header.remove(draw_fn) + + +# Classes to register +classes = ( + click_bw_icon, +) + +draw_fn = draw_bw_icon +status_icons = {} + +def register(): + # Set up custom status icon collection + import os + import bpy.utils.previews + icons_col = bpy.utils.previews.new() + icons_dir = os.path.join(os.path.dirname(__file__), "icons") + # Load icons in + icons_col.load("bw_good", os.path.join(icons_dir, "bw_good.png"), 'IMAGE') + icons_col.load("bw_working", os.path.join(icons_dir, "bw_working.png"), 'IMAGE') + icons_col.load("bw_error", os.path.join(icons_dir, "bw_error.png"), 'IMAGE') + status_icons["main"] = icons_col + from bpy.utils import register_class + for cls in classes: + register_class(cls) + # Add the icon drawing function to the status bar + bpy.types.STATUSBAR_HT_header.append(draw_fn) + + +def unregister(): + # Remove the icon drawing function from the status bar + bpy.types.STATUSBAR_HT_header.remove(draw_fn) + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + # Remove custom icons + for icon_col in status_icons.values(): + bpy.utils.previews.remove(icon_col) + status_icons.clear() + + +if __name__ == "__main__": + register() diff --git a/cg/blender/scripts/addons/BakeWrangler/vert/ipc.py b/cg/blender/scripts/addons/BakeWrangler/vert/ipc.py new file mode 100644 index 0000000..4bc446a --- /dev/null +++ b/cg/blender/scripts/addons/BakeWrangler/vert/ipc.py @@ -0,0 +1,133 @@ +import os +import pickle +import bpy +import sys +try: + from BakeWrangler.nodes.node_tree import _print +except: + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + from nodes.node_tree import _print + + +# Complete process of putting baked verts into a temp file ready for reimport +def bake_verts(verts=None, object=None, name=None, type=None, domain=None): + # Create temp file + err = 0 + fd, fname = next_pickle_jar() + if fd: + # Export vert data to python list + vcols = export_verts(name=name, cols=verts) + # Add object name and data type to data list + vcols.insert(0, [object, type, domain]) + if vcols: + # Pickle the vcols in the jar, this also closes the file + err = pickle_verts(file=fd, verts=vcols) + if err: + _print(" Error - Pickling failed.", tag=True, wrap=True) + else: + _print(" Error - Exporting data failed.", tag=True, wrap=True) + err = 1 + else: + _print(" Error - Creating temp file failed.", tag=True, wrap=True) + err = 1 + if err: + return 1, None + _print(" Done", tag=True) + return 0, fname + + +# Import vertex colors into currently open blend find, returns 0 on success else 1 +def import_verts(cols=None): + object = name = type = domain = None + try: + objinfo = cols.pop(0) + object = objinfo[0] + type = objinfo[1] + domain = objinfo[2] + name = cols.pop(0) + blend_obj = bpy.data.objects[object] + # See if the object already has data with the provided name, adding if needed + if name not in blend_obj.data.color_attributes.keys(): + blend_obj.data.color_attributes.new(name, type, domain) + elif blend_obj.data.color_attributes[name].domain != domain or blend_obj.data.color_attributes[name].data_type != type: + blend_obj.data.color_attributes.remove(blend_obj.data.color_attributes[name]) + blend_obj.data.color_attributes.new(name, type, domain) + # Use internal setter function to apply array + obj_cols = blend_obj.data.color_attributes[name] + obj_cols.data.foreach_set('color', cols) + + except: + return 1, "Object: %s, Data: %s, Type %s, Domain %s" % (object, name, type, domain) + else: + return 0, "Object: %s, Data: %s, Type %s, Domain %s" % (object, name, type, domain) + + +# Extract vertex colors into a py list and return it or None on error +def export_verts(name=None, cols=None): + # List needs to be set to the correct size first, which is the length of the data * 4 + vlist = [0.0] * (len(cols.data) * 4) + try: + # Use internal foreach get function on the data + cols.data.foreach_get('color', vlist) + except: + return None + else: + # Insert the color data name at the front of the list and return it + vlist.insert(0, name) + return vlist + + +# Pickle vertex color dict, return 1 on error, else 0 +def pickle_verts(file=None, verts=None): + try: + pickle.dump(verts, file, pickle.HIGHEST_PROTOCOL) + file.close() + except: + return 1 + else: + return 0 + + +# Depickle vertex color dict from file, return None on error, else verts +def depickle_verts(file=None): + try: + verts = pickle.load(file) + except: + return None + else: + return verts + + +# Create temp file to hold pickle, return None, filename on error, else opened file, filename +def next_pickle_jar(): + blend = bpy.data.filepath + pickl = blend + ".vert" + fd = None + # Find free file name by adding numbers + if os.path.exists(pickl): + fno = 1 + while os.path.exists(pickl): + fno = fno + 1 + pickl = pickl + "_%03i" % (fno) + # Open the file for binary write + try: + fd = open(pickl, "wb") + except: + return None, pickl + else: + return fd, pickl + + +# Open pickled temp, return None on error, else opened file +def open_pickle_jar(file=None): + #Open the file for binary read + try: + fd = open(file, 'rb') + except: + return None + else: + return fd + + +if __name__ == "__main__": + pass diff --git a/cg/blender/scripts/startup/cg_environment.py b/cg/blender/scripts/startup/cg_environment.py new file mode 100644 index 0000000..0c69d21 --- /dev/null +++ b/cg/blender/scripts/startup/cg_environment.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +__doc__ = 'Environment for use Blender with Robossembler Framework' +__version__ = '0.1' + +import os +import bpy +from bpy.app.handlers import persistent +import addon_utils + + +@persistent +def rs_env(): + '''Scripts environment''' + rs_blender_scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + if rs_blender_scripts_dir not in bpy.utils.script_paths_pref(): + script_directories = bpy.context.preferences.filepaths.script_directories + new_dir = script_directories.new() + new_dir.directory = rs_blender_scripts_dir + new_dir.name = 'RS_BLENDER_SCRIPTS_DIR' + bpy.ops.wm.save_userpref() + bpy.ops.wm.quit_blender() + else: + if not addon_utils.check('BakeWrangler')[0]: + addon_utils.enable('BakeWrangler', default_set=True) + bpy.ops.wm.save_userpref() + + print('Robossembler Framework Environment activated!') + +rs_env() diff --git a/cg/blender/texturing/bake_submitter.py b/cg/blender/texturing/bake_submitter.py new file mode 100644 index 0000000..31f5245 --- /dev/null +++ b/cg/blender/texturing/bake_submitter.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Basic mesh processing for asset pipeline. +''' +__version__ = '0.2' + +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Preparing and execution methods for the baking process. +''' +__version__ = '0.1' + +import logging +import os +import bpy +import addon_utils +import BakeWrangler +from BakeWrangler.nodes.node_tree import BW_TREE_VERSION + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +# COLLECTIONS NAMIG CONVENTION +parts_col_name = 'Parts' +lcs_col_name = 'LCS' +hierarchy_col_name = 'Hierarchy' +lowpoly_col_name = 'Lowpoly' +# LCS POINT'S SUFFIXES CONVENTION +inlet = '_in' +outlet = '_out' +root = '_root' +# CG ASSETS SUFFIXES CONVENTION +hightpoly = '_hp' +midpoly = 'mp' +lowpoly = '_lp' +render = '_render' + + +def bw_submit(lowpoly_obj_names, resolution=4096, tree_name='robossembler', area=None): + ''' Submit session and bake textures with BakeWrangler addon. ''' + BakeWrangler.register() + + asm_name = os.path.basename(bpy.context.blend_data.filepath).split('.')[0] + asm_path = os.path.dirname(bpy.context.blend_data.filepath) + textures_path = os.path.join(asm_path,"textures").replace('\\', '/') + bake_path = os.path.join(textures_path,"bake") + os.makedirs(bake_path, exist_ok=True) + + # create node tree + tree = bpy.data.node_groups.new(name=tree_name, type='BakeWrangler_Tree') + + # for default used Compositing Node Editor area + if not area: + if 'Compositing' in bpy.data.workspaces: + for target_area in bpy.data.workspaces['Compositing'].screens[0].areas: + if target_area.type == 'NODE_EDITOR': + area = target_area + break + elif 'Rendering' in bpy.data.workspaces: + # only if fail for Compositing Workspace + for target_area in bpy.data.workspaces['Rendering'].screens[0].areas: + if target_area.type == 'IMAGE_EDITOR': + area = target_area + break + else: + logger.info('Please, set default Bledner settings or set "area" parameter!') + area.spaces[0].tree_type = 'BakeWrangler_Tree' + area.spaces[0].path.start(tree) + + tree.tree_version = BW_TREE_VERSION + tree.initialised = True + + # clear default nodes + for node in tree.nodes: + tree.nodes.remove(node) + + # bw settings + tree.nodes.new('BakeWrangler_MeshSettings').location = (0, 0) + tree.nodes.new('BakeWrangler_SampleSettings').location = (200, 0) + tree.nodes.new('BakeWrangler_PassSettings').location = (400, 0) + tree.nodes.new('BakeWrangler_OutputSettings').location = (600, 0) + + tree.nodes['Mesh Settings'].pinned = True + tree.nodes['Mesh Settings']['margin'] = 8 + tree.nodes['Mesh Settings']['ray_dist'] = 0.002 + tree.nodes['Sample Settings'].pinned = True + tree.nodes['Sample Settings']['bake_samples'] = 4 + tree.nodes['Pass Settings'].pinned = True + tree.nodes['Pass Settings']['res_bake_x'] = resolution + tree.nodes['Pass Settings']['res_bake_y'] = resolution + tree.nodes['Output Settings'].pinned = True + tree.nodes['Output Settings']['img_xres'] = resolution + tree.nodes['Output Settings']['img_yres'] = resolution + tree.nodes['Output Settings']['img_type'] = 2 # PNG + tree.nodes['Output Settings']['img_compression'] = 50 + tree.nodes['Output Settings']['img_color_mode'] = 1 # RGB + + ## batch bake node + node_batch = tree.nodes.new('BakeWrangler_Output_Batch_Bake') + node_batch.location = (1000, -500) + + node_y_pos = 0 + pass_socket = 0 + for lp_name in lowpoly_obj_names: + """ run for eatch low poly object """ + lp = bpy.data.objects[lp_name] + mp = bpy.data.objects['_'.join(lp.name.split('_')[:-1] + [midpoly])] + img_name = '_'.join(lp.name.split('_')[:-1]) + '_' + + node_inputs = tree.nodes.new('BakeWrangler_Bake_Mesh') + node_inputs.location = (-700, node_y_pos -500) + + node_inputs.inputs['Target'].value = lp + node_inputs.inputs['Source'].value = mp + + # bake passes + ## Diffuse + node_d_pass = tree.nodes.new('BakeWrangler_Bake_Pass') + node_d_pass.location = (-200, node_y_pos -500) + node_d_img = tree.nodes.new('BakeWrangler_Output_Image_Path') + node_d_img.location = (200, node_y_pos -500) + + tree.links.new(node_d_pass.outputs['Color'], node_d_img.inputs['Color']) + + node_d_pass.bake_cat = 'PBR' + node_d_pass.bake_pbr = 'ALBEDO' + node_d_img.inputs['Color'].suffix = 'D' + node_d_img.inputs['Split Output'].disp_path = bake_path + node_d_img.inputs['Split Output'].img_name = img_name + ''' + ## Curvature + node_c_pass = tree.nodes.new('BakeWrangler_Bake_Pass') + node_c_pass.location = (-200, node_y_pos -1000) + node_c_img = tree.nodes.new('BakeWrangler_Output_Image_Path') + node_c_img.location = (200, node_y_pos -1000) + + tree.links.new(node_c_pass.outputs['Color'], node_c_img.inputs['Color']) + + node_c_pass.bake_cat = 'WRANG' + node_c_pass.bake_wrang = 'CURVATURE' + node_c_img.inputs['Color'].suffix = 'C' + node_c_img.inputs['Split Output'].disp_path = bake_path + node_c_img.inputs['Split Output'].img_name = img_name + ''' + ## Normal + node_n_pass = tree.nodes.new('BakeWrangler_Bake_Pass') + #tree.nodes['Bake Pass.001'].name = 'normal_pass' + node_n_pass.location = (-200, node_y_pos -1500) + node_n_img = tree.nodes.new('BakeWrangler_Output_Image_Path') + node_n_img.location = (200, node_y_pos -1500) + + tree.links.new(node_n_pass.outputs['Color'], node_n_img.inputs['Color']) + + node_n_pass.bake_cat = 'CORE' + node_n_pass.bake_core = 'NORMAL' + node_n_img.inputs['Color'].suffix = 'N' + node_n_img.inputs['Split Output'].disp_path = bake_path + node_n_img.inputs['Split Output'].img_name = img_name + + ## AO + node_ao_pass = tree.nodes.new('BakeWrangler_Bake_Pass') + node_ao_pass.location = (-200, node_y_pos -2000) + node_ao_img = tree.nodes.new('BakeWrangler_Output_Image_Path') + node_ao_img.location = (200, node_y_pos -2000) + + tree.links.new(node_ao_pass.outputs['Color'], node_ao_img.inputs['Color']) + + node_ao_pass.bake_cat = 'CORE' + node_ao_pass.bake_core = 'AO' + node_ao_pass.bake_samples = 32 + node_ao_img.inputs['Color'].suffix = 'AO' + node_ao_img.inputs['Split Output'].disp_path = bake_path + node_ao_img.inputs['Split Output'].img_name = img_name + + ## Roughness + node_r_pass = tree.nodes.new('BakeWrangler_Bake_Pass') + node_r_pass.location = (-200, node_y_pos -2500) + node_r_img = tree.nodes.new('BakeWrangler_Output_Image_Path') + node_r_img.location = (200, node_y_pos -2500) + + tree.links.new(node_r_pass.outputs['Color'], node_r_img.inputs['Color']) + + node_r_pass.bake_cat = 'CORE' + node_r_pass.bake_core = 'ROUGHNESS' + node_r_img.inputs['Color'].suffix = 'R' + node_r_img.inputs['Split Output'].disp_path = bake_path + node_r_img.inputs['Split Output'].img_name = img_name + + ## Metallic + node_m_pass = tree.nodes.new('BakeWrangler_Bake_Pass') + node_m_pass.location = (-200, node_y_pos -3000) + node_m_img = tree.nodes.new('BakeWrangler_Output_Image_Path') + node_m_img.location = (200, node_y_pos -3000) + + tree.links.new(node_m_pass.outputs['Color'], node_m_img.inputs['Color']) + + node_m_pass.bake_cat = 'PBR' + node_m_pass.bake_pbr = 'METALLIC' + node_m_img.inputs['Color'].suffix = 'M' + node_m_img.inputs['Split Output'].disp_path = bake_path + node_m_img.inputs['Split Output'].img_name = img_name + + ## UV + node_uv_pass = tree.nodes.new('BakeWrangler_Bake_Pass') + node_uv_pass.location = (-200, node_y_pos -3500) + node_uv_img = tree.nodes.new('BakeWrangler_Output_Image_Path') + node_uv_img.location = (200, node_y_pos -3500) + + tree.links.new(node_uv_pass.outputs['Color'], node_uv_img.inputs['Color']) + + node_uv_pass.bake_cat = 'WRANG' + node_uv_pass.bake_wrang = 'ISLANDID' + node_uv_img.inputs['Color'].suffix = 'UV' + node_uv_img.inputs['Split Output'].disp_path = bake_path + node_uv_img.inputs['Split Output'].img_name = img_name + + # connect meshes to passes + tree.links.new(node_inputs.outputs['Mesh'], node_d_pass.inputs[1]) + ''' + tree.links.new(node_inputs.outputs['Mesh'], node_c_pass.inputs[1]) + ''' + tree.links.new(node_inputs.outputs['Mesh'], node_n_pass.inputs[1]) + tree.links.new(node_inputs.outputs['Mesh'], node_ao_pass.inputs[1]) + tree.links.new(node_inputs.outputs['Mesh'], node_r_pass.inputs[1]) + tree.links.new(node_inputs.outputs['Mesh'], node_m_pass.inputs[1]) + tree.links.new(node_inputs.outputs['Mesh'], node_uv_pass.inputs[1]) + + ## batch bake node + tree.links.new(node_d_img.outputs['Bake'], node_batch.inputs[pass_socket]) + ''' + pass_socket += 1 + tree.links.new(node_c_img.outputs['Bake'], node_batch.inputs[pass_socket]) + ''' + pass_socket += 1 + tree.links.new(node_n_img.outputs['Bake'], node_batch.inputs[pass_socket]) + pass_socket += 1 + tree.links.new(node_ao_img.outputs['Bake'], node_batch.inputs[pass_socket]) + pass_socket += 1 + tree.links.new(node_r_img.outputs['Bake'], node_batch.inputs[pass_socket]) + pass_socket += 1 + tree.links.new(node_m_img.outputs['Bake'], node_batch.inputs[pass_socket]) + pass_socket += 1 + tree.links.new(node_uv_img.outputs['Bake'], node_batch.inputs[pass_socket]) + pass_socket += 1 + + node_y_pos -= 4000 + + return bpy.ops.bake_wrangler.bake_pass(tree=tree_name, node="Batch Bake", sock=-1) diff --git a/cg/pipeline/cg_pipeline.py b/cg/pipeline/cg_pipeline.py index 44a211a..cd481aa 100644 --- a/cg/pipeline/cg_pipeline.py +++ b/cg/pipeline/cg_pipeline.py @@ -22,6 +22,7 @@ from blender.processing.highpoly_setup import setup_meshes from blender.processing.midpoly_setup import hightpoly_collections_to_midpoly from blender.processing.lowpoly_setup import parts_to_shells from blender.processing.uv_setup import uv_unwrap +from blender.texturing.bake_submitter import bw_submit from blender.export.dae import export_dae from blender.export.stl import export_stl import bpy @@ -74,6 +75,7 @@ def cg_pipeline(**kwargs): config = kwargs.pop('config', None) # prepare blend file + remove_collections() cleanup_orphan_data() @@ -88,16 +90,16 @@ def cg_pipeline(**kwargs): ) ) - # restructuring hierarchy by lcs points - if imported_objects['objs_lcs']: - restruct_hierarchy(imported_objects['objs_lcs']) - # save import in blender scene if blend_path is not None: if not os.path.isdir(os.path.dirname(blend_path)): os.makedirs(os.path.dirname(blend_path)) bpy.ops.wm.save_as_mainfile(filepath=blend_path) + # restructuring hierarchy by lcs points + if imported_objects['objs_lcs']: + restruct_hierarchy(imported_objects['objs_lcs']) + # prepare highpoly if imported_objects['objs_foreground']: setup_meshes(imported_objects['objs_foreground'], sharpness=True, shading=True) @@ -115,7 +117,6 @@ def cg_pipeline(**kwargs): ) hightpoly_collections_to_midpoly(part_names) - # prepare lowpoly lowpoly_obj_names = parts_to_shells(part_names) uv_unwrap(lowpoly_obj_names) @@ -126,6 +127,10 @@ def cg_pipeline(**kwargs): os.makedirs(os.path.dirname(blend_path)) bpy.ops.wm.save_as_mainfile(filepath=blend_path) + # bake textures + bpy.ops.wm.open_mainfile(filepath=blend_path) + bw_submit(lowpoly_obj_names) + # export object meshes and urdf to_urdf = collections.defaultdict(list)