From 3854131d083b01dfd9fd08a24de12e632820b866 Mon Sep 17 00:00:00 2001 From: meta-androcto Date: Fri, 5 Aug 2016 09:27:33 +1000 Subject: update materias utils & conversion re: T48642 --- materials_utils/LICENSE | 674 ++++++++ materials_utils/README.md | 1 + materials_utils/__init__.py | 2244 +++++++++++++++++++++++++ materials_utils/material_converter.py | 777 +++++++++ materials_utils/materials_cycles_converter.py | 683 ++++++++ materials_utils/texture_rename.py | 112 ++ materials_utils/warning_messages_utils.py | 177 ++ space_view3d_materials_utils.py | 855 ---------- 8 files changed, 4668 insertions(+), 855 deletions(-) create mode 100644 materials_utils/LICENSE create mode 100644 materials_utils/README.md create mode 100644 materials_utils/__init__.py create mode 100644 materials_utils/material_converter.py create mode 100644 materials_utils/materials_cycles_converter.py create mode 100644 materials_utils/texture_rename.py create mode 100644 materials_utils/warning_messages_utils.py delete mode 100644 space_view3d_materials_utils.py diff --git a/materials_utils/LICENSE b/materials_utils/LICENSE new file mode 100644 index 00000000..9cecc1d4 --- /dev/null +++ b/materials_utils/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. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + 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: + + {project} Copyright (C) {year} {fullname} + 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/materials_utils/README.md b/materials_utils/README.md new file mode 100644 index 00000000..133e5302 --- /dev/null +++ b/materials_utils/README.md @@ -0,0 +1 @@ +# Materials_Utils \ No newline at end of file diff --git a/materials_utils/__init__.py b/materials_utils/__init__.py new file mode 100644 index 00000000..990dbf03 --- /dev/null +++ b/materials_utils/__init__.py @@ -0,0 +1,2244 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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 2 +# 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, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### +# (c) 2016 meta-androcto, parts based on work by Saidenka, lijenstina +# Materials Utils: by MichaleW, lijenstina, +# (some code thanks to: CoDEmanX, SynaGl0w, +# ideasman42) +# Materials Conversion: Silvio Falcinelli, johnzero7#, +# fixes by angavrilov and others +# Link to base names: Sybren, Texture renamer: Yadoob + +bl_info = { + "name": "Materials Utils Specials", + "author": "Community", + "version": (1, 0, 0), + "blender": (2, 77, 0), + "location": "Materials Properties Specials/Shift Q", + "description": "Materials Utils & Convertors", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6" + "/Py/Scripts", + "tracker_url": "", + "category": "Material"} + +if "bpy" in locals(): + import importlib + importlib.reload(material_converter) + importlib.reload(materials_cycles_converter) + importlib.reload(texture_rename) + importlib.reload(warning_messages_utils) +else: + from . import material_converter + from . import materials_cycles_converter + from . import texture_rename + from . import warning_messages_utils + +import bpy +import os +from os import ( + path as os_path, + access as os_access, + remove as os_remove, + ) +from bpy.props import ( + StringProperty, + BoolProperty, + EnumProperty, + ) +from bpy.types import ( + Menu, + Operator, + Panel, + AddonPreferences, + PropertyGroup, + ) +from .warning_messages_utils import ( + warning_messages, + c_is_cycles_addon_enabled, + c_data_has_materials, + ) + +# ----------------------------------------------------------------------------- +# Globals # + +# set the default name of the new added material +MAT_DEFAULT_NAME = "New Material" + + +# ----------------------------------------------------------------------------- +# Functions # + +def fake_user_set(fake_user='ON', materials='UNUSED', operator=None): + warn_mesg, w_mesg = '', "" + if materials == 'ALL': + mats = (mat for mat in bpy.data.materials if mat.library is None) + w_mesg = "(All Materials in this .blend file)" + elif materials == 'UNUSED': + mats = (mat for mat in bpy.data.materials if mat.library is None and mat.users == 0) + w_mesg = "(Unused Materials - Active/Selected Objects)" + else: + mats = [] + if materials == 'ACTIVE': + objs = [bpy.context.active_object] + w_mesg = "(All Materials on Active Object)" + elif materials == 'SELECTED': + objs = bpy.context.selected_objects + w_mesg = "(All Materials on Selected Objects)" + elif materials == 'SCENE': + objs = bpy.context.scene.objects + w_mesg = "(All Scene Objects)" + else: + # used materials + objs = bpy.data.objects + w_mesg = "(All Used Materials)" + + mats = (mat for ob in objs if hasattr(ob.data, "materials") for + mat in ob.data.materials if mat.library is None) + + # collect mat names for warning_messages + matnames = [] + + warn_mesg = ('FAKE_SET_ON' if fake_user == 'ON' else 'FAKE_SET_OFF') + + for mat in mats: + mat.use_fake_user = (fake_user == 'ON') + matnames.append(getattr(mat, "name", "NO NAME")) + + if operator: + if matnames: + warning_messages(operator, warn_mesg, matnames, 'MAT', w_mesg) + else: + warning_messages(operator, 'FAKE_NO_MAT') + + for area in bpy.context.screen.areas: + if area.type in ('PROPERTIES', 'NODE_EDITOR', 'OUTLINER'): + area.tag_redraw() + + +def replace_material(m1, m2, all_objects=False, update_selection=False, operator=None): + # replace material named m1 with material named m2 + # m1 is the name of original material + # m2 is the name of the material to replace it with + # 'all' will replace throughout the blend file + + matorg = bpy.data.materials.get(m1) + matrep = bpy.data.materials.get(m2) + + if matorg != matrep and None not in (matorg, matrep): + # store active object + if all_objects: + objs = bpy.data.objects + else: + objs = bpy.context.selected_editable_objects + + for ob in objs: + if ob.type == 'MESH': + match = False + for m in ob.material_slots: + if m.material == matorg: + m.material = matrep + # don't break the loop as the material can be + # ref'd more than once + + # Indicate which objects were affected + if update_selection: + ob.select = True + match = True + + if update_selection and not match: + ob.select = False + else: + if operator: + warning_messages(operator, "REP_MAT_NONE") + + +def select_material_by_name(find_mat_name): + # in object mode selects all objects with material find_mat_name + # in edit mode selects all polygons with material find_mat_name + + find_mat = bpy.data.materials.get(find_mat_name) + + if find_mat is None: + return + + # check for editmode + editmode = False + + scn = bpy.context.scene + + # set selection mode to polygons + scn.tool_settings.mesh_select_mode = False, False, True + + actob = bpy.context.active_object + if actob.mode == 'EDIT': + editmode = True + bpy.ops.object.mode_set() + + if not editmode: + objs = bpy.data.objects + for ob in objs: + if included_object_types(ob.type): + ms = ob.material_slots + for m in ms: + if m.material == find_mat: + ob.select = True + # the active object may not have the mat! + # set it to one that does! + scn.objects.active = ob + break + else: + ob.select = False + # deselect non-meshes + else: + ob.select = False + else: + # it's editmode, so select the polygons + ob = actob + ms = ob.material_slots + + # same material can be on multiple slots + slot_indeces = [] + i = 0 + + for m in ms: + if m.material == find_mat: + slot_indeces.append(i) + i += 1 + me = ob.data + + for f in me.polygons: + if f.material_index in slot_indeces: + f.select = True + else: + f.select = False + me.update() + + if editmode: + bpy.ops.object.mode_set(mode='EDIT') + + +def mat_to_texface(operator=None): + # assigns the first image in each material to the polygons in the active + # uvlayer for all selected objects + + # check for editmode + editmode = False + + actob = bpy.context.active_object + if actob.mode == 'EDIT': + editmode = True + bpy.ops.object.mode_set() + + # collect object names for warning messages + message_a = [] + # Flag if there are non MESH objects selected + mixed_obj = False + + for ob in bpy.context.selected_editable_objects: + if ob.type == 'MESH': + # get the materials from slots + ms = ob.material_slots + + # build a list of images, one per material + images = [] + # get the textures from the mats + for m in ms: + if m.material is None: + continue + gotimage = False + textures = zip(m.material.texture_slots, m.material.use_textures) + for t, enabled in textures: + if enabled and t is not None: + tex = t.texture + if tex.type == 'IMAGE': + img = tex.image + images.append(img) + gotimage = True + break + + if not gotimage: + images.append(None) + + # check materials for warning messages + mats = ob.material_slots.keys() + if operator and not mats and mixed_obj is False: + message_a.append(ob.name) + + # now we have the images + # apply them to the uvlayer + me = ob.data + + # got uvs? + if not me.uv_textures: + scn = bpy.context.scene + scn.objects.active = ob + bpy.ops.mesh.uv_texture_add() + scn.objects.active = actob + + # get active uvlayer + for t in me.uv_textures: + if t.active: + uvtex = t.data + for f in me.polygons: + # check that material had an image! + if images and images[f.material_index] is not None: + uvtex[f.index].image = images[f.material_index] + else: + uvtex[f.index].image = None + me.update() + else: + message_a.append(ob.name) + mixed_obj = True + + if editmode: + bpy.ops.object.mode_set(mode='EDIT') + + if operator and message_a: + warn_mess = ('MAT_TEX_NO_MESH' if mixed_obj is True else 'MAT_TEX_NO_MAT') + warning_messages(operator, warn_mess, message_a) + + +def assignmatslots(ob, matlist): + # given an object and a list of material names + # removes all material slots from the object + # adds new ones for each material in matlist + # adds the materials to the slots as well. + + scn = bpy.context.scene + ob_active = bpy.context.active_object + scn.objects.active = ob + + for s in ob.material_slots: + bpy.ops.object.material_slot_remove() + + # re-add them and assign material + if matlist: + for m in matlist: + try: + mat = bpy.data.materials[m] + ob.data.materials.append(mat) + except: + # there is no material with that name in data + # or an empty mat is for some reason assigned + # to face indices, mat tries to get an '' as mat index + pass + + # restore active object: + scn.objects.active = ob_active + + +def cleanmatslots(operator=None): + # check for edit mode + editmode = False + actob = bpy.context.active_object + + # active object? + if actob: + if actob.mode == 'EDIT': + editmode = True + bpy.ops.object.mode_set() + + # is active object selected ? + selected = bool(actob.select) + + actob.select = True + + objs = bpy.context.selected_editable_objects + # collect all object names for warning_messages + message_a = [] + # Flag if there are non MESH objects selected + mixed_obj = False + + for ob in objs: + if ob.type == 'MESH': + mats = ob.material_slots.keys() + + # if mats is empty then mats[faceindex] will be out of range + if mats: + # check the polygons on the mesh to build a list of used materials + usedMatIndex = [] # we'll store used materials indices here + faceMats = [] + me = ob.data + for f in me.polygons: + # get the material index for this face... + faceindex = f.material_index + + # indices will be lost: Store face mat use by name + currentfacemat = mats[faceindex] + faceMats.append(currentfacemat) + + # check if index is already listed as used or not + found = False + for m in usedMatIndex: + if m == faceindex: + found = True + # break + + if found is False: + # add this index to the list + usedMatIndex.append(faceindex) + + # re-assign the used mats to the mesh and leave out the unused + ml = [] + mnames = [] + for u in usedMatIndex: + ml.append(mats[u]) + # we'll need a list of names to get the face indices... + mnames.append(mats[u]) + + assignmatslots(ob, ml) + + # restore face indices: + i = 0 + for f in me.polygons: + matindex = mnames.index(faceMats[i]) + f.material_index = matindex + i += 1 + else: + message_a.append(getattr(ob, "name", "NO NAME")) + continue + else: + message_a.append(getattr(ob, "name", "NO NAME")) + if mixed_obj is False: + mixed_obj = True + continue + + if message_a and operator: + warn_mess = ('C_OB_MIX_NO_MAT' if mixed_obj is True else 'C_OB_NO_MAT') + warning_messages(operator, warn_mess, message_a) + + if actob: + # restore selection state + actob.select = selected + + if editmode: + bpy.ops.object.mode_set(mode='EDIT') + + +# separate edit mode mesh function +# (faster than iterating through all faces) + +def assign_mat_mesh_edit(matname="Default", operator=None): + actob = bpy.context.active_object + + found = False + for m in bpy.data.materials: + if m.name == matname: + target = m + found = True + break + if not found: + target = bpy.data.materials.new(matname) + + if (actob.type in {'MESH'} and actob.mode in {'EDIT'}): + # check material slots for matname material + found = False + i = 0 + mats = actob.material_slots + for m in mats: + if m.name == matname: + found = True + # make slot active + actob.active_material_index = i + break + i += 1 + + if not found: + # the material is not attached to the object + actob.data.materials.append(target) + + # is selected ? + selected = bool(actob.select) + # select active object + actob.select = True + + # activate the chosen material + actob.active_material_index = i + + # assign the material to the object + bpy.ops.object.material_slot_assign() + + actob.data.update() + + # restore selection state + actob.select = selected + + if operator: + mat_names = ("A New Untitled" if matname in ("", None) else matname) + warning_messages(operator, 'A_MAT_NAME_EDIT', mat_names, 'MAT') + + +def assign_mat(matname="Default", operator=None): + # get active object so we can restore it later + actob = bpy.context.active_object + + # is active object selected ? + selected = bool(actob.select) + + actob.select = True + + # check if material exists, if it doesn't then create it + found = False + for m in bpy.data.materials: + if m.name == matname: + target = m + found = True + break + + if not found: + target = bpy.data.materials.new(matname) + + # if objectmode then set all polygons + editmode = False + allpolygons = True + if actob.mode == 'EDIT': + editmode = True + allpolygons = False + bpy.ops.object.mode_set() + + objs = bpy.context.selected_editable_objects + + # collect non mesh object names + message_a = [] + + for ob in objs: + # skip the objects that can't have mats + if not included_object_types(ob.type): + message_a.append(ob.name) + continue + else: + # set the active object to our object + scn = bpy.context.scene + scn.objects.active = ob + + if ob.type in {'CURVE', 'SURFACE', 'FONT', 'META'}: + found = False + i = 0 + for m in bpy.data.materials: + if m.name == matname: + found = True + index = i + break + i += 1 + if not found: + index = i - 1 + targetlist = [index] + assignmatslots(ob, targetlist) + + elif ob.type == 'MESH': + # check material slots for matname material + found = False + i = 0 + mats = ob.material_slots + for m in mats: + if m.name == matname: + found = True + index = i + # make slot active + ob.active_material_index = i + break + i += 1 + + if not found: + index = i + # the material is not attached to the object + ob.data.materials.append(target) + + # now assign the material: + me = ob.data + if allpolygons: + for f in me.polygons: + f.material_index = index + elif allpolygons is False: + for f in me.polygons: + if f.select: + f.material_index = index + me.update() + + # restore the active object + bpy.context.scene.objects.active = actob + + # restore selection state + actob.select = selected + + if editmode: + bpy.ops.object.mode_set(mode='EDIT') + + if operator and message_a: + warning_messages(operator, 'A_OB_MIX_NO_MAT', message_a) + + +def check_texture(img, mat): + # finds a texture from an image + # makes a texture if needed + # adds it to the material if it isn't there already + + tex = bpy.data.textures.get(img.name) + + if tex is None: + tex = bpy.data.textures.new(name=img.name, type='IMAGE') + + tex.image = img + + # see if the material already uses this tex + # add it if needed + found = False + for m in mat.texture_slots: + if m and m.texture == tex: + found = True + break + if not found and mat: + mtex = mat.texture_slots.add() + mtex.texture = tex + mtex.texture_coords = 'UV' + mtex.use_map_color_diffuse = True + + +def texface_to_mat(operator=None): + # editmode check here! + editmode = False + ob = bpy.context.object + if ob.mode == 'EDIT': + editmode = True + bpy.ops.object.mode_set() + + for ob in bpy.context.selected_editable_objects: + + faceindex = [] + unique_images = [] + # collect object names for warning messages + message_a = [] + + # check if object has UV and texture data and active image in Editor + if check_texface_to_mat(ob): + # get the texface images and store indices + for f in ob.data.uv_textures.active.data: + if f.image: + img = f.image + # build list of unique images + if img not in unique_images: + unique_images.append(img) + faceindex.append(unique_images.index(img)) + else: + img = None + faceindex.append(None) + else: + message_a.append(ob.name) + continue + + # check materials for images exist; create if needed + matlist = [] + + for i in unique_images: + if i: + try: + m = bpy.data.materials[i.name] + except: + m = bpy.data.materials.new(name=i.name) + continue + + finally: + matlist.append(m.name) + # add textures if needed + check_texture(i, m) + + # set up the object material slots + assignmatslots(ob, matlist) + + # set texface indices to material slot indices.. + me = ob.data + + i = 0 + for f in faceindex: + if f is not None: + me.polygons[i].material_index = f + i += 1 + if editmode: + bpy.ops.object.mode_set(mode='EDIT') + + if operator and message_a: + warning_messages(operator, "TEX_MAT_NO_CRT", message_a) + + +def remove_materials(operator=None, setting="SLOT"): + # Remove material slots from active object + # SLOT - removes the object's active material + # ALL - removes the all the object's materials + actob = bpy.context.active_object + actob_name = getattr(actob, "name", "NO NAME") + + if actob: + if not included_object_types(actob.type): + if operator: + warning_messages(operator, 'OB_CANT_MAT', actob_name) + else: + if (hasattr(actob.data, "materials") and + len(actob.data.materials) > 0): + if setting == "SLOT": + bpy.ops.object.material_slot_remove() + elif setting == "ALL": + for mat in actob.data.materials: + try: + bpy.ops.object.material_slot_remove() + except: + pass + + if operator: + warn_mess = ('R_ACT_MAT_ALL' if setting == "ALL" else 'R_ACT_MAT') + warning_messages(operator, warn_mess, actob_name) + elif operator: + warning_messages(operator, 'R_OB_NO_MAT', actob_name) + + +def remove_materials_all(operator=None): + # Remove material slots from all selected objects + # counter for material slots warning messages, collect errors + mat_count, collect_mess = False, [] + + for ob in bpy.context.selected_editable_objects: + if not included_object_types(ob.type): + continue + else: + # code from blender stackexchange (by CoDEmanX) + ob.active_material_index = 0 + + if (hasattr(ob.data, "materials") and + len(ob.material_slots) >= 1): + mat_count = True + + # Ctx - copy the context for operator override + Ctx = bpy.context.copy() + # for this operator it needs only the active object replaced + Ctx['object'] = ob + + for i in range(len(ob.material_slots)): + try: + bpy.ops.object.material_slot_remove(Ctx) + except: + ob_name = getattr(ob, "name", "NO NAME") + collect_mess.append(ob_name) + pass + + if operator: + warn_msg = ('R_ALL_NO_MAT' if mat_count is False else 'R_ALL_SL_MAT') + if not collect_mess: + warning_messages(operator, warn_msg) + else: + warning_messages(operator, 'R_OB_FAIL_MAT', collect_mess) + + +# ----------------------------------------------------------------------------- +# Operator Classes # + +class VIEW3D_OT_show_mat_preview(Operator): + bl_label = "Preview Active Material" + bl_idname = "view3d.show_mat_preview" + bl_description = ("Show the preview of Active Material \n" + "and context related settings") + bl_options = {'REGISTER', 'UNDO'} + + is_not_undo = False # prevent drawing props on undo + + @classmethod + def poll(cls, context): + return (context.active_object is not None and + context.object.active_material is not None and + included_object_types(context.object.type)) + + def invoke(self, context, event): + self.is_not_undo = True + return context.window_manager.invoke_props_dialog(self, width=200) + + def draw(self, context): + layout = self.layout + ob = context.active_object + prw_size = size_preview() + + if self.is_not_undo is True: + if ob and hasattr(ob, "active_material"): + mat = ob.active_material + is_opaque = (True if (ob and hasattr(ob, "show_transparent") and + ob.show_transparent is True) + else False) + is_opaque_bi = (True if (mat and hasattr(mat, "use_transparency") and + mat.use_transparency is True) + else False) + is_mesh = (True if ob.type == 'MESH' else False) + + if size_type_is_preview(): + layout.template_ID_preview(ob, "active_material", new="material.new", + rows=prw_size['Width'], cols=prw_size['Height']) + else: + layout.template_ID(ob, "active_material", new="material.new") + layout.separator() + + if c_render_engine("Both"): + layout.prop(mat, "use_nodes", icon='NODETREE') + + if c_need_of_viewport_colors(): + color_txt = ("Viewport Color:" if c_render_engine("Cycles") else "Diffuse") + spec_txt = ("Viewport Specular:" if c_render_engine("Cycles") else "Specular") + col = layout.column(align=True) + col.label(color_txt) + col.prop(mat, "diffuse_color", text="") + if c_render_engine("BI"): + # Blender Render + col.prop(mat, "diffuse_intensity", text="Intensity") + col.separator() + + col.label(spec_txt) + col.prop(mat, "specular_color", text="") + col.prop(mat, "specular_hardness") + + if (c_render_engine("BI") and not c_context_use_nodes()): + # Blender Render + col.separator() + col.prop(mat, "use_transparency") + col.separator() + if is_opaque_bi: + col.prop(mat, "transparency_method", text="") + col.separator() + col.prop(mat, "alpha") + elif (c_render_engine("Cycles") and is_mesh): + # Cycles + col.separator() + col.prop(ob, "show_transparent", text="Transparency") + if is_opaque: + col.separator() + col.prop(mat, "alpha") + col.separator() + col.label("Viewport Alpha:") + col.prop(mat.game_settings, "alpha_blend", text="") + layout.separator() + else: + other_render = ("*Unavailable with this Renderer*" if not c_render_engine("Both") + else "*Unavailable in this Context*") + no_col_label = ("*Only available in Solid Shading*" if c_render_engine("Cycles") + else other_render) + layout.label(no_col_label, icon="INFO") + else: + layout.label(text="**Only Undo is available**", icon="INFO") + + def check(self, context): + if self.is_not_undo is True: + return True + + def execute(self, context): + self.is_not_undo = False + return {'FINISHED'} + + +class VIEW3D_OT_copy_material_to_selected(Operator): + bl_idname = "view3d.copy_material_to_selected" + bl_label = "Copy Materials to others" + bl_description = ("Copy Material From Active to Selected objects \n" + "Works on Object's Data linked Materials") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (c_data_has_materials() and + context.active_object is not None and + included_object_types(context.active_object.type) and + context.object.active_material is not None and + context.selected_editable_objects) + + def execute(self, context): + warn_mess = "DEFAULT" + if (len(context.selected_editable_objects) < 2): + warn_mess = 'CPY_MAT_ONE_OB' + else: + if check_is_excluded_obj_types(context): + warn_mess = 'CPY_MAT_MIX_OB' + try: + bpy.ops.object.material_slot_copy() + warn_mess = 'CPY_MAT_DONE' + except: + warning_messages(self, 'CPY_MAT_FAIL') + return {'CANCELLED'} + + warning_messages(self, warn_mess) + return {'FINISHED'} + + +class VIEW3D_OT_texface_to_material(Operator): + bl_idname = "view3d.texface_to_material" + bl_label = "Texface Images to Material/Texture" + bl_description = ("Create texture materials for images assigned in UV editor \n" + "Needs an UV Unwrapped Mesh and an image active in the \n" + "UV/Image Editor for each Selected Object") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def execute(self, context): + if context.selected_editable_objects: + texface_to_mat(self) + return {'FINISHED'} + else: + warning_messages(self, 'TEX_MAT_NO_SL') + return {'CANCELLED'} + + +class VIEW3D_OT_assign_material(Operator): + bl_idname = "view3d.assign_material" + bl_label = "Assign Material" + bl_description = "Assign a material to the selection" + bl_options = {'REGISTER', 'UNDO'} + + is_edit = False + + matname = StringProperty( + name="Material Name", + description="Name of Material to Assign", + default=MAT_DEFAULT_NAME, + maxlen=128, + ) + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + actob = context.active_object + mn = self.matname + + if (actob.type in {'MESH'} and actob.mode in {'EDIT'}): + assign_mat_mesh_edit(mn, self) + else: + assign_mat(mn, self) + + if use_cleanmat_slots(): + cleanmatslots() + + mat_to_texface() + + # reset the passing string + self.matname = "" + + return {'FINISHED'} + + +class VIEW3D_OT_clean_material_slots(Operator): + bl_idname = "view3d.clean_material_slots" + bl_label = "Clean Material Slots" + bl_description = ("Removes any unused material slots \n" + "from selected objects in Object mode") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + # materials can't be removed in Edit mode + def poll(cls, context): + return (c_data_has_materials() and + context.active_object is not None and + not context.object.mode == 'EDIT') + + def execute(self, context): + cleanmatslots(self) + return {'FINISHED'} + + +class VIEW3D_OT_material_to_texface(Operator): + bl_idname = "view3d.material_to_texface" + bl_label = "Material Images to Texface" + bl_description = ("Transfer material assignments to UV editor \n" + "Works on a Mesh Object with a Material and Texture\n" + "assigned. Used primarily with MultiTexture Shading") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (c_data_has_materials() and + context.active_object is not None) + + def execute(self, context): + if context.selected_editable_objects: + mat_to_texface(self) + return {'FINISHED'} + else: + warning_messages(self, "MAT_TEX_NO_SL") + return {'CANCELLED'} + + +class VIEW3D_OT_material_remove_slot(Operator): + bl_idname = "view3d.material_remove_slot" + bl_label = "Remove Active Slot (Active Object)" + bl_description = ("Remove active material slot from active object\n" + "Can't be used in Edit Mode") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + # materials can't be removed in Edit mode + def poll(cls, context): + return (c_data_has_materials() and + context.active_object is not None and + not context.object.mode == 'EDIT') + + def execute(self, context): + if context.selected_editable_objects: + remove_materials(self, "SLOT") + return {'FINISHED'} + else: + warning_messages(self, 'R_NO_SL_MAT') + return {'CANCELLED'} + + +class VIEW3D_OT_material_remove_object(Operator): + bl_idname = "view3d.material_remove_object" + bl_label = "Remove all Slots (Active Object)" + bl_description = ("Remove all material slots from active object\n" + "Can't be used in Edit Mode") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + # materials can't be removed in Edit mode + def poll(cls, context): + return (c_data_has_materials() and + context.active_object is not None and + not context.object.mode == 'EDIT') + + def execute(self, context): + if context.selected_editable_objects: + remove_materials(self, "ALL") + return {'FINISHED'} + else: + warning_messages(self, 'R_NO_SL_MAT') + return {'CANCELLED'} + + +class VIEW3D_OT_material_remove_all(Operator): + bl_idname = "view3d.material_remove_all" + bl_label = "Remove All Material Slots" + bl_description = ("Remove all material slots from all selected objects \n" + "Can't be used in Edit Mode") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + # materials can't be removed in Edit mode + def poll(cls, context): + return (c_data_has_materials() and + context.active_object is not None and + not context.object.mode == 'EDIT') + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def execute(self, context): + if context.selected_editable_objects: + remove_materials_all(self) + return {'FINISHED'} + else: + warning_messages(self, 'R_NO_SL_MAT') + return {'CANCELLED'} + + +class VIEW3D_OT_select_material_by_name(Operator): + bl_idname = "view3d.select_material_by_name" + bl_label = "Select Material By Name" + bl_description = "Select geometry with this material assigned to it" + bl_options = {'REGISTER', 'UNDO'} + matname = StringProperty( + name='Material Name', + description='Name of Material to Select', + maxlen=63, + ) + + @classmethod + def poll(cls, context): + return (c_data_has_materials() and + context.active_object is not None) + + def execute(self, context): + mn = self.matname + select_material_by_name(mn) + warning_messages(self, 'SL_MAT_BY_NAME', mn) + return {'FINISHED'} + + +class VIEW3D_OT_replace_material(Operator): + bl_idname = "view3d.replace_material" + bl_label = "Replace Material" + bl_description = "Replace a material by name" + bl_options = {'REGISTER', 'UNDO'} + + matorg = StringProperty( + name="Original", + description="Material to replace", + maxlen=63, + ) + matrep = StringProperty( + name="Replacement", + description="Replacement material", + maxlen=63, + ) + all_objects = BoolProperty( + name="All objects", + description="Replace for all objects in this blend file", + default=True, + ) + update_selection = BoolProperty( + name="Update Selection", + description="Select affected objects and deselect unaffected", + default=True, + ) + + @classmethod + def poll(cls, context): + return c_data_has_materials() + + def draw(self, context): + layout = self.layout + layout.prop_search(self, "matorg", bpy.data, "materials") + layout.prop_search(self, "matrep", bpy.data, "materials") + layout.prop(self, "all_objects") + layout.prop(self, "update_selection") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + replace_material(self.matorg, self.matrep, self.all_objects, + self.update_selection, self) + self.matorg, self.matrep = "", "" + return {'FINISHED'} + + +class VIEW3D_OT_fake_user_set(Operator): + bl_idname = "view3d.fake_user_set" + bl_label = "Set Fake User" + bl_description = "Enable/disable fake user for materials" + bl_options = {'REGISTER', 'UNDO'} + + fake_user = EnumProperty( + name="Fake User", + description="Turn fake user on or off", + items=(('ON', "On", "Enable fake user"), ('OFF', "Off", "Disable fake user")), + default='ON', + ) + + materials = EnumProperty( + name="Materials", + description="Which materials of objects to affect", + items=(('ACTIVE', "Active object", "Materials of active object only"), + ('SELECTED', "Selected objects", "Materials of selected objects"), + ('SCENE', "Scene objects", "Materials of objects in current scene"), + ('USED', "Used", "All materials used by objects"), + ('UNUSED', "Unused", "Currently unused materials"), + ('ALL', "All", "All materials in this blend file")), + default='UNUSED', + ) + + @classmethod + def poll(cls, context): + return c_data_has_materials() + + def draw(self, context): + layout = self.layout + layout.prop(self, "fake_user", expand=True) + layout.prop(self, "materials") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + fake_user_set(self.fake_user, self.materials, self) + return {'FINISHED'} + + +class MATERIAL_OT_set_transparent_back_side(Operator): + bl_idname = "material.set_transparent_back_side" + bl_label = "Transparent back (BI)" + bl_description = ("Creates BI nodes with Alpha output connected to" + "Front/Back Geometry node \n" + "on Object's Active Material Slot") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.active_object + if (not obj): + return False + mat = obj.active_material + if (not mat): + return False + if (mat.node_tree): + if (len(mat.node_tree.nodes) == 0): + return True + if (not mat.use_nodes): + return True + return False + + def execute(self, context): + obj = context.active_object + mat = obj.active_material + try: + mat.use_nodes = True + if (mat.node_tree): + for node in mat.node_tree.nodes: + if (node): + mat.node_tree.nodes.remove(node) + + mat.use_transparency = True + node_mat = mat.node_tree.nodes.new('ShaderNodeMaterial') + node_out = mat.node_tree.nodes.new('ShaderNodeOutput') + node_geo = mat.node_tree.nodes.new('ShaderNodeGeometry') + node_mat.material = mat + node_out.location = [node_out.location[0] + 500, node_out.location[1]] + node_geo.location = [node_geo.location[0] + 150, node_geo.location[1] - 150] + mat.node_tree.links.new(node_mat.outputs[0], node_out.inputs[0]) + mat.node_tree.links.new(node_geo.outputs[8], node_out.inputs[1]) + except: + warning_messages(self, 'E_MAT_TRNSP_BACK') + return {'CANCELLED'} + + if hasattr(mat, "name"): + warning_messages(self, 'MAT_TRNSP_BACK', mat.name, 'MAT') + + return {'FINISHED'} + + +class MATERIAL_OT_move_slot_top(Operator): + bl_idname = "material.move_material_slot_top" + bl_label = "Slot to the top" + bl_description = "Move the active material slot on top" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.active_object + if (not obj): + return False + if (len(obj.material_slots) <= 2): + return False + if (obj.active_material_index <= 0): + return False + return True + + def execute(self, context): + activeObj = context.active_object + + for i in range(activeObj.active_material_index): + bpy.ops.object.material_slot_move(direction='UP') + + active_mat = context.object.active_material + if active_mat and hasattr(active_mat, "name"): + warning_messages(self, 'MOVE_SLOT_UP', active_mat.name, 'MAT') + + return {'FINISHED'} + + +class MATERIAL_OT_move_slot_bottom(Operator): + bl_idname = "material.move_material_slot_bottom" + bl_label = "Slots to the bottom" + bl_description = "Move the active material slot to the bottom" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.active_object + if (not obj): + return False + if (len(obj.material_slots) <= 2): + return False + if (len(obj.material_slots) - 1 <= obj.active_material_index): + return False + return True + + def execute(self, context): + activeObj = context.active_object + lastSlotIndex = len(activeObj.material_slots) - 1 + + for i in range(lastSlotIndex - activeObj.active_material_index): + bpy.ops.object.material_slot_move(direction='DOWN') + + active_mat = context.object.active_material + if active_mat and hasattr(active_mat, "name"): + warning_messages(self, 'MOVE_SLOT_DOWN', active_mat.name, 'MAT') + + return {'FINISHED'} + + +class MATERIAL_OT_link_to_base_names(Operator): + bl_idname = "material.link_to_base_names" + bl_label = "Merge Base Names" + bl_description = ("Replace .001, .002 slots with Original \n" + "Material/Name on All Materials/Objects") + bl_options = {'REGISTER', 'UNDO'} + + mat_keep = StringProperty( + name="Material to keep", + default="", + ) + + is_auto = BoolProperty( + name="Auto Rename/Replace", + description=("Automatically Replace names " + "by stripping numerical suffix"), + default=False, + ) + + mat_error = [] # collect mat for warning messages + is_not_undo = False # prevent drawing props on undo + check_no_name = True # check if no name is passed + + @classmethod + def poll(cls, context): + return (c_data_has_materials() and context.active_object is not None) + + def draw(self, context): + layout = self.layout + if self.is_not_undo is True: + boxee = layout.box() + boxee.prop_search(self, "mat_keep", bpy.data, "materials") + boxee.enabled = not self.is_auto + layout.separator() + + boxs = layout.box() + boxs.prop(self, "is_auto", text="Auto Rename/Replace", icon="SYNTAX_ON") + else: + layout.label(text="**Only Undo is available**", icon="INFO") + + def invoke(self, context, event): + self.is_not_undo = True + return context.window_manager.invoke_props_dialog(self) + + def replace_name(self): + # use the chosen material as a base one + # check if there is a name + self.check_no_name = (False if self.mat_keep in {""} else True) + + if self.check_no_name is True: + for mat in bpy.data.materials: + name = mat.name + if name == self.mat_keep: + try: + base, suffix = name.rsplit('.', 1) + # trigger the except + num = int(suffix, 10) + self.mat_keep = base + mat.name = self.mat_keep + return + except ValueError: + if name not in self.mat_error: + self.mat_error.append(name) + return + return + + def split_name(self, material): + name = material.name + + if '.' not in name: + return name, None + + base, suffix = name.rsplit('.', 1) + + try: + # trigger the except + num = int(suffix, 10) + except ValueError: + # Not a numeric suffix + if name not in self.mat_error: + self.mat_error.append(name) + return name, None + + if self.is_auto is False: + if base == self.mat_keep: + return base, suffix + else: + return name, None + + return base, suffix + + def fixup_slot(self, slot): + if not slot.material: + return + + base, suffix = self.split_name(slot.material) + + if suffix is None: + return + + try: + base_mat = bpy.data.materials[base] + except KeyError: + print("Link to base names: Base material %r not found" % base) + return + + slot.material = base_mat + + def check(self, context): + return self.is_not_undo + + def main_loop(self, context): + for ob in context.scene.objects: + for slot in ob.material_slots: + self.fixup_slot(slot) + + def execute(self, context): + if self.is_auto is False: + self.replace_name() + if self.check_no_name is True: + self.main_loop(context) + else: + warning_messages(self, 'MAT_LINK_NO_NAME') + self.is_not_undo = False + return {'CANCELLED'} + + self.main_loop(context) + + if use_cleanmat_slots(): + cleanmatslots() + + if self.mat_error: + warning_messages(self, 'MAT_LINK_ERROR', self.mat_error, 'MAT') + + self.is_not_undo = False + return {'FINISHED'} + + +class MATERIAL_OT_check_converter_path(Operator): + bl_idname = "material.check_converter_path" + bl_label = "Check Converters images/data save path" + bl_description = ("Checks if the given path is writeable \n" + "(has OS writing privileges)") + bl_options = {'REGISTER', 'INTERNAL'} + + @classmethod + def poll(cls, context): + return True + + def check_valid_path(self, context): + sc = context.scene + paths = bpy.path.abspath(sc.mat_specials.conv_path) + + if os_path.exists(paths): + if os_access(paths, os.W_OK | os.X_OK): + try: + path_test = os_path.join(paths, "XYfoobartestXY.txt") + with open(path_test, 'w') as f: + f.closed + os_remove(path_test) + return True + except (OSError, IOError): + warning_messages(self, 'DIR_PATH_W_ERROR') + return False + else: + warning_messages(self, 'DIR_PATH_A_ERROR') + return False + else: + warning_messages(self, 'DIR_PATH_N_ERROR') + return False + + return True + + def execute(self, context): + if not self.check_valid_path(context): + return {'CANCELLED'} + else: + warning_messages(self, 'DIR_PATH_W_OK') + + return {'FINISHED'} + + +# ----------------------------------------------------------------------------- +# Menu classes # + +class VIEW3D_MT_assign_material(Menu): + bl_label = "Assign Material" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + if c_data_has_materials(): + # no materials + for material_name in bpy.data.materials.keys(): + layout.operator("view3d.assign_material", + text=material_name, + icon='MATERIAL_DATA').matname = material_name + use_separator(self, context) + + if (not context.active_object): + # info why the add material is innactive + layout.label(text="*No active Object in the Scene*", icon="INFO") + use_separator(self, context) + + layout.operator("view3d.assign_material", + text="Add New", + icon='ZOOMIN') + + +class VIEW3D_MT_select_material(Menu): + bl_label = "Select by Material" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + ob = context.object + + if (not c_data_has_materials()): + # (sad mall music is playing nearby) + layout.label(text="*No Materials in the data*", icon="INFO") + elif (not ob): + # don't worry, i don't like the default cubes, lamps and cameras too + layout.label(text="*No Objects to select*", icon="INFO") + else: + # we did what we could, now you're at the mercy of universe's entropy + if ob.mode == 'OBJECT': + # show all used materials in entire blend file + for material_name, material in bpy.data.materials.items(): + if (material.users > 0): + layout.operator("view3d.select_material_by_name", + text=material_name, + icon='MATERIAL_DATA', + ).matname = material_name + elif ob.mode == 'EDIT': + # show only the materials on this object + mats = ob.material_slots.keys() + for m in mats: + layout.operator("view3d.select_material_by_name", + text=m, + icon='MATERIAL_DATA').matname = m + + +class VIEW3D_MT_remove_material(Menu): + bl_label = "Clean Slots" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.operator("view3d.clean_material_slots", + text="Clean Material Slots", + icon='COLOR_BLUE') + use_separator(self, context) + + if not c_render_engine("Lux"): + layout.operator("view3d.material_remove_slot", icon='COLOR_GREEN') + layout.operator("view3d.material_remove_object", icon='COLOR_RED') + + if use_remove_mat_all(): + use_separator(self, context) + layout.operator("view3d.material_remove_all", + text="Remove Material Slots " + "(All Selected Objects)", + icon='CANCEL') + else: + layout.label(text="Sorry, other Menu functions are", icon="INFO") + layout.label(text="unvailable with Lux Renderer") + + +class VIEW3D_MT_master_material(Menu): + bl_label = "Material Specials Menu" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + if use_mat_preview() is True: + layout.operator("view3d.show_mat_preview", icon="VISIBLE_IPO_ON") + use_separator(self, context) + + layout.menu("VIEW3D_MT_assign_material", icon='ZOOMIN') + layout.menu("VIEW3D_MT_select_material", icon='HAND') + use_separator(self, context) + + layout.operator("view3d.copy_material_to_selected", icon="COPY_ID") + use_separator(self, context) + + layout.menu("VIEW3D_MT_remove_material", icon="COLORSET_10_VEC") + use_separator(self, context) + + layout.operator("view3d.replace_material", + text='Replace Material', + icon='ARROW_LEFTRIGHT') + layout.operator("view3d.fake_user_set", + text='Set Fake User', + icon='UNPINNED') + use_separator(self, context) + + layout.menu("VIEW3D_MT_mat_special", icon="SOLO_ON") + + +class VIEW3D_MT_mat_special(Menu): + bl_label = "Specials" + + def draw(self, context): + layout = self.layout + + if c_render_engine("Cycles"): + if (enable_converters() is True and converter_type('BI_CONV')): + ml_restore_1 = layout.operator("ml.restore", + text='To BI Nodes Off', + icon="BLENDER") + ml_restore_1.switcher = False + ml_restore_1.renderer = "BI" + + ml_restore_2 = layout.operator("ml.restore", + text='To BI Nodes On', + icon="APPEND_BLEND") + ml_restore_2.switcher = True + ml_restore_2.renderer = "BI" + use_separator(self, context) + + elif c_render_engine("BI"): + if (enable_converters() is True and converter_type('CYC_CONV')): + layout.operator("ml.refresh_active", + text='Convert Active to Cycles', + icon='NODE_INSERT_OFF') + layout.operator("ml.refresh", + text='Convert All to Cycles', + icon='NODE_INSERT_ON') + use_separator(self, context) + ml_restore_1 = layout.operator("ml.restore", + text='To Cycles Nodes Off', + icon="SOLID") + ml_restore_1.switcher = False + ml_restore_1.renderer = "CYCLES" + + ml_restore_2 = layout.operator("ml.restore", + text='To Cycles Nodes On', + icon="IMGDISPLAY") + ml_restore_2.switcher = True + ml_restore_2.renderer = "CYCLES" + use_separator(self, context) + + layout.operator("material.set_transparent_back_side", + icon='IMAGE_RGB_ALPHA', + text="Transparent back (BI)") + layout.operator("view3d.material_to_texface", + text="Material to Texface", + icon='MATERIAL_DATA') + layout.operator("view3d.texface_to_material", + text="Texface to Material", + icon='TEXTURE_SHADED') + use_separator(self, context) + + layout.operator("material.link_to_base_names", icon="KEYTYPE_BREAKDOWN_VEC") + use_separator(self, context) + layout.operator("texture.patern_rename", + text='Rename Image As Texture', + icon='TEXTURE') + + +# Specials Menu's # + +def menu_func(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + use_separator(self, context) + layout.menu("VIEW3D_MT_assign_material", icon='ZOOMIN') + layout.menu("VIEW3D_MT_select_material", icon='HAND') + layout.operator("view3d.replace_material", + text='Replace Material', + icon='ARROW_LEFTRIGHT') + use_separator(self, context) + + layout.menu("VIEW3D_MT_remove_material", icon="COLORSET_10_VEC") + use_separator(self, context) + + layout.operator("view3d.fake_user_set", + text='Set Fake User', + icon='UNPINNED') + use_separator(self, context) + + layout.menu("VIEW3D_MT_mat_special", icon="SOLO_ON") + + +def menu_move(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.operator("material.move_material_slot_top", + icon='TRIA_UP', text="Slot to top") + layout.operator("material.move_material_slot_bottom", + icon='TRIA_DOWN', text="Slot to bottom") + use_separator(self, context) + + +# Converters Menu's # + +class MATERIAL_MT_scenemassive_opt(Menu): + bl_idname = "scenemassive.opt" + bl_description = "Additional Options for Convert BI to Cycles" + bl_label = "Options" + bl_options = {'REGISTER'} + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.prop(sc.mat_specials, "EXTRACT_ALPHA", + text="Extract Alpha Textures (slow)") + use_separator(self, context) + layout.prop(sc.mat_specials, "EXTRACT_PTEX", + text="Extract Procedural Textures (slow)") + use_separator(self, context) + layout.prop(sc.mat_specials, "EXTRACT_OW", text="Re-extract Textures") + use_separator(self, context) + layout.prop(sc.mat_specials, "SET_FAKE_USER", text="Set Fake User on unused images") + use_separator(self, context) + + layout.label("Set the Bake Resolution") + res = str(sc.mat_specials.img_bake_size) + layout.label("Current Setting is : %s" % (res + "x" + res), icon='INFO') + use_separator(self, context) + layout.prop(sc.mat_specials, "img_bake_size", icon='NODE_SEL', expand=True) + + +class MATERIAL_PT_scenemassive(Panel): + bl_label = "Convert BI Materials to Cycles" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + return (enable_converters() is True and converter_type('BI_CONV')) + + def draw(self, context): + layout = self.layout + sc = context.scene + row = layout.row() + box = row.box() + + split = box.box().split(0.5) + split.operator("ml.refresh", + text="Convert All to Cycles", icon='MATERIAL') + split.operator("ml.refresh_active", + text="Convert Active to Cycles", icon='MATERIAL') + box = box.box() + ml_restore = box.operator("ml.restore", + text="To BI Nodes Off", + icon='MATERIAL') + ml_restore.switcher = False + ml_restore.renderer = "BI" + + row = layout.row() + box = row.box() + box.menu("scenemassive.opt", text="Advanced Options", icon='SCRIPTWIN') + box = row.box() + box.menu("help.biconvert", + text="Usage Information Guide", icon='MOD_EXPLODE') + + box = layout.box() + box.label("Save Directory") + split = box.split(0.85) + split.prop(sc.mat_specials, "conv_path", text="", icon="RENDER_RESULT") + split.operator("material.check_converter_path", + text="", icon="EXTERNAL_DATA") + + +class MATERIAL_PT_xps_convert(Panel): + bl_label = "Convert to BI and Cycles Nodes" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + return (enable_converters() is True and converter_type('CYC_CONV')) + + def draw(self, context): + layout = self.layout + row = layout.row() + box = row.box() + + box.label(text="Multi Image Support (Imports)") + split = box.box().split(0.5) + split.operator("xps_tools.convert_to_cycles_all", + text="Convert All to Nodes", icon="TEXTURE") + split.operator("xps_tools.convert_to_cycles_selected", + text="Convert Selected to Nodes", icon="TEXTURE") + + col = layout.column() + row = col.row() + box = row.box() + ml_restore = box.operator("ml.restore", + text="To BI Nodes ON", + icon='MATERIAL') + ml_restore.switcher = True + ml_restore.renderer = "BI" + + box = row.box() + box.menu("help.nodeconvert", + text="Usage Information Guide", icon="MOD_EXPLODE") + + +# Converters Help # + +class MATERIAL_MT_biconv_help(Menu): + bl_idname = "help.biconvert" + bl_description = "Read Instructions & Current Limitations" + bl_label = "Usage Information Guide" + bl_options = {'REGISTER'} + + def draw(self, context): + layout = self.layout + layout.label(text="Save Your Work Often", icon="ERROR") + use_separator(self, context) + layout.label(text="Select the texture loaded in the image node") + layout.label(text="Press Ctrl/T to create the image nodes") + layout.label(text="In the Node Editor, Select the Diffuse Node") + layout.label(text="Enable Node Wrangler addon", icon="NODETREE") + layout.label(text="If Unconnected or No Image Node Error:", icon="MOD_EXPLODE") + use_separator(self, context) + layout.label(text="The default path is the folder where the current .blend is") + layout.label(text="During Baking, the script will check writting privileges") + layout.label(text="Set the save path for extracting images with full access") + layout.label(text="May Require Run As Administrator on Windows OS", icon="ERROR") + layout.label(text="Converts Bi Textures to Image Files:", icon="MOD_EXPLODE") + use_separator(self, context) + layout.label(text="The Converter report can point out to some failures") + layout.label(text="Some material combinations are unsupported") + layout.label(text="Single BI Texture/Image per convert is only supported") + layout.label(text="Converts Basic BI non node materials to Cycles") + use_separator(self, context) + layout.label(text="Convert Bi Materials to Cycles Nodes:", icon="INFO") + + +class MATERIAL_MT_nodeconv_help(Menu): + bl_idname = "help.nodeconvert" + bl_description = "Read Instructions & Current Limitations" + bl_label = "Usage Information Guide" + bl_options = {'REGISTER'} + + def draw(self, context): + layout = self.layout + layout.label(text="Save Your Work Often", icon="ERROR") + use_separator(self, context) + layout.label(text="Relinking and removing some not needed nodes") + layout.label(text="The result Node tree will need some cleaning up") + use_separator(self, context) + layout.label(text="Select the texture loaded in the image node") + layout.label(text="Press Ctrl/T to create the image nodes") + layout.label(text="In the Node Editor, Select the Diffuse Node") + layout.label(text="Enable Node Wrangler addon", icon="NODETREE") + layout.label(text="If Unconnected or No Image Node Error:", icon="MOD_EXPLODE") + use_separator(self, context) + layout.label(text="Generated images (i.e. Noise and others) are not converted") + layout.label(text="The Converter report can point out to some failures") + layout.label(text="Not all Files will produce good results", icon="ERROR") + layout.label(text="fbx, .dae, .obj, .3ds, .xna and more") + layout.label(text="**Supports Imported Files**:", icon="IMPORT") + use_separator(self, context) + layout.label(text="For some file types") + layout.label(text="Supports Alpha, Normals, Specular and Diffuse") + layout.label(text="Then Converts BI Nodes to Cycles Nodes") + layout.label(text="Converts BI non node materials to BI Nodes") + use_separator(self, context) + layout.label(text="Convert Materials/Image Textures from Imports:", icon="INFO") + + +# Make Report # +class material_converter_report(Operator): + bl_idname = "mat_converter.reports" + bl_label = "Material Converter Report" + bl_description = "Report about done Material Conversions" + bl_options = {'REGISTER', 'INTERNAL'} + + message = StringProperty(maxlen=8192) + + def draw(self, context): + layout = self.layout + box = layout.box() + + box.label(text="Converter Report", icon='INFO') + + if self.message and type(self.message) is str: + list_string = self.message.split("*") + for line in range(len(list_string)): + box.label(text=str(list_string[line])) + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=500) + + def execute(self, context): + return {'FINISHED'} + + +# ----------------------------------------------------------------------------- +# Scene Properties + +class material_specials_scene_props(PropertyGroup): + conv_path = StringProperty( + name="Save Directory", + description=("Path to save images during conversion \n" + "Default is the location of the blend file"), + default="//", + subtype='DIR_PATH', + ) + EXTRACT_ALPHA = BoolProperty( + attr="EXTRACT_ALPHA", + default=False, + ) + SET_FAKE_USER = BoolProperty( + attr="SET_FAKE_USER", + default=False, + ) + EXTRACT_PTEX = BoolProperty( + attr="EXTRACT_PTEX", + default=False, + ) + EXTRACT_OW = BoolProperty( + attr="Overwrite", + default=False, + description="Extract textures again instead of re-using priorly extracted textures", + ) + img_bake_size = EnumProperty( + name="Bake Image Size", + description="Set the resolution size of baked images \n", + items=(('512', "Set : 512 x 512", "Bake Resolution 512 x 512"), + ('1024', "Set : 1024 x 1024", "Bake Resolution 1024 x 1024"), + ('2048', "Set : 2048 x 2048", "Bake Resolution 2048 x 2048")), + default='1024', + ) + + +# ----------------------------------------------------------------------------- +# Addon Preferences + +class VIEW3D_MT_material_utils_pref(AddonPreferences): + bl_idname = __name__ + + show_warnings = BoolProperty( + name="Enable Warning messages", + default=False, + description="Show warning messages \n" + "when an action is executed or failed.\n \n" + "Advisable if you don't know how the tool works", + ) + + show_remove_mat = BoolProperty( + name="Enable Remove all Materials", + default=False, + description="Enable Remove all Materials \n" + "for all Selected Objects \n \n" + "Use with care - if you want to keep materials after \n" + "closing \ reloading Blender Set Fake User for them", + ) + + show_mat_preview = BoolProperty( + name="Enable Material Preview", + default=True, + description="Material Preview of the Active Object \n" + "Contains the preview of the active Material, \n" + "Use nodes, Color, Specular and Transparency \n" + "settings depending on the Context and Preferences", + ) + + set_cleanmatslots = BoolProperty( + name="Enable Auto Clean", + default=True, + description="Enable Automatic Removal of unused Material Slots \n" + "called together with the Assign Material menu option. \n \n" + "Apart from preference and the cases when it affects \n" + "adding materials, enabling it can have some \n" + "performance impact on very dense meshes", + ) + + show_separators = BoolProperty( + name="Use Separators in the menus", + default=True, + description="Use separators in the menus, a trade-off between \n" + "readability vs. using more space for displaying items", + ) + + show_converters = BoolProperty( + name="Enable Converters", + default=True, + description=" \n ", + ) + + set_preview_size = EnumProperty( + name="Preview Menu Size", + description="Set the preview menu size \n" + "depending on the number of materials \n" + "in the scene (width and height)", + items=(('2x2', "Size 2x2", "Width 2 Height 2"), + ('2x3', "Size 2x3", "Width 3 Height 2"), + ('3x3', "Size 3x3", "Width 3 Height 3"), + ('3x4', "Size 3x4", "Width 4 Height 3"), + ('4x4', "Size 4x4", "Width 4 Height 4"), + ('5x5', "Size 5x5", "Width 5 Height 5"), + ('6x6', "Size 6x6", "Width 6 Height 6"), + ('0x0', "List", "Display as a List")), + default='3x3', + ) + + set_preview_type = EnumProperty( + name="Preview Menu Type", + description="Set the the Preview menu type \n", + items=(('LIST', "Classic", + "Display as a Classic List like in Blender Propreties. \n \n" + "Preview of Active Material not available"), + ('PREVIEW', "Preview Display", + "Display as a preview of Thumbnails \n" + "It can have some performance issues with \n" + "scenes containing a lot of materials \n \n" + "Preview of Active Material available")), + default='PREVIEW', + ) + + set_experimental_type = EnumProperty( + name="Experimental Features", + description=" \n", + items=(('ALL', "All Converters", + "Enable all Converters"), + ('CYC_CONV', "BI and Cycles Nodes", + "Enable Cycles related Convert"), + ('BI_CONV', "BI To Cycles", + "Enable Blender Internal related Converters")), + default='ALL', + ) + + def draw(self, context): + layout = self.layout + + box = layout.box() + split = box.split(align=True) + col = split.column() + + col.prop(self, "show_warnings") + cola = split.column() + cola.alignment = 'RIGHT' + cola.prop(self, "set_cleanmatslots") + cola.prop(self, "show_separators") + col.prop(self, "show_remove_mat") + + boxie = box.box() + row = boxie.row() + row.prop(self, "show_mat_preview") + rowsy = row.split() + rowsy.enabled = (True if self.show_mat_preview else False) + rowsy.alignment = 'CENTER' + rowsy.prop(self, "set_preview_type", text="") + rowsa = rowsy.row() + rowsa.enabled = (True if self.set_preview_type in {'PREVIEW'} else False) + rowsa.alignment = 'CENTER' + rowsa.prop(self, "set_preview_size", text="") + + boxif = box.box() + rowf = boxif.row() + rowf.prop(self, "show_converters") + rowsf = rowf.split() + rowsf.enabled = (True if self.show_converters else False) + rowsf.alignment = 'RIGHT' + rowsf.prop(self, "set_experimental_type", text="") + + +# ----------------------------------------------------------------------------- +# utility functions: + +def included_object_types(objects): + # Pass the bpy.data.objects.type to avoid needless assigning/removing + # included - type that can have materials + included = ['MESH', 'CURVE', 'SURFACE', 'FONT', 'META'] + + obj = objects + return bool(obj and obj in included) + + +def check_is_excluded_obj_types(contxt): + # pass the context to check if selected objects have excluded types + if contxt and contxt.selected_editable_objects: + for obj in contxt.selected_editable_objects: + if not included_object_types(obj.type): + return True + return False + + +def check_texface_to_mat(obj): + # check for UV data presence + if obj: + if hasattr(obj.data, "uv_textures"): + if hasattr(obj.data.uv_textures, "active"): + if hasattr(obj.data.uv_textures.active, "data"): + return True + return False + + +def c_context_mat_preview(): + # returns the type of viewport shading + # needed for using the optional UI elements (the context gets lost) + + # code from BA user SynaGl0w + # if there are multiple 3d views return the biggest screen area one + views_3d = [area for area in bpy.context.screen.areas if + area.type == 'VIEW_3D' and area.spaces.active] + + if views_3d: + main_view_3d = max(views_3d, key=lambda area: area.width * area.height) + return main_view_3d.spaces.active.viewport_shade + return "NONE" + + +def c_context_use_nodes(): + # checks if Use Nodes is ticked on + actob = bpy.context.active_object + u_node = (actob.active_material.use_nodes if + hasattr(actob, "active_material") else False) + + return bool(u_node) + + +def c_render_engine(cyc=None): + # valid cyc inputs "Cycles", "BI", "Both", "Lux" + scene = bpy.context.scene + render_engine = scene.render.engine + + r_engines = {"Cycles": 'CYCLES', + "BI": 'BLENDER_RENDER', + "Both": ('CYCLES', 'BLENDER_RENDER'), + "Lux": 'LUXRENDER_RENDER'} + if cyc: + return (True if cyc in r_engines and render_engine in r_engines[cyc] else False) + return render_engine + + +def c_need_of_viewport_colors(): + # check the context where using Viewport color and friends are needed + # Cycles and BI are supported + if c_render_engine("Cycles"): + if c_context_use_nodes() and c_context_mat_preview() == 'SOLID': + return True + elif c_context_mat_preview() in ('SOLID', 'TEXTURED', 'MATERIAL'): + return True + elif (c_render_engine("BI") and not c_context_use_nodes()): + return True + + return False + + +# Draw Separator # +def use_separator(operator, context): + # pass the preferences show_separators bool to enable/disable them + pref = return_preferences() + useSep = pref.show_separators + if useSep: + operator.layout.separator() + + +# preferences utilities # + +def return_preferences(): + return bpy.context.user_preferences.addons[__name__].preferences + + +def use_remove_mat_all(): + pref = return_preferences() + show_rmv_mat = pref.show_remove_mat + + return bool(show_rmv_mat) + + +def use_mat_preview(): + pref = return_preferences() + show_mat_prw = pref.show_mat_preview + + return bool(show_mat_prw) + + +def use_cleanmat_slots(): + pref = return_preferences() + use_mat_clean = pref.set_cleanmatslots + + return bool(use_mat_clean) + + +def size_preview(): + pref = return_preferences() + set_size_prw = pref.set_preview_size + + cell_w = int(set_size_prw[0]) + cell_h = int(set_size_prw[-1]) + cell_tbl = {'Width': cell_w, 'Height': cell_h} + + return cell_tbl + + +def size_type_is_preview(): + pref = return_preferences() + set_prw_type = pref.set_preview_type + + return bool(set_prw_type in {'PREVIEW'}) + + +def enable_converters(): + pref = return_preferences() + shw_conv = pref.show_converters + + return shw_conv + + +def converter_type(types='ALL'): + # checks the type of the preferences 'ALL', 'CYC_CONV', 'BI_CONV' + pref = return_preferences() + set_exp_type = pref.set_experimental_type + + return bool(set_exp_type in {'ALL'} or types == set_exp_type) + + +def register(): + bpy.utils.register_module(__name__) + + warning_messages_utils.MAT_SPEC_NAME = __name__ + + # Register Scene Properties + bpy.types.Scene.mat_specials = bpy.props.PointerProperty( + type=material_specials_scene_props + ) + + kc = bpy.context.window_manager.keyconfigs.addon + if kc: + km = kc.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new('wm.call_menu', 'Q', 'PRESS', shift=True) + kmi.properties.name = "VIEW3D_MT_master_material" + + bpy.types.MATERIAL_MT_specials.prepend(menu_move) + bpy.types.MATERIAL_MT_specials.append(menu_func) + + +def unregister(): + kc = bpy.context.window_manager.keyconfigs.addon + if kc: + km = kc.keymaps["3D View"] + for kmi in km.keymap_items: + if kmi.idname == 'wm.call_menu': + if kmi.properties.name == "VIEW3D_MT_master_material": + km.keymap_items.remove(kmi) + break + + bpy.types.MATERIAL_MT_specials.remove(menu_move) + bpy.types.MATERIAL_MT_specials.remove(menu_func) + + del bpy.types.Scene.mat_specials + + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register() diff --git a/materials_utils/material_converter.py b/materials_utils/material_converter.py new file mode 100644 index 00000000..86e6ada7 --- /dev/null +++ b/materials_utils/material_converter.py @@ -0,0 +1,777 @@ +# -*- coding: utf-8 -*- + +import bpy +import mathutils +from mathutils import Vector +from bpy.types import Operator +from .warning_messages_utils import ( + warning_messages, + c_is_cycles_addon_enabled, + c_data_has_materials, + collect_report, + ) + +# ----------------------------------------------------------------------------- +# Globals # + +nodesDictionary = None + +NODE_FRAME = 'NodeFrame' +BI_MATERIAL_NODE = 'ShaderNodeMaterial' +BI_OUTPUT_NODE = 'ShaderNodeOutput' +TEXTURE_IMAGE_NODE = 'ShaderNodeTexImage' +OUTPUT_NODE = 'ShaderNodeOutputMaterial' +RGB_MIX_NODE = 'ShaderNodeMixRGB' +MAPPING_NODE = 'ShaderNodeMapping' +NORMAL_MAP_NODE = 'ShaderNodeNormalMap' +SHADER_MIX_NODE = 'ShaderNodeMixShader' +SHADER_ADD_NODE = 'ShaderNodeAddShader' +COORD_NODE = 'ShaderNodeTexCoord' +RGB_TO_BW_NODE = 'ShaderNodeRGBToBW' +BSDF_DIFFUSE_NODE = 'ShaderNodeBsdfDiffuse' +BSDF_EMISSION_NODE = 'ShaderNodeEmission' +BSDF_TRANSPARENT_NODE = 'ShaderNodeBsdfTransparent' +BSDF_GLOSSY_NODE = 'ShaderNodeBsdfGlossy' +BSDF_GLASS_NODE = 'ShaderNodeBsdfGlass' + +textureNodeSizeX = 150 +textureNodeSizeY = 350 + + +# ----------------------------------------------------------------------------- +# Functions # + +def makeTextureNodeDict(cmat): + global nodesDictionary + nodesDictionary = {} + textures = {textureSlot.texture for textureSlot in cmat.texture_slots if textureSlot} + + for tex in textures: + texNode = None + if tex.type == 'IMAGE': + texNode = makeNodeUsingImage1(cmat, tex) + if texNode: + nodesDictionary[tex] = texNode + return nodesDictionary + + +def getTexNodeDic(texture): + return nodesDictionary.get(texture) + + +def clearNodes(TreeNodes): + TreeNodes.nodes.clear() + + +def clearCycleMaterial(cmat): + TreeNodes = cmat.node_tree + clearNodes(TreeNodes) + + +def copyMapping(textureSlot, textureMapping): + textureMapping.scale.x = textureSlot.scale.x + textureMapping.scale.y = textureSlot.scale.y + textureMapping.scale.z = textureSlot.scale.z + + +def addRGBMixNode(TreeNodes, textureSlot, mixRgbNode, prevTexNode, newTexNode, nodeType, textureIdx): + try: + links = TreeNodes.links + mixRgbNode.name = '{} Mix {:d}'.format(nodeType, textureIdx) + mixRgbNode.blend_type = textureSlot.blend_type + mixRgbNode.inputs['Fac'].default_value = textureSlot.diffuse_color_factor + links.new(prevTexNode.outputs['Color'], mixRgbNode.inputs['Color2']) + links.new(newTexNode.outputs['Color'], mixRgbNode.inputs['Color1']) + except: + collect_report("ERROR: Failure to find link with a Mix node") + + +def makeBiNodes(cmat): + # Create Blender Internal Material Nodes + TreeNodes = cmat.node_tree + links = TreeNodes.links + + BIFrame = TreeNodes.nodes.new(NODE_FRAME) + BIFrame.name = 'BI Frame' + BIFrame.label = 'BI Material' + + biShaderNodeMaterial = TreeNodes.nodes.new(BI_MATERIAL_NODE) + biShaderNodeMaterial.parent = BIFrame + biShaderNodeMaterial.name = 'BI Material' + biShaderNodeMaterial.material = cmat + biShaderNodeMaterial.location = 0, 600 + + biShaderNodeOutput = TreeNodes.nodes.new(BI_OUTPUT_NODE) + biShaderNodeOutput.parent = BIFrame + biShaderNodeOutput.name = 'BI Output' + biShaderNodeOutput.location = 200, 600 + try: + links.new(biShaderNodeMaterial.outputs['Color'], biShaderNodeOutput.inputs['Color']) + links.new(biShaderNodeMaterial.outputs['Alpha'], biShaderNodeOutput.inputs['Alpha']) + except: + collect_report("ERROR: Failure to find links with the BI Shader Material") + + +def placeNode(node, posX, posY, deltaX, deltaY, countX, countY): + nodeX = posX - (deltaX * countX) + nodeY = posY - (deltaY * countY) + node.location = nodeX, nodeY + + +def makeImageTextureNode(TreeNodes, img): + texNode = TreeNodes.nodes.new(TEXTURE_IMAGE_NODE) + texNode.image = img + return texNode + + +def makeNodeUsingImage1(cmat, texture): + TreeNodes = cmat.node_tree + img = texture.image + texNode = makeImageTextureNode(TreeNodes, img) + return texNode + + +def makeMainShader(TreeNodes): + mainShader = TreeNodes.nodes.new(BSDF_DIFFUSE_NODE) + mainShader.name = 'Diffuse BSDF' + mainShader.location = 0, 0 + return mainShader + + +def makeEmissionShader(TreeNodes): + mainShader = TreeNodes.nodes.new(BSDF_EMISSION_NODE) + mainShader.name = 'Emmission' + mainShader.location = 0, 0 + return mainShader + + +def makeMaterialOutput(TreeNodes): + shout = TreeNodes.nodes.new(OUTPUT_NODE) + shout.location = 200, 0 + return shout + + +def replaceNode(oldNode, newNode): + newNode.location = oldNode.location + try: + for link in oldNode.outputs['BSDF'].links: + links.new(newNode.outputs['BSDF'], link.to_socket) + for link in oldNode.inputs['Color'].links: + links.new(newNode.inputs['Color'], link.from_socket) + for link in oldNode.inputs['Normal'].links: + links.new(newNode.inputs['Normal'], link.from_socket) + except: + collect_report("ERROR: Failure to replace node") + + +def BIToCycleTexCoord(links, textureSlot, texCoordNode, textureMappingNode): + # Texture Coordinates + linkOutput = None + if textureSlot.texture_coords in {'TANGENT', 'STRESS', 'STRAND'}: + linkOutput = None + elif textureSlot.texture_coords == 'REFLECTION': + linkOutput = 'Reflection' + elif textureSlot.texture_coords == 'NORMAL': + linkOutput = 'Normal' + elif textureSlot.texture_coords == 'WINDOW': + linkOutput = 'Window' + elif textureSlot.texture_coords == 'UV': + linkOutput = 'UV' + elif textureSlot.texture_coords == 'ORCO': + linkOutput = 'Generated' + elif textureSlot.texture_coords == 'OBJECT': + linkOutput = 'Object' + elif textureSlot.texture_coords == 'GLOBAL': + linkOutput = 'Camera' + + if linkOutput: + links.new(texCoordNode.outputs[linkOutput], textureMappingNode.inputs['Vector']) + + +def createDiffuseNodes(cmat, texCoordNode, mainShader, materialOutput): + TreeNodes = cmat.node_tree + links = TreeNodes.links + texCount = len([node for node in TreeNodes.nodes if node.type == 'MAPPING']) + currPosY = -textureNodeSizeY * texCount + + textureSlots = [textureSlot for textureSlot in cmat.texture_slots if + (textureSlot and textureSlot.use_map_color_diffuse)] + + texCount = len(textureSlots) + texNode = None + latestNode = None + groupName = 'Diffuse' + + if any(textureSlots): + diffuseFrame = TreeNodes.nodes.new(NODE_FRAME) + diffuseFrame.name = '{} Frame'.format(groupName) + diffuseFrame.label = '{}'.format(groupName) + + for textureIdx, textureSlot in enumerate(textureSlots): + texNode = getTexNodeDic(textureSlot.texture) + if texNode: + tex_node_name = getattr(texNode.image, "name", "") + collect_report("INFO: Generating {} Nodes for: ".format(groupName) + tex_node_name) + texNode.parent = diffuseFrame + placeNode(texNode, -500 - ((texCount - 1) * 200), + currPosY, textureNodeSizeX, textureNodeSizeY, 0, textureIdx) + + # Add mapping node + textureMapping = TreeNodes.nodes.new(MAPPING_NODE) + textureMapping.parent = diffuseFrame + renameNode(textureMapping, '{} Mapping'.format(groupName), texCount, textureIdx) + textureMapping.location = texNode.location + Vector((-400, 0)) + copyMapping(textureSlot, textureMapping) + + # Texture Coordinates + BIToCycleTexCoord(links, textureSlot, texCoordNode, textureMapping) + + # Place the texture node + renameNode(texNode, '{} Texture'.format(groupName), texCount, textureIdx) + links.new(textureMapping.outputs['Vector'], texNode.inputs['Vector']) + + # Add multiply node + colorMult = TreeNodes.nodes.new(RGB_MIX_NODE) + colorMult.parent = diffuseFrame + renameNode(colorMult, 'Color Mult', texCount, textureIdx) + colorMult.blend_type = 'MIX' + colorMult.inputs['Fac'].default_value = 1 + colorMult.inputs['Color1'].default_value = (1, 1, 1, 1) + + colorMult.location = texNode.location + Vector((200, 0)) + links.new(texNode.outputs['Color'], colorMult.inputs['Color2']) + + texNode = colorMult + if textureSlot.use and textureIdx == 0: + latestNode = texNode + + if textureSlot.use and textureIdx > 0: + try: + # Create a node to mix multiple texture nodes + mixRgbNode = TreeNodes.nodes.new(RGB_MIX_NODE) + mixRgbNode.parent = diffuseFrame + addRGBMixNode(TreeNodes, textureSlot, mixRgbNode, texNode, latestNode, + '{}'.format(groupName), textureIdx) + mixRgbNode.location = Vector((max(texNode.location.x, latestNode.location.x), + (texNode.location.y + latestNode.location.y) / 2)) + Vector((200, 0)) + latestNode = mixRgbNode + except: + continue + + if latestNode: + links.new(latestNode.outputs['Color'], mainShader.inputs['Color']) + + # Y Position next texture node + currPosY = currPosY - (textureNodeSizeY * (texCount)) + + # BI Material to Cycles - Alpha Transparency + textureSlots = [textureSlot for textureSlot in cmat.texture_slots if + (textureSlot and textureSlot.use_map_alpha)] + texCount = len(textureSlots) + texNode = None + latestNode = None + for textureIdx, textureSlot in enumerate(textureSlots): + texNode = getTexNodeDic(textureSlot.texture) + if texNode: + tex_node_name = getattr(texNode.image, "name", "") + collect_report("INFO: Generating Transparency Nodes for: " + tex_node_name) + if textureSlot.use and textureIdx == 0: + latestNode = texNode + + if textureSlot.use and textureIdx > 0: + try: + # Create a node to mix multiple texture nodes + mixAlphaNode = TreeNodes.nodes.new(RGB_MIX_NODE) + mixAlphaNode.name = 'Alpha Mix {:d}'.format(textureIdx) + mixAlphaNode.blend_type = textureSlot.blend_type + mixAlphaNode.inputs['Fac'].default_value = textureSlot.diffuse_color_factor + placeNode(mixAlphaNode, -200 - ((texCount - textureIdx - 1) * 200), 400 - 240, + textureNodeSizeX, textureNodeSizeY, 0, 0) + links.new(texNode.outputs['Alpha'], mixAlphaNode.inputs['Color2']) + links.new(latestNode.outputs['Alpha'], mixAlphaNode.inputs['Color1']) + latestNode = mixAlphaNode + except: + continue + if latestNode: + alphaMixShader = TreeNodes.nodes.get('Alpha Mix Shader') + if alphaMixShader: + if latestNode.type == 'TEX_IMAGE': + outputLink = 'Alpha' + else: + outputLink = 'Color' + links.new(latestNode.outputs[outputLink], alphaMixShader.inputs['Fac']) + + +def createNormalNodes(cmat, texCoordNode, mainShader, materialOutput): + TreeNodes = cmat.node_tree + links = TreeNodes.links + texCount = len([node for node in TreeNodes.nodes if node.type == 'MAPPING']) + currPosY = -textureNodeSizeY * texCount + + textureSlots = [textureSlot for textureSlot in cmat.texture_slots if + (textureSlot and textureSlot.use_map_normal)] + texCount = len(textureSlots) + texNode = None + latestNode = None + groupName = 'Normal' + if any(textureSlots): + normalFrame = TreeNodes.nodes.new(NODE_FRAME) + normalFrame.name = '{} Frame'.format(groupName) + normalFrame.label = '{}'.format(groupName) + + for textureIdx, textureSlot in enumerate(textureSlots): + texNode = getTexNodeDic(textureSlot.texture) + if texNode: + tex_node_name = getattr(texNode.image, "name", "") + collect_report("INFO: Generating Normal Nodes for: " + tex_node_name) + texNode.parent = normalFrame + placeNode(texNode, -500 - ((texCount) * 200), currPosY, textureNodeSizeX, textureNodeSizeY, 0, textureIdx) + + # Add mapping node + normalMapping = TreeNodes.nodes.new(MAPPING_NODE) + normalMapping.parent = normalFrame + renameNode(normalMapping, '{} Mapping'.format(groupName), texCount, textureIdx) + normalMapping.location = texNode.location + Vector((-400, 0)) + copyMapping(textureSlot, normalMapping) + + # Texture Coordinates + BIToCycleTexCoord(links, textureSlot, texCoordNode, normalMapping) + + # Place the texture node + renameNode(texNode, '{} Texture'.format(groupName), texCount, textureIdx) + texNode.color_space = 'NONE' + links.new(normalMapping.outputs['Vector'], texNode.inputs['Vector']) + + # Add multiply node + normalMult = TreeNodes.nodes.new(RGB_MIX_NODE) + normalMult.parent = normalFrame + renameNode(normalMult, 'Normal Mult', texCount, textureIdx) + normalMult.blend_type = 'MIX' + normalMult.inputs['Fac'].default_value = 1 + normalMult.inputs['Color1'].default_value = (.5, .5, 1, 1) + + normalMult.location = texNode.location + Vector((200, 0)) + links.new(texNode.outputs['Color'], normalMult.inputs['Color2']) + + texNode = normalMult + if textureSlot.use and textureIdx == 0: + latestNode = texNode + + if textureSlot.use and textureIdx > 0: + try: + # Create a node to mix multiple texture nodes + mixRgbNode = TreeNodes.nodes.new(RGB_MIX_NODE) + mixRgbNode.parent = normalFrame + addRGBMixNode(TreeNodes, textureSlot, mixRgbNode, texNode, latestNode, + '{}'.format(groupName), textureIdx) + mixRgbNode.location = Vector((max(texNode.location.x, latestNode.location.x), + (texNode.location.y + latestNode.location.y) / 2)) + Vector((200, 0)) + latestNode = mixRgbNode + except: + continue + + if latestNode: + normalMapNode = TreeNodes.nodes.new(NORMAL_MAP_NODE) + normalMapNode.parent = normalFrame + normalMapNode.location = latestNode.location + Vector((200, 0)) + links.new(latestNode.outputs['Color'], normalMapNode.inputs['Color']) + links.new(normalMapNode.outputs['Normal'], mainShader.inputs['Normal']) + + +def createSpecularNodes(cmat, texCoordNode, mainShader, mainDiffuse, materialOutput): + TreeNodes = cmat.node_tree + links = TreeNodes.links + texCount = len([node for node in TreeNodes.nodes if node.type == 'MAPPING']) + currPosY = -textureNodeSizeY * texCount + + textureSlots = [textureSlot for textureSlot in cmat.texture_slots if + (textureSlot and textureSlot.use_map_color_spec)] + texCount = len(textureSlots) + texNode = None + latestNode = None + groupName = 'Specular' + if any(textureSlots): + specularFrame = TreeNodes.nodes.new(NODE_FRAME) + specularFrame.name = '{} Frame'.format(groupName) + specularFrame.label = '{}'.format(groupName) + + for textureIdx, textureSlot in enumerate(textureSlots): + texNode = getTexNodeDic(textureSlot.texture) + if texNode: + tex_node_name = getattr(texNode.image, "name", "") + collect_report("INFO: Generating {} Nodes for: ".format(groupName) + tex_node_name) + texNode.parent = specularFrame + placeNode(texNode, -500 - ((texCount) * 200), currPosY, textureNodeSizeX, textureNodeSizeY, 0, textureIdx) + + # Add mapping node + specularMapping = TreeNodes.nodes.new(MAPPING_NODE) + specularMapping.parent = specularFrame + renameNode(specularMapping, '{} Mapping'.format(groupName), texCount, textureIdx) + specularMapping.location = texNode.location + Vector((-400, 0)) + copyMapping(textureSlot, specularMapping) + + # Texture Coordinates + BIToCycleTexCoord(links, textureSlot, texCoordNode, specularMapping) + + # Place the texture node + renameNode(texNode, '{} Texture'.format(groupName), texCount, textureIdx) + links.new(specularMapping.outputs['Vector'], texNode.inputs['Vector']) + + # Add multiply node + specularMult = TreeNodes.nodes.new(RGB_MIX_NODE) + specularMult.parent = specularFrame + renameNode(specularMult, 'Specular Mult', texCount, textureIdx) + specularMult.blend_type = 'MULTIPLY' + specularMult.inputs['Fac'].default_value = 1 + specularMult.inputs['Color1'].default_value = (1, 1, 1, 1) + + specularMult.location = texNode.location + Vector((200, 0)) + links.new(texNode.outputs['Color'], specularMult.inputs['Color2']) + + texNode = specularMult + if textureSlot.use and textureIdx == 0: + latestNode = texNode + + if textureSlot.use and textureIdx > 0: + try: + # Create a node to mix multiple texture nodes + mixRgbNode = TreeNodes.nodes.new(RGB_MIX_NODE) + mixRgbNode.parent = specularFrame + addRGBMixNode(TreeNodes, textureSlot, mixRgbNode, texNode, latestNode, + '{}'.format(groupName), textureIdx) + mixRgbNode.location = Vector((max(texNode.location.x, latestNode.location.x), + (texNode.location.y + latestNode.location.y) / 2)) + Vector((200, 0)) + latestNode = mixRgbNode + except: + continue + + if latestNode: + try: + glossShader = TreeNodes.nodes.new(BSDF_GLOSSY_NODE) + RGBToBW = TreeNodes.nodes.new(RGB_TO_BW_NODE) + RGBToBW.location = Vector((0, latestNode.location.y)) + Vector((0, 0)) + glossShader.location = Vector((0, latestNode.location.y)) + Vector((0, -80)) + + links.new(latestNode.outputs['Color'], glossShader.inputs['Color']) + links.new(latestNode.outputs['Color'], RGBToBW.inputs['Color']) + + outputNode = TreeNodes.nodes.get('Material Output') + spec_mixer_1 = TreeNodes.nodes.new(SHADER_MIX_NODE) + spec_mixer_1.location = outputNode.location + spec_mixer_2 = TreeNodes.nodes.new(SHADER_MIX_NODE) + spec_mixer_2.inputs['Fac'].default_value = .4 + spec_mixer_2.location = outputNode.location + Vector((180, 0)) + links.new(spec_mixer_1.outputs['Shader'], spec_mixer_2.inputs[2]) + links.new(spec_mixer_2.outputs['Shader'], outputNode.inputs['Surface']) + links.new(RGBToBW.outputs['Val'], spec_mixer_1.inputs['Fac']) + + links.new(glossShader.outputs['BSDF'], spec_mixer_1.inputs[2]) + + outputNode.location += Vector((360, 0)) + normalMapNode = TreeNodes.nodes.get('Normal Map') + links.new(normalMapNode.outputs['Normal'], glossShader.inputs['Normal']) + + if mainDiffuse.type == 'BSDF_DIFFUSE': + outputLink = 'BSDF' + else: + outputLink = 'Shader' + + links.new(mainDiffuse.outputs[outputLink], spec_mixer_1.inputs[1]) + links.new(mainDiffuse.outputs[outputLink], spec_mixer_2.inputs[1]) + except: + return + + +def createEmissionNodes(cmat, texCoordNode, mainShader, materialOutput): + TreeNodes = cmat.node_tree + links = TreeNodes.links + texCount = len([node for node in TreeNodes.nodes if node.type == 'MAPPING']) + currPosY = -textureNodeSizeY * texCount + + textureSlots = [textureSlot for textureSlot in cmat.texture_slots if + (textureSlot and textureSlot.use_map_emit)] + texCount = len(textureSlots) + texNode = None + latestNode = None + groupName = 'Emission' + if any(textureSlots): + emissionFrame = TreeNodes.nodes.new(NODE_FRAME) + emissionFrame.name = '{} Frame'.format(groupName) + emissionFrame.label = '{}'.format(groupName) + + for textureIdx, textureSlot in enumerate(textureSlots): + texNode = getTexNodeDic(textureSlot.texture) + if texNode: + tex_node_name = getattr(texNode.image, "name", "") + collect_report("INFO: Generating {} Nodes for: ".format(groupName) + tex_node_name) + texNode.parent = emissionFrame + placeNode(texNode, -500 - ((texCount) * 200), currPosY, textureNodeSizeX, textureNodeSizeY, 0, textureIdx) + + # Add mapping node + emissionMapping = TreeNodes.nodes.new(MAPPING_NODE) + emissionMapping.parent = emissionFrame + renameNode(emissionMapping, '{} Mapping'.format(groupName), texCount, textureIdx) + emissionMapping.location = texNode.location + Vector((-400, 0)) + copyMapping(textureSlot, emissionMapping) + + # Texture Coordinates + BIToCycleTexCoord(links, textureSlot, texCoordNode, emissionMapping) + + # Place the texture node + renameNode(texNode, '{} Texture'.format(groupName), texCount, textureIdx) + texNode.color_space = 'NONE' + links.new(emissionMapping.outputs['Vector'], texNode.inputs['Vector']) + + # Add multiply node + emissionMult = TreeNodes.nodes.new(RGB_MIX_NODE) + emissionMult.parent = emissionFrame + renameNode(emissionMult, 'Emission Mult', texCount, textureIdx) + emissionMult.blend_type = 'MIX' + emissionMult.inputs['Fac'].default_value = 1 + emissionMult.inputs['Color1'].default_value = (0, 0, 0, 1) + + emissionMult.location = texNode.location + Vector((200, 0)) + links.new(texNode.outputs['Color'], emissionMult.inputs['Color2']) + + texNode = emissionMult + if textureSlot.use and textureIdx == 0: + latestNode = texNode + + if textureSlot.use and textureIdx > 0: + try: + # Create a node to mix multiple texture nodes + mixRgbNode = TreeNodes.nodes.new(RGB_MIX_NODE) + mixRgbNode.parent = emissionFrame + addRGBMixNode(TreeNodes, textureSlot, mixRgbNode, texNode, latestNode, + '{}'.format(groupName), textureIdx) + mixRgbNode.location = Vector((max(texNode.location.x, latestNode.location.x), + (texNode.location.y + latestNode.location.y) / 2)) + Vector((200, 0)) + latestNode = mixRgbNode + except: + continue + + if latestNode: + try: + emissionNode = TreeNodes.nodes.new(BSDF_EMISSION_NODE) + emissionNode.inputs['Strength'].default_value = 1 + addShaderNode = TreeNodes.nodes.new(SHADER_ADD_NODE) + addShaderNode.location = materialOutput.location + Vector((0, -100)) + xPos = mainShader.location.x + yPos = latestNode.location.y + + emissionNode.location = Vector((xPos, yPos)) + materialOutput.location += Vector((400, 0)) + + node = materialOutput.inputs[0].links[0].from_node + node.location += Vector((400, 0)) + + links.new(latestNode.outputs['Color'], emissionNode.inputs['Color']) + links.new(emissionNode.outputs['Emission'], addShaderNode.inputs[1]) + links.new(mainShader.outputs['BSDF'], addShaderNode.inputs[0]) + links.new(addShaderNode.outputs['Shader'], node.inputs[2]) + except: + return + + +def renameNode(node, baseName, nodesCount, nodeIndex): + if nodesCount == 1: + node.name = baseName + else: + node.name = '{} {:d}'.format(baseName, nodeIndex + 1) + + +def hasAlphaTex(cmat): + tex_is_transp = False + for textureSlot in cmat.texture_slots: + if textureSlot: + if textureSlot.use: + if textureSlot.use_map_alpha: + tex_is_transp = tex_is_transp or True + return tex_is_transp + + +def AutoNode(active=False, operator=None): + collect_report("________________________________________", True, False) + collect_report("START CYCLES CONVERSION") + + if active: + materials = [mat for obj in bpy.context.selected_objects if + obj.type == 'MESH' for mat in obj.data.materials] + else: + materials = bpy.data.materials + + # No Materials for the chosen action - abort + if not materials: + if operator: + if active: + warning_messages(operator, 'CONV_NO_SEL_MAT') + else: + warning_messages(operator, 'CONV_NO_SC_MAT') + return + + for cmat in materials: + # check for empty material (it will fall through the first check) + test_empty = getattr(cmat, "name", None) + if test_empty is None: + collect_report("INFO: An empty material was hit, skipping") + continue + else: + cmat.use_nodes = True + clearCycleMaterial(cmat) + makeBiNodes(cmat) + makeCyclesFromBI(cmat) + + collect_report("Conversion finished !", False, True) + + bpy.context.scene.render.engine = 'CYCLES' + + +def makeCyclesFromBI(cmat): + mat_name = getattr(cmat, "name", "NO NAME") + collect_report("Converting Material: " + mat_name) + + global nodesDictionary + TreeNodes = cmat.node_tree + links = TreeNodes.links + + # Convert this material from non-nodes to Cycles nodes + mainShader = None + mainDiffuse = None + Mix_Alpha = None + + tex_is_transp = hasAlphaTex(cmat) + + cmat_use_transp = cmat.use_transparency and cmat.alpha < 1 + cmat_trans_method = cmat.transparency_method + cmat_ior = cmat.raytrace_transparency.ior + cmat_transp_z = cmat_use_transp and cmat_trans_method == 'Z_TRANSPARENCY' + cmat_transp_ray = cmat_use_transp and cmat_trans_method == 'RAYTRACE' and cmat_ior == 1 + cmat_mirror = cmat.raytrace_mirror.use + cmat_mirror_fac = cmat.raytrace_mirror.reflect_factor + + # -------------------------------------- + # Material Shaders + # Diffuse nodes + # -------------------------------------- + + # Make Diffuse and Output nodes + mainShader = makeMainShader(TreeNodes) + mainShader.inputs['Roughness'].default_value = cmat.specular_intensity + mainDiffuse = mainShader + materialOutput = makeMaterialOutput(TreeNodes) + links.new(mainShader.outputs['BSDF'], materialOutput.inputs['Surface']) + + texCoordNode = TreeNodes.nodes.new(COORD_NODE) + texCoordNode.name = 'Texture Coordinate' + + # Material Transparent + if not cmat_mirror and cmat_use_transp and tex_is_transp and (cmat_transp_z or cmat_transp_ray): + collect_report("INFO: Make TRANSPARENT material nodes: " + cmat.name) + Mix_Alpha = TreeNodes.nodes.new(SHADER_MIX_NODE) + Mix_Alpha.name = 'Alpha Mix Shader' + Mix_Alpha.location = materialOutput.location + materialOutput.location += Vector((180, 0)) + Mix_Alpha.inputs['Fac'].default_value = cmat.alpha + transparentShader = TreeNodes.nodes.new(BSDF_TRANSPARENT_NODE) + transparentShader.location = mainShader.location + mainShader.location += Vector((0, -100)) + links.new(transparentShader.outputs['BSDF'], Mix_Alpha.inputs[1]) + links.new(mainShader.outputs['BSDF'], Mix_Alpha.inputs[2]) + links.new(Mix_Alpha.outputs['Shader'], materialOutput.inputs['Surface']) + mainDiffuse = Mix_Alpha + + if cmat_mirror and cmat_mirror_fac > 0.001: + if cmat_use_transp: + # Material Glass + collect_report("INFO: Make GLASS shader node: " + cmat.name) + newShader = TreeNodes.nodes.new(BSDF_GLASS_NODE) + shader = newShader + replaceNode(shader, newShader) + TreeNodes.nodes.remove(shader) + else: + # Material Mirror + collect_report("INFO: Make MIRROR shader node: " + cmat.name) + newShader = TreeNodes.nodes.new(BSDF_GLOSSY_NODE) + shader = newShader + replaceNode(shader, newShader) + TreeNodes.nodes.remove(shader) + + nodesDictionary = makeTextureNodeDict(cmat) + + # -------------------------------------- + # Texture nodes + + # BI Material to Cycles - Diffuse Textures + createDiffuseNodes(cmat, texCoordNode, mainShader, materialOutput) + + # BI Material to Cycles - Normal map + createNormalNodes(cmat, texCoordNode, mainShader, materialOutput) + + # BI Material to Cycles - Specular map + createSpecularNodes(cmat, texCoordNode, mainShader, mainDiffuse, materialOutput) + + # BI Material to Cycles - Emission map + createEmissionNodes(cmat, texCoordNode, mainShader, materialOutput) + + # Texture coordinates + # list all nodes conected to outputs + mappingNodes = [link.to_node for output in texCoordNode.outputs for link in output.links] + mappingNodesCount = len(mappingNodes) + + if mappingNodes: + xList = [node.location.x for node in mappingNodes] + yList = [node.location.y for node in mappingNodes] + minPosX = min(xList) - 400 + avgPosY = sum(yList) / mappingNodesCount + texCoordNode.location = Vector((minPosX, avgPosY)) + + +# ----------------------------------------------------------------------------- +# Operator Classes # + +class material_convert_all(Operator): + bl_idname = "xps_tools.convert_to_cycles_all" + bl_label = "Convert All Materials" + bl_description = "Convert All Materials to BI and Cycles Nodes" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (c_is_cycles_addon_enabled() and c_data_has_materials()) + + def execute(self, context): + AutoNode(False, self) + return {'FINISHED'} + + +class material_convert_selected(Operator): + bl_idname = "xps_tools.convert_to_cycles_selected" + bl_label = "Convert All Materials From Selected Objects" + bl_description = "Convert All Materials on Selected Objects to BI and Cycles Nodes" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (c_data_has_materials() and c_is_cycles_addon_enabled() and + bool( + next((obj for obj in context.selected_objects if obj.type == 'MESH'), + None) + ) + ) + + def execute(self, context): + AutoNode(True, self) + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + pass + + +def unregister(): + bpy.utils.unregister_module(__name__) + pass + +if __name__ == "__main__": + register() diff --git a/materials_utils/materials_cycles_converter.py b/materials_utils/materials_cycles_converter.py new file mode 100644 index 00000000..600de7e2 --- /dev/null +++ b/materials_utils/materials_cycles_converter.py @@ -0,0 +1,683 @@ +# gpl: author Silvio Falcinelli. Fixes by angavrilov and others. +# special thanks to user blenderartists.org cmomoney +# -*- coding: utf-8 -*- + +import bpy +import os +from os import path as os_path +from bpy.types import Operator +from bpy.props import ( + BoolProperty, + EnumProperty, + ) +from .warning_messages_utils import ( + warning_messages, + c_is_cycles_addon_enabled, + c_data_has_materials, + collect_report, + ) + +# ----------------------------------------------------------------------------- +# Globals # + +# switch for operator's function called after AutoNodeInitiate +CHECK_AUTONODE = False + +# set the node color for baked textures (default greenish) +NODE_COLOR = (0.32, 0.75, 0.32) + +# ----------------------------------------------------------------------------- +# Functions # + + +def AutoNodeSwitch(renderer="CYCLES", switch="OFF", operator=None): + mats = bpy.data.materials + use_nodes = (True if switch in ("ON") else False) + warn_message = ('BI_SW_NODES_ON' if switch in ("ON") else + 'BI_SW_NODES_OFF') + warn_message_2 = ('CYC_SW_NODES_ON' if switch in ("ON") else + 'CYC_SW_NODES_OFF') + for cmat in mats: + cmat.use_nodes = use_nodes + renders = ('CYCLES' if renderer and renderer == "CYCLES" else + 'BLENDER_RENDER') + bpy.context.scene.render.engine = renders + if operator: + warning_messages(operator, (warn_message_2 if renders in ('CYCLES') else + warn_message)) + + +def SetFakeUserTex(): + images = bpy.data.images + for image in images: + has_user = getattr(image, "users", -1) + image_name = getattr(image, "name", "NONAME") + + if has_user == 0: + image.use_fake_user = True + collect_report("INFO: Set fake user for unused image: " + image_name) + + +def BakingText(tex, mode, tex_type=None): + collect_report("INFO: start bake texture named: " + tex.name) + saved_img_path = None + bpy.ops.object.mode_set(mode='OBJECT') + sc = bpy.context.scene + tmat = '' + img = '' + Robj = bpy.context.active_object + for n in bpy.data.materials: + if n.name == 'TMP_BAKING': + tmat = n + if not tmat: + tmat = bpy.data.materials.new('TMP_BAKING') + tmat.name = "TMP_BAKING" + + bpy.ops.mesh.primitive_plane_add() + tm = bpy.context.active_object + tm.name = "TMP_BAKING" + tm.data.name = "TMP_BAKING" + bpy.ops.object.select_pattern(extend=False, pattern="TMP_BAKING", + case_sensitive=False) + sc.objects.active = tm + bpy.context.scene.render.engine = 'BLENDER_RENDER' + tm.data.materials.append(tmat) + if len(tmat.texture_slots.items()) == 0: + tmat.texture_slots.add() + tmat.texture_slots[0].texture_coords = 'UV' + tmat.texture_slots[0].use_map_alpha = True + tmat.texture_slots[0].texture = tex.texture + tmat.texture_slots[0].use_map_alpha = True + tmat.texture_slots[0].use_map_color_diffuse = False + tmat.use_transparency = True + tmat.alpha = 0 + tmat.use_nodes = False + tmat.diffuse_color = 1, 1, 1 + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.uv.unwrap() + + # clean up temporary baking images if any + for n in bpy.data.images: + if n.name == 'TMP_BAKING': + n.user_clear() + bpy.data.images.remove(n) + + if mode == "ALPHA" and tex.texture.type == 'IMAGE': + sizeX = tex.texture.image.size[0] + sizeY = tex.texture.image.size[1] + else: + bake_size = (int(sc.mat_specials.img_bake_size) if + sc.mat_specials.img_bake_size else 1024) + sizeX = bake_size + sizeY = bake_size + + bpy.ops.image.new(name="TMP_BAKING", width=sizeX, height=sizeY, + color=(0.0, 0.0, 0.0, 1.0), alpha=True, float=False) + bpy.data.screens['UV Editing'].areas[1].spaces[0].image = bpy.data.images["TMP_BAKING"] + sc.render.engine = 'BLENDER_RENDER' + img = bpy.data.images["TMP_BAKING"] + img = bpy.data.images.get("TMP_BAKING") + img.file_format = ("JPEG" if not mode == "ALPHA" else "PNG") + + paths = bpy.path.abspath(sc.mat_specials.conv_path) + tex_name = getattr(getattr(tex.texture, "image", None), "name", None) + texture_name = (tex_name.rpartition(".")[0] if tex_name else tex.texture.name) + new_tex_name = "baked" + name_append = ("_BAKING" if mode == "ALPHA" and + tex.texture.type == 'IMAGE' else "_PTEXT") + new_appendix = (".jpg" if not mode == "ALPHA" else ".png") + + if name_append in texture_name: + new_tex_name = texture_name + elif tex_type: + new_tex_name = tex_type + name_append + else: + new_tex_name = texture_name + name_append + + img.filepath_raw = paths + new_tex_name + new_appendix + saved_img_path = img.filepath_raw + + sc.render.bake_type = 'ALPHA' + sc.render.use_bake_selected_to_active = True + sc.render.use_bake_clear = True + + # try to bake if it fails give report + try: + bpy.ops.object.bake_image() + img.save() + except: + # no return value so the image loading is skipped + saved_img_path = None + collect_report("ERROR: Baking could not be completed. " + "Check System Console for info") + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.delete() + bpy.ops.object.select_pattern(extend=False, pattern=Robj.name, case_sensitive=False) + sc.objects.active = Robj + img.user_clear() + bpy.data.images.remove(img) + + if tmat.users == 0: + bpy.data.materials.remove(tmat) + + if saved_img_path: + collect_report("________________________________________") + return saved_img_path + + +def AutoNodeInitiate(active=False, operator=None): + # Checks with bpy.ops.material.check_converter_path + # if it's possible to write in the output path + # if it passes procedes with calling AutoNode + + # if CheckImagePath(operator): + check_path = bpy.ops.material.check_converter_path() + + global CHECK_AUTONODE + + if 'FINISHED' in check_path: + sc = bpy.context.scene + CHECK_AUTONODE = True + collect_report("________________________________________", True, False) + AutoNode(active, operator) + if sc.mat_specials.SET_FAKE_USER: + SetFakeUserTex() + collect_report("Conversion finished !", False, True) + else: + warning_messages(operator, 'DIR_PATH_CONVERT') + + +def AutoNode(active=False, operator=None): + global CHECK_AUTONODE + sc = bpy.context.scene + if active: + # fix for empty slots by angavrilov + mats = [slot.material for slot in bpy.context.active_object.material_slots if + slot.material] + else: + mats = bpy.data.materials + + # No Materials for the chosen action - abort + if not mats: + CHECK_AUTONODE = False + if operator: + if active: + act_obj = bpy.context.active_object + warning_messages(operator, 'CONV_NO_OBJ_MAT', act_obj.name) + else: + warning_messages(operator, 'CONV_NO_SC_MAT') + return + + for cmat in mats: + # check for empty material (it will fall through the first check) + test_empty = getattr(cmat, "name", None) + if test_empty is None: + collect_report("An empty material was hit, skipping") + continue + + cmat.use_nodes = True + TreeNodes = cmat.node_tree + links = TreeNodes.links + + # Don't alter nodes of locked materials + locked = False + for n in TreeNodes.nodes: + if n.type == 'ShaderNodeOutputMaterial': + if n.label == 'Locked': + locked = True + break + + if not locked: + # Convert this material from non-nodes to Cycles nodes + shader = '' + shtsl = '' + Add_Emission = '' + Add_Translucent = '' + Mix_Alpha = '' + sT = False + + for n in TreeNodes.nodes: + TreeNodes.nodes.remove(n) + + # Starting point is diffuse BSDF and output material + shader = TreeNodes.nodes.new('ShaderNodeBsdfDiffuse') + shader.location = 0, 470 + shout = TreeNodes.nodes.new('ShaderNodeOutputMaterial') + shout.location = 200, 400 + links.new(shader.outputs[0], shout.inputs[0]) + + cmat_is_transp = cmat.use_transparency and cmat.alpha < 1 + + if not cmat.raytrace_mirror.use and not cmat_is_transp: + if not shader.type == 'ShaderNodeBsdfDiffuse': + collect_report("INFO: Make DIFFUSE shader node for: " + cmat.name) + TreeNodes.nodes.remove(shader) + shader = TreeNodes.nodes.new('ShaderNodeBsdfDiffuse') + shader.location = 0, 470 + links.new(shader.outputs[0], shout.inputs[0]) + + if cmat.raytrace_mirror.use and cmat.raytrace_mirror.reflect_factor > 0.001 and cmat_is_transp: + if not shader.type == 'ShaderNodeBsdfGlass': + collect_report("INFO: Make GLASS shader node for: " + cmat.name) + TreeNodes.nodes.remove(shader) + shader = TreeNodes.nodes.new('ShaderNodeBsdfGlass') + shader.location = 0, 470 + links.new(shader.outputs[0], shout.inputs[0]) + + if cmat.raytrace_mirror.use and not cmat_is_transp and cmat.raytrace_mirror.reflect_factor > 0.001: + if not shader.type == 'ShaderNodeBsdfGlossy': + collect_report("INFO: Make MIRROR shader node for: " + cmat.name) + TreeNodes.nodes.remove(shader) + shader = TreeNodes.nodes.new('ShaderNodeBsdfGlossy') + shader.location = 0, 520 + links.new(shader.outputs[0], shout.inputs[0]) + + if cmat.emit > 0.001: + if (not shader.type == 'ShaderNodeEmission' and not + cmat.raytrace_mirror.reflect_factor > 0.001 and not cmat_is_transp): + + collect_report("INFO: Mix EMISSION shader node for: " + cmat.name) + TreeNodes.nodes.remove(shader) + shader = TreeNodes.nodes.new('ShaderNodeEmission') + shader.location = 0, 450 + links.new(shader.outputs[0], shout.inputs[0]) + else: + if not Add_Emission: + collect_report("INFO: Add EMISSION shader node for: " + cmat.name) + shout.location = 550, 330 + Add_Emission = TreeNodes.nodes.new('ShaderNodeAddShader') + Add_Emission.location = 370, 490 + + shem = TreeNodes.nodes.new('ShaderNodeEmission') + shem.location = 180, 380 + + links.new(Add_Emission.outputs[0], shout.inputs[0]) + links.new(shem.outputs[0], Add_Emission.inputs[1]) + links.new(shader.outputs[0], Add_Emission.inputs[0]) + + shem.inputs['Color'].default_value = (cmat.diffuse_color.r, + cmat.diffuse_color.g, + cmat.diffuse_color.b, 1) + shem.inputs['Strength'].default_value = cmat.emit + + if cmat.translucency > 0.001: + collect_report("INFO: Add BSDF_TRANSLUCENT shader node for: " + cmat.name) + shout.location = 770, 330 + Add_Translucent = TreeNodes.nodes.new('ShaderNodeAddShader') + Add_Translucent.location = 580, 490 + + shtsl = TreeNodes.nodes.new('ShaderNodeBsdfTranslucent') + shtsl.location = 400, 350 + + links.new(Add_Translucent.outputs[0], shout.inputs[0]) + links.new(shtsl.outputs[0], Add_Translucent.inputs[1]) + + if Add_Emission: + links.new(Add_Emission.outputs[0], Add_Translucent.inputs[0]) + pass + else: + links.new(shader.outputs[0], Add_Translucent.inputs[0]) + pass + shtsl.inputs['Color'].default_value = (cmat.translucency, + cmat.translucency, + cmat.translucency, 1) + + shader.inputs['Color'].default_value = (cmat.diffuse_color.r, + cmat.diffuse_color.g, + cmat.diffuse_color.b, 1) + + if shader.type == 'ShaderNodeBsdfDiffuse': + shader.inputs['Roughness'].default_value = cmat.specular_intensity + + if shader.type == 'ShaderNodeBsdfGlossy': + shader.inputs['Roughness'].default_value = 1 - cmat.raytrace_mirror.gloss_factor + + if shader.type == 'ShaderNodeBsdfGlass': + shader.inputs['Roughness'].default_value = 1 - cmat.raytrace_mirror.gloss_factor + shader.inputs['IOR'].default_value = cmat.raytrace_transparency.ior + + if shader.type == 'ShaderNodeEmission': + shader.inputs['Strength'].default_value = cmat.emit + + # texture frame check and tex count (if bigger than 1 add frame) + frame_check, tex_count, node_frame = False, 0, None + + for tex in cmat.texture_slots: + if tex: + tex_count += 1 + if not frame_check: + frame_check = True + if tex_count > 1: + break + + if frame_check: + if tex_count > 1: + node_frame = TreeNodes.nodes.new('NodeFrame') + node_frame.name = 'Converter Textures' + node_frame.label = 'Converter Textures' + + # count the number of texture nodes created + # for spreading a bit the texture nodes + row_node, col_node = -1, False + sM = True + baked_path = None + + for tex in cmat.texture_slots: + sT = False + tex_use = getattr(tex, "use", None) + if tex_use: + row_node = (row_node + 1 if not col_node else row_node) + col_node = not col_node + tex_node_loc = -(200 + (row_node * 150)), (400 if col_node else 650) + ma_alpha = getattr(tex, "use_map_alpha", None) + sM = (False if ma_alpha else True) + + if tex.texture.type == 'IMAGE': + if sc.mat_specials.EXTRACT_ALPHA and tex.texture.use_alpha: + if (not + os_path.exists(bpy.path.abspath(tex.texture.image.filepath + "_BAKING.png")) or + sc.mat_specials.EXTRACT_OW): + baked_path = BakingText(tex, 'ALPHA') + try: + if baked_path: + img = bpy.data.images.load(baked_path) + collect_report("INFO: Loading Baked texture path:") + collect_report(baked_path) + else: + img = tex.texture.image + + img_name = (img.name if hasattr(img, "name") else "NO NAME") + shtext = TreeNodes.nodes.new('ShaderNodeTexImage') + shtext.location = tex_node_loc + shtext.image = img + shtext.name = img_name + shtext.label = "Image " + img_name + if baked_path: + shtext.use_custom_color = True + shtext.color = NODE_COLOR + collect_report("INFO: Creating Image Node for image: " + img_name) + if node_frame: + shtext.parent = node_frame + sT = True + except: + collect_report("ERROR: A problem occured with loading an image for {} " + "(possibly missing)".format(tex.texture.name)) + else: + if sc.mat_specials.EXTRACT_PTEX or (sc.mat_specials.EXTRACT_ALPHA and ma_alpha): + if (not os_path.exists(bpy.path.abspath(tex.texture.name + "_PTEXT.jpg")) or + sc.mat_specials.EXTRACT_OW): + tex_type = tex.texture.type.lower() + collect_report("Attempting to Extract Procedural Texture type: " + tex_type) + baked_path = BakingText(tex, 'PTEX', tex_type) + + if baked_path: + try: + img = bpy.data.images.load(baked_path) + collect_report("Loading Baked texture path:") + collect_report(baked_path) + img_name = (img.name if hasattr(img, "name") else "NO NAME") + shtext = TreeNodes.nodes.new('ShaderNodeTexImage') + shtext.location = tex_node_loc + shtext.image = img + shtext.name = img_name + shtext.label = "Baked Image " + img_name + shtext.use_custom_color = True + shtext.color = NODE_COLOR + collect_report("Creating Image Node for baked image: " + img_name) + if node_frame: + shtext.parent = node_frame + sT = True + except: + collect_report("ERROR: Failure to load baked image: " + img_name) + else: + collect_report("ERROR: Failure during baking, no images loaded") + + if cmat_is_transp and cmat.raytrace_transparency.ior == 1 and not cmat.raytrace_mirror.use and sM: + if not shader.type == 'ShaderNodeBsdfTransparent': + collect_report("INFO: Make TRANSPARENT shader node for: " + cmat.name) + TreeNodes.nodes.remove(shader) + shader = TreeNodes.nodes.new('ShaderNodeBsdfTransparent') + shader.location = 0, 470 + links.new(shader.outputs[0], shout.inputs[0]) + + shader.inputs['Color'].default_value = (cmat.diffuse_color.r, + cmat.diffuse_color.g, + cmat.diffuse_color.b, 1) + + if sT: + if tex.use_map_color_diffuse: + links.new(shtext.outputs[0], shader.inputs[0]) + + if tex.use_map_emit: + if not Add_Emission: + collect_report("INFO: Mix EMISSION + Texture shader node for: " + cmat.name) + intensity = 0.5 + (tex.emit_factor / 2) + + shout.location = 550, 330 + Add_Emission = TreeNodes.nodes.new('ShaderNodeAddShader') + Add_Emission.name = "Add_Emission" + Add_Emission.location = 370, 490 + + shem = TreeNodes.nodes.new('ShaderNodeEmission') + shem.location = 180, 380 + + links.new(Add_Emission.outputs[0], shout.inputs[0]) + links.new(shem.outputs[0], Add_Emission.inputs[1]) + links.new(shader.outputs[0], Add_Emission.inputs[0]) + + shem.inputs['Color'].default_value = (cmat.diffuse_color.r, + cmat.diffuse_color.g, + cmat.diffuse_color.b, 1) + shem.inputs['Strength'].default_value = intensity * 2 + + links.new(shtext.outputs[0], shem.inputs[0]) + + if tex.use_map_mirror: + links.new(shader.inputs[0], shtext.outputs[0]) + + if tex.use_map_translucency: + if not Add_Translucent: + collect_report("INFO: Add Translucency + Texture shader node for: " + cmat.name) + + intensity = 0.5 + (tex.emit_factor / 2) + shout.location = 550, 330 + Add_Translucent = TreeNodes.nodes.new('ShaderNodeAddShader') + Add_Translucent.name = "Add_Translucent" + Add_Translucent.location = 370, 290 + + shtsl = TreeNodes.nodes.new('ShaderNodeBsdfTranslucent') + shtsl.location = 180, 240 + + links.new(shtsl.outputs[0], Add_Translucent.inputs[1]) + + if Add_Emission: + links.new(Add_Translucent.outputs[0], shout.inputs[0]) + links.new(Add_Emission.outputs[0], Add_Translucent.inputs[0]) + pass + else: + links.new(Add_Translucent.outputs[0], shout.inputs[0]) + links.new(shader.outputs[0], Add_Translucent.inputs[0]) + + links.new(shtext.outputs[0], shtsl.inputs[0]) + + if tex.use_map_alpha: + if not Mix_Alpha: + collect_report("INFO: Mix Alpha + Texture shader node for: " + cmat.name) + + shout.location = 750, 330 + Mix_Alpha = TreeNodes.nodes.new('ShaderNodeMixShader') + Mix_Alpha.name = "Add_Alpha" + Mix_Alpha.location = 570, 290 + sMask = TreeNodes.nodes.new('ShaderNodeBsdfTransparent') + sMask.location = 250, 180 + tMask, imask = None, None + + # search if the texture node already exists, if not create + nodes = getattr(cmat.node_tree, "nodes", None) + img_name = getattr(img, "name", "NO NAME") + for node in nodes: + if type(node) == bpy.types.ShaderNodeTexImage: + node_name = getattr(node, "name") + if img_name in node_name: + tMask = node + collect_report("INFO: Using existing Texture Node for Mask: " + node_name) + break + + if tMask is None: + tMask = TreeNodes.nodes.new('ShaderNodeTexImage') + + if node_frame: + tMask.parent = node_frame + tex_node_loc = -(200 + ((row_node + 1) * 150)), (650 if col_node else 400) + tMask.location = tex_node_loc + + try: + file_path = getattr(img, "filepath", None) + if file_path: + imask = bpy.data.images.load(file_path) + else: + imask = bpy.data.images.get(img_name) + collect_report("INFO: Attempting to load image for Mask: " + img_name) + except: + collect_report("ERROR: Failure to load image for Mask: " + img_name) + + if imask: + tMask.image = imask + + if tMask: + links.new(Mix_Alpha.inputs[0], tMask.outputs[1]) + links.new(shout.inputs[0], Mix_Alpha.outputs[0]) + links.new(sMask.outputs[0], Mix_Alpha.inputs[1]) + + if not Add_Translucent: + if Add_Emission: + links.new(Mix_Alpha.inputs[2], Add_Emission.outputs[0]) + else: + links.new(Mix_Alpha.inputs[2], shader.outputs[0]) + else: + links.new(Mix_Alpha.inputs[2], Add_Translucent.outputs[0]) + else: + collect_report("ERROR: Mix Alpha could not be created " + "(mask image could not be loaded)") + + if tex.use_map_normal: + t = TreeNodes.nodes.new('ShaderNodeRGBToBW') + t.location = -0, 300 + links.new(t.outputs[0], shout.inputs[2]) + links.new(shtext.outputs[0], t.inputs[0]) + else: + collect_report("No textures in the Scene, no Image Nodes to add") + + bpy.context.scene.render.engine = 'CYCLES' + + +# ----------------------------------------------------------------------------- +# Operator Classes # + +class mllock(Operator): + bl_idname = "ml.lock" + bl_label = "Lock" + bl_description = "Lock/unlock this material against modification by conversions" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (c_is_cycles_addon_enabled() and c_data_has_materials()) + + def execute(self, context): + cmat = bpy.context.selected_objects[0].active_material + TreeNodes = cmat.node_tree + for n in TreeNodes.nodes: + if n.type == 'ShaderNodeOutputMaterial': + if n.label == 'Locked': + n.label = '' + else: + n.label = 'Locked' + return {'FINISHED'} + + +class mlrefresh(Operator): + bl_idname = "ml.refresh" + bl_label = "Convert All Materials" + bl_description = "Convert All Materials in the scene from non-nodes to Cycles" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (c_is_cycles_addon_enabled() and c_data_has_materials()) + + def execute(self, context): + AutoNodeInitiate(False, self) + + if CHECK_AUTONODE is True: + bpy.ops.object.editmode_toggle() + bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.001) + bpy.ops.object.editmode_toggle() + + return {'FINISHED'} + + +class mlrefresh_active(Operator): + bl_idname = "ml.refresh_active" + bl_label = "Convert All Materials From Active Object" + bl_description = "Convert all Active Object's Materials from non-nodes to Cycles" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (c_is_cycles_addon_enabled() and c_data_has_materials() and + context.active_object is not None) + + def execute(self, context): + AutoNodeInitiate(True, self) + if CHECK_AUTONODE is True: + bpy.ops.object.editmode_toggle() + bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.001) + bpy.ops.object.editmode_toggle() + return {'FINISHED'} + + +class mlrestore(Operator): + bl_idname = "ml.restore" + bl_label = "Switch Between Renderers" + bl_description = ("Switch between Renderers \n" + "(Doesn't create new nor converts existing materials)") + bl_options = {'REGISTER', 'UNDO'} + + switcher = BoolProperty( + name="Use Nodes", + description="When restoring, switch Use Nodes On/Off", + default=True + ) + renderer = EnumProperty( + name="Renderer", + description="Choose Cycles or Blender Internal", + items=(('CYCLES', "Cycles", "Switch to Cycles"), + ('BI', "Blender Internal", "Switch to Blender Internal")), + default='CYCLES', + ) + + @classmethod + def poll(cls, context): + return c_is_cycles_addon_enabled() + + def execute(self, context): + if self.switcher: + AutoNodeSwitch(self.renderer, "ON", self) + else: + AutoNodeSwitch(self.renderer, "OFF", self) + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + pass + + +def unregister(): + bpy.utils.unregister_module(__name__) + pass + +if __name__ == "__main__": + register() diff --git a/materials_utils/texture_rename.py b/materials_utils/texture_rename.py new file mode 100644 index 00000000..8b6fc45f --- /dev/null +++ b/materials_utils/texture_rename.py @@ -0,0 +1,112 @@ +# gpl: author Yadoob +# -*- coding: utf-8 -*- + +import bpy +from bpy.types import ( + Operator, + Panel, + ) +from bpy.props import StringProperty +from .warning_messages_utils import ( + warning_messages, + c_data_has_images, + ) + + +class TEXTURE_OT_patern_rename(Operator): + bl_idname = "texture.patern_rename" + bl_label = "Texture Renamer" + bl_description = ("Replace the Texture names pattern with \n" + "the attached Image ones. \n" + "Works on all Textures (Including Brushes) \n \n" + "The First field - the name pattern to replace \n" + "The Second - searches for existing names \n") + bl_options = {'REGISTER', 'UNDO'} + + def_name = "Texture" # default name + is_not_undo = False # prevent drawing props on undo + named = StringProperty( + name="Search for name", + default=def_name + ) + + @classmethod + def poll(cls, context): + return c_data_has_images() + + def draw(self, context): + layout = self.layout + if self.is_not_undo is True: + box = layout.box() + + box.prop(self, "named", text="Name pattern", icon="SYNTAX_ON") + layout.separator() + boxs = layout.box() + boxs.prop_search(self, "named", bpy.data, "textures") + else: + layout.label(text="**Only Undo is available**", icon="INFO") + + def invoke(self, context, event): + self.is_not_undo = True + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + errors = [] # collect texture names without images attached + tex_count = 0 # check if there is textures at all + + for texture in bpy.data.textures: + try: + if texture and self.named in texture.name and texture.type in {"IMAGE"}: + tex_count += 1 + textname = "" + img = (bpy.data.textures[texture.name].image if bpy.data.textures[texture.name] else None) + if not img: + errors.append(str(texture.name)) + for word in img.name: + if word != ".": + textname = textname + word + else: + break + texture.name = textname + if texture.type != "IMAGE": # rename specific textures as clouds, environnement map,... + texture.name = texture.type.lower() + except: + continue + + if tex_count == 0: + warning_messages(self, 'NO_TEX_RENAME') + elif errors: + warning_messages(self, 'TEX_RENAME_F', errors, 'TEX') + + # reset name to default + self.named = self.def_name + + self.is_not_undo = False + + return {'FINISHED'} + + +class TEXTURE_PT_rename_panel(Panel): + # Creates a Panel in the scene context of the properties editor + bl_label = "Texture Rename" + bl_idname = "SCENE_PT_layout" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "texture" + + def draw(self, context): + layout = self.layout + layout.operator("texture.patern_rename") + + +def register(): + bpy.utils.register_module(__name__) + pass + + +def unregister(): + bpy.utils.unregister_module(__name__) + pass + +if __name__ == "__main__": + register() diff --git a/materials_utils/warning_messages_utils.py b/materials_utils/warning_messages_utils.py new file mode 100644 index 00000000..595e0e31 --- /dev/null +++ b/materials_utils/warning_messages_utils.py @@ -0,0 +1,177 @@ +# gpl: author lijenstina +# -*- coding: utf-8 -*- + +import bpy + +# ----------------------------------------------------------------------------- +# Globals # + +# change the name for the properties settings +MAT_SPEC_NAME = "materials_specials" + +# collect messages for the report operator +COLLECT_REPORT = [] + + +# ----------------------------------------------------------------------------- +# Functions # + +def warning_messages(operator=None, warn='DEFAULT', object_name="", is_mat=None, fake=""): + # Enter warning messages to the message dictionary + # warn - if nothing passed falls back to DEFAULT + # a list of strings can be passed and concatenated in obj_name too + # is_mat a switch to change to materials or textures for obj_name('MAT','TEX', 'FILE', None) + # fake - optional string that can be passed + # MAX_COUNT - max members of an list to be displayed + + # pass the show_warnings bool to enable/disable them + addon = bpy.context.user_preferences.addons[MAT_SPEC_NAME] + show_warn = (addon.preferences.show_warnings if addon else False) + + if show_warn and operator: + obj_name = "" + MAX_COUNT = 6 + gramma_s, gramma_p = " - has ", " - have " + + if is_mat: + if is_mat in ('MAT'): + gramma_s, gramma_p = " - Material has ", " - Materials have " + elif is_mat in ('TEX'): + gramma_s, gramma_p = " - Texture has ", " - Textures have " + elif is_mat in ('FILE'): + gramma_s, gramma_p = " - File ", " - Files " + + # print the whole list in the console if abbreviated + obj_size_big = False + + if object_name: + if type(object_name) is list: + obj_name = ", ".join(object_name) + obj_size = len(object_name) + + # compare string list size + if (1 < obj_size <= MAX_COUNT): + obj_name = "{}{}".format(obj_name, gramma_p) + elif (obj_size > MAX_COUNT): + abbrevation = ("(Multiple)" if is_mat else "(Multiple Objects)") + obj_size_big = True + obj_name = "{}{}".format(abbrevation, gramma_p) + elif (obj_size == 1): + obj_name = "{}{}".format(obj_name, gramma_s) + else: + obj_name = "{}{}".format(object_name, gramma_s) + + message = { + 'DEFAULT': "No editable selected objects, could not finish", + 'PLACEHOLDER': "{}{}".format(warn, " - Message key is not present in the warning_message_utils"), + 'RMV_EDIT': "{}{}".format(obj_name, "Unable to remove material slot in edit mode)"), + 'A_OB_MIX_NO_MAT': "{}{}".format(obj_name, "No Material applied. Object type can't have materials"), + 'A_MAT_NAME_EDIT': "{}{}".format(obj_name, " been applied to selection"), + 'C_OB_NO_MAT': "{}{}".format(obj_name, "No Materials. Unused material slots are " + "not cleaned"), + 'C_OB_MIX_NO_MAT': "{}{}".format(obj_name, "No Materials or an Object type that " + "can't have Materials (Clean Material Slots)"), + 'R_OB_NO_MAT': "{}{}".format(obj_name, "No Materials. Nothing to remove"), + 'R_OB_FAIL_MAT': "{}{}".format(obj_name, "Failed to remove materials - (Operator Error)"), + 'R_NO_SL_MAT': "No Selection. Material slots are not removed", + 'R_ALL_SL_MAT': "All materials removed from selected objects", + 'R_ALL_NO_MAT': "Object(s) have no materials to remove", + 'R_ACT_MAT': "{}{}".format(obj_name, "Removed active Material"), + 'R_ACT_MAT_ALL': "{}{}".format(obj_name, "Removed all Material from the Object"), + 'SL_MAT_BY_NAME': "{}{}{}".format("Objects with the Material ", obj_name, "been selected"), + 'OB_CANT_MAT': "{}{}".format(obj_name, "Object type that can't have Materials"), + 'REP_MAT_NONE': "Replace Material: No materials replaced", + 'FAKE_SET_ON': "{}{}{}".format(obj_name, "set Fake user ", fake), + 'FAKE_SET_OFF': "{}{}{}".format(obj_name, "disabled Fake user ", fake), + 'FAKE_NO_MAT': "Fake User Settings: Object(s) with no Materials or no changes needed", + 'CPY_MAT_MIX_OB': "Copy Materials to others: Some of the Object types can't have Materials", + 'CPY_MAT_ONE_OB': "Copy Materials to others: Only one object selected", + 'CPY_MAT_FAIL': "Copy Materials to others: (Operator Error)", + 'CPY_MAT_DONE': "Materials are copied from active to selected objects", + 'TEX_MAT_NO_SL': "Texface to Material: No Selected Objects", + 'TEX_MAT_NO_CRT': "{}{}".format(obj_name, "not met the conditions for the tool (UVs, Active Images)"), + 'MAT_TEX_NO_SL': "Material to Texface: No Selected Objects", + 'MAT_TEX_NO_MESH': "{}{}".format(obj_name, "not met the conditions for the tool (Mesh)"), + 'MAT_TEX_NO_MAT': "{}{}".format(obj_name, "not met the conditions for the tool (Material)"), + 'BI_SW_NODES_OFF': "Switching to Blender Render, Use Nodes disabled", + 'BI_SW_NODES_ON': "Switching to Blender Render, Use Nodes enabled", + 'CYC_SW_NODES_ON': "Switching back to Cycles, Use Nodes enabled", + 'CYC_SW_NODES_OFF': "Switching back to Cycles, Use Nodes disabled", + 'TEX_RENAME_F': "{}{}".format(obj_name, "no Images assigned, skipping"), + 'NO_TEX_RENAME': "No Textures in Data, nothing to rename", + 'DIR_PATH_W_ERROR': "ERROR: Directory without writing privileges", + 'DIR_PATH_N_ERROR': "ERROR: Directory not existing", + 'DIR_PATH_A_ERROR': "ERROR: Directory not accessible", + 'DIR_PATH_W_OK': "Directory has writing privileges", + 'DIR_PATH_CONVERT': "Conversion Cancelled. Problem with chosen Directory, check System Console", + 'MAT_LINK_ERROR': "{}{}".format(obj_name, "not be renamed or set as Base(s)"), + 'MAT_LINK_NO_NAME': "No Base name given, No changes applied", + 'MOVE_SLOT_UP': "{}{}".format(obj_name, "been moved on top of the stack"), + 'MOVE_SLOT_DOWN': "{}{}".format(obj_name, "been moved to the bottom of the stack"), + 'MAT_TRNSP_BACK': "{}{}".format(obj_name, "been set with Alpha connected to Front/Back Geometry node"), + 'E_MAT_TRNSP_BACK': "Transparent back (BI): Failure to set the action", + 'CONV_NO_OBJ_MAT': "{}{}".format(obj_name, "has no Materials. Nothing to convert"), + 'CONV_NO_SC_MAT': "No Materials in the Scene. Nothing to convert", + 'CONV_NO_SEL_MAT': "No Materials on Selected Objects. Nothing to convert", + } + + # doh! did we passed an non existing dict key + warn = (warn if warn in message else 'PLACEHOLDER') + + operator.report({'INFO'}, message[warn]) + + if obj_size_big is True: + print("\n** MATERIAL SPECIALS **: \n Full list for the Info message is: \n", + ", ".join(object_name), "\n") + + +def collect_report(collection="", is_start=False, is_final=False): + # collection passes a string for appending to COLLECT_REPORT global + # is_final swithes to the final report with the operator in __init__ + global COLLECT_REPORT + + if is_start: + # there was a crash somewhere before the is_final call + COLLECT_REPORT = [] + + if collection and type(collection) is str: + COLLECT_REPORT.append(collection) + print(collection) + + if is_final: + # final operator pass uses * as delimiter for splitting into new lines + messages = "*".join(COLLECT_REPORT) + bpy.ops.mat_converter.reports('INVOKE_DEFAULT', message=messages) + COLLECT_REPORT = [] + + +# ----------------------------------------------------------------------------- +# utility functions: + +def c_is_cycles_addon_enabled(): + # checks if Cycles is enabled + # thanks to ideasman42 + return ('cycles' in bpy.context.user_preferences.addons.keys()) + + +def c_data_has_materials(): + # check for material presence in data + return (len(bpy.data.materials) > 0) + + +def c_data_has_images(): + # check for image presence in data + return (len(bpy.data.images) > 0) + + +def register(): + bpy.utils.register_module(__name__) + pass + + +def unregister(): + bpy.utils.unregister_module(__name__) + pass + +if __name__ == "__main__": + register() diff --git a/space_view3d_materials_utils.py b/space_view3d_materials_utils.py deleted file mode 100644 index 0e80e9f9..00000000 --- a/space_view3d_materials_utils.py +++ /dev/null @@ -1,855 +0,0 @@ -# (c) 2010 Michael Williamson (michaelw) -# ported from original by Michael Williamson - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# 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 2 -# 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, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -bl_info = { - "name": "Material Utils", - "author": "michaelw", - "version": (1, 6), - "blender": (2, 74, 0), - "location": "View3D > shift-Q key", - "description": "Menu of material tools (assign, select..) in the 3D View", - "warning": "", - "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" - "Scripts/3D interaction/Materials Utils", - "category": "Material", -} - -""" -This script has several functions and operators, grouped for convenience: - -* assign material: - offers the user a list of ALL the materials in the blend file and an - additional "new" entry the chosen material will be assigned to all the - selected objects in object mode. - - in edit mode the selected polygons get the selected material applied. - - if the user chose "new" the new material can be renamed using the - "last operator" section of the toolbox. - After assigning the material "clean material slots" and - "material to texface" are auto run to keep things tidy - (see description bellow) - - -* select by material - in object mode this offers the user a menu of all materials in the blend - file any objects using the selected material will become selected, any - objects without the material will be removed from selection. - - in edit mode: the menu offers only the materials attached to the current - object. It will select the polygons that use the material and deselect those - that do not. - -* clean material slots - for all selected objects any empty material slots or material slots with - materials that are not used by the mesh polygons will be removed. - -* remove material slots - removes all material slots of the active object. - -* material to texface - transfers material assignments to the UV editor. This is useful if you - assigned materials in the properties editor, as it will use the already - set up materials to assign the UV images per-face. It will use the first - enabled image texture it finds. - -* texface to materials - creates texture materials from images assigned in UV editor. - -* replace materials - lets your replace one material by another. Optionally for all objects in - the blend, otherwise for selected editable objects only. An additional - option allows you to update object selection, to indicate which objects - were affected and which not. - -* set fake user - enable/disable fake user for materials. You can chose for which materials - it shall be set, materials of active / selected / objects in current scene - or used / unused / all materials. - -""" - - -import bpy -from bpy.props import StringProperty, BoolProperty, EnumProperty - - -def fake_user_set(fake_user='ON', materials='UNUSED'): - if materials == 'ALL': - mats = (mat for mat in bpy.data.materials if mat.library is None) - elif materials == 'UNUSED': - mats = (mat for mat in bpy.data.materials if mat.library is None and mat.users == 0) - else: - mats = [] - if materials == 'ACTIVE': - objs = [bpy.context.active_object] - elif materials == 'SELECTED': - objs = bpy.context.selected_objects - elif materials == 'SCENE': - objs = bpy.context.scene.objects - else: # materials == 'USED' - objs = bpy.data.objects - # Maybe check for users > 0 instead? - - """ more reable than the following generator: - for ob in objs: - if hasattr(ob.data, "materials"): - for mat in ob.data.materials: - if mat.library is None: #and not in mats: - mats.append(mat) - """ - mats = (mat for ob in objs if hasattr(ob.data, "materials") for mat in ob.data.materials if mat.library is None) - - for mat in mats: - mat.use_fake_user = fake_user == 'ON' - - for area in bpy.context.screen.areas: - if area.type in ('PROPERTIES', 'NODE_EDITOR'): - area.tag_redraw() - - -def replace_material(m1, m2, all_objects=False, update_selection=False): - # replace material named m1 with material named m2 - # m1 is the name of original material - # m2 is the name of the material to replace it with - # 'all' will replace throughout the blend file - - matorg = bpy.data.materials.get(m1) - matrep = bpy.data.materials.get(m2) - - if matorg != matrep and None not in (matorg, matrep): - #store active object - scn = bpy.context.scene - - if all_objects: - objs = bpy.data.objects - - else: - objs = bpy.context.selected_editable_objects - - for ob in objs: - if ob.type == 'MESH': - - match = False - - for m in ob.material_slots: - if m.material == matorg: - m.material = matrep - # don't break the loop as the material can be - # ref'd more than once - - # Indicate which objects were affected - if update_selection: - ob.select = True - match = True - - if update_selection and not match: - ob.select = False - - #else: - # print('Replace material: nothing to replace') - - -def select_material_by_name(find_mat_name): - #in object mode selects all objects with material find_mat_name - #in edit mode selects all polygons with material find_mat_name - - find_mat = bpy.data.materials.get(find_mat_name) - - if find_mat is None: - return - - #check for editmode - editmode = False - - scn = bpy.context.scene - - #set selection mode to polygons - scn.tool_settings.mesh_select_mode = False, False, True - - actob = bpy.context.active_object - if actob.mode == 'EDIT': - editmode = True - bpy.ops.object.mode_set() - - if not editmode: - objs = bpy.data.objects - for ob in objs: - if ob.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}: - ms = ob.material_slots - for m in ms: - if m.material == find_mat: - ob.select = True - # the active object may not have the mat! - # set it to one that does! - scn.objects.active = ob - break - else: - ob.select = False - - #deselect non-meshes - else: - ob.select = False - - else: - #it's editmode, so select the polygons - ob = actob - ms = ob.material_slots - - #same material can be on multiple slots - slot_indeces = [] - i = 0 - # found = False # UNUSED - for m in ms: - if m.material == find_mat: - slot_indeces.append(i) - # found = True # UNUSED - i += 1 - me = ob.data - for f in me.polygons: - if f.material_index in slot_indeces: - f.select = True - else: - f.select = False - me.update() - if editmode: - bpy.ops.object.mode_set(mode='EDIT') - - -def mat_to_texface(): - # assigns the first image in each material to the polygons in the active - # uvlayer for all selected objects - - #check for editmode - editmode = False - - actob = bpy.context.active_object - if actob.mode == 'EDIT': - editmode = True - bpy.ops.object.mode_set() - - for ob in bpy.context.selected_editable_objects: - if ob.type == 'MESH': - #get the materials from slots - ms = ob.material_slots - - #build a list of images, one per material - images = [] - #get the textures from the mats - for m in ms: - if m.material is None: - continue - gotimage = False - textures = zip(m.material.texture_slots, m.material.use_textures) - for t, enabled in textures: - if enabled and t is not None: - tex = t.texture - if tex.type == 'IMAGE': - img = tex.image - images.append(img) - gotimage = True - break - - if not gotimage: - print('noimage on', m.name) - images.append(None) - - # now we have the images - # applythem to the uvlayer - - me = ob.data - #got uvs? - if not me.uv_textures: - scn = bpy.context.scene - scn.objects.active = ob - bpy.ops.mesh.uv_texture_add() - scn.objects.active = actob - - #get active uvlayer - for t in me.uv_textures: - if t.active: - uvtex = t.data - for f in me.polygons: - #check that material had an image! - if images[f.material_index] is not None: - uvtex[f.index].image = images[f.material_index] - else: - uvtex[f.index].image = None - - me.update() - - if editmode: - bpy.ops.object.mode_set(mode='EDIT') - - -def assignmatslots(ob, matlist): - #given an object and a list of material names - #removes all material slots form the object - #adds new ones for each material in matlist - #adds the materials to the slots as well. - - scn = bpy.context.scene - ob_active = bpy.context.active_object - scn.objects.active = ob - - for s in ob.material_slots: - bpy.ops.object.material_slot_remove() - - # re-add them and assign material - i = 0 - for m in matlist: - mat = bpy.data.materials[m] - ob.data.materials.append(mat) - i += 1 - - # restore active object: - scn.objects.active = ob_active - - -def cleanmatslots(): - #check for edit mode - editmode = False - actob = bpy.context.active_object - if actob.mode == 'EDIT': - editmode = True - bpy.ops.object.mode_set() - - objs = bpy.context.selected_editable_objects - - for ob in objs: - if ob.type == 'MESH': - mats = ob.material_slots.keys() - - #check the polygons on the mesh to build a list of used materials - usedMatIndex = [] # we'll store used materials indices here - faceMats = [] - me = ob.data - for f in me.polygons: - #get the material index for this face... - faceindex = f.material_index - - #indices will be lost: Store face mat use by name - currentfacemat = mats[faceindex] - faceMats.append(currentfacemat) - - # check if index is already listed as used or not - found = 0 - for m in usedMatIndex: - if m == faceindex: - found = 1 - #break - - if found == 0: - #add this index to the list - usedMatIndex.append(faceindex) - - #re-assign the used mats to the mesh and leave out the unused - ml = [] - mnames = [] - for u in usedMatIndex: - ml.append(mats[u]) - #we'll need a list of names to get the face indices... - mnames.append(mats[u]) - - assignmatslots(ob, ml) - - # restore face indices: - i = 0 - for f in me.polygons: - matindex = mnames.index(faceMats[i]) - f.material_index = matindex - i += 1 - - if editmode: - bpy.ops.object.mode_set(mode='EDIT') - - -def assign_mat(matname="Default"): - # get active object so we can restore it later - actob = bpy.context.active_object - - # check if material exists, if it doesn't then create it - found = False - for m in bpy.data.materials: - if m.name == matname: - target = m - found = True - break - if not found: - target = bpy.data.materials.new(matname) - - # if objectmode then set all polygons - editmode = False - allpolygons = True - if actob.mode == 'EDIT': - editmode = True - allpolygons = False - bpy.ops.object.mode_set() - - objs = bpy.context.selected_editable_objects - - for ob in objs: - # set the active object to our object - scn = bpy.context.scene - scn.objects.active = ob - - if ob.type in {'CURVE', 'SURFACE', 'FONT', 'META'}: - found = False - i = 0 - for m in bpy.data.materials: - if m.name == matname: - found = True - index = i - break - i += 1 - if not found: - index = i - 1 - targetlist = [index] - assignmatslots(ob, targetlist) - - elif ob.type == 'MESH': - # check material slots for matname material - found = False - i = 0 - mats = ob.material_slots - for m in mats: - if m.name == matname: - found = True - index = i - #make slot active - ob.active_material_index = i - break - i += 1 - - if not found: - index = i - #the material is not attached to the object - ob.data.materials.append(target) - - #now assign the material: - me = ob.data - if allpolygons: - for f in me.polygons: - f.material_index = index - elif allpolygons == False: - for f in me.polygons: - if f.select: - f.material_index = index - me.update() - - #restore the active object - bpy.context.scene.objects.active = actob - if editmode: - bpy.ops.object.mode_set(mode='EDIT') - - -def check_texture(img, mat): - #finds a texture from an image - #makes a texture if needed - #adds it to the material if it isn't there already - - tex = bpy.data.textures.get(img.name) - - if tex is None: - tex = bpy.data.textures.new(name=img.name, type='IMAGE') - - tex.image = img - - #see if the material already uses this tex - #add it if needed - found = False - for m in mat.texture_slots: - if m and m.texture == tex: - found = True - break - if not found and mat: - mtex = mat.texture_slots.add() - mtex.texture = tex - mtex.texture_coords = 'UV' - mtex.use_map_color_diffuse = True - - -def texface_to_mat(): - # editmode check here! - editmode = False - ob = bpy.context.object - if ob.mode == 'EDIT': - editmode = True - bpy.ops.object.mode_set() - - for ob in bpy.context.selected_editable_objects: - - faceindex = [] - unique_images = [] - - # get the texface images and store indices - if (ob.data.uv_textures): - for f in ob.data.uv_textures.active.data: - if f.image: - img = f.image - #build list of unique images - if img not in unique_images: - unique_images.append(img) - faceindex.append(unique_images.index(img)) - - else: - img = None - faceindex.append(None) - - # check materials for images exist; create if needed - matlist = [] - for i in unique_images: - if i: - try: - m = bpy.data.materials[i.name] - except: - m = bpy.data.materials.new(name=i.name) - continue - - finally: - matlist.append(m.name) - # add textures if needed - check_texture(i, m) - - # set up the object material slots - assignmatslots(ob, matlist) - - #set texface indices to material slot indices.. - me = ob.data - - i = 0 - for f in faceindex: - if f is not None: - me.polygons[i].material_index = f - i += 1 - if editmode: - bpy.ops.object.mode_set(mode='EDIT') - -def remove_materials(): - for ob in bpy.data.objects: - try: - bpy.ops.object.material_slot_remove() - print ("removed material from " + ob.name) - except: - print (ob.name + " does not have materials.") - -# ----------------------------------------------------------------------------- -# operator classes: - -class VIEW3D_OT_texface_to_material(bpy.types.Operator): - """Create texture materials for images assigned in UV editor""" - bl_idname = "view3d.texface_to_material" - bl_label = "Texface Images to Material/Texture (Material Utils)" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - if context.selected_editable_objects: - texface_to_mat() - return {'FINISHED'} - else: - self.report({'WARNING'}, - "No editable selected objects, could not finish") - return {'CANCELLED'} - - -class VIEW3D_OT_assign_material(bpy.types.Operator): - """Assign a material to the selection""" - bl_idname = "view3d.assign_material" - bl_label = "Assign Material (Material Utils)" - bl_options = {'REGISTER', 'UNDO'} - - matname = StringProperty( - name='Material Name', - description='Name of Material to Assign', - default="", - maxlen=63, - ) - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - mn = self.matname - print(mn) - assign_mat(mn) - cleanmatslots() - mat_to_texface() - return {'FINISHED'} - - -class VIEW3D_OT_clean_material_slots(bpy.types.Operator): - """Removes any material slots from selected objects """ \ - """that are not used by the mesh""" - bl_idname = "view3d.clean_material_slots" - bl_label = "Clean Material Slots (Material Utils)" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - cleanmatslots() - return {'FINISHED'} - - -class VIEW3D_OT_material_to_texface(bpy.types.Operator): - """Transfer material assignments to UV editor""" - bl_idname = "view3d.material_to_texface" - bl_label = "Material Images to Texface (Material Utils)" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - mat_to_texface() - return {'FINISHED'} - -class VIEW3D_OT_material_remove(bpy.types.Operator): - """Remove all material slots from active objects""" - bl_idname = "view3d.material_remove" - bl_label = "Remove All Material Slots (Material Utils)" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - remove_materials() - return {'FINISHED'} - - -class VIEW3D_OT_select_material_by_name(bpy.types.Operator): - """Select geometry with this material assigned to it""" - bl_idname = "view3d.select_material_by_name" - bl_label = "Select Material By Name (Material Utils)" - bl_options = {'REGISTER', 'UNDO'} - matname = StringProperty( - name='Material Name', - description='Name of Material to Select', - maxlen=63, - ) - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - mn = self.matname - select_material_by_name(mn) - return {'FINISHED'} - - -class VIEW3D_OT_replace_material(bpy.types.Operator): - """Replace a material by name""" - bl_idname = "view3d.replace_material" - bl_label = "Replace Material (Material Utils)" - bl_options = {'REGISTER', 'UNDO'} - - matorg = StringProperty( - name="Original", - description="Material to replace", - maxlen=63, - ) - matrep = StringProperty(name="Replacement", - description="Replacement material", - maxlen=63, - ) - all_objects = BoolProperty( - name="All objects", - description="Replace for all objects in this blend file", - default=True, - ) - update_selection = BoolProperty( - name="Update Selection", - description="Select affected objects and deselect unaffected", - default=True, - ) - - # Allow to replace all objects even without a selection / active object - #@classmethod - #def poll(cls, context): - # return context.active_object is not None - - def draw(self, context): - layout = self.layout - layout.prop_search(self, "matorg", bpy.data, "materials") - layout.prop_search(self, "matrep", bpy.data, "materials") - layout.prop(self, "all_objects") - layout.prop(self, "update_selection") - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def execute(self, context): - replace_material(self.matorg, self.matrep, self.all_objects, self.update_selection) - return {'FINISHED'} - - -class VIEW3D_OT_fake_user_set(bpy.types.Operator): - """Enable/disable fake user for materials""" - bl_idname = "view3d.fake_user_set" - bl_label = "Set Fake User (Material Utils)" - bl_options = {'REGISTER', 'UNDO'} - - fake_user = EnumProperty( - name="Fake User", - description="Turn fake user on or off", - items=(('ON', "On", "Enable fake user"),('OFF', "Off", "Disable fake user")), - default='ON' - ) - - materials = EnumProperty( - name="Materials", - description="Which materials of objects to affect", - items=(('ACTIVE', "Active object", "Materials of active object only"), - ('SELECTED', "Selected objects", "Materials of selected objects"), - ('SCENE', "Scene objects", "Materials of objects in current scene"), - ('USED', "Used", "All materials used by objects"), - ('UNUSED', "Unused", "Currently unused materials"), - ('ALL', "All", "All materials in this blend file")), - default='UNUSED' - ) - - def draw(self, context): - layout = self.layout - layout.prop(self, "fake_user", expand=True) - layout.prop(self, "materials") - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def execute(self, context): - fake_user_set(self.fake_user, self.materials) - return {'FINISHED'} - - -# ----------------------------------------------------------------------------- -# menu classes - -class VIEW3D_MT_master_material(bpy.types.Menu): - bl_label = "Material Utils Menu" - - def draw(self, context): - layout = self.layout - layout.operator_context = 'INVOKE_REGION_WIN' - - layout.menu("VIEW3D_MT_assign_material", icon='ZOOMIN') - layout.menu("VIEW3D_MT_select_material", icon='HAND') - layout.separator() - layout.operator("view3d.clean_material_slots", - text="Clean Material Slots", - icon='CANCEL') - layout.operator("view3d.material_remove", - text="Remove Material Slots", - icon='CANCEL') - layout.operator("view3d.material_to_texface", - text="Material to Texface", - icon='MATERIAL_DATA') - layout.operator("view3d.texface_to_material", - text="Texface to Material", - icon='MATERIAL_DATA') - - layout.separator() - layout.operator("view3d.replace_material", - text='Replace Material', - icon='ARROW_LEFTRIGHT') - - layout.operator("view3d.fake_user_set", - text='Set Fake User', - icon='UNPINNED') - - -class VIEW3D_MT_assign_material(bpy.types.Menu): - bl_label = "Assign Material" - - def draw(self, context): - layout = self.layout - layout.operator_context = 'INVOKE_REGION_WIN' - for material_name in bpy.data.materials.keys(): - layout.operator("view3d.assign_material", - text=material_name, - icon='MATERIAL_DATA').matname = material_name - - layout.operator("view3d.assign_material", - text="Add New", - icon='ZOOMIN') - - -class VIEW3D_MT_select_material(bpy.types.Menu): - bl_label = "Select by Material" - - def draw(self, context): - layout = self.layout - layout.operator_context = 'INVOKE_REGION_WIN' - - ob = context.object - layout.label - if ob.mode == 'OBJECT': - #show all used materials in entire blend file - for material_name, material in bpy.data.materials.items(): - if material.users > 0: - layout.operator("view3d.select_material_by_name", - text=material_name, - icon='MATERIAL_DATA', - ).matname = material_name - - elif ob.mode == 'EDIT': - #show only the materials on this object - mats = ob.material_slots.keys() - for m in mats: - layout.operator("view3d.select_material_by_name", - text=m, - icon='MATERIAL_DATA').matname = m - - -def register(): - bpy.utils.register_module(__name__) - - kc = bpy.context.window_manager.keyconfigs.addon - if kc: - km = kc.keymaps.new(name="3D View", space_type="VIEW_3D") - kmi = km.keymap_items.new('wm.call_menu', 'Q', 'PRESS', shift=True) - kmi.properties.name = "VIEW3D_MT_master_material" - - -def unregister(): - bpy.utils.unregister_module(__name__) - - kc = bpy.context.window_manager.keyconfigs.addon - if kc: - km = kc.keymaps["3D View"] - for kmi in km.keymap_items: - if kmi.idname == 'wm.call_menu': - if kmi.properties.name == "VIEW3D_MT_master_material": - km.keymap_items.remove(kmi) - break - -if __name__ == "__main__": - register() -- cgit v1.2.3