├── LICENSE
├── README.md
├── __init__.py
├── tmb_render.py
├── tmb_support.py
├── tmb_ui.py
└── tmb_uninstall.py
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TRUE MOTION BLUR
2 | Free subframes-based motion blur add-on
3 | for BLENDER 2.8 EEVEE and WORKBENCH render engines
4 | Version 1.0.1.
5 |
6 | For each scene frame calculates and renders several subframes based on add-on's settings
7 | and blends them together to a final frame.
8 |
9 | - Affects camera movements, objects, particles, mesh-simulations like cloth and softbody,
10 | animated textures, volumetric, semi-transparent and refractive materials.
11 | - Doesn't affect image-, Open VDB- and other sequences which are represented as sequences
12 | of independent frames
13 |
14 | # !!!WARNING!!!
15 | Add-on creates and DELETES temporary nodes, files and folders.
16 | Be careful:
17 | - If you have folders named "\_True_Motion_Blur_tmp" or "\_TMB_Output"
18 | in your Blender default temporary directory (e.g. C:\users\username\AppData\local\Temp for Windows)
19 | they will be completely DELETED from your computer, including all their subfolders and files in them!
20 | - If you have Image or Alpha Over nodes in your Compositor with
21 | name starting with "TMB" (e.g. "TMB_Mix") they will be also deleted!
22 | - Add-on currently doesn't support render in AVI JPEG, AVI Raw and FFmpeg video.
23 | If you need to render animation please render image sequences instead.
24 | - When render animation don't forget to set any certain render output directory in Output Properties tab -> Output ->
25 | Output Path tab, otherwise render results may be not saved or in some specific cases it may cause errors
26 | - In some cases when Cycles and Eevee scenes are mixed in Compositor and rendered simultaneously it
27 | may cause Blender internal "Exception Access Violated" error and crash Blender. Assumably it happens because previous
28 | render operator doesn't release GPU when the next launches, and unfortunately there's no handler in Blender API which
29 | could allow to fix it. So in some cases you may need to render such scenes separately and combine them later on as image sequences.
30 |
31 | # New in this version:
32 | - Now add-on stores its temporary files and folders for subframe and frame render results in the
33 | Blender default temporary directory instead of storing them in project folder or render folder.
34 | It should help to prevent some errors in case scene render output path hasn't been changed from default
35 | - Fixed error when user tries to render in AVI JPEG, AVI Raw and FFmpeg video file formats: now Blender raises Warning
36 | instead of error
37 | - Fixed disabling/enabling back add-on in Preferences -> Add-ons error
38 |
39 | # Install
40 | 1. Download zip-archive from github, don't unpack it!
41 | 2. Open Blender. From top menu go to -> Edit -> Preferences -> Add-ons -> Install
42 | 3. Find downloaded zip-archive and click "Install Add-on" button
43 | 4. After add-on installed check enabling checkbox near its name
44 | 5. Also enable it in Render Preferences tab
45 |
46 | # Uninstall
47 | 1. Open Blender. From top menu go to -> Edit -> Preferences -> Add-ons
48 | 2. Start to type in "True Motion Blur" until it is shown
49 | 3. If you want to temporary disable it for all your projects uncheck enabling checkbox
50 | 4. If you want to completely remove it from your computer press the small arrow near its name
51 | and press "Remove" button
52 |
53 | # Controls
54 | Add-on replaces native render operator's shortcuts and buttons. Use all the same familiar commands
55 | to start render.
56 | - Top menu -> Render Image (or `F12` on the keyboard)
57 | - Top menu -> Render Animation (or `Ctrl`(/`Cmnd` on mac) + `F12` )
58 |
59 | # Settings
60 | - *Position*:
61 | Offset for the shutter's time interval, allows to change motion blur trails
62 | - *Shutter*:
63 | Time taken in frames between shutter open and close. Soft limit is 1, no maximum limit
64 | - *Samples*:
65 | Number of subframes to be rendered per frame. More subframes - more smooth blur, but more render time.
66 | - *Quality Boost*:
67 | Increases render samples for each subframe from its normal amount (lowered versus original scene render samplesamount) up to scene original render samples.Render time increases proportionally
68 | - *Render Passes*:
69 | - When unchecked subframes are rendered only for those Render Layers outputs which links lead to Composite or File Outputs nodes.
70 | - When checked renders subframes for all outputs of all Render Layers whose scenes has enabled True motion Blur.
71 |
72 | # What add-on actually does
73 | - If there isn't any Compositor node tree in the scene yet, it opens Compositor (this is also necessary
74 | to display render results properly), turns on "Use nodes" and "Backdrop" buttons
75 | - If there are nodes in Compositor add-on collect all enabled Render Layers outputs whose links
76 | lead to Composite or File Outputs nodes
77 | - If "Render Passes" checkbox is checked or if file format in the scene render image settings is set to "Open EXR Multilayer"
78 | collect all enabled Render Layers outputs no matter if theit links lead anywhere or not
79 | - For each active links add-on creates:
80 | - creates generated Blender Image node to store render results to
81 | - creates Alpha Over node to switch between Image and original Render Layers output
82 | - creates File Output node to store temporary images into temporary folder while render subframes
83 | - Than add-on:
84 | - deletes user Viewer node (don't worry it will be restored in the end) and creates its own
85 | - calculates number and position of subframes on the timeline based on add-on's "Position", "Shutter" and "Samples" settings
86 | - decreases scene's sample rate proportionally to subframes number and add-on's "Quality Boost" parameter
87 | - renders subframes for all Render Layers whose scenes has add-on enabled
88 | - mixes all prerendered subframes to the final results
89 | - replaces previously generated images' pixels with those results
90 | - changes mix-factor of all add-on's Alpha Over mix nodes to 1 to use mixed results
91 | - if there are scenes in Compositor without enabled add-on including Cycles scenes
92 | renders them together with subframes mix results
93 | - if not renders just mixed results
94 | - if Render Animation was launched saves the frame
95 | - Clean up:
96 | - unmutes all temporary muted nodes
97 | - restores all deleted nodes
98 | - deletes all temporary created nodes except images with the latest mixed results and Alpha Over mix nodes
99 | - deletes all temporary files and folders from disc
100 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # ##### BEGIN GPL LICENSE BLOCK #####
2 | #
3 | # This program is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU General Public License
5 | # as published by the Free Software Foundation; either version 2
6 | # of the License, or (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software Foundation,
15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 | #
17 | # ##### END GPL LICENSE BLOCK #####
18 |
19 | # True Motion Blur add-on
20 | # Initializing script
21 | # (c) 2020 Andrey Sokolov (so_records)
22 |
23 | bl_info = {
24 | "name": "True Motion Blur",
25 | "author": "Andrey Sokolov",
26 | "version": (1, 0, 1),
27 | "blender": (2, 83, 0),
28 | "location": "Render Settings > True Motion Blur",
29 | "description": "True subframe motion blur for Eevee",
30 | "warning": "Creates and deletes folders and files in Blender tmp directory.\
31 | Changes Compositor nodes (see Readme file)",
32 | "wiki_url": "https://github.com/sorecords/true_motion_blur/blob/master/README.md",
33 | "tracker_url": "https://github.com/sorecords/true_motion_blur/issues",
34 | "category": "Render"
35 | }
36 |
37 | #------------------------------- Import Modules --------------------------------
38 | import bpy
39 | from bpy.props import PointerProperty
40 | from bpy.utils import register_class, unregister_class
41 | from .tmb_ui import *
42 | from .tmb_render import *
43 | from .tmb_support import *
44 |
45 | #--------------------------- Operators to register -----------------------------
46 | classes = [
47 | TMB_TrueMB,
48 | TMB_Keyconfig,
49 | TMB_PT_true_mb_panel,
50 | TMB_Warning,
51 | TMB_Store,
52 | TMB_RLayers,
53 | TMB_Links,
54 | TMB_SaveBuffers,
55 | TMB_AddMixImages,
56 | TMB_UserOutputs,
57 | TMB_ScenesSetup,
58 | TMB_Backdrop,
59 | TMB_Setup,
60 | TMB_UpdatePreview,
61 | TMB_RenderVariables,
62 | TMB_RenderHelpers,
63 | TMB_Render,
64 | TMB_Restore,
65 | TOPBAR_MT_render,
66 | ]
67 |
68 | #---------------------------------- Register -----------------------------------
69 | def register():
70 | for cl in classes:
71 | register_class(cl)
72 | bpy.types.Scene.true_mb = PointerProperty(type=TMB_TrueMB)
73 | bpy.app.handlers.persistent(keyconfig)
74 | bpy.app.handlers.load_pre.append(keyconfig)
75 |
76 | def unregister():
77 | op = bpy.types.TMB_OT_store
78 | op.enable = False
79 | from .tmb_uninstall import TMB_KeyconfigRestore
80 | register_class(TMB_KeyconfigRestore)
81 | bpy.ops.tmb.keyconfig_restore()
82 | for i in reversed(classes):
83 | unregister_class(i)
84 | from .tmb_uninstall import TOPBAR_MT_render
85 | register_class(TOPBAR_MT_render)
86 | unregister_class(TMB_KeyconfigRestore)
87 |
88 | #--------------------------- For test purposes only ----------------------------
89 | if __name__ == '__main__':
90 | register()
91 |
--------------------------------------------------------------------------------
/tmb_render.py:
--------------------------------------------------------------------------------
1 | # ##### BEGIN GPL LICENSE BLOCK #####
2 | #
3 | # This program is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU General Public License
5 | # as published by the Free Software Foundation; either version 2
6 | # of the License, or (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software Foundation,
15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 | #
17 | # ##### END GPL LICENSE BLOCK #####
18 |
19 | # True Motion Blur add-on
20 | # TMB Render
21 | # (c) 2020 Andrey Sokolov (so_records)
22 |
23 | import bpy, time, datetime, pathlib, shutil
24 | import numpy as np
25 | from .tmb_support import TMB_Helpers
26 | from bpy.props import BoolProperty, StringProperty, IntProperty
27 | from bpy.utils import register_class, unregister_class
28 | from time import perf_counter
29 |
30 | #------------------ Add and remove Viewer for correct preview ------------------
31 |
32 | class TMB_UpdatePreview(bpy.types.Operator):
33 | '''Add and remove Viewer to update render preview display'''
34 | bl_idname = "tmb.update"
35 | bl_label = "Update Preview"
36 |
37 | store = None
38 | project = None
39 | sc = None
40 | resx = None
41 | resy = None
42 | resprc = None
43 | pixlen = None
44 | timer = None
45 | viewer = None
46 | viewer_image = None
47 | wm = None
48 | win = None
49 |
50 | def structure(self):
51 | self.store = bpy.types.TMB_OT_store.store
52 | self.project = self.store['Project']
53 | self.sc = self.project['main_sc']
54 | self.wm = self.project['wm']
55 | self.win = self.project['window']
56 | self.resprc = self.sc.render.resolution_percentage/100
57 | self.resx = int(self.sc.render.resolution_x*self.resprc)
58 | self.resy = int(self.sc.render.resolution_y*self.resprc)
59 | self.pixlen = self.project['pix_len']
60 | self.viewer = None
61 | self.viewer_image = (
62 | bpy.data.images['Viewer Node']
63 | if 'Viewer Node' in bpy.data.images else
64 | bpy.data.images.new('Viewer Node', self.resx, self.resy)
65 | )
66 | self.viewer_image.pixels[:] = np.ones(
67 | len(self.viewer_image.pixels))[:]
68 | self.timer = None
69 |
70 | def timer_add(self, tick=0.01):
71 | '''Add timer event and set it as self.timer'''
72 | self.timer = self.wm.event_timer_add(tick, window=self.win)
73 |
74 | def timer_remove(self):
75 | '''Remove timer event and clear self.timer'''
76 | try:
77 | for _ in range(3):
78 | self.wm.event_timer_remove(self.timer)
79 | except:
80 | pass
81 | self.timer = None
82 |
83 | def viewer_add(self):
84 | self.viewer = self.sc.node_tree.nodes.new('CompositorNodeViewer')
85 | self.viewer.name = 'TMB_Viewer'
86 | self.viewer.location.x += 500
87 | self.viewer.location.y -= 100
88 |
89 | def viewer_remove(self):
90 | self.sc.node_tree.nodes.remove(self.viewer)
91 | self.viewer = None
92 |
93 | def cleanup(self):
94 | self.viewer_image = None
95 | self.viewer = None
96 | self.wm = None
97 | self.sc = None
98 | self.resx = None
99 | self.resy = None
100 | self.resprc = None
101 | self.pixlen = None
102 | self.project = None
103 | self.store = None
104 | self.win = None
105 |
106 | def execute(self, context):
107 | self.structure()
108 | self.viewer_add()
109 | self.timer_add()
110 | self.wm.modal_handler_add(self)
111 | return {'RUNNING_MODAL'}
112 |
113 | def modal(self, context, event):
114 | if event.type == 'ESC':
115 | self.cleanup()
116 | return {'CANCELLED'}
117 | elif event.type == 'TIMER':
118 | if (
119 | self.viewer_image.pixels ==
120 | np.ones(len(self.viewer_image.pixels)).all()
121 | ):
122 | return {'PASS_THROUGH'}
123 | else:
124 | self.timer_remove()
125 | self.viewer_remove()
126 | self.cleanup()
127 | return {'FINISHED'}
128 | return {'PASS_THROUGH'}
129 |
130 | def invoke(self, context, event):
131 | return self.execute(context)
132 |
133 | #------------------------------- Render Variables ------------------------------
134 |
135 | class TMB_RenderVariables(TMB_Helpers, bpy.types.Operator):
136 | '''Render variables'''
137 | bl_idname = "tmb_render.variables"
138 | bl_label = "Render Variables"
139 |
140 | frames = []
141 | passes = []
142 | images = []
143 | frame = None
144 | subframe = None
145 | rendering_frame = None
146 | rendering_subframe = None
147 | started = None
148 | handler_complete = None
149 | handler_pre = None
150 | handler_final = None
151 | viewer = None
152 | timer = None
153 | final_completed = None
154 | skipped_frame = None
155 |
156 | class TMB_RenderHelpers(TMB_RenderVariables, bpy.types.Operator):
157 | '''Render help functions'''
158 | bl_idname = "tmb_render.helpers"
159 | bl_label = "Render Helpers"
160 |
161 | op = None
162 | variables = None
163 |
164 | store = None
165 | project = None
166 | scenes = None
167 | rlayers = None
168 | render = None
169 | restore = None
170 | viewer = None
171 | viewer_image = None
172 | sc = None
173 | render_passes = None
174 |
175 | def structure(self):
176 | '''Sync to the main storage'''
177 |
178 | self.op = bpy.types.TMB_RENDER_OT_helpers
179 | self.variables = bpy.types.TMB_RENDER_OT_variables
180 | self.store = bpy.types.TMB_OT_store.store
181 |
182 | self.project = self.store['Project']
183 | self.scenes = self.store['Scenes']
184 | self.rlayers = self.store['RLayers']
185 | self.render = self.store['Render']
186 | self.restore = self.store['Restore']
187 | self.frames = self.render['frames']
188 | self.sc = self.project['main_sc']
189 | self.variables.timer = None
190 | self.variables.rendering_subframe = None
191 | self.variables.started = None
192 | self.variables.final_completed = None
193 | self.render_passes = self.project['render_passes']
194 | bpy.ops.tmb.update('INVOKE_DEFAULT')
195 | self.viewer_image = bpy.data.images['Viewer Node']
196 |
197 | def _complete(self, context):
198 | '''subframe render_complete handler function'''
199 |
200 | _vars = bpy.types.TMB_RENDER_OT_variables
201 | _prj = bpy.types.TMB_OT_store.store['Project']
202 | _vars.rendering_subframe = False
203 | _vars.started = False
204 |
205 | def _pre(self, context):
206 | '''subframe render_pre handler function'''
207 |
208 | _vars = bpy.types.TMB_RENDER_OT_variables
209 | _vars.started = True
210 |
211 | def _final_complete(self, context):
212 | '''mixing frame render_complete handler function'''
213 |
214 | _vars = bpy.types.TMB_RENDER_OT_variables
215 | _vars.rendering_subframe = False
216 | _vars.final_completed = True
217 | _vars.started = False
218 |
219 | self.handler_complete = _complete
220 | self.handler_pre = _pre
221 | self.handler_final = _final_complete
222 | while self.handler_complete in bpy.app.handlers.render_complete:
223 | bpy.app.handlers.render_complete.remove(self.handler_complete)
224 | while self.handler_pre in bpy.app.handlers.render_pre:
225 | bpy.app.handlers.render_pre.remove(self.handler_pre)
226 | while self.handler_final in bpy.app.handlers.render_complete:
227 | bpy.app.handlers.render_copmlete.remove(self.handler_final)
228 |
229 | def cleanup(self):
230 | '''Reset Operator variables'''
231 |
232 | self.op = None
233 | self.variables = None
234 | self.store = None
235 | self.project = None
236 | self.scenes = None
237 | self.rlayers = None
238 | self.render = None
239 | self.restore = None
240 | self.viewer = None
241 | self.viewer_image = None
242 | self.sc = None
243 | self.render_passes = None
244 | self.frames = []
245 | self.passes = []
246 | self.images = []
247 | self.frame = None
248 | self.subframe = None
249 | self.rendering_frame = None
250 | self.rendering_subframe = None
251 | self.rendering = None
252 | self.handler_complete = None
253 | self.handler_pre = None
254 | self.viewer = None
255 | self.timer = None
256 | self.skipped_frame = None
257 | bpy.types.TMB_OT_store.store = {}
258 |
259 | def finalize(self):
260 | '''Restore project settings'''
261 |
262 | if self.animation:
263 | bpy.ops.tmb.keyconfig()
264 | bpy.ops.tmb.restore()
265 |
266 | def get_frames(self):
267 | '''Store list of frames into the self.render["frames"]'''
268 |
269 | sc = self.sc
270 | if self.animation:
271 | _start = sc.frame_start
272 | _end = sc.frame_end+1
273 | _step = sc.frame_step
274 | for fr in range(_start, _end, _step):
275 | self.frames.append(fr)
276 | else:
277 | self.frames.append(sc.frame_current)
278 |
279 | def get_subframes(self, sc):
280 | '''Calculate subframes for current frame in certain scene'''
281 |
282 | _tmb = sc.true_mb
283 | _position = _tmb.position
284 | _samples = _tmb.samples
285 | _shutter = round(_tmb.shutter/2, 3 )
286 | _frame = self.render['frame']
287 | #-------------------------start and end subframes from the TMB position:
288 | if _position == 'START':
289 | _start = _frame
290 | _end = _frame + _shutter*2
291 | elif _position == 'FRAME':
292 | _start = _frame - _shutter*2
293 | _end = _frame
294 | elif _position == 'CENTER':
295 | _start = _frame - _shutter
296 | _end = _frame + _shutter
297 | #----------------------------------------------------------storage sync:
298 | self.rlayers[sc]['subframes'] = []
299 | subframes = self.rlayers[sc]['subframes']
300 | conc_subframes = self.render['conc_subframes']
301 | #----------------------------------------------store the first subframe:
302 | subframes.append(_start)
303 | if _start not in conc_subframes:
304 | conc_subframes.append(_start)
305 | #--------------------------------------if the number of samples is even:
306 | if _samples % 2 == 0:
307 | _step = (_end - _start) / (_samples - 1 )
308 | while round( (_start + _step), 4 ) <= _end:
309 | _start += _step
310 | _sub = round(_start, 4)
311 | subframes.append(_sub)
312 | if _sub not in conc_subframes:
313 | conc_subframes.append(_sub)
314 | #---------------------------------------if the number of samples is odd:
315 | else:
316 | _step = (self.frame - _start) / (_samples // 2 )
317 | while round( (_start + _step), 4 ) <= _end:
318 | _start += _step
319 | _sub = round(_start, 4)
320 | subframes.append(_sub)
321 | if _sub not in conc_subframes:
322 | conc_subframes.append(_sub)
323 |
324 | def set_frame(self):
325 | '''
326 | Pop frame from project['frames']
327 | Get subframes of the frame for all non-Cycles scenes with active TMB
328 | Store concatenated scenes subframes lists
329 | and store result into self.render['conc_subframes']
330 | '''
331 |
332 | self.frame = self.render['frames'].pop(0)
333 | self.render['frame'] = self.frame
334 | self.render['conc_subframes'] = []
335 | for sc in list(self.scenes.keys()):
336 | if (
337 | sc.render.engine != 'CYCLES' and
338 | sc.true_mb.activate and
339 | self.rlayers[sc] and
340 | self.rlayers[sc]['rlayers']
341 | ):
342 | sc.frame_set(self.frame, subframe = 0.0)
343 | self.get_subframes(sc)
344 | self.scenes[sc]['tmb']['position'] = sc.true_mb.position
345 | self.scenes[sc]['tmb']['shutter'] = sc.true_mb.shutter
346 | self.scenes[sc]['tmb']['samples'] = sc.true_mb.samples
347 | self.scenes[sc]['tmb']['boost'] = sc.true_mb.boost
348 |
349 | else:
350 | self.rlayers[sc]['subframes']= [self.frame]
351 | self.render['conc_subframes'].sort()
352 | self.render['conc_subframes'].append(self.frame) #------for final mixing
353 |
354 | def set_subframe(self):
355 | '''
356 | Pop subframe from self.render["conc_subframes"]
357 | Set it as current project subframe
358 | '''
359 |
360 | sc = self.sc
361 | self.render['subframe'] = self.render["conc_subframes"].pop(0)
362 | if self.render['subframe'] == self.render['frame']:
363 | if not self.skipped_frame:
364 | self.render["conc_subframes"].append(self.render['subframe'])
365 | self.render['subframe'] = self.render["conc_subframes"].pop(0)
366 | self.skipped_frame = True
367 | else:
368 | self.skipped_frame = False
369 | _subfr = self.render['subframe']
370 | _fr = int(_subfr) #------------------------------------------- set frame
371 | _sbfr = round (_subfr - _fr, 3) #-------------------------- set subframe
372 | for scene in list(self.rlayers.keys()):
373 | scene.frame_set(_fr, subframe=_sbfr) # -----------set current fr/sub
374 | for sc in list(self.rlayers.keys()):
375 | if (
376 | self.scenes[sc]['engine'] == 'CYCLES' or
377 | not self.scenes[sc]['tmb'] or
378 | not self.scenes[sc]['tmb']['activate']
379 | ):
380 | continue
381 | _rlayers = self.rlayers[sc]['rlayers']
382 | for rl in list(_rlayers.keys()):
383 | for npass in list(_rlayers[rl].keys()):
384 | _fo = _rlayers[rl][npass]['file_output']
385 | _path = _rlayers[rl][npass]['path']
386 | _fo.base_path = pathlib.os.path.join(
387 | _path, str(self.render['subframe'])
388 | )
389 |
390 | def set_rlayers(self):
391 | '''
392 | Unmute TMB Render Layers if current subframe is in its scene's
393 | subframes list.
394 | '''
395 |
396 | self.images = []
397 | self.passes = []
398 | self.render['rlayers'] = []
399 | for sc in list(self.rlayers.keys()):
400 | _rlayers = self.rlayers[sc]['rlayers']
401 | for rl in list(_rlayers.keys()):
402 | _mute = False
403 | # if this is the last subframe, which is only for non-TMB layers
404 | # or current subframe is not in this RL's scene subframes list:
405 | # mute Render Layer. Or unmute otherwise.
406 | if (
407 | len(self.rlayers[sc]['subframes']) == 1 or
408 | not self.render['subframe'] in self.rlayers[sc]['subframes']
409 | ):
410 | _mute = True
411 | rl.mute = _mute
412 |
413 | if (
414 | self.scenes[sc]['engine'] == 'CYCLES' or
415 | not self.scenes[sc]['tmb'] or
416 | not self.scenes[sc]['tmb']['activate']
417 | ):
418 | continue
419 | # if scene is non-Cycles, is TMB, and there are several scenes
420 | # with different TMB settings: mute or unmute TMB subframes
421 | # File Outputs depending on mute status of Render Layer
422 | else:
423 | for npass in list(_rlayers[rl].keys()):
424 | _rlayers[rl][npass]['file_output'].mute = _mute
425 |
426 | if _mute:
427 | continue
428 |
429 | # if Render Layer is not muted:
430 | _rl_sets = self.rlayers[sc]['rlayers'][rl]
431 | for npass in rl.outputs:
432 | if (
433 | npass.enabled and
434 | npass in list(_rl_sets.keys())
435 | ):
436 | _img = _rl_sets[npass]['image']
437 | self.images.append(_img)
438 | self.passes.append(npass)
439 |
440 | def reset_images(self):
441 | '''Replace all temporary image pixels with numpy zeros arrays'''
442 |
443 | for img in self.render['images']:
444 | img.pixels[:] = np.zeros(self.project['pix_len'], dtype = 'f')[:]
445 |
446 | def timer_add(self, tick=0.01):
447 | '''Add timer event and set it as self.timer'''
448 |
449 | _wm = self.project['wm']
450 | _win = self.project['window']
451 | self.timer = _wm.event_timer_add(tick, window=_win)
452 |
453 | def timer_remove(self, vars=False):
454 | '''Remove timer event and clear self.timer'''
455 |
456 | _wm = self.project['wm']
457 | try:
458 | for _ in range(3):
459 | _wm.event_timer_remove(self.timer)
460 | except:
461 | pass
462 | self.timer = None
463 |
464 | def render_native(self):
465 | '''Native Blender render operator for instant renders'''
466 |
467 | bpy.ops.render.render(
468 | 'INVOKE_DEFAULT',
469 | animation = self.animation,
470 | write_still = self.write_still,
471 | use_viewport = self.use_viewport,
472 | layer = self.layer,
473 | scene = self.scene
474 | )
475 |
476 | def render_subframe(self):
477 | '''Main TMB subframe render operator'''
478 |
479 | _vars = bpy.types.TMB_RENDER_OT_variables
480 | _vars.rendering_subframe = True
481 | bpy.ops.render.render(
482 | 'INVOKE_DEFAULT',
483 | animation = False,
484 | write_still = False,
485 | use_viewport = self.use_viewport
486 | )
487 |
488 | def open_images(self, path):
489 | '''Open all subframe images files as images in Blender'''
490 |
491 | _fpath = pathlib.Path(path)
492 | images = []
493 | for child in _fpath.glob('*'):
494 | if child.is_file():
495 | img = bpy.data.images.load(str(child))
496 | images.append(img)
497 | else:
498 | images += self.open_images(str(child))
499 | return images
500 |
501 | def buffers_to_image(self, path, img, samples):
502 | '''Open subframe images, mix them to frame image and delete them'''
503 |
504 | _sub_images = self.open_images(path)
505 | _sub_arrays = np.zeros(
506 | (len(_sub_images), self.project['pix_len']),
507 | dtype = 'f'
508 | )
509 | for num in range(len(_sub_images)):
510 | _sub_arrays[num][:] = np.array(_sub_images[num].pixels[:])[:]
511 | _result = _sub_arrays.sum(axis=0)
512 | _divided = np.divide(_result[:], samples)
513 | img.pixels[:] = _divided[:]
514 | for img in _sub_images:
515 | bpy.data.images.remove(img)
516 |
517 | def delete_images(self, fpath):
518 | '''Delete all files in file path'''
519 |
520 | _fpath = pathlib.Path(fpath)
521 | for child in _fpath.glob('*'):
522 | if child.is_file():
523 | child.unlink()
524 | else:
525 | self.delete_images(child)
526 |
527 | def mix_buffers(self):
528 | '''Mix rendered subframes files to Blender images'''
529 | _scenes = self.rlayers
530 | for sc in list(_scenes.keys()):
531 | if (
532 | self.scenes[sc]['engine'] == 'CYCLES' or
533 | not self.scenes[sc]['tmb'] or
534 | not self.scenes[sc]['tmb']['activate']
535 | ):
536 | continue
537 | _rlayers = _scenes[sc]['rlayers']
538 | for rl in list(_rlayers.keys()):
539 | for npass in list(_rlayers[rl].keys()):
540 | _path = _rlayers[rl][npass]['path']
541 | _image = _rlayers[rl][npass]['image']
542 | _samples = self.scenes[rl.scene]['tmb']['samples']
543 | self.buffers_to_image(_path, _image, _samples)
544 | self.delete_images(_path)
545 |
546 | def img_to_path(self):
547 | '''Move images from temp. File Output folder to scene render folder'''
548 |
549 | _dest = self.project['render_path'] #-----------------destination folder
550 | _tmp = self.project['path'] #---------------------------subframes folder
551 | _source = pathlib.os.path.join(_tmp, '_TMB_Output') #-------source folder
552 | _spath = pathlib.Path(_source)
553 | if not _spath.is_dir():
554 | for child in pathlib.Path(_tmp).glob('*'):
555 | if child.is_file() and '_TMB_Output' in str(child):
556 | _new_name = str(child).replace( '_TMB_Output',
557 | self.project['base_name'])
558 | child.rename(_new_name)
559 | shutil.move(_new_name, _dest)
560 | return
561 | for child in _spath.glob('*'):
562 | if child.is_file():
563 | _new_name = str(child).replace( 'Image',
564 | self.project['base_name'])
565 | _check_path = _new_name.replace(str(_source), str(_dest))
566 | if pathlib.Path(_check_path).is_file():
567 | pathlib.Path(_check_path).unlink()
568 | child.rename(_new_name)
569 | shutil.move(_new_name, _dest)
570 |
571 | def save_frame_prepare(self):
572 | '''Prepare project for frame saving'''
573 |
574 | _links = self.sc.node_tree.links
575 | #------------------------- mute all TMB render layers, unmute all others
576 | for rl in self.project['rlayers']:
577 | if (
578 | self.scenes[rl.scene]['tmb'] and
579 | self.scenes[rl.scene]['tmb']['activate']
580 | ):
581 | rl.mute = True
582 | else:
583 | rl.mute = False
584 | #--------------------------------------------- mute all TMB File Outputs
585 | for fo in self.restore['tmb_f_outs']:
586 | fo.mute = True
587 | #-------- relink all TMB images directly to TMB mix nodes children-links
588 | if self.restore['mix_nodes']:
589 | for node in self.restore['mix_nodes']:
590 | for lnk in node.outputs[0].links:
591 | _links.new( node.inputs[2].links[0].from_socket,
592 | lnk.to_socket)
593 | #------------------------------------------ unmute all user File Outputs
594 | if self.project['has_f_outs']:
595 | for fo in self.restore['file_outputs']:
596 | fo.mute = False
597 | #------------------------------- change render_copmlete handler function
598 | while self.handler_complete in bpy.app.handlers.render_complete:
599 | bpy.app.handlers.render_complete.remove(self.handler_complete)
600 | bpy.app.handlers.render_complete.append(self.handler_final)
601 | #----------------------------------------- set current frame as subframe
602 | _frame = self.render['conc_subframes'].pop(0)
603 | self.sc.frame_set(_frame, subframe = 0.0)
604 | #--------------------------- if render animation unmute main File Output
605 | if self.animation:
606 | self.project['output'].mute = False
607 | #------------------------------------------- update preview just in case
608 | bpy.ops.tmb.update('INVOKE_DEFAULT')
609 |
610 | def save_frame_restore(self):
611 | '''Restore project from frame saving'''
612 |
613 | _links = self.sc.node_tree.links
614 | self.sc.render.filepath = self.project['path']
615 | #------------------------------------------------- mute main File Output
616 | self.project['output'].mute = True
617 | #------------------------------------------------ mute user File Outputs
618 | if self.project['has_f_outs']:
619 | for fo in self.restore['file_outputs']:
620 | fo.mute = True
621 | #----------------- relink back all TMB images and mix (Alpha Over) nodes
622 | if self.restore['mix_nodes']:
623 | for node in self.restore['mix_nodes']:
624 | for lnk in node.inputs[2].links[0].from_socket.links:
625 | if lnk.to_node == node:
626 | continue
627 | _links.new(node.outputs[0],lnk.to_socket)
628 | #---------------------------------------- umute all active Render Layers
629 | for rl in self.project['rlayers']:
630 | rl.mute = False
631 | #-------------------------------------------- umute all TMB File Outputs
632 | for fo in self.restore['tmb_f_outs']:
633 | fo.mute = False
634 | #------------------------------- change render_copmlete handler function
635 | while self.handler_final in bpy.app.handlers.render_complete:
636 | bpy.app.handlers.render_complete.remove(self.handler_final)
637 | bpy.app.handlers.render_complete.append(self.handler_complete)
638 | #---------------- move rendered image file from main File Outputs folder
639 | #--------------------------------- to render folder (for animation only)
640 | self.img_to_path()
641 | #------------------------------------------- update preview just in case
642 | bpy.ops.tmb.update('INVOKE_DEFAULT')
643 |
644 | def instant(self, context):
645 | '''Check cases when it is possible to start native render instantly'''
646 |
647 | sc = context.scene
648 | _instant = False
649 | #----------------------------------------- if scene is Cycles or non-TMB
650 | if sc.render.engine == 'CYCLES' or not sc.true_mb.activate:
651 | #--------- if there's no node tree or no nodes or nodes are not used
652 | if not sc.node_tree or not sc.node_tree.nodes or not sc.use_nodes:
653 | _instant = True
654 | #---------------------- if there's node tree but all nodes are muted
655 | elif (
656 | sc.node_tree.nodes and
657 | not [node for node in sc.node_tree.nodes if not node.mute]
658 | ):
659 | _instant = True
660 | #--------------------------- if there's node tree and nodes are used
661 | elif sc.node_tree.nodes and sc.use_nodes:
662 | _instant = True
663 | #---------------- except there is active TMB scene in Compositor
664 | for node in sc.node_tree.nodes:
665 | if (
666 | node.type == 'R_LAYERS' and
667 | not node.mute and
668 | node.scene.render.engine != 'CYCLES' and
669 | node.scene.true_mb.activate
670 | ):
671 | _instant = False
672 | return _instant
673 | return _instant
674 |
675 | def instant_prepare(self, context):
676 | '''Prepare project for instant native render'''
677 |
678 | sc = context.scene
679 | if not sc.node_tree or not sc.node_tree.nodes:
680 | return
681 | #------------------------------- Turn off all TMB Mix (Alpha Over) nodes
682 | for node in sc.node_tree.nodes:
683 | if node.name.startswith('TMB_Mix') and node.type == 'ALPHAOVER':
684 | node.inputs[0].default_value = 0
685 |
686 | #################################### RENDER ####################################
687 | class TMB_Render(TMB_RenderHelpers, bpy.types.Operator):
688 | bl_idname = 'tmb_render.render'
689 | bl_label = 'Render'
690 | bl_description = 'Render active scene'
691 | animation : BoolProperty(
692 | name="Animation",
693 | description="Render Animation",
694 | default=False,
695 | )
696 | write_still : BoolProperty(
697 | name="Write Still",
698 | description="Write Still",
699 | default=False,
700 | )
701 | use_viewport : BoolProperty(
702 | name="Use viewport",
703 | description="Use Viewport",
704 | default=False,
705 | )
706 | layer : StringProperty(
707 | name="Render Layer",
708 | description="Render Layer",
709 | default="",
710 | )
711 | scene : StringProperty(
712 | name="Scene",
713 | description="Scene",
714 | default="",
715 | )
716 |
717 | def __init__(self):
718 | self.t1 = perf_counter()
719 |
720 | def __del__(self):
721 | #------------------------------ clear render handlers from TMB functions
722 | while self.handler_complete in bpy.app.handlers.render_complete:
723 | bpy.app.handlers.render_complete.remove(self.handler_complete)
724 | while self.handler_pre in bpy.app.handlers.render_pre:
725 | bpy.app.handlers.render_pre.remove(self.handler_pre)
726 | while self.handler_final in bpy.app.handlers.render_complete:
727 | bpy.app.handlers.render_complete.remove(self.handler_final)
728 | #--------------------------------------- reset TMB_Render Operator class
729 | self.cleanup()
730 | #------------------------------------ make report with total render time
731 | self.t2 = perf_counter()
732 | try:
733 | _total_time = str(datetime.timedelta(seconds=(self.t2-self.t1)))
734 | _msg = f'Total Render Time: {_total_time[:-3]}'
735 | bpy.ops.tmb.warning('INVOKE_DEFAULT', type = "INFO", msg = _msg)
736 | except:
737 | pass
738 |
739 | def execute(self, context):
740 | sc = context.scene
741 | if self.instant(context):
742 | self.instant_prepare(context)
743 | self.render_native()
744 | self.cleanup()
745 | return {'FINISHED'}
746 | self.cleanup()
747 | if sc.render.image_settings.file_format in (
748 | 'AVI_JPEG', 'AVI_RAW', 'FFMPEG'):
749 | _msg = "Sorry!\nTrue Motion Blur currently doesn't support render\
750 | in\n\"AVI JPEG\", \"AVI Raw\" and \"FFmpeg video\" File Formats.\n\nPlease\
751 | change Output File Format in\nOutput Properties -> Output\n\nTip: If you need\
752 | to render animation, you may render image sequences."
753 | bpy.ops.tmb.warning('INVOKE_DEFAULT', type = 'ERROR', msg=_msg)
754 | return {'CANCELLED'}
755 | if not [obj for obj in sc.objects if obj.type == 'CAMERA']:
756 | _msg = f'Error: No camera found in scene "{sc.name}"'
757 | bpy.ops.tmb.warning('INVOKE_DEFAULT', type = 'ERROR', msg=_msg)
758 | return {'CANCELLED'}
759 | bpy.ops.tmb.setup(animation=self.animation)
760 | self.structure()
761 | if not self.project['composite'].inputs[0].links:
762 | bpy.ops.tmb.restore()
763 | _nt = self.sc.node_tree
764 | if '_TMB_Output' in _nt.nodes:
765 | _nt.nodes.remove(self.sc.node_tree.nodes['_TMB_Output'])
766 | _msg = "No Composite output. Can't render"
767 | bpy.ops.tmb.warning('INVOKE_DEFAULT', type = 'ERROR', msg=_msg)
768 | return {'FINISHED'}
769 | if not [rl for rl in self.project['rlayers'] if rl.scene.view_layers[rl.layer].use]:
770 | bpy.ops.tmb.restore()
771 | _nt = self.sc.node_tree
772 | if '_TMB_Output' in _nt.nodes:
773 | _nt.nodes.remove(self.sc.node_tree.nodes['_TMB_Output'])
774 | _msg = "All render layers are disabled for rendering"
775 | bpy.ops.tmb.warning('INVOKE_DEFAULT', type = 'ERROR', msg=_msg)
776 | return {'FINISHED'}
777 | self.get_frames()
778 | bpy.app.handlers.render_complete.append(self.handler_complete)
779 | bpy.app.handlers.render_pre.append(self.handler_pre)
780 | self.timer_add()
781 | self.project['wm'].modal_handler_add(self)
782 | return {"RUNNING_MODAL"}
783 |
784 | def modal(self, context, event):
785 |
786 | #------------------------------------------- if aborted by pressing ESC:
787 | if event.type == 'ESC':
788 | self.timer_remove()
789 | self.finalize()
790 | return {'CANCELLED'}
791 | #-------------------------------------------------- on each Timer event:
792 | elif event.type == 'TIMER':
793 | self.timer_remove() #---------------------------------- remove Timer
794 | _vars = bpy.types.TMB_RENDER_OT_variables
795 |
796 | #---- if subframe rendering haven't started after the first command:
797 | if _vars.rendering_subframe:
798 | if not _vars.started:
799 | self.render_subframe()
800 |
801 | #---------------------------------------- if frame is not rendering:
802 | elif not self.rendering_frame:
803 | #------------------------ if there are no more frames to render:
804 | if not self.frames:
805 | self.finalize()
806 | return {'FINISHED'}
807 | #--------------------- otherwise reset TMB images, set frame and
808 | #------------------------ command to start rendering algorithms:
809 | else:
810 | self.reset_images()
811 | self.set_frame()
812 | self.rendering_frame = True
813 |
814 | #--------------- if there's only one subframe (which is frame) left:
815 | elif len(self.render['conc_subframes']) == 1:
816 | #----------------- check if subframe rendering status is active:
817 | if not _vars.rendering_subframe:
818 | _vars.rendering_subframe = True
819 | #---------------------- mix subframes to images, prepare saving,
820 | #--------------------------------------- and render mixed frame:
821 | self.mix_buffers()
822 | self.save_frame_prepare()
823 | bpy.ops.render.render(
824 | 'INVOKE_DEFAULT',
825 | animation = False,
826 | write_still = False)
827 |
828 | #------- if render haven't started after previous command, force it:
829 | elif (
830 | not self.render['conc_subframes'] and
831 | not _vars.final_completed and
832 | not _vars.started
833 | ):
834 | self.render_subframe()
835 |
836 | #--------------------- if final subframe (frame) render is completed
837 | #----------------------------- set all command statuses as inactive:
838 | elif (
839 | not self.render['conc_subframes'] and
840 | _vars.final_completed
841 | ):
842 | _vars.final_completed = False
843 | self.save_frame_restore()
844 | _vars.rendering_subframe = False
845 | self.rendering_frame = False
846 | #----------------- if subframe rendering is inactive, make it active
847 | #---------------- setup subframe and render layers and start render:
848 | elif not _vars.rendering_subframe:
849 | _vars.rendering_subframe = True
850 | self.set_subframe()
851 | self.set_rlayers()
852 | self.render_subframe()
853 | self.timer_add()#------------------------------------ add Timer back
854 | return {'PASS_THROUGH'}
855 |
856 | def invoke(self, context, event):
857 | return self.execute(context)
858 |
859 | #--------------------------- For test purposes only ----------------------------
860 |
861 | classes = [
862 | TMB_UpdatePreview,
863 | TMB_RenderVariables,
864 | TMB_RenderHelpers,
865 | TMB_Render,
866 | ]
867 |
868 | def render_register():
869 | for cl in classes:
870 | register_class(cl)
871 |
872 | def render_unregister():
873 | for cl in classes:
874 | unregister_class(cl)
875 |
876 | if __name__ == '__main__':
877 | render_register()
878 |
--------------------------------------------------------------------------------
/tmb_support.py:
--------------------------------------------------------------------------------
1 | # ##### BEGIN GPL LICENSE BLOCK #####
2 | #
3 | # This program is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU General Public License
5 | # as published by the Free Software Foundation; either version 2
6 | # of the License, or (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software Foundation,
15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 | #
17 | # ##### END GPL LICENSE BLOCK #####
18 |
19 | # True Motion Blur add-on
20 | # TMB render support operators
21 | # (c) 2020 Andrey Sokolov (so_records)
22 |
23 | import bpy, pathlib, shutil
24 | import numpy as np
25 | from bpy.utils import register_class, unregister_class
26 | from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty
27 |
28 | #------------------------------ Warning Operator -------------------------------
29 |
30 | class TMB_Warning(bpy.types.Operator):
31 | '''Warning!'''
32 | bl_idname = "tmb.warning"
33 | bl_label = "Warning!"
34 | type: StringProperty()
35 | msg : StringProperty()
36 |
37 | @classmethod
38 | def poll(cls, context):
39 | return True
40 |
41 | def execute(self, context):
42 | return {'FINISHED'}
43 |
44 | def modal(self, context, event):
45 | if event:
46 | self.report({self.type}, self.msg)
47 | return {'FINISHED'}
48 |
49 | def invoke(self, context, event):
50 | context.window_manager.modal_handler_add(self)
51 | return {'RUNNING_MODAL'}
52 |
53 | ############################## MAIN PROJECT STORAGE ############################
54 |
55 | class TMB_Store(bpy.types.Operator):
56 | '''
57 | MAIN STORAGE
58 | Clear storage
59 | dict store {
60 | "Project": { #-------------------------------------Project constant info
61 | "context" : context
62 | "main_sc" : start scene blender path,
63 | "frame" : self.scene.frame_current,
64 | "wm" : context.window_manager,
65 | "window" : context.window,
66 | "render_path": project Render Output path
67 | "path" : directory path for creating temporary folders to save subframes
68 | and animation render results to,
69 | "format" : file format,
70 | "rlayers" : [ list of Render Layers nodes ],
71 | "composite" : composite output
72 | "output" : main file output
73 | "scenes" : [ list of scenes used for render ],
74 | "res_x" : x resolution
75 | "res_y" : y resolution
76 | "res_prc" : resolution percentage
77 | "pix_len" : int number of pixels per frame,
78 | "single" : True if main_sc.render.use_single_layer,
79 | "render_passes" : true_mb.render_passes,
80 | "has_f_outs" : True if compositor has active file outputs
81 | "links" : [ all active used Render Layers outputs ]
82 | "image_settings" : {
83 | "path" : _prj["main_sc"].render.filepath,
84 | "file_format" : _imgsets.file_format,
85 | "cineon_black" : _imgsets.cineon_black,
86 | "cineon_gamma" : _imgsets.cineon_gamma,
87 | "cineon_white" : _imgsets.cineon_white,
88 | "color_depth" : _imgsets.color_depth,
89 | "color_mode" : _imgsets.color_mode,
90 | "compression" : _imgsets.compression,
91 | "exr_codec" : _imgsets.exr_codec,
92 | "jpeg2k_codec" : _imgsets.jpeg2k_codec,
93 | "quality" : _imgsets.quality,
94 | "tiff_codec" : _imgsets.tiff_codec,
95 | "use_cineon_log" : _imgsets.use_cineon_log,
96 | "use_jpeg2k_cinema_48" : _imgsets.use_jpeg2k_cinema_48,
97 | "use_jpeg2k_cinema_preset" : _imgsets.use_jpeg2k_cinema_preset,
98 | "use_jpeg2k_ycc" : _imgsets.use_jpeg2k_ycc,
99 | "use_preview" : _imgsets.use_preview,
100 | "use_zbuffer" : _imgsets.use_zbuffer,
101 | "views_format" : _imgsets.views_format,
102 | }
103 | },
104 | "Scenes": { #----------------------Settings of the scenes used in render
105 | Scene 1: {
106 | "is_main" : True if scene is main scene,
107 | "engine" : sc.render.engine,
108 | "mb" : sc.eevee.use_motion_blur,*
109 | "samples" : sc.eevee.taa_render_samples.*
110 | "tmb" : { **
111 | "activate" : true_mb.activate
112 | "position" : true_mb.position
113 | "shutter" : true_mb.shutter
114 | "samples" : true_mb.samples
115 | "boost" : true_mb.boost
116 | },
117 | ---------------------------
118 | * for Eevee TMB scenes
119 | ** for non-Cycles TMB scenes
120 | },
121 | Scene 2: {...{...},{...}...},
122 | },
123 | "Rlayers": { #------------------------------Render Layers used in render
124 | Scene 1 : {
125 | "rlayers" : {
126 | RLayer1 : {
127 | Pass1 : {
128 | "index" : output index,
129 | "links" : boolean,
130 | "image" : pass image
131 | "img_node" : pass image node,
132 | "mix_node" : pass mix node,
133 | "array" : ndarray for all subframes pixels
134 | "file_output" : save buffer file output node
135 | "path" : temporary save buffers directory
136 | },
137 | Pass2 : {...},
138 | .....},
139 | Rlayer2 : {...{...},{...},{...}},
140 | .....},
141 | "subframes" : [ list of subframes for current frame ]
142 | Scene 2 : {...{...},{...},{...}},
143 | .....},
144 | "Render": { #--------------------------------------Temporary render data
145 | "images" : [ tmb_images ],
146 | "frames" : [ frames to render ],
147 | "frame" : context frame,
148 | "conc_subframes" : [concatenated subframes from all scenes],
149 | "subframe" : context subframe,
150 | "rlayers" : [ not muted rlayers for current subframe ],
151 | "scene" : current rlayer scene,
152 | "rlayer" : current rlayer,
153 | "image" : current image,
154 | "npass" : current npass,
155 | "img_node" : if not render passes - TMB Image node,
156 | "mix_node" : if not render passes - TMB Mix node,
157 | "file_output" : if not render passes - file output,
158 | "path" : if not render passes - file output path
159 | }
160 | "Restore": { #--------------------------------Data to process in the end
161 | "muted" : [ muted nodes to unmute when script is finished ],
162 | "deleted" : [ deleted viewers to recreate ],
163 | "tmb_nodes" : [ created nodes to delete when script is finished ],
164 | "main_dir" : main temporary directory (to delete),
165 | "mix_nodes" : [ list of TMB mix nodes ]
166 | "folders" : [ temporary folders created by script in main_dir ],
167 | "area" : { area : type }
168 | "file_outputs" : [ list of user file outputs ]
169 | "tmb_f_outs" : [ list of tmb file outputs ]
170 | "viewers" : {
171 | V1.name : {
172 | "center_x" : node.center_x,
173 | "center_y" : node.center_y,
174 | "color" : node.color,
175 | "height" : node.height,
176 | "hide" : node.hide,
177 | "label" : node.label,
178 | "location" : node.location,
179 | "mute" : node.mute,
180 | "name" : node.name,
181 | "show_options" : node.show_options,
182 | "show_preview" : node.show_preview,
183 | "show_texture" : node.show_texture,
184 | "use_alpha" : node.use_alpha,
185 | "use_custom_color" : node.use_custom_color,
186 | "width" : node.width,
187 | "width_hidden" : node.width_hidden,
188 | "inputs" : {
189 | In 1.name : {
190 | "type" : input.type,
191 | "name" : input.name,
192 | "identifier" : input.identifier,
193 | "enabled" : input.enabled,
194 | "hide" : input.hide,
195 | "links" : input.links[0].from_socket
196 | },
197 | In2.name : {....},
198 | ...}
199 | ...},
200 | V2.name : {...},
201 | ...},
202 | },
203 | }
204 | Set context scene as "main_sc"
205 | '''
206 |
207 | bl_idname = "tmb.store"
208 | bl_label = "Storage"
209 | bl_description = "Project storage"
210 | animation : BoolProperty(
211 | name="Animation",
212 | description="Render Animation",
213 | default=False,
214 | )
215 | op = None
216 | store = {}
217 | scene = None
218 | enable = False
219 |
220 | def structure(self):
221 | '''Setup main storage common structure'''
222 |
223 | op = self.op
224 | op.store = {}
225 | op.store["Project"] = {}
226 | op.store["RLayers"] = {}
227 | op.store["Scenes"] = {}
228 | op.store["Render"] = {}
229 | _render = op.store["Render"]
230 | _render["images"] = []
231 | _render["frames"] = []
232 | _render["frame"] = None
233 | _render["rlayers"] = []
234 | _render["conc_subframes"] = []
235 | _render["subframe"] = None
236 | _render["file_output"] = None
237 | op.store["Restore"] = {}
238 | _restore = op.store["Restore"]
239 | _restore["muted"] = []
240 | _restore["file_outputs"] = []
241 | _restore["tmb_f_outs"] = []
242 | _restore["viewers"] = {}
243 | _restore["tmb_nodes"] = []
244 | _restore["main_dir"] = None
245 | _restore["mix_nodes"] = []
246 | _restore["folders"] = []
247 | _restore["area"] = {}
248 |
249 | def single(self):
250 | '''
251 | Get and set or create basic Compositor node setup:
252 | Single Render Layers and Composite nodes.
253 | Make sure they are connected.
254 | '''
255 |
256 | _scene = self.scene
257 | _nodes = _scene.node_tree.nodes
258 | _links = _scene.node_tree.links
259 | _rl_name = 'TMB Render Layers'
260 | _c_name = 'TMB Composite'
261 | _rl_node = (
262 | _nodes.new(type='CompositorNodeRLayers')
263 | if _rl_name not in _nodes else
264 | _nodes[_rl_name]
265 | )
266 | _c_node = (
267 | _nodes.new(type='CompositorNodeComposite')
268 | if _c_name not in _nodes else
269 | _nodes[_c_name]
270 | )
271 | self.store['Project']["composite"] = _c_node
272 | _rl_node.mute = False
273 | _c_node.mute = False
274 | _rl_node.name = _rl_name
275 | _c_node.name = _c_name
276 | self.store['Restore']['tmb_nodes'].append(_rl_node)
277 | self.store['Restore']['tmb_nodes'].append(_c_node)
278 | _links.new( _rl_node.outputs[0], _c_node.inputs[0] )
279 | _c_node.location.x = _rl_node.location.x + 500
280 | _c_node.location.y = _rl_node.location.y
281 |
282 | def mute_all(self):
283 | '''Mute all unmuted nodes in the main scene's Compositor'''
284 |
285 | nodes = self.scene.node_tree.nodes
286 | for node in nodes:
287 | if not node.mute:
288 | node.mute = True
289 | if node.type == 'COMPOSITE':
290 | self.store['Restore']['muted'].append(node)
291 |
292 | def get_rlayers_and_scenes(self):
293 | '''
294 | Return:
295 | a list of all unmuted Render Layers in the main scene's Compositor
296 | a list of all scenes used in the main scene's Compositor
297 | '''
298 |
299 | sc = self.scene
300 | _scenes = [sc,]
301 | _rlayers = []
302 | if not sc.node_tree:
303 | sc.use_nodes = True
304 | elif not sc.node_tree.nodes:
305 | sc.use_nodes = True
306 | self.single()
307 | elif not sc.use_nodes and sc.node_tree.nodes:
308 | sc.use_nodes = True
309 | self.mute_all()
310 | self.single()
311 | _nodes = sc.node_tree.nodes
312 | for node in _nodes:
313 | if (
314 | node.type == 'R_LAYERS' and
315 | not node.mute and
316 | node.scene.view_layers[node.layer].use
317 | ):
318 | _rlayers.append(node)
319 | if node.scene not in _scenes:
320 | _scenes.append(node.scene)
321 | return _rlayers, _scenes
322 |
323 | def get_pixels_len(self):
324 | '''Return the main scene's number of pixels'''
325 |
326 | op = self.op
327 | sc = self.scene
328 | _prj = op.store['Project']
329 | _prj["res_x"] = sc.render.resolution_x
330 | _prj["res_y"] = sc.render.resolution_y
331 | _prj["res_prc"] = sc.render.resolution_percentage
332 | _resX = _prj['res_x']
333 | _resY = _prj['res_y']
334 | _resPrc = _prj['res_prc']
335 | _pixels = (int(_resX*(_resPrc/100)) * int(_resY*(_resPrc/100)) * 4)
336 | return _pixels
337 |
338 | def get_render_passes(self):
339 | '''
340 | Return True if there is at least one non-Cycles scene
341 | in the main scene's Compositor setup
342 | with enabled TMB and Render Passes options
343 | '''
344 |
345 | op = self.op
346 | sc = self.scene
347 | _proj = op.store['Project']
348 | _render_passes = False
349 | if sc.render.engine != 'CYCLES' and sc.true_mb.activate:
350 | _render_passes = sc.true_mb.render_passes
351 | elif (
352 | len(_proj['rlayers']) > 1 and
353 | len(_proj['scenes']) > 1
354 | ):
355 | for scene in _proj['scenes']:
356 | if (
357 | scene.render.engine != 'CYCLES' and
358 | scene.true_mb.activate and
359 | not _render_passes
360 | ):
361 | _render_passes = sc.true_mb.render_passes
362 | return _render_passes
363 |
364 | def get_composite(self):
365 | '''
366 | Find the main scene Compositor's Composite node.
367 | Return the first one found or None if not found at all.
368 | '''
369 |
370 | sc = self.scene
371 | _composite = None
372 | if sc.node_tree and sc.node_tree.nodes and sc.use_nodes:
373 | nodes = sc.node_tree.nodes
374 | for node in nodes:
375 | if node.type == 'COMPOSITE' and not node.mute:
376 | _composite = node
377 | break
378 | return _composite
379 |
380 | def project(self, context):
381 | '''
382 | Setup the main storage "Project" dictionary.
383 | Contains the common information about the whole project.
384 | '''
385 |
386 | op = self.op
387 | _prj = op.store['Project']
388 | _prj["context"] = {
389 | "window" : context.window,
390 | "screen" : context.screen,
391 | "area" : context.area,
392 | "region" : context.region,
393 | }
394 | _prj["main_sc"] = context.scene
395 | self.scene = _prj['main_sc']
396 | _prj["frame"] = self.scene.frame_current
397 | _prj["wm"] = context.window_manager
398 | _prj["window"] = context.window
399 | _prj["user_path"] = self.scene.render.filepath
400 | _prj["base_name"] = bpy.path.display_name_from_filepath(
401 | _prj["user_path"]
402 | )
403 | if not _prj["base_name"]:
404 | _prj["render_path"] = bpy.path.abspath(_prj["user_path"])
405 | else:
406 | _prj["render_path"] = bpy.path.abspath(
407 | str(_prj["user_path"]).replace(
408 | _prj["base_name"], ""
409 | )
410 | )
411 | _prj["path"] = bpy.path.abspath(
412 | context.preferences.filepaths.temporary_directory)
413 | _prj["format"] = self.scene.render.image_settings.file_format
414 | _rlscenes = self.get_rlayers_and_scenes()
415 | _prj["rlayers"] = _rlscenes[0]
416 | _prj["composite"] = self.get_composite()
417 | _prj["output"] = None
418 | _prj["scenes"] = _rlscenes[1]
419 | _prj["pix_len"] = self.get_pixels_len()
420 | _prj["single"] = context.scene.render.use_single_layer
421 | _prj["render_passes"]= self.get_render_passes()
422 | _prj["has_f_outs"] = False
423 | _prj["links"] = []
424 | _imgsets = _prj["main_sc"].render.image_settings
425 | _prj["image_settings"] = {
426 | "path" : _prj["main_sc"].render.filepath,
427 | "file_format" : _imgsets.file_format,
428 | "cineon_black" : _imgsets.cineon_black,
429 | "cineon_gamma" : _imgsets.cineon_gamma,
430 | "cineon_white" : _imgsets.cineon_white,
431 | "color_depth" : _imgsets.color_depth,
432 | "color_mode" : _imgsets.color_mode,
433 | "compression" : _imgsets.compression,
434 | "exr_codec" : _imgsets.exr_codec,
435 | "jpeg2k_codec" : _imgsets.jpeg2k_codec,
436 | "quality" : _imgsets.quality,
437 | "tiff_codec" : _imgsets.tiff_codec,
438 | "use_cineon_log" : _imgsets.use_cineon_log,
439 | "use_jpeg2k_cinema_48" : _imgsets.use_jpeg2k_cinema_48,
440 | "use_jpeg2k_cinema_preset" : _imgsets.use_jpeg2k_cinema_preset,
441 | "use_jpeg2k_ycc" : _imgsets.use_jpeg2k_ycc,
442 | "use_preview" : _imgsets.use_preview,
443 | "use_zbuffer" : _imgsets.use_zbuffer,
444 | "views_format" : _imgsets.views_format,
445 | }
446 |
447 | def scenes(self):
448 | '''
449 | Setup the main storage "Scenes" dictionary.
450 | Contains necessary information about the settings of all scenes
451 | used in the main scene's Compositor.
452 | '''
453 |
454 | op = self.op
455 | for sc in op.store['Project']['scenes']:
456 | op.store['Scenes'][sc] = {}
457 | _sets = op.store['Scenes'][sc]
458 | _sets["is_main"] = True if sc is self.scene else False
459 | _sets["engine"] = sc.render.engine
460 | if _sets['engine'] == 'BLENDER_EEVEE' and sc.true_mb.activate:
461 | _sets["samples"] = sc.eevee.taa_render_samples
462 | _sets["mb"] = sc.eevee.use_motion_blur
463 | if _sets['engine'] != 'CYCLES' and sc.true_mb.activate:
464 | _sets["tmb"] = {
465 | "activate" : sc.true_mb.activate,
466 | "position" : sc.true_mb.position,
467 | "shutter" : sc.true_mb.shutter,
468 | "samples" : sc.true_mb.samples,
469 | "boost" : sc.true_mb.boost
470 | }
471 | else:
472 | _sets["tmb"] = False
473 |
474 | def execute(self, context):
475 | self.op = bpy.types.TMB_OT_store
476 | self.structure()
477 | self.project(context)
478 | self.scenes()
479 | return {'FINISHED'}
480 |
481 | ################################### PREPARE ####################################
482 |
483 | #------------------------------- Check Passes ----------------------------------
484 |
485 | class TMB_Helpers():
486 | '''Helper methods for multiple scenes render without TMB render passes'''
487 |
488 | def npass_used(self, npass, node_type = 'COMPOSITE'):
489 | '''
490 | Check if npass's links lead to an active Composite or File Output node
491 | npass == node pass (node output in other words)
492 | Return True or False
493 | '''
494 |
495 | if not len(npass.links):
496 | return False
497 | else:
498 | for lnk in npass.links:
499 | _child = lnk.to_node
500 | _used = False
501 | if _child.type == 'COMPOSITE' and not _child.mute:
502 | return True
503 | elif (
504 | _child.type == node_type == 'OUTPUT_FILE' and
505 | _child in list(self.restore['file_outputs'].keys())
506 | ):
507 | return True
508 | # there's no need to check if outputs don't exist at all
509 | # because in this case they are represented as an empty list,
510 | # not None
511 | for out in _child.outputs:
512 | _used = self.npass_used(out, node_type=node_type)
513 | if not _used:
514 | continue
515 | return _used
516 |
517 | def clear_path(self, fpath):
518 | '''Remove directory and all its content'''
519 |
520 | _fpath = pathlib.Path(fpath)
521 | for child in _fpath.glob('*'):
522 | if child.is_file():
523 | child.unlink()
524 | else:
525 | self.clear_path(child)
526 | _fpath.rmdir()
527 |
528 | #-------------------------------- Get Rlayers ----------------------------------
529 |
530 | class TMB_RLayers(TMB_Helpers, bpy.types.Operator):
531 | '''
532 | Collect main scene Compositor's Render Layers nodes' passes info
533 | or create Render Layers from scratch if none exists
534 | '''
535 |
536 | bl_idname = 'tmb.rlayers'
537 | bl_label = 'Get Render Layers'
538 | store = None
539 | project = None
540 | scenes = None
541 | restore = None
542 | scene = None
543 | advanced = None
544 | render_passes = None
545 |
546 | def structure(self):
547 | '''Sync to the main storage'''
548 |
549 | self.store = bpy.types.TMB_OT_store.store
550 | self.project = self.store['Project']
551 | self.scenes = self.store['Scenes']
552 | self.restore = self.store['Restore']
553 | self.scene = self.project['main_sc']
554 | self.render_passes = self.project['render_passes']
555 |
556 | def get_layers_passes(self):
557 | '''Collect passes info for all main scene Compositor's Render Layers'''
558 |
559 | _rlayers = {}
560 | for rl in self.project['rlayers']:
561 | _rlayers[rl] = {}
562 | for out in range(len(rl.outputs)):
563 | if (
564 | not self.render_passes and
565 | not self.project['image_settings']['file_format'] ==
566 | 'OPEN_EXR_MULTILAYER' and
567 | not self.npass_used(rl.outputs[out])
568 | ):
569 | continue
570 | npass = rl.outputs[out]
571 | if npass.enabled:
572 | _rlayers[rl][npass] = {}
573 | _rlayers[rl][npass]["index"] = out
574 | _rlayers[rl][npass]["links"] = (
575 | True if npass.links else False
576 | )
577 | return _rlayers
578 |
579 | def get_scenes_rlayers(self):
580 | '''
581 | Collect main scene Compositor's Render Layers passes info for each scene
582 | used in the main scene Compositor's Render Layers'''
583 |
584 | _rlayers = self.get_layers_passes()
585 | _rdict = {}
586 | for sc in self.project['scenes']:
587 | _rdict[sc] = {}
588 | _rdict[sc]["rlayers"] = {}
589 | _rdict[sc]["subframes"] = []
590 | for rl in list(_rlayers.keys()):
591 | if rl.scene == sc:
592 | _rdict[sc]["rlayers"][rl] = _rlayers[rl]
593 | return _rdict
594 |
595 | def rlayers(self):
596 | '''
597 | Store main scene Compositor's Render Layers info into the main storage:
598 | RL Scene 1 : {
599 | "subframes" : [],
600 | "rlayers" : {
601 | RL 1 : {
602 | Pass 1 : {
603 | "index" : Pass 1 index,
604 | "links" : True if Pass 1 has links,
605 | },
606 | Pass 2: {...},
607 | },
608 | RL 2 : { "passes" : {{...},{...},{...}}
609 | },
610 | },
611 | RL Scene 2 : {... ...},
612 | '''
613 | self.store['RLayers'] = self.get_scenes_rlayers()
614 |
615 | def execute(self, context):
616 | self.structure()
617 | self.rlayers()
618 | return {'FINISHED'}
619 |
620 | #----------------- Mute user File Outputs and delete Viewers -------------------
621 |
622 | class TMB_UserOutputs(bpy.types.Operator):
623 | '''Mute file outputs and remove viewers'''
624 |
625 | bl_idname = "tmb.userouts"
626 | bl_label = "User Outputs"
627 | store = None
628 | project = None
629 | restore = None
630 | rlayers = None
631 | f_outputs = None
632 | viewers = None
633 | scene = None
634 | rl_links = []
635 |
636 | def structure(self):
637 | '''Sync to the main storage'''
638 |
639 | self.store = bpy.types.TMB_OT_store.store
640 | self.project = self.store['Project']
641 | self.restore = self.store['Restore']
642 | self.rlayers = self.store['RLayers']
643 | self.f_outputs = self.restore['file_outputs']
644 | self.viewers = self.restore['viewers']
645 | self.scene = self.project['main_sc']
646 | self.rl_links = []
647 |
648 | def has_links(self, node):
649 | '''Return False if there's no input links to node, True otherwise'''
650 |
651 | _links = False
652 | for input in node.inputs:
653 | if input.links:
654 | _links = True
655 | break
656 | return _links
657 |
658 | def links_from_rl(self, input):
659 | '''Append to "self.rl_links" any links to Render Layer nodes' outputs'''
660 |
661 | if not input.links:
662 | return False
663 | elif (
664 | input.links[0].from_node.type == 'R_LAYERS' and
665 | not input.links[0].from_node.mute
666 | ):
667 | _rl = input.links[0].from_node
668 | _rl_dict = self.rlayers[_rl.scene]['rlayers'][_rl]
669 | _rl_dict[input.links[0].from_socket] = {}
670 | return True
671 | elif not input.links[0].from_node.inputs:
672 | return False
673 | else:
674 | for i in input.links[0].from_node.inputs:
675 | if self.links_from_rl(i):
676 | return True
677 | return False
678 |
679 | def get_rl_links(self, node):
680 | if not node.inputs or not self.has_links(node):
681 | return False
682 | for num in range(len(node.inputs)):
683 | input = node.inputs[num]
684 | if self.links_from_rl(input):
685 | return True
686 | return False
687 |
688 | def outputs(self):
689 | '''
690 | Store and mute user File Outputs in the main scene Compositor.
691 | Collect info about Viewer nodes in the main scene Compositor
692 | to recreate them on Restore step - and delete them.
693 | This is needed because Blender Viewers' behavior is pretty
694 | unpredictable and the only way to make sure render results
695 | are displayed correctly is to delete Viewer and to create it back then.
696 | Just muting or unlinking unfortunately is not always a solution
697 | '''
698 |
699 | sc = self.scene
700 | nodes = sc.node_tree.nodes
701 | for node in nodes:
702 | if node.type == 'OUTPUT_FILE' and not node.mute:
703 | if self.get_rl_links(node):
704 | if not self.project['has_f_outs']:
705 | self.project['has_f_outs'] = True
706 | self.f_outputs.append(node)
707 | node.mute = True
708 | self.restore['muted'].append(node)
709 | elif node.type == 'VIEWER':
710 | self.viewers[node.name] = {
711 | "center_x" : node.center_x,
712 | "center_y" : node.center_y,
713 | "color" : node.color,
714 | "height" : node.height,
715 | "hide" : node.hide,
716 | "label" : node.label,
717 | "location" : node.location,
718 | "mute" : node.mute,
719 | "name" : node.name,
720 | "show_options" : node.show_options,
721 | "show_preview" : node.show_preview,
722 | "show_texture" : node.show_texture,
723 | "use_alpha" : node.use_alpha,
724 | "use_custom_color" : node.use_custom_color,
725 | "width" : node.width,
726 | "width_hidden" : node.width_hidden,
727 | }
728 | self.viewers[node.name]["inputs"] = {}
729 | inputs = self.viewers[node.name]["inputs"]
730 | for input in node.inputs:
731 | inputs[input.name] = {}
732 | inputs[input.name]["type"] = input.type
733 | inputs[input.name]["name"] = input.name
734 | inputs[input.name]["identifier"] = input.identifier
735 | inputs[input.name]["enabled"] = input.enabled
736 | inputs[input.name]["hide"] = input.hide
737 | inputs[input.name]["links"] = (
738 | None if not input.links else
739 | input.links[0].from_socket
740 | )
741 | self.restore['deleted'] = node.name
742 | nodes.remove(node)
743 |
744 | def execute(self, context):
745 | self.structure()
746 | self.outputs()
747 | return {'FINISHED'}
748 |
749 | #----------------- Get all Render Layers used active outputs -------------------
750 |
751 | class TMB_Links(TMB_Helpers, bpy.types.Operator):
752 | '''Get active outputs'''
753 | bl_idname = 'tmb.links'
754 | bl_label = 'Get Links'
755 | store = None
756 | project = None
757 |
758 | def structure(self):
759 | '''Sync to the main storage'''
760 |
761 | self.store = bpy.types.TMB_OT_store.store
762 | self.project = self.store['Project']
763 | self.scenes = self.store['Scenes']
764 | self.restore = self.store['Restore']
765 | self.render_passes = self.project['render_passes']
766 | self.links = self.project['links']
767 | self.rl_links = []
768 |
769 | def get_connected_rl_outs(self, input):
770 | '''
771 | Check if File Output input's links lead to active Render Layers' pass
772 | Append pass to self.rl_links if found
773 | '''
774 |
775 | if not input.links:
776 | return
777 | elif (
778 | input.links[0].from_node.type == 'R_LAYERS' and
779 | not input.links[0].from_node.mute
780 | ):
781 | self.rl_links.append(input.links[0].from_socket)
782 | elif not input.links[0].from_node.inputs:
783 | return
784 | else:
785 | for i in input.links[0].from_node.inputs:
786 | self.get_connected_rl_outs(i)
787 | return
788 |
789 | def get_all_links(self):
790 | '''
791 | Collect all active Render Layers' outputs to the self.links list
792 | '''
793 |
794 | # Collect all npasses leading to Composite
795 | for rl in self.project['rlayers']:
796 | if (
797 | self.scenes[rl.scene]['engine'] == 'CYCLES' or
798 | not self.scenes[rl.scene]['tmb'] or
799 | not self.scenes[rl.scene]['tmb']['activate'] or
800 | not rl.scene.view_layers[rl.layer].use
801 | ):
802 | continue
803 |
804 | # if RL-scene is non-Cycles, TMB is on and RL is used for rendering:
805 | for num in range(len(rl.outputs)):
806 | if (
807 | not rl.outputs[num].enabled or
808 | (
809 | not self.render_passes and
810 | not self.project['image_settings']['file_format'] ==
811 | 'OPEN_EXR_MULTILAYER' and
812 | not self.npass_used(rl.outputs[num])
813 | )
814 | ):
815 | continue
816 | else:
817 | # if npass is enabled and used for rendering
818 | # or TMB Render Passes is on
819 | self.links.append(rl.outputs[num])
820 |
821 | # Collect all npasses leading to the user active File Outputs
822 | if not self.project['has_f_outs']:
823 | return
824 |
825 | # Execute only if user File Outputs exist
826 | _fouts = self.restore['file_outputs']
827 | for fo in _fouts:
828 | for input in fo.inputs:
829 | if not input.links:
830 | continue
831 | self.rl_links = []
832 | self.get_connected_rl_outs(input)
833 | if not self.rl_links:
834 | continue
835 | for lnk in self.rl_links:
836 | if lnk not in self.links:
837 | self.links.append(lnk)
838 | self.rl_links = []
839 |
840 | def execute(self, context):
841 | self.structure()
842 | self.get_all_links()
843 | return {'FINISHED'}
844 |
845 | #------------------- Add File Outputs for saving subframes ---------------------
846 |
847 | class TMB_SaveBuffers(TMB_Helpers, bpy.types.Operator):
848 | '''Add Save Buffers File Outputs'''
849 |
850 | bl_idname = 'tmb.savebuffers'
851 | bl_label = 'Save Buffers'
852 | store = None
853 | project = None
854 | scenes = None
855 | render_passes = None
856 | rlayers = None
857 | restore = None
858 | scene = None
859 | links = None
860 |
861 | def structure(self):
862 | '''Sync to the main storage'''
863 |
864 | self.store = bpy.types.TMB_OT_store.store
865 | self.project = self.store['Project']
866 | self.scenes = self.store['Scenes']
867 | self.render_passes = self.project['render_passes']
868 | self.rlayers = self.store['RLayers']
869 | self.restore = self.store['Restore']
870 | self.scene = self.project['main_sc']
871 | self.links = self.project['links']
872 |
873 | def get_fo(self, fo_name):
874 | '''Create/use existing TMB File Output for saving buffers'''
875 |
876 | sc = self.scene
877 | _fo = ( sc.node_tree.nodes.new('CompositorNodeOutputFile')
878 | if fo_name not in sc.node_tree.nodes else
879 | sc.node_tree.nodes[fo_name]
880 | )
881 | _fo.name = fo_name
882 | _fo.mute = False
883 | if _fo not in self.restore['tmb_nodes']:
884 | self.restore['tmb_nodes'].append(_fo)
885 | if _fo not in self.restore['tmb_f_outs']:
886 | self.restore['tmb_f_outs'].append(_fo)
887 | return _fo
888 |
889 | def add_main_dir(self):
890 | '''
891 | Create main temporary directory in the project render folder.
892 | Store it into the main storage
893 | '''
894 |
895 | _proj_dir = self.project['path']
896 | if (
897 | not pathlib.os.path.exists(_proj_dir) or
898 | pathlib.os.path.isfile(_proj_dir)
899 | ):
900 | _proj_dir = pathlib.Path(str(_proj_dir))
901 | _proj_dir = str(_proj_dir.parents[0])
902 | _tmb_dir = pathlib.os.path.join(_proj_dir, "_True_Motion_Blur_tmp")
903 | _main_out = pathlib.os.path.join(_proj_dir, '_TMB_Output')
904 | if pathlib.os.path.exists(_tmb_dir):
905 | self.clear_path(_tmb_dir)
906 | if pathlib.os.path.exists(_main_out):
907 | self.clear_path(_main_out)
908 | pathlib.Path(_tmb_dir).mkdir(parents=True, exist_ok=True)
909 | self.restore["main_dir"] = _tmb_dir
910 |
911 | def get_path(self, sc_name, vl_name, out_index):
912 | '''
913 | Create File Output folder (individual for each pass)
914 | and store it to the main storage
915 | '''
916 |
917 | main_dir = self.restore['main_dir']
918 | fo_path = pathlib.os.path.join(
919 | main_dir, sc_name, vl_name, str(out_index)
920 | )
921 | pathlib.Path(fo_path).mkdir(parents=True, exist_ok=True)
922 | self.restore['folders'].append(fo_path)
923 | return fo_path
924 |
925 | def save_buffers_add(self):
926 | '''Create, setup and link File Outputs for each active render pass'''
927 |
928 | sc = self.scene
929 | _links = sc.node_tree.links
930 | _f_outs = self.restore['file_outputs']
931 | y_loc = 0
932 | for lnk in self.links:
933 | y_loc += 1
934 | _rl = lnk.node
935 | _num = [
936 | out for out in range(len(_rl.outputs))
937 | if _rl.outputs[out] == lnk
938 | ][0]
939 | _fo_name = f'{_rl.scene.name}_{_rl.layer}_{_num:02d}'
940 | _fo = self.get_fo(_fo_name)
941 | _fo.base_path = self.get_path(_rl.scene.name, _rl.layer, _num)
942 | _fo.format.file_format = "OPEN_EXR"
943 | _fo.format.color_mode = "RGB"
944 | _fo.format.color_depth = "32"
945 | _fo.location.x = _rl.location.x + 300
946 | _fo.location.y = _rl.location.y + 300 - (22 * y_loc)
947 | _fo.hide = True
948 | _links.new(lnk,_fo.inputs[0])
949 |
950 | _sets = self.rlayers[_rl.scene]['rlayers'][_rl][_rl.outputs[_num]]
951 | _sets["file_output"] = _fo
952 | _sets["path"] = _fo.base_path
953 |
954 | def output_fo_add(self):
955 | '''Add main File Output which will act as a render result writer'''
956 |
957 | _imgsets = self.project['image_settings']
958 | _fo = self.get_fo("_TMB_Output")
959 | self.restore['tmb_f_outs'].remove(_fo)
960 | self.project['output'] = _fo
961 | _frm = _fo.format
962 |
963 | _fo.base_path = pathlib.os.path.join(self.project['path'],'_TMB_Output')
964 | _frm.file_format = _imgsets["file_format"]
965 | if _imgsets["cineon_black"]:
966 | _frm.cineon_black = _imgsets["cineon_black"]
967 | if _imgsets["cineon_gamma"]:
968 | _frm.cineon_gamma = _imgsets["cineon_gamma"]
969 | if _imgsets["cineon_white"]:
970 | _frm.cineon_white = _imgsets["cineon_white"]
971 | if _imgsets["color_depth"]:
972 | _frm.color_depth = _imgsets["color_depth"]
973 | if _imgsets["color_mode"]:
974 | _frm.color_mode = _imgsets["color_mode"]
975 | if _imgsets["compression"]:
976 | _frm.compression = _imgsets["compression"]
977 | if _imgsets["exr_codec"]:
978 | _frm.exr_codec = _imgsets["exr_codec"]
979 | if _imgsets["jpeg2k_codec"]:
980 | _frm.jpeg2k_codec = _imgsets["jpeg2k_codec"]
981 | if _imgsets["quality"]:
982 | _frm.quality = _imgsets["quality"]
983 | if _imgsets["tiff_codec"]:
984 | _frm.tiff_codec = _imgsets["tiff_codec"]
985 | if _imgsets["use_cineon_log"]:
986 | _frm.use_cineon_log = _imgsets["use_cineon_log"]
987 | if _imgsets["use_jpeg2k_cinema_48"]:
988 | _frm.use_jpeg2k_cinema_48 = _imgsets["use_jpeg2k_cinema_48"]
989 | if _imgsets["use_jpeg2k_cinema_preset"]:
990 | _frm.use_jpeg2k_cinema_preset = _imgsets["use_jpeg2k_cinema_preset"]
991 | if _imgsets["use_jpeg2k_ycc"]:
992 | _frm.use_jpeg2k_ycc = _imgsets["use_jpeg2k_ycc"]
993 | _frm.use_preview = _imgsets["use_preview"]
994 | _frm.use_zbuffer = _imgsets["use_zbuffer"]
995 | _frm.views_format = _imgsets["views_format"]
996 |
997 | if not self.project['composite'].inputs[0].links:
998 | # If there's no active connected Composite node the whole operation
999 | # will be aborted from the main render operator right after setup
1000 | return
1001 |
1002 | _to_mix = self.project['composite'].inputs[0].links[0].from_socket
1003 | self.scene.node_tree.links.new(_to_mix, _fo.inputs[0])
1004 | if _imgsets['file_format'] == 'OPEN_EXR_MULTILAYER':
1005 | for lnk in self.links:
1006 | _new_in = _fo.layer_slots.new(name = lnk.name)
1007 | self.scene.node_tree.links.new(lnk, _new_in)
1008 | _fo.mute = True
1009 |
1010 | def execute(self, context):
1011 | self.structure()
1012 | self.add_main_dir()
1013 | self.save_buffers_add()
1014 | self.output_fo_add()
1015 | return {'FINISHED'}
1016 |
1017 | #------------------------- Get TMB Mix and Image nodes -------------------------
1018 |
1019 | class TMB_AddMixImages(TMB_Helpers, bpy.types.Operator):
1020 | '''
1021 | Generate Blender Images to store render results to. Add Alpha Over nodes
1022 | to control wether Render Layers or Images will be used for render output'''
1023 | bl_idname = "tmb.miximgs"
1024 | bl_label = "Mix and Images"
1025 | store = None
1026 | project = None
1027 | scenes = None
1028 | render_passes = None
1029 | rlayers = None
1030 | render = None
1031 | restore = None
1032 | links = None
1033 | i_name = ""
1034 | m_name = ""
1035 | scene = None
1036 |
1037 | def structure(self):
1038 | '''Sync to the main storage'''
1039 |
1040 | self.store = bpy.types.TMB_OT_store.store
1041 | self.project = self.store['Project']
1042 | self.scenes = self.store['Scenes']
1043 | self.rlayers = self.store['RLayers']
1044 | self.restore = self.store['Restore']
1045 | self.render = self.store['Render']
1046 | self.scene = self.project['main_sc']
1047 | self.render_passes = self.project['render_passes']
1048 | self.links = self.project['links']
1049 | self.i_name = "TMB_Image"
1050 | self.m_name = "TMB_Mix"
1051 |
1052 | def remove_existing(self):
1053 | '''
1054 | Find and delete previously created TMB Images and Mix (Alpha Over) nodes
1055 | '''
1056 |
1057 | sc = self.scene
1058 | _links = sc.node_tree.links
1059 | for node in sc.node_tree.nodes:
1060 | # For mix nodes - reconnect neighbour nodes and delete mix nodes
1061 | if (
1062 | node.name.startswith(self.m_name) and
1063 | node.type == 'ALPHAOVER'
1064 | ):
1065 | _in_links = node.inputs[1].links
1066 |
1067 | if (
1068 | _in_links and
1069 | (
1070 | _in_links[0].from_node.type == 'R_LAYERS' or
1071 | node.name == self.m_name
1072 | )
1073 | ):
1074 | _out_links = node.outputs[0].links
1075 | _in_links = node.inputs[1].links
1076 | if _out_links and _in_links:
1077 | for lnk in _out_links:
1078 | _links.new(_in_links[0].from_socket, lnk.to_socket)
1079 | sc.node_tree.nodes.remove(node)
1080 | # For Image nodes - delete them
1081 | elif (
1082 | node.name.startswith(self.i_name) and
1083 | node.type == 'IMAGE'
1084 | ):
1085 | sc.node_tree.nodes.remove(node)
1086 |
1087 | def get_image(self, i_name):
1088 | '''
1089 | Generate and return Blender Image
1090 | or update settings if it already exists
1091 | '''
1092 |
1093 | sc = self.scene
1094 | prj = self.project
1095 | _images = bpy.data.images
1096 | _res_x = int(prj['res_x']*(prj['res_prc']/100))
1097 | _res_y = int(prj['res_y']*(prj['res_prc']/100))
1098 |
1099 | # if image exists:
1100 | if i_name in _images:
1101 |
1102 | # if existing image pixel number is different from project's:
1103 | if _images[i_name].pixels-self.project['pix_len']:
1104 | _images.remove(_images[i_name])
1105 | _img = _images.new(i_name, _res_x, _res_y)
1106 |
1107 | # if existing image pixel number is the same as project's:
1108 | else:
1109 | _img = _images[i_name]
1110 | else:
1111 | _img = _images.new(i_name, _res_x, _res_y)
1112 |
1113 | _img.file_format = 'OPEN_EXR'
1114 | _img.use_generated_float = True
1115 | return _img
1116 |
1117 | def add_mix_img(self):
1118 | ''''Return new Image and new Alpha Over node'''
1119 |
1120 | nodes = self.scene.node_tree.nodes
1121 | i_node = nodes.new('CompositorNodeImage')
1122 | m_node = nodes.new('CompositorNodeAlphaOver')
1123 | return i_node, m_node
1124 |
1125 | def set_mix_node(self, m_node, rl, npass, y_loc):
1126 | '''Setup and return mix node: name, location, hide, reconnect links'''
1127 |
1128 | _links = self.scene.node_tree.links
1129 | m_name = self.get_name('mix', rl, npass)
1130 | m_node.name = m_name
1131 | m_node.location.x = rl.location.x + 260
1132 | m_node.location.y = rl.location.y - 22 * y_loc - 4
1133 | m_node.inputs[0].default_value = 0
1134 | m_node.hide = True
1135 | m_node.use_premultiply = True
1136 | if npass.links:
1137 | for lnk in npass.links:
1138 | _links.new(m_node.outputs[0], lnk.to_socket)
1139 | return m_node
1140 |
1141 | def set_image(self, i_name):
1142 | '''
1143 | Find/add Blender generated image (make sure an image number
1144 | of pixels matches the scene number of pixels). Assign numpy
1145 | zeros array as Image pixels. Return Image.
1146 | '''
1147 |
1148 | images = bpy.data.images
1149 | if (
1150 | i_name in images and
1151 | len(images[i_name].pixels) != self.project['pix_len']
1152 | ):
1153 | images.remove(images[i_name])
1154 | i_image = (
1155 | self.get_image(i_name)
1156 | if i_name not in images else
1157 | images[i_name]
1158 | )
1159 | return i_image
1160 |
1161 | def set_img_node(self, i_node, i_image, m_node):
1162 | '''Setup and return Image node: name, location, hide, Image'''
1163 |
1164 | i_name = i_image.name
1165 | i_node.name = i_name
1166 | i_node.image = i_image
1167 | i_node.location.x = m_node.location.x - 410
1168 | i_node.location.y = m_node.location.y - 15
1169 | i_node.hide = True
1170 | return i_node
1171 |
1172 | def get_name(self, type, rl, npass):
1173 | '''
1174 | Generate a name based on the node type, scene,
1175 | view layer and the render pass
1176 | '''
1177 |
1178 | base_name = self.i_name if type == 'img' else self.m_name
1179 | name = f'{base_name}_{rl.scene.name}_{rl.layer}_{npass.name}'
1180 | return name
1181 |
1182 | def add_passes_mix_imgs(self):
1183 | '''
1184 | Add image and mix nodes for each individual pass of each active
1185 | Render layers node in the main scene Compositor
1186 | '''
1187 |
1188 | _links = self.project['main_sc'].node_tree.links
1189 | y_loc = 0
1190 | for lnk in self.links:
1191 | y_loc += 1
1192 | nodes = self.add_mix_img()
1193 | m_node = self.set_mix_node(nodes[1], lnk.node, lnk, y_loc)
1194 | i_name = self.get_name('img', lnk.node, lnk)
1195 | i_image = self.set_image(i_name)
1196 | i_node = self.set_img_node(nodes[0], i_image, m_node)
1197 | i_node.label = m_node.label = lnk.name
1198 | _links.new(lnk, m_node.inputs[1])
1199 | _links.new(i_node.outputs[0], m_node.inputs[2])
1200 | _passes = self.rlayers[lnk.node.scene]['rlayers'][lnk.node]
1201 | _passes[lnk]['image'] = i_image
1202 | _passes[lnk]['img_node'] = i_node
1203 | _passes[lnk]['mix_node'] = m_node
1204 | self.restore['mix_nodes'].append(m_node)
1205 | self.render['images'].append(i_image)
1206 |
1207 | def execute(self, context):
1208 | self.structure()
1209 | self.remove_existing()
1210 | self.add_passes_mix_imgs()
1211 | return {'FINISHED'}
1212 |
1213 | #-------------------------------- Scenes Setup ---------------------------------
1214 |
1215 | class TMB_ScenesSetup(bpy.types.Operator):
1216 | '''Setup all Eevee scenes with TMB enabled before render'''
1217 | bl_idname = "tmb.scsetup"
1218 | bl_label = "Scenes Setup"
1219 | store = None
1220 | project = None
1221 | scenes = None
1222 | scene = None
1223 |
1224 | def structure(self):
1225 | '''Sync to the main storage'''
1226 |
1227 | self.store = bpy.types.TMB_OT_store.store
1228 | self.project = self.store['Project']
1229 | self.scenes = self.store['Scenes']
1230 | self.scene = self.project['main_sc']
1231 |
1232 | def getsamples(self, scene):
1233 | '''Calculate Eevee render samples based on the TMB settings'''
1234 |
1235 | sc = self.scenes[scene]
1236 | tmb = sc['tmb']
1237 | sc_samples = sc['samples']
1238 | samples = tmb['samples']
1239 | boost = tmb['boost']
1240 | basic_samples = max(
1241 | 1, int(
1242 | sc_samples // max(1, samples)
1243 | )
1244 | )
1245 | samples = basic_samples + int(
1246 | (sc_samples-basic_samples) * min(1, boost)
1247 | )
1248 | samples = min(samples, sc_samples)
1249 | return samples
1250 |
1251 | def scsetup(self, sc):
1252 | '''Set render samples number for all Eevee scenes with TMB enabled'''
1253 |
1254 | _sets = self.scenes[sc]
1255 | _tmb = _sets['tmb']
1256 | if _sets['engine'] == 'BLENDER_EEVEE' and _tmb and _tmb['activate']:
1257 | sc.eevee.taa_render_samples = self.getsamples(sc)
1258 | sc.eevee.use_motion_blur = False
1259 |
1260 | def execute(self, context):
1261 | self.structure()
1262 | for sc in list(self.scenes.keys()):
1263 | self.scsetup(sc)
1264 | return {'FINISHED'}
1265 |
1266 | #----------------------- Enable Backdrop in Compositor -------------------------
1267 |
1268 | class TMB_Backdrop(bpy.types.Operator):
1269 | '''Enable Show Backdrop in Compositor'''
1270 | bl_idname = "tmb.backdrop"
1271 | bl_label = "Enable Backdrop"
1272 | store = None
1273 | project = None
1274 | restore = None
1275 | override = None
1276 |
1277 | def structure(self):
1278 | '''sync to the main storage'''
1279 |
1280 | self.store = bpy.types.TMB_OT_store.store
1281 | self.project = self.store['Project']
1282 | self.restore = self.store['Restore']
1283 |
1284 | def context_override(self):
1285 | window = self.project['window']
1286 | screen = window.screen
1287 | area = None
1288 | for ar in window.screen.areas:
1289 | if ar.ui_type not in ('PROPERTIES','OUTLINER'):
1290 | area = ar
1291 | if not area:
1292 | area = window.screen.areas[0]
1293 | region = area.regions[0]
1294 | scene = bpy.context.scene
1295 |
1296 | self.override = {'window':window,
1297 | 'screen':screen,
1298 | 'area' :area,
1299 | 'region':region,
1300 | 'scene' :scene,
1301 | }
1302 |
1303 | def execute(self, context):
1304 | self.store = bpy.types.TMB_OT_store.store
1305 | self.project = self.store['Project']
1306 | self.restore = self.store['Restore']
1307 | _areas = self.project['window'].screen.areas
1308 | for area in _areas:
1309 | if area.ui_type == 'CompositorNodeTree':
1310 | area.spaces[0].show_backdrop = True
1311 | return {'FINISHED'}
1312 | self.context_override()
1313 | override = self.override
1314 | bpy.ops.screen.area_split(override, direction='VERTICAL', factor = .3)
1315 | _areas[-1].ui_type = 'CompositorNodeTree'
1316 | _areas[-1].spaces[0].show_backdrop = True
1317 | return {'FINISHED'}
1318 |
1319 | def invoke(self, context, event):
1320 | return self.execute(context)
1321 |
1322 | ############################# EXECUTE SETUP OPERATOR ###########################
1323 |
1324 | class TMB_Setup(bpy.types.Operator):
1325 | '''Setup project for TMB render'''
1326 | bl_idname = "tmb.setup"
1327 | bl_label = "Setup"
1328 | animation : BoolProperty(
1329 | name="Animation",
1330 | description="Render Animation",
1331 | default=False,
1332 | )
1333 | project = None
1334 |
1335 | def execute(self, context):
1336 | sc = context.scene
1337 | _tmb = sc.true_mb
1338 | bpy.ops.tmb.store(animation = self.animation)
1339 | self.project = bpy.types.TMB_OT_store.store['Project']
1340 | bpy.ops.tmb.rlayers()
1341 | if not [
1342 | rl for rl in self.project['rlayers']
1343 | if rl.scene.view_layers[rl.layer].use
1344 | ]:
1345 | return {'CANCELLED'}
1346 | bpy.ops.tmb.userouts()
1347 | bpy.ops.tmb.links()
1348 | bpy.ops.tmb.savebuffers()
1349 | bpy.ops.tmb.miximgs()
1350 | bpy.ops.tmb.scsetup()
1351 | bpy.ops.tmb.backdrop('INVOKE_DEFAULT')
1352 | return {'FINISHED'}
1353 |
1354 | ############################### RESTORE OPERATOR ###############################
1355 |
1356 | class TMB_Restore(TMB_Helpers, bpy.types.Operator):
1357 | '''Restore settings'''
1358 | bl_idname = "tmb.restore"
1359 | bl_label = 'Restore'
1360 | store = None
1361 | project = None
1362 | scenes = None
1363 | restore = None
1364 | scene = None
1365 |
1366 | def structure(self):
1367 | '''Sync to the main storage'''
1368 |
1369 | self.store = bpy.types.TMB_OT_store.store
1370 | self.project = self.store['Project']
1371 | self.scenes = self.store['Scenes']
1372 | self.restore = self.store['Restore']
1373 | self.scene = self.project['main_sc']
1374 |
1375 | def restore_settings(self, context):
1376 | '''Retore initial render settings for Eevee TMB scenes'''
1377 |
1378 | self.project['main_sc'].frame_set(self.project['frame'], subframe=0.0)
1379 | for sc in list(self.scenes.keys()):
1380 | _sets = self.scenes[sc]
1381 | _tmb = _sets['tmb']
1382 | if _sets['engine'] == 'BLENDER_EEVEE' and _tmb and _tmb['activate']:
1383 | sc.eevee.taa_render_samples = _sets['samples']
1384 | sc.eevee.use_motion_blur = _sets['mb']
1385 | self.project['main_sc'].render.filepath = self.project['user_path']
1386 |
1387 | def restore_viewer(self, sets):
1388 | '''Recreate deleted Viewer'''
1389 |
1390 | sc = self.scene
1391 | links = self.scene.node_tree.links
1392 | viewer = sc.node_tree.nodes.new('CompositorNodeViewer')
1393 | viewer.name = sets['name']
1394 | viewer.label = sets['label']
1395 | viewer.color = sets['color']
1396 | viewer.height = sets['height']
1397 | viewer.hide = sets['hide']
1398 | viewer.location = sets['location']
1399 | viewer.center_x = sets['center_x']
1400 | viewer.center_y = sets['center_y']
1401 | viewer.mute = False
1402 | viewer.show_options = sets['show_options']
1403 | viewer.show_preview = sets['show_preview']
1404 | viewer.show_texture = sets['show_texture']
1405 | viewer.use_alpha = sets['use_alpha']
1406 | viewer.use_custom_color = sets['use_custom_color']
1407 | viewer.width = sets['width']
1408 | viewer.width_hidden = sets['width_hidden']
1409 | _viewer_inputs = [inp.name for inp in viewer.inputs]
1410 | for i in list(sets['inputs'].keys()):
1411 | input = sets['inputs'][i]
1412 | if input['name'] in _viewer_inputs:
1413 | inp = viewer.inputs[input['name']]
1414 | else:
1415 | inp = viewer.inputs.new(
1416 | input['type'], input['name'], input['identifier']
1417 | )
1418 | inp.hide = input['hide']
1419 | inp.enabled = input['enabled']
1420 | for node in sc.node_tree.nodes:
1421 | if node.type == 'COMPOSITE' and node.inputs[0].links:
1422 | links.new(
1423 | node.inputs[0].links[0].from_socket,
1424 | viewer.inputs[0])
1425 | viewer.location = node.location
1426 | viewer.location.y -= 150
1427 | break
1428 |
1429 | def restore_compositor(self):
1430 | '''
1431 | Set all TMB Mix (Alpha Over) nodes mix factor to 1
1432 | Unmute temporary muted nodes
1433 | Remove temporary TMB supporting nodes
1434 | except TMB Render Layers and Composite nodes
1435 | Recreate and relink the Viewer
1436 | '''
1437 |
1438 | sc = self.scene
1439 | if not sc.node_tree or not sc.node_tree.links:
1440 | return
1441 | links = sc.node_tree.links
1442 | if self.restore['mix_nodes']:
1443 | for node in self.restore['mix_nodes']:
1444 | node.inputs[0].default_value = 1
1445 | if self.restore['muted']:
1446 | for node in self.restore['muted']:
1447 | node.mute = False
1448 | if self.restore['tmb_nodes']:
1449 | for node in self.restore['tmb_nodes']:
1450 | if node.type not in ('R_LAYERS', 'COMPOSITE'):
1451 | sc.node_tree.nodes.remove(node)
1452 | elif node.type == 'COMPOSITE':
1453 | comps = [
1454 | nd for nd in sc.node_tree.nodes
1455 | if nd.type == 'COMPOSITE'
1456 | ]
1457 | if len(comps) > 1:
1458 | for nd in sc.node_tree.nodes:
1459 | if nd.type == 'COMPOSITE' and nd is not node:
1460 | if node.inputs[0].links:
1461 | links.new(
1462 | node.inputs[0].links[0].from_socket,
1463 | nd.inputs[0]
1464 | )
1465 | sc.node_tree.nodes.remove(node)
1466 | break
1467 | if self.restore['viewers']:
1468 | for viewer in list(self.restore['viewers'].keys()):
1469 | self.restore_viewer(self.restore['viewers'][viewer])
1470 | for rl in self.project['rlayers']:
1471 | rl.mute = False
1472 |
1473 | def remove_out_dir(self):
1474 | '''Remove temporary main out folder from disc'''
1475 |
1476 | _dest = self.project['path']
1477 | _source = pathlib.os.path.join(_dest, '_TMB_Output')
1478 | _spath = pathlib.Path(_source)
1479 | if not _spath.is_dir():
1480 | return
1481 | self.clear_path(_spath)
1482 |
1483 | def cleanup(self):
1484 | '''Remove temporary subframes folders from disc'''
1485 |
1486 | if (
1487 | self.restore['main_dir'] and
1488 | pathlib.os.path.isdir(self.restore["main_dir"])
1489 | ):
1490 | self.clear_path(self.restore["main_dir"])
1491 |
1492 | def execute(self, context):
1493 | self.structure()
1494 | self.restore_settings(context)
1495 | self.restore_compositor()
1496 | self.remove_out_dir()
1497 | self.cleanup()
1498 | return {'FINISHED'}
1499 |
1500 | #--------------------------- For test purposes only ----------------------------
1501 |
1502 | classes = [
1503 | TMB_Warning,
1504 | TMB_Store,
1505 | TMB_RLayers,
1506 | TMB_Links,
1507 | TMB_SaveBuffers,
1508 | TMB_AddMixImages,
1509 | TMB_UserOutputs,
1510 | TMB_ScenesSetup,
1511 | TMB_Backdrop,
1512 | TMB_Setup,
1513 | TMB_Restore,
1514 | ]
1515 |
1516 | def support_register():
1517 | for cl in classes:
1518 | register_class(cl)
1519 |
1520 | def support_unregister():
1521 | for cl in classes:
1522 | unregister_class(cl)
1523 |
1524 | if __name__ == '__main__':
1525 | support_register()
1526 | bpy.ops.tmb.setup()
1527 | bpy.ops.tmb.restore()
1528 |
--------------------------------------------------------------------------------
/tmb_ui.py:
--------------------------------------------------------------------------------
1 | # ##### BEGIN GPL LICENSE BLOCK #####
2 | #
3 | # This program is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU General Public License
5 | # as published by the Free Software Foundation; either version 2
6 | # of the License, or (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software Foundation,
15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 | #
17 | # ##### END GPL LICENSE BLOCK #####
18 |
19 | # True Motion Blur add-on
20 | # Add UI-items to Blender interface
21 | # (c) 2020 Andrey Sokolov (so_records)
22 |
23 | #------------------------------- Import Modules --------------------------------
24 |
25 | import bpy
26 | from bpy.props import (
27 | BoolProperty,
28 | EnumProperty,
29 | IntProperty,
30 | FloatProperty,
31 | StringProperty,
32 | PointerProperty #--------------------------- this one for test purposes only
33 | )
34 | from bpy.utils import register_class, unregister_class
35 |
36 | #------------------------------ Change Keyconfig -------------------------------
37 |
38 | class TMB_Keyconfig(bpy.types.Operator):
39 | '''
40 | Redirect shortcuts for native Render Operator bpy.ops.render.render()
41 | (F12 and Ctrl+F12 by default)
42 | with TMB render operator bpy.ops.tmb_render.render()
43 | '''
44 | bl_idname = 'tmb.keyconfig'
45 | bl_label = 'Keyconfig'
46 |
47 | def execute(self, context):
48 | configs = context.window_manager.keyconfigs
49 | items = configs.active.keymaps['Screen'].keymap_items
50 | keymap = [i for i in items if i.idname == "render.render"]
51 | keymap_tmb = [i for i in items if i.idname == "render.render"]
52 | for i in keymap:
53 | i.idname = "tmb_render.render"
54 | i.properties.use_viewport = True
55 | if keymap_tmb:
56 | keymap_tmb[0].properties.animation = True
57 | keymap_tmb[0].properties.animation = False
58 | if keymap:
59 | keymap[1].properties.animation = True
60 | return {'FINISHED'}
61 |
62 | #----------------------------- Activate Keyconfig ------------------------------
63 |
64 | def keyconfig(self, context):
65 | '''
66 | Activate Keyconfig Operator
67 | Function to be loaded from Blender on_load handler
68 | It is necessary because Blender Operators can not be invoked on startup
69 | '''
70 |
71 | op = bpy.types.TMB_OT_store
72 | if not op.enable:
73 | op.enable = True
74 | bpy.ops.tmb.keyconfig()
75 | _on_load = bpy.app.handlers.load_pre
76 | if keyconfig in _on_load:
77 | _on_load.remove(keyconfig)
78 |
79 | #----------------------------- Set TMB Properties ------------------------------
80 |
81 | class TMB_TrueMB(bpy.types.PropertyGroup):
82 | '''Properties Group for UI Panel'''
83 |
84 | activate : BoolProperty(
85 | name="",
86 | description="Enable true subframe motion blur effect\
87 | (render only)",
88 | default=False,
89 | update=keyconfig
90 | )
91 | position : EnumProperty(
92 | name = "Position",
93 | description = "Offset for the shutter's time interval,\
94 | allows to change motion blur trails:",
95 | items = [
96 | ("START", "Start of Frame",
97 | "The shutter opens on the current frame."),
98 | ("CENTER", "Center of Frame",
99 | "The shutter is open during the current frame."),
100 | ("FRAME", "End of Frame",
101 | "The shutter closes on the current frame."),
102 | ],
103 | default="CENTER"
104 | )
105 | samples : IntProperty(
106 | name="Samples",
107 | description="Number of subframes per frame",
108 | default=16,
109 | min=2,
110 | max=128
111 | )
112 | shutter : FloatProperty(
113 | name="Shutter",
114 | description="Time taken in frames between shutter open and close",
115 | default=.5,
116 | min=0,
117 | soft_max=1,
118 | subtype = "FACTOR"
119 | )
120 | boost : FloatProperty(
121 | name="Quality boost",
122 | description="Boost render samples for each subframe from normal amount\
123 | to original sample rate.\nRender time increases proportionally",
124 | default=0,
125 | min=0,
126 | max=1,
127 | subtype = "FACTOR"
128 | )
129 | render_passes : BoolProperty(
130 | name="Render Passes",
131 | description="Render all enabled render passes, even if they are \
132 | not linked to any output",
133 | default=False,
134 | options={'HIDDEN'}
135 | )
136 |
137 | #-------------------- Create UI Panel in Render Properties ---------------------
138 | class TMB_Panel:
139 | '''Not an Operator class, doesn't need to be registered as Operator'''
140 | bl_space_type = 'PROPERTIES'
141 | bl_region_type = 'WINDOW'
142 | bl_options = {'DEFAULT_CLOSED'}
143 | bl_context = "render"
144 | bl_category = 'True Motion Blur add-on'
145 | COMPAT_ENGINES = {'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}
146 |
147 | @classmethod
148 | def poll(cls, context):
149 | return (context.engine in cls.COMPAT_ENGINES)
150 |
151 | class TMB_PT_true_mb_panel(TMB_Panel, bpy.types.Panel):
152 | '''Create UI Panel in the render properties window'''
153 | bl_label = "True Motion Blur"
154 | bl_idname = "RENDER_PT_true_mb"
155 |
156 | def draw_header(self, context):
157 | layout = self.layout
158 | scene = context.scene
159 | props = scene.true_mb
160 |
161 | col = layout.column()
162 | col.prop(props, "activate")
163 |
164 | def draw(self, context):
165 | layout = self.layout
166 | layout.use_property_split = True
167 | scene = context.scene
168 | props = scene.true_mb
169 | layout.active = props.activate
170 | col = layout.column()
171 | col.prop(props, "position")
172 | col.prop(props, "shutter")
173 | col.separator()
174 | col.prop(props, "samples")
175 | col.prop(props, "boost")
176 | col.prop(props, "render_passes")
177 |
178 | #-------------------------- Replace native Top Menu ----------------------------
179 |
180 | # This is almost exact copy of original Blender Render tab from top menu:
181 | # 'render.render()' operator is replaced with 'tmb_render.render()'.
182 | # Note: in your scripts use `bpy.ops.render.render()` for native render
183 | # and `bpy.ops.tmb_render.render()` for True Motion Blur render.
184 |
185 | class TOPBAR_MT_render(bpy.types.Menu):
186 |
187 | bl_label = "Render"
188 | bl_category = 'Render'
189 |
190 | def draw(self, context):
191 | layout = self.layout
192 |
193 | rd = context.scene.render
194 |
195 | props = layout.operator("tmb_render.render", text="Render Image",
196 | icon='RENDER_STILL')
197 | props.animation = False
198 | props.use_viewport = True
199 | props = layout.operator("tmb_render.render",
200 | text="Render Animation", icon='RENDER_ANIMATION')
201 | props.animation = True
202 | props.use_viewport = True
203 |
204 | layout.separator()
205 |
206 | layout.operator("sound.mixdown", text="Render Audio...")
207 |
208 | layout.separator()
209 |
210 | layout.operator("render.view_show", text="View Render")
211 | layout.operator("render.play_rendered_anim", text="View Animation")
212 |
213 | layout.separator()
214 |
215 | layout.prop(rd, "use_lock_interface", text="Lock Interface")
216 |
217 | #--------------------------- For test purposes only ----------------------------
218 |
219 | classes = [
220 | TMB_TrueMB,
221 | TMB_PT_true_mb_panel,
222 | ]
223 |
224 | def ui_register():
225 | for cl in classes:
226 | register_class(cl)
227 |
228 | def ui_unregister():
229 | for cl in classes:
230 | unregister_class(cl)
231 |
232 | if __name__ == '__main__':
233 | ui_register()
234 | bpy.types.Scene.true_mb = PointerProperty(type=TMB_TrueMB)
--------------------------------------------------------------------------------
/tmb_uninstall.py:
--------------------------------------------------------------------------------
1 | # ##### BEGIN GPL LICENSE BLOCK #####
2 | #
3 | # This program is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU General Public License
5 | # as published by the Free Software Foundation; either version 2
6 | # of the License, or (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software Foundation,
15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 | #
17 | # ##### END GPL LICENSE BLOCK #####
18 |
19 | # True Motion Blur add-on
20 | # TMB uninstall operators
21 | # (c) 2020 Andrey Sokolov (so_records)
22 |
23 | import bpy
24 |
25 | #------------------------- Bring back native Top Menu --------------------------
26 |
27 | class TOPBAR_MT_render(bpy.types.Menu):
28 | bl_label = "Render"
29 |
30 | def draw(self, context):
31 | layout = self.layout
32 |
33 | rd = context.scene.render
34 |
35 | layout.operator("render.render", text="Render Image",
36 | icon='RENDER_STILL').use_viewport = True
37 | props = layout.operator(
38 | "render.render", text="Render Animation", icon='RENDER_ANIMATION')
39 | props.animation = True
40 | props.use_viewport = True
41 |
42 | layout.separator()
43 |
44 | layout.operator("sound.mixdown", text="Render Audio...")
45 |
46 | layout.separator()
47 |
48 | layout.operator("render.view_show", text="View Render")
49 | layout.operator("render.play_rendered_anim", text="View Animation")
50 |
51 | layout.separator()
52 |
53 | layout.prop(rd, "use_lock_interface", text="Lock Interface")
54 |
55 | #------------------------------ Restore shortcuts ------------------------------
56 |
57 | class TMB_KeyconfigRestore(bpy.types.Operator):
58 | bl_idname = 'tmb.keyconfig_restore'
59 | bl_label = 'Keyconfig'
60 |
61 | def execute(self, context):
62 | try:
63 | bpy.ops.tmb.keyconfig()
64 | except:
65 | pass
66 | configs = context.window_manager.keyconfigs
67 | items = configs.active.keymaps['Screen'].keymap_items
68 | keymap = [i for i in items if i.idname == "tmb_render.render"]
69 | for i in keymap:
70 | i.idname = "render.render"
71 | i.properties.use_viewport = True
72 | keymap[1].properties.animation = True
73 | return {'FINISHED'}
--------------------------------------------------------------------------------