diff --git a/.vscode/settings.json b/.vscode/settings.json index b6a972b..96bd814 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "istream": "cpp", "variant": "cpp", "tuple": "cpp", - "iostream": "cpp" + "iostream": "cpp", + "string_view": "cpp" } } \ No newline at end of file diff --git a/Game.cpp b/Game.cpp index cc7af75..e358c3f 100644 --- a/Game.cpp +++ b/Game.cpp @@ -295,7 +295,6 @@ class App : public OgreBites::ApplicationContext { SkyBoxRenderer *sky; bool mGrab; KeyboardListener mKbd; - bool enabldDbgDraw; public: App() @@ -304,7 +303,6 @@ public: , mDynWorld(new Ogre::Bullet::DynamicsWorld( Ogre::Vector3(0, -9.8, 0))) , mGrab(false) - , enabldDbgDraw(false) { } virtual ~App() @@ -404,8 +402,10 @@ public: std::cout << "Create content" << "\n"; createContent(); std::cout << "Setup done" << "\n"; +#if 0 mDbgDraw->setDebugMode(mDbgDraw->getDebugMode() | btIDebugDraw::DBG_DrawContactPoints); +#endif } Ogre::SceneManager *getSceneManager() { @@ -440,10 +440,8 @@ public: } ECS::update(delta); - /* - if (enabldDbgDraw) + if (ECS::get().enableDbgDraw) mDbgDraw->update(); - */ } class InputListenerChainFlexible : public OgreBites::InputListener { protected: @@ -737,11 +735,12 @@ public: } void enableDbgDraw(bool enable) { - enabldDbgDraw = enable; + ECS::get_mut().enableDbgDraw = enable; + ECS::modified(); } bool isEnabledDbgDraw() const { - return enabldDbgDraw; + return ECS::get().enableDbgDraw; } }; void KeyboardListener::frameRendered(const Ogre::FrameEvent &evt) diff --git a/assets/blender/scripts/blender2ogre/.github/ISSUE_TEMPLATE/bug_report.md b/assets/blender/scripts/blender2ogre/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..84a5d17 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve `blender2ogre` +title: '' +labels: '' +assignees: '' +--- + + + +#### System Information +- Ogre Version: :grey_question: +- Operating System / Platform: :grey_question: +- Blender Version: :grey_question: + +#### Detailed description + + +#### blender2ogre.log + + +#### OgreXMLConverter.log or OgreMeshTool.log + diff --git a/assets/blender/scripts/blender2ogre/.github/workflows/ci-build.yml b/assets/blender/scripts/blender2ogre/.github/workflows/ci-build.yml new file mode 100644 index 0000000..80ac503 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/.github/workflows/ci-build.yml @@ -0,0 +1,25 @@ +name: CI Build +on: + push: + branches: [master] + pull_request: + branches: [master] +jobs: + linux: + runs-on: ubuntu-22.04 + steps: + - name: Install Dependencies + run: | + sudo apt update + sudo apt install -y blender ogre-1.12-tools + - uses: actions/checkout@v2 + - name: Test + run: | + mkdir -p ~/.config/blender/3.0/scripts/addons/ + ln -s `pwd`/io_ogre ~/.config/blender/3.0/scripts/addons/ + blender examples/armature-test.blend -b --python test/run.py + # verify that files were created + test -f test.scene + test -f Cube.mesh + test -f Cube.skeleton + test -f Material.material diff --git a/assets/blender/scripts/blender2ogre/.gitignore b/assets/blender/scripts/blender2ogre/.gitignore new file mode 100644 index 0000000..66ec0aa --- /dev/null +++ b/assets/blender/scripts/blender2ogre/.gitignore @@ -0,0 +1,26 @@ +syntax: glob +# This line is a comment, and will be skipped. +# Empty lines are skipped too. + +# Backup/Lock files left behind by the Emacs editor. +*~ +\#*.*\# + +# Lock files used by the Emacs editor. +# Notice that the "#" character is quoted with a backslash. +# This prevents it from being interpreted as starting a comment. +.\#* + +# Temporary files used by the vim editor. +.*.swp + +# A hidden file created by the Mac OS X Finder. +.DS_Store + +# python bytecode +*.pyc + +__pycache__ +_ogre_debug.txt +test/blender/ + diff --git a/assets/blender/scripts/blender2ogre/CHANGELOG.txt b/assets/blender/scripts/blender2ogre/CHANGELOG.txt new file mode 100644 index 0000000..becf341 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/CHANGELOG.txt @@ -0,0 +1,68 @@ +CHANGELOG + 0.6.2 + * pulled fixed added to the monoltic python file + * added exporting of custom vertex groups + * console export documented + 0.6.1 + * code refactored, tested to work with 2.71 + 0.6.0 + * patched to work with 2.66. + 0.5.9 + * apply patch from Thomas for Blender 2.6x support + 0.5.8 + * Clean all names that will be used as filenames on disk. Adjust all places + that use these names for refs instead of ob.name/ob.data.name. Replaced chars + are \, /, :, *, ?, ", <, >, | and spaces. Tested on work with ogre + material, mesh and skeleton writing/refs inside the files and txml refs. + Shows warning at final report if we had to resort to the renaming so user + can possibly rename the object. + * Added silent auto update checks if blender2ogre was installed using + the .exe installer. This will keep people up to date when new versions are out. + * Fix tracker issue 48: Needs to check if outputting to /tmp or + ~/.wine/drive_c/tmp on Linux. Thanks to vax456 for providing the patch, + added him to contributors. Preview mesh's are now placed under /tmp + on Linux systems if the OgreMeshy executable ends with .exe + * Fix tracker issue 46: add operationtype to + * Implement a modal dialog that reports if material names have invalid + characters and cant be saved on disk. This small popup will show until + user presses left or right mouse (anywhere). + * Fix tracker issue 44: XML Attributes not properly escaped in .scene file + * Implemented reading OgreXmlConverter path from windows registry. + The .exe installer will ship with certain tools so we can stop guessing + and making the user install tools separately and setting up paths. + * Fix bug that .mesh files were not generated while doing a .txml export. + This was result of the late 2.63 mods that forgot to update object + facecount before determining if mesh should be exported. + * Fix bug that changed settings in the export dialog were forgotten when you + re-exported without closing blender. Now settings should persist always + from the last export. They are also stored to disk so the same settings + are in use when if you restart Blender. + * Fix bug that once you did a export, the next time the export location was + forgotten. Now on sequential exports, the last export path is remembered in + the export dialog. + * Remove all local:// from asset refs and make them relative in .txml export. + Having relative refs is the best for local preview and importing the txml + to existing scenes. + * Make .material generate what version of this plugins was used to generate + the material file. Will be helpful in production to catch things. + Added pretty printing line endings so the raw .material data is easier to read. + * Improve console logging for the export stages. Run Blender from + cmd prompt to see this information. + * Clean/fix documentation in code for future development + * Add todo to code for future development + * Restructure/move code for easier readability + * Remove extra white spaces and convert tabs to space + 0.5.7 + * Update to Blender 2.6.3. + * Fixed xz-y Skeleton rotation (again) + * Added additional Keyframe at the end of each animation to prevent + ogre from interpolating back to the start + * Added option to ignore non-deformable bones + * Added option to export nla-strips independently from each other + +TODO + * Remove this section and integrate below with code :) + * Fix terrain collision offset bug + * Add realtime transform (rotation is missing) + * Fix camera rotated -90 ogre-dot-scene + * Set description field for all pyRNA diff --git a/assets/blender/scripts/blender2ogre/CustomSplitNormals.md b/assets/blender/scripts/blender2ogre/CustomSplitNormals.md new file mode 100644 index 0000000..e32bbef --- /dev/null +++ b/assets/blender/scripts/blender2ogre/CustomSplitNormals.md @@ -0,0 +1,82 @@ + +# Custom Split Normals +Custom Split Normals is a way to tweak the mesh shading by pointing normals towards directions other than the default, auto-computed ones. +It is mostly used in game development, where it helps counterbalance some issues generated by low-poly objects +(the most common examples are low-poly trees, bushes, grass, etc. and the ‘rounded’ corners). + +## Documentation + - [Normals - Blender Manual](https://docs.blender.org/manual/en/latest/modeling/meshes/structure.html#modeling-meshes-normals-custom) + +The first step towards working with Custom Normals is having Blender show them. + +Select the object you are working with, enter `Edit Mode`, and in the upper right corner, there is an `Overlays` menu. +In the `Overlays` menu we have the option of displaying "Vertex Normals", "Split Normals" and "Face Normals" + +![Showing the Normals of the default Cube](images/normals/show-custom-split-normals.png) + +In this example, we are showing both "Vertex Normals" (colored blue) and "Split Normals" (colored pink) + +## Custom Normals +[Blender Normal Editing TOOLS (In 2 Minutes!!)](https://www.youtube.com/watch?v=hwhM437Xvks) + +Custom Normals refer to the feature of being able to modify the vertex normals as one sees fit. +This can be useful for example in anime characters that usually need specific shading profiles to look good with the toon shader. +One way to accomplish that is by editing the normals of the Mesh. + +There are many ways to edit the Normals, one option is to use modifiers and another is to edit the normals directly. + +![Blenders Normal Editing Menu](images/normals/normals-menu.png) + +Please consult the video and documentation to know more about editing normals in this way + +## Sharp Edges +One of the uses for Custom Normals is to have hard edges on an otherwise smooth mesh. +This allows for a simpler geometry since a smooth mesh gets exported to fewer vertices. +(That is because to have flat shading in OGRE, there have to be as many vertices as normals) + +![Smooth vs Flat shading](images/normals/smooth-vs-flat-shading.png) + +In the Smooth vs Flat shading world, there is a third option in Blender: Auto Smooth, or Smooth with hard edges + +![Smooth vs Flat vs Auto Smooth shading](images/normals/flat-vs-smooth-vs-auto-smooth.png) + +Thanks to the Auto Smooth feature, it is possible to have hard edges but also smooth ones in the same mesh. +(This example mesh was made by adding a `Boolean` modifier to the Cube and then performing a Union operation with the Cylinder) + +To accomplish this kind of shading, select the Object and make sure you are in `Object Mode`. +Then set the Object shading to "Shade Smooth" (Object -> Shade Smooth (Blender 2.8+)) + +![Auto Smooth and Split Normals menu](images/normals/auto-smooth-and-split-normals.png) + +Then enable "Auto Smooth": (▽ -> Normals -> Auto Smooth) and choose a proper angle. + +If you are happy with the results of "Auto Smooth", then enter `Edit Mode` and click on `Add Custom Split Normal Data` (▽ -> Geometry Data -> Add Custom Split Normals Data). + +With this operation, Blender automatically marks the Sharp Edges in the mesh and stores the "Custom Split Normal Data" for `blender2ogre`. + +If you need to mark other Edges as sharp, because the "Auto Smooth" is insufficient then it is possible to mark more Edges as sharp. + +Make sure you are in `Edit Mode` and `Edge Selection Mode`, select the Edges you want to mark as sharp, and then select the option `Edge->Mark Sharp` + +![Showing Sharp Edges](images/normals/sharp-edges-marked.png) + + +## Fillet Edges +[A short explanation about custom vertex normals](https://polycount.com/discussion/154664/a-short-explanation-about-custom-vertex-normals-tutorial) +[Blender: Understanding Custom Split Normals](https://www.youtube.com/watch?v=o-jr_q_pkF0) +Another use for Custom Normals is to have smooth edges (also called Fillet Edges). + +![Object with Fillet Edges](images/normals/fillet-edges.png) + +To get this kind of shading, select the object. + +Add the `Bevel` Modifier, then add the `Weighted Normal` modifier. + +![Fillet Edges Modifiers](images/normals/fillet-edges-modifiers.png) + +The option `Clamp Overlap` should be disabled in some cases where the geometry of the mesh is more complex. + +For the `Weighted Normal` modifier to work well, you have to enable "Auto Smooth": (▽ -> Normals -> Auto Smooth) + +> NOTE: Until this is fixed, for the Fillet Edges to look correct in the exported Mesh you have to create a copy of the object, apply the modifiers then go into `Edit Mode` and perform a manual triangulation of the Mesh (Face -> Triangulate Faces or Ctrl-T) + diff --git a/assets/blender/scripts/blender2ogre/LICENSE.txt b/assets/blender/scripts/blender2ogre/LICENSE.txt new file mode 100644 index 0000000..05924b7 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/LICENSE.txt @@ -0,0 +1,140 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + +a) The modified work must itself be a software library. +b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. +c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. +d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) +b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. +c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. +d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. +e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. +b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/assets/blender/scripts/blender2ogre/MaterialsJSON.md b/assets/blender/scripts/blender2ogre/MaterialsJSON.md new file mode 100644 index 0000000..30e1485 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/MaterialsJSON.md @@ -0,0 +1,72 @@ +# OGRE Next JSON Materials +Support implemented based on the Ogre Next documentation. +https://ogrecave.github.io/ogre-next/api/latest/hlmspbsdatablockref.html + +Only the metallic workflow is supported at this time. + +## Metallic Workflow +Metalness texture fetching expects a single image with the metal +texture in the Blue channel and the roughness texture in the Green +channel. The channels are expected to have been split via a 'Separate RGB' node +before passing to the Principled BSDF. This is in line with the glTF standard +setup. + +## Specular Workflow +Unsupported. + +## Unsupported features + +### emissive.use_emissive_lightmap +This requires features only found in Blender 2.9+ + +### fresnel +This is used in the Specular workflows supported by Ogre. Right now we +only support the metallic workflow. + +### blendblock +Blendblocks are used for advanced effects and don't fit into the +standard Blender workflow. One commmon use would be to have better +alpha blending on complex textures. Limit of 32 blend blocks at +runtime also means we shouldn't "just generate them anyway." +doc: https://ogrecave.github.io/ogre-next/api/latest/hlmsblendblockref.html + +### macroblock +Macroblocks are used for advanced effects and don't fit into the +standard Blender workflow. One common use would be to render a skybox +behind everything else in a scene. Limit of 32 macroblocks at runtime +also means we shouldn't "just generate them anyway." +doc: https://ogrecave.github.io/ogre-next/api/latest/hlmsmacroblockref.html + +### sampler +Samplerblocks are used for advanced texture handling like filtering, +addressing, LOD, etc. These settings have signifigant visual and +performance effects. Limit of 32 samplerblocks at runtime also means +we shouldn't "just generate them anyway." + +### recieve_shadows +No receive shadow setting in Blender 2.8+ but was available in 2.79. +We leave this unset which defaults to true. Maybe add support in +the 2.7 branch? +See: https://docs.blender.org/manual/en/2.79/render/blender_render/materials/properties/shadows.html#shadow-receiving-object-material + +### shadow_const_bias +Leave shadow const bias undefined to default. It is usually used to +fix specific self-shadowing issues and is an advanced feature. + +### brdf +Leave brdf undefined to default. This setting has huge visual and +performance impacts and is for specific use cases. +doc: https://ogrecave.github.io/ogre-next/api/latest/hlmspbsdatablockref.html#dbParamBRDF + +### reflection +Leave reflection undefined to default. In most cases for reflections +users will want to use generated cubemaps in-engine. + +### detail_diffuse[0-3] +Layered diffuse maps for advanced effects. + +### detail_normal[0-3] +Layered normal maps for advanced effects. + +### detail_weight +Texture acting as a mask for the detail maps. diff --git a/assets/blender/scripts/blender2ogre/MeshTriangulation.md b/assets/blender/scripts/blender2ogre/MeshTriangulation.md new file mode 100644 index 0000000..98be1d3 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/MeshTriangulation.md @@ -0,0 +1,91 @@ + +# Mesh Triangulation + +While Blender can create meshes featuring triangles, quads and n-gons, OGRE3D being a realtime rendering engine deals only with triangles. +That means that blender2ogre needs to perform a process of mesh triangulation to export the meshes. +Sometimes this triangulation process leads to some problems described here. + +## Documentation + - [How can I manually fix nonplanar geometry?](https://blender.stackexchange.com/questions/119874/how-can-i-manually-fix-nonplanar-geometry) + - [How to flatten a face to avoid distortion (make an ngon planar)](https://blender.stackexchange.com/questions/7729/how-to-flatten-a-face-to-avoid-distortion-make-an-ngon-planar) + - [How do I fix this issue with weird faces?](https://blender.stackexchange.com/questions/55466/how-do-i-fix-this-issue-with-weird-faces) + - [Mesh Analysis - Blender Manual](https://docs.blender.org/manual/en/latest/modeling/meshes/mesh_analysis.html) + - [When should N-Gons be used, and when shouldn't they?](https://blender.stackexchange.com/questions/89/when-should-n-gons-be-used-and-when-shouldnt-they) + - [Exporting Blender models for Godot](https://tam7t.com/blender-godot-export) + +## Non-coplanar Faces +The first problem happens when during the process of creating a mesh an artist ends up with faces that are not coplanar. +This means that the vertices of the face are not all on the same plane. +For a triangle, the vertices are always on the same plane, but for quads and n-gons that is not necessarily true. + +When a mesh has non-coplanar faces, the resulting triangulated mesh looks different than the original. + +For a simple example, take a look at Blenders' Suzanne (this mesh has almost 90% non-coplanar faces) with and without triangulation + +Suzanne without triangulation | Suzanne with `Triangulate` modifier +:-------------------------:|:-------------------------: +![Suzanne without triangulation](images/triangulate/suzanne-original.png) | ![Suzanne with `Triangulate` modifier](images/triangulate/suzanne-triangulated.png) + +Blender has tools to detect this type of problems in the mesh analysis section of the Viewport Overlays. + +Select the object, then go into `Edit Mode` and in the upper right corner, there is an `Overlays` menu. + +![Viewport Overlays](images/triangulate/viewport-overlays.png) + +Select `Mesh Analysis` and then Type: `Distortion` + +Blender will show all the faces that are non-coplanar and how much they deviate from a planar face. + +![Viewport Overlays](images/triangulate/suzanne-distortion.png) + +Blender also has a tool to help with this issue. + +While in `Edit Mode`, select Mesh -> Clean Up -> Make Planar Faces + +It might be necessary to perform this action many times since it is a progressive improvement if there are many non-coplanar faces. + +So increasing the factor and the iterations might help, the factor goes from [-10, 10] and the iterations go from [1, 10.000] + +```python +import bpy + +bpy.ops.mesh.face_make_planar(factor=1, repeat=10) +``` + +### Shape Keys + +An issue caused by having non-coplanar faces is that since the triangulated mesh looks different from the original, the shape keys will tend to look more like the original mesh. +That is because the data that Blender is providing to the exporter (particularly the normals) is not triangulated because the modifier does not apply to the Shape Keys. +The result is that when using the Shape Keys or Shape Animations in OGRE the mesh changes in unintended ways. + +In the following example, we created a Shape Key where Suzanne is worried. +When applying the Shape Key in OGRE the normals are that of the original mesh and so it "looks" like the triangulation is gone. + +Suzanne with `Triangulate` modifier | Suzanne in "worried" pose +:-------------------------:|:-------------------------: +![Suzanne with `Triangulate` modifier](images/triangulate/suzanne-triangulated.png) | ![Suzanne in "worried" pose](images/triangulate/suzanne-worried.png) + +The fix is to avoid using non-coplanar faces or trying to fix them when they appear as a result of the modeling process. + + +## Custom Normals +Bugs reported to https://developer.blender.org + - [Triangulate modifier breaks custom normals](https://developer.blender.org/T61942) + - [Triangulate faces sometimes produce degenerate triangle faces](https://developer.blender.org/T103913) + - [Triangulate non-planar faces with "beauty" method produces faces that point in different directions](https://developer.blender.org/T85402) + - [Shading is broken with Custom Normals + Triangulate Modifier](https://developer.blender.org/T104244) + +There are outstanding issues when using custom normals and Blenders' mesh triangulation, this is not only relegated to mesh triangulation but other tools as well. + +So be careful when using Custom Normals (that is modifying normas by using some Modifier like Weighted Normal or directly editing the Normals in `Edit Mode`). + +One way to check if the exported object will look good is to add `Triangulate` modifier with quad method `Fixed`, if the shading looks wrong compared to removing the modifier then it will look wrong in OGRE as well since the exporter is using Blenders triangulation code to perform the calculations. + +For example, this cube has had a `Bevel` modifier and a `Weighted Normals` modifier applied, then we add a `Triangulate` modifier and the result is that there are visible triangles in the shading of the cube. + +Cube without triangulation | Cube with triangulation +:-------------------------:|:-------------------------: +![Cube without triangulation](images/triangulate/beveled-cube.png) | ![Cube with triangulation](images/triangulate/beveled-broken-cube.png) + +> NOTE: Until this is fixed, for the Fillet Edges to look correct in the exported Mesh you have to create a copy of the object, apply the modifiers then go into `Edit Mode` and perform a manual triangulation of the Mesh (Face -> Triangulate Faces or Ctrl-T) + diff --git a/assets/blender/scripts/blender2ogre/Modifiers.md b/assets/blender/scripts/blender2ogre/Modifiers.md new file mode 100644 index 0000000..6874f3e --- /dev/null +++ b/assets/blender/scripts/blender2ogre/Modifiers.md @@ -0,0 +1,105 @@ + +# Blender Modifiers +Modifiers are automatic operations that affect an object’s geometry in a non-destructive way. +With modifiers, you can perform many effects automatically that would otherwise be too tedious to do manually +(such as subdivision surfaces) and without affecting the base geometry of your object. + +`blender2ogre` supports exporting meshes with modifiers, but not all modifiers are supported. +Also, some modifiers have special treatment (Array and Armature), please check the corresponding sections + +> NOTE: Support for modifiers is *best effort*, in most cases the modifiers have been tested individually and not all combinations have been tried. + +> **WARNING**: Beware of using Modifiers that increase the vertex o poly count of the models when exporting (like Subdivision Surface) since the exported mesh might not be very optimal for realtime rendering. Retopology is advised in these cases to improve render times. + +## Index + - [Documentation](#documentation) + - [Modify type Modifiers](#modify-type-modifiers) + - [Generate type Modifiers](#generate-type-modifiers) + - [Deform type Modifiers](#deform-type-modifiers) + - [Array Modifier](#array-modifier) + - [Boolean Modifier](#boolean-modifier) + +## Documentation + - [Modifiers - Introduction; Blender Manual](https://docs.blender.org/manual/en/latest/modeling/modifiers/introduction.html) + +## Modify type Modifiers +Modifier | Supported | Notes +:-------:|:---------:|:----: +[Data Transfer](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/data_transfer.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK, with normals properly modified +[Mesh Cache](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/mesh_cache.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but the exported mesh won't be animated. Please check [XXX] to see how to bake Animations +[Mesh Sequence Cache](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/mesh_sequence_cache.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but the exported mesh won't be animated. Please check [XXX] to see how to bake Animations +[Normal Edit](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/normal_edit.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK, with normals properly modified +[UV Project](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/uv_project.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK and UV Maps are projected, although any UV animations made in Blender won't be exported and are not supported in OGRE. +[UV Warp](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/uv_warp.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK and UV Maps are warped, although any UV animations made in Blender won't be exported and are not supported in OGRE. +[Vertex Weight Edit](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/weight_edit.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK, with normals properly modified +[Vertex Weight Mix](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/weight_mix.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK, with normals properly modified +[Vertex Weight Proximity](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/weight_proximity.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but it does not do animation like the Blender example shows +[Weighted Normals](https://docs.blender.org/manual/en/latest/modeling/modifiers/modify/weighted_normal.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but affected by [Blenders' triangulation bug](MeshTriangulation.md) + +## Generate type Modifiers +Modifier | Supported | Notes +:-------:|:---------:|:----: +[Array](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/array.html) | ![Supported](images/modifiers/ok.png) | Has full support: [Array Modifier](#array-modifier) +[Bevel](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/bevel.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK with bevel +[Boolean](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/booleans.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK with boolean operation applied, but check [Boolean Modifier](#boolean-modifier) section for more information +[Build](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/build.html) | ![Not Supported](images/modifiers/fail.png) | Can't export a mesh with varying vertex count +[Decimate](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/decimate.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK properly decimated +[Edge Split](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/edge_split.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK with modified normals +[Geometry Nodes](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/geometry_nodes.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK with the proper geometry +[Mask](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/mask.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK with applied mask +[Mirror](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/mirror.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK with applied mirroring +[Multiresolution](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/multiresolution.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK, the `Level Viewport` parameter should be more than 0, otherwise the base mesh will be exported. Also, this potentially exports an insane amount of geometry, you might want to do a retopology and use normal maps to bake the details. +[Remesh](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/remesh.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but UV Maps are removed from the Mesh +[Screw](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/screw.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but the base object has to be a mesh otherwise nothing is exported +[Skin](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/skin.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but UV Maps are removed from the Mesh +[Solidify](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/solidify.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK with thickness added +[Subdivision Surface](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/subdivision_surface.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK, as with the `Multiresolution Modifier` beware of the vertex count of the exported mesh (which affects performance). +[Triangulate](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/triangulate.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but affected by [Blenders' triangulation bug](MeshTriangulation.md) +[Volume to Mesh](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/volume_to_mesh.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but UV Maps are removed from the Mesh +[Weld Modifier](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/weld.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Wireframe](https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/wireframe.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK + +## Deform type Modifiers +Modifier | Supported | Notes +:-------:|:---------:|:----: +[Armature](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/armature.html) | ![Supported](images/modifiers/ok.png) | Has full support: [Exporting Skeletal Animations](SkeletalAnimation.md) +[Cast](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/cast.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Curve](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/curve.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Displace](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/displace.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Hook](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/hooks.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Laplacian Deform](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/laplacian_deform.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Lattice](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/lattice.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Mesh Deform](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/mesh_deform.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Shrinkwrap](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/shrinkwrap.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Simple Deform](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/simple_deform.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Smooth](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/smooth.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Smooth Laplacian](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/laplacian_smooth.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Surface Deform](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/surface_deform.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Volume Displace](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/volume_displace.html) | ![Not Supported](images/modifiers/fail.png) | Only works on volumes, not meshes +[Warp](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/warp.html) | ![Supported](images/modifiers/ok.png) | Mesh exports OK +[Wave](https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/wave.html) | ![Supported*](images/modifiers/warning.png) | Mesh exports OK, but only in the first frame there is no motion. To bake the animation, consult [xxx] + +## Array Modifier +This modifier as well as the `Armature Modifier` get their special section because they are treated differently by `blender2ogre`. +Most modifiers are applied before exporting the model (without affecting the object) by converting an evaluated copy of the object into a mesh. + +However the case of the `Array Modifier` is different, since the presence of this modifier has `blender2ogre` treat the object differently. + +What happens is that in `scene.py` (which creates the .scene output) the `Array Modifier` is used to place instances of the mesh in positions indicated by the modifier. +This means that the exported mesh appearance is not modified by the `Array Modifier`, but only its placement in the scene. + +As a result, there is only one copy of the mesh in the scene that is repeated many times, which could lead to a performance increase if using instancing. + +> NOTE: To disable this behavior and have the `Array Modifier` be applied to the mesh directly, then set the option: `ARRAY` to true in the mesh options + +## Boolean Modifier +This modifier works well and is in principle fully supported, but you might get this error when exporting meshes with the `Boolean Modifier`: +``` +FAILED to assign material to face - you might be using a Boolean Modifier between objects with different materials! [ mesh : Cube ] +``` + +The issue here is that `blender2ogre` has a problem when the two objects that make contact to perform the boolean operation don't have the same material assigned to the faces which enter into contact. + +To solve this you need to assign the same material to the faces which are in contact, this might be as simple as assigning a Material to the whole secondary object or having to do something more complex like assigning the same material to the faces that come into contact by entering `Edit Mode` and assigning the material by hand to each face. + +As a last resort, it is also possible to make a copy by hand of the object by applying the `Boolean Modifier` and exporting that mesh. diff --git a/assets/blender/scripts/blender2ogre/NodeAnimations.md b/assets/blender/scripts/blender2ogre/NodeAnimations.md new file mode 100644 index 0000000..844bf30 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/NodeAnimations.md @@ -0,0 +1,209 @@ + +# Exporting Node Animations + +## Introduction +It is possible to export some animated properties of objects in Blender, that is location, rotation and scale. + +This results in what is called SceneNode Animation (https://ogrecave.github.io/ogre/api/latest/_animation.html#SceneNode-Animation) in Ogre. + +This kind of SceneNode animation is very useful to animate the movement of a camera, a character patrolling in a game and any other kind of animation one can think of. + +The advantage over skeletal animation is that it is more performant due to its simplicity, and the effect (translation/rotation/scale) is applied to the Node and its children (if `setInherit()` is true). + +## About exporting and importing into Ogre +The exported animation data goes into the .scene file describing the whole Scene crafted in Blender, there isn't a serializer for SceneNode Animation data, so this data has to be processed by the DotScene plugin. + +Refer to the DotScene documentation (https://github.com/OGRECave/ogre/tree/master/PlugIns/DotScene) on how to load the .scene file into your Ogre application. + +The animation is being exported frame by frame with Blender doing the interpolation between keyframes. +This has the advantage that any tuning done to the F-Curves is preserved in the exported animation. +Another advantage is that it is not necessary to choose an interpolation method other than IM_LINEAR on the Ogre side, making the animation more performant. + +The disadvantage is that the .scene file is now less human-readable due to the chunk of data from all the animation frames and there is no control of the animation in the code. +Another disadvantage is that the timing of the frames will be interpreted according to Blender's current FPS setting which is by default 24 fps. +This setting can be changed in the `Render Tab`, `Dimensions Panel`, there is a drop-down list named `Frame Rate` where you can select a value or create a new one with custom. + +In a similar fashion to the exporting of Skeletal Animation, every action should go into an NLA Track to have it exported (the name of the exported animation being that of the action). + +The reason for using NLA Tracks is to have better control over what actions should be exported as animations. + +## Animating an object +Here is a short video on how to animate objects in Blender: +[How To Move Animated Objects (Blender Tutorial)](https://www.youtube.com/watch?v=HPD3LhCLxCE) + +We will use the default cube to simplify things. + +Just select the cube and toggle the `Transform` properties with the keyboard shortcut `N`. + +![transform-panel.png](images/transform-panel.png) + +Make sure that the current frame is `1` and with the mouse cursor over one of the `Location` properties, press `I`. + +This has the effect of automatically creating an action called `CubeAction` and inserting a Keyframe at frame `1`. + +Do the same for `Rotation` and `Scale`. + +Then jump to frame `30` and change `Location.Z` to `2m`, `Rotation.Z` to `45º` and `Scale.XYZ` to `1.5`. + +Hover the mouse cursor on `Location`, `Rotation` and `Scale` pressing `I` to insert a Keyframe at frame `30`. + +![cube-action.png](images/cube-action.png) + +Play the animation and you will see the cube go upwards, increase in size 1.5 times and rotate 45º degrees to the right. + +Don't forget to `Push Down` the action from the `Action Editor` to put it into the NLA stack before exporting. + +![push-down.png](images/push-down.png) + +## Having an object follow a path +Here is a short video on how to have an object follow a path in Blender +[Create Path For Any Object To Follow - Blender (BEGINNERS)](https://www.youtube.com/watch?v=G6NdQGySZhU) + +### Simple Path Following +For this simple tutorial, we will use the default cube to follow a path in Blender. + +Create a new scene and select the default cube. + +Press `Shift-A` to open the `Add` menu and add a circle (`Curve -> Circle`). + +Scale the circle to four times its size by pressing `S` and then move the mouse to increase the size of the circle so you have a decent-sized path. + +Now, select the cube and go to the `Constraints` tab and click on `Add Object Constraint` to add a new constraint. + +Select the `Follow Path` constraint to have the cube following the circle as a path. + +![follow-path-const.png](images/follow-path-const.png) + +Now, set the "BezierCircle" as the `Target`, click on `Follow Curve` and `Fixed Position`. + +With meshes not as symmetrical as the cube you might have to fiddle with the `Forward Axis` to have the meshes front facing the direction of the path. + +To have an animation of the cube following the path, hover the mouse over the `Offset` slider and press `I` to insert a Keyframe at frame 1. + +Select frame 60 and change the value of the `Offset` slider to 1.0 and press `I` to insert a Keyframe at frame 60. + +The cube follows the path but the speed is not constant, to have a constant speed it is necessary to modify the `F-Curve` and set it to linear interpolation. + +To do this, go to the `Graph Editor` and select `Key->Interpolation Mode->Linear`, now the cube will follow the path at a constant speed. + +To have a path that is more sinusoidal than circular it is possible to also use a `Bezier Curve` as the `Target` of the `Follow Path` constraint. + +If you want this path to loop on itself press `Alt-C` when editing the `Bezier Curve` in edit mode (also `E` adds more segments to the curve). + +To export a .scene with the animated cube following the path, the last step is to add an NLA Track so that the exporter knows which action to export. + +Go to the `Dope Sheet`, change the mode to `Action Editor` and select `Push Down` to add the action to the NLA stack. + +![push-down.png](images/push-down.png) + +### Path Following with Skeletal Animation +To have an animated character follow a path there are a couple of things to take into account. + +It is very important to parent the animated character armature to an empty object. +Add the empty object with `Shift-A->Empty->Plain Axes`, then select the armature and afterwards select the empty by pressing `Shift` to have both objects selected in the proper order. +Press `Ctrl-P` to get the `Parent` menu and select `Object (Keep Transform)`. +Having the Armature being a child of the empty object allows us to have the path following be independent of the Armature's actions. + +The second advantage of having the Armature parented to an empty is that it is possible to control the scale of the animated character without breaking the animations. + +Just set the desired scale of the empty object and the armature will inherit that scale, the exported .scene will have an Ogre SceneNode with the name of the empty as a parent of the Armature SceneNode. + +### Path Following with Camera Tracking +To have a camera following a path and tracking a target we will be combining the things learned in the previous sections. + +Create a new scene and delete the default cube. + +Add three empty objects (3 x `Shift-A->Empty->Plain Axes`) and name them as follows: *Tracked*, *CameraBase* and *CameraArm*. + +Parent the empty *CameraArm* to *CameraBase* so that the arm is a child of the base, and then parent the camera to *CameraArm*. + +Clear the camera location and rotation by pressing `Alt-G`, `Alt-R`. + +Add a Monkey mesh with `Shift-A->Mesh->Monkey` to have some objects for the camera to look at. + +Then add a BezierCircle to have a path to follow, scale it and name it *CameraPath*. + +Select the *CameraBase* empty and add a constraint to follow the *CameraPath* bezier curve with `Forward:` -X and `Up:` Z (also check `Follow Curve` and `Fixed Position`). + +Select the *CameraArm* empty and move it upwards so that the camera is looking at the object from a vantage point and not the floor. + +Then select the camera and add a constraint of type `Locked Track`, with `Target:` *Tracked*, `To:` -Z and `Lock:` X. + +The purpose of having the camera track the *Tracked* empty object instead of the Monkey mesh is that now it is not locked to the center of the object. + +If the Monkey object is going to move as well, then the *Tracked* empty object can be made a child of the Monkey object or use more empty objects to move the monkey. + +It is a good general rule to separate movements into independent nodes as was done with *CameraBase* and *CameraArm* which are generally used to separate the yaw and pitch of the camera. + +Now, select *CameraBase* and repeat the process as in the previous tutorial to have the *CameraBase* empty follow the circular path, in the process the camera will keep on tracking the *Tracked* empty object. + +It should be noted that Ogre has a `setAutoTracking()` function for the SceneNodes, which would be a preferable way to track objects. + +Please consult the manual for more details: https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_node.html#ae4c8588895d3623bbe0007ea157af1a4 + +## Using Node Animations in your Ogre app +If you are using DotScene to load the .scene into your Ogre app, by default the animations won't play because they are disabled by default. + +Besides enabling the animations it is also required to `addTime()` to their *AnimationStates*, we will use a controller for that. + +``` +int main() +{ + ... + auto& controllerMgr = Ogre::ControllerManager::getSingleton(); + + for (auto animationState : mSceneMgr->getAnimationStates()) + { + // Print the animation name + std::cout << "animationState: " << animationState.second->getAnimationName() << std::endl; + + // Enable the Animation State (they are disabled by default) + animationState.second->setEnabled(true); + + // Set the animation to looping (if your node animation loops) + animationState.second->setLoop(true); + + // Create a controller to pass the frame time to the Animation State, otherwise the animation won't play + // (this is a better method than using animationState->addTime() in your main loop) + controllerMgr.createFrameTimePassthroughController(Ogre::AnimationStateControllerValue::create(animationState.second, true)); + } +} +``` + +Consult the Ogre manual for further information: + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_manager.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_animation_state.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_node.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_controller_manager.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_controller.html + +## Troubleshooting +Some tips for troubleshooting: + - Make sure that the actions/animations you want to export have their corresponding NLA tracks associated with the object. + - To use Skeletal Animation combined with the path following remember to have the armature be a child of an empty to avoid problems. + - Check the .scene file to see if the animation was exported as expected, the problem might not be in the exported data. + +## Automation +If you have many actions it can become tiresome to push every one of them into the NLA stack to have them exported. + +Here is a very simple Blender script to add every action as an NLA track, any action which should not be exported can simply be deleted from the list of NLA Tracks + +Switch to the `Scripting` layout and copy the following script: + +``` +import bpy + +def add_NLA_strips(object): + + for action in bpy.data.actions: + track = bpy.data.objects[object].animation_data.nla_tracks.new() + track.strips.new(action.name, action.frame_range[0], action) + track.name = action.name + +# Add actions from a single object: +add_NLA_strips('Cube') + +# Add actions for every object: +for object in bpy.data.objects: + add_NLA_strips(object) +``` diff --git a/assets/blender/scripts/blender2ogre/Options.md b/assets/blender/scripts/blender2ogre/Options.md new file mode 100644 index 0000000..6a83e8d --- /dev/null +++ b/assets/blender/scripts/blender2ogre/Options.md @@ -0,0 +1,219 @@ + +# `blender2ogre` Options + +## Index + - [Exporter](#exporter) + - [Exporter Options](#exporter-options) + - [Exporter Script](#exporter-script) + - [Importer](#importer) + - [Importer Options](#importer-options) + - [Importer Script](#importer-script) + +## Exporter + +### Exporter Options +Option|Name|Description|Default Value +|---|---|---|---| +|**General**| +|EX_SWAP_AXIS|Swap Axis|Axis swapping mode|'xyz'| +|EX_V2_MESH_TOOL_VERSION|Mesh Export Version|Specify Ogre version format to write|'v2'| +|EX_XML_DELETE|Clean up xml files|Remove the generated xml files after binary conversion.[^1]|True| +|**Scene**| +|EX_SCENE|Export Scene|Export current scene (OgreDotScene xml file)|True| +|EX_SELECTED_ONLY|Export Selected Only|Export only selected objects. Turn on to avoid exporting non-selected stuff|True| +|EX_EXPORT_HIDDEN|Export Hidden Also|Export hidden meshes in addition to visible ones. Turn off to avoid exporting hidden stuff|True| +|EX_EXPORT_USER|Export User Properties|Export user properties such as as physical properties. Turn off to avoid exporting the user data|True| +|EX_FORCE_CAMERA|Force Camera|Export active camera|True| +|EX_FORCE_LAMPS|Force Lamps|Export all Lamps|True| +|EX_NODE_ANIMATION|Export Node Animations|Export Node Animations, these are animations of the objects properties like position, rotation and scale|True| +|**Materials**| +|EX_MATERIALS|Export Materials|Exports .material scripts|True| +|EX_SEPARATE_MATERIALS|Separate Materials|Exports a .material for each material (rather than putting all materials into a single .material file)|True| +|EX_COPY_SHADER_PROGRAMS|Copy Shader Programs|When using script inheritance copy the source shader programs to the output path|True| +|EX_USE_FFP_PARAMETERS|Fixed Function Parameters|Convert material parameters to Blinn-Phong model|False| +|**Textures**| +|EX_DDS_MIPS|DDS Mips|Number of Mip Maps (DDS)|16| +|EX_FORCE_IMAGE_FORMAT|Convert Images|Convert all textures to selected image format|'NONE'| +|**Armature**| +|EX_ARMATURE_ANIMATION|Armature Animation|Export armature animations (updates the .skeleton file)|True| +|EX_SHARED_ARMATURE|Shared Armature|Export a single .skeleton file for objects that have the same Armature parent[^2]|False| +|EX_ONLY_KEYFRAMES|Only Keyframes|Only export Keyframes.[^3]|False| +|EX_ONLY_DEFORMABLE_BONES|Only Deformable Bones|Only exports bones that are deformable.[^4]|False| +|EX_ONLY_KEYFRAMED_BONES|Only Keyframed Bones|Only exports bones that have been keyframed for a given animation. Useful to limit the set of bones on a per-animation basis|False| +|EX_OGRE_INHERIT_SCALE|OGRE Inherit Scale|Whether the OGRE bones have the 'inherit scale' flag on.[^5]|False| +|EX_TRIM_BONE_WEIGHTS|Trim Weights|Ignore bone weights below this value (Ogre supports 4 bones per vertex)|0.01| +|**Mesh Options**| +|EX_MESH|Export Meshes|Export meshes|True| +|EX_MESH_OVERWRITE|Export Meshes (overwrite)|Export meshes (overwrite existing files)|True| +|EX_ARRAY|Optimise Arrays|Optimise array modifiers as instances (constant offset only)|True| +|EX_V1_EXTREMITY_POINTS|Extremity Points|[^6]|0| +|EX_Vx_GENERATE_EDGE_LISTS|Generate Edge Lists|Generate Edge Lists (for Stencil Shadows)|False| +|EX_GENERATE_TANGENTS|Tangents|Export tangents generated by Blender[^7]|0| +|EX_Vx_OPTIMISE_ANIMATIONS|Optimise Animations|DON"T optimise out redundant tracks & keyframes|True| +|EX_V2_OPTIMISE_VERTEX_BUFFERS|Optimise Vertex Buffers For Shaders|Optimise vertex buffers for shaders.[^8]|True| +|EX_V2_OPTIMISE_VERTEX_BUFFERS_OPTIONS|Vertex Buffers Options|Used when optimizing vertex buffers for shaders.[^9]|'puqs'| +|**LOD**| +|EX_LOD_LEVELS|LOD Levels|Number of LOD levels|0| +|EX_LOD_DISTANCE|LOD Distance|Distance increment to reduce LOD|300| +|EX_LOD_PERCENT|LOD Percentage|LOD percentage reduction|40| +|EX_LOD_MESH_TOOLS|Use OgreMesh Tools|Use OgreMeshUpgrader/OgreMeshTool instead of Blender to generate the mesh LODs.[^10]|False| +|**Pose Animation**| +|EX_SHAPE_ANIMATIONS|Shape Animation|Export shape animations (updates the .mesh file)|True| +|EX_SHAPE_NORMALS|Shape Normals|Export normals in shape animations (updates the .mesh file)|True| +|**Logging**| +|EX_Vx_ENABLE_LOGGING|Write Exporter Logs|Write Log file to the output directory (blender2ogre.log)|False| +|EX_Vx_DEBUG_LOGGING|Debug Logging|Whether to show DEBUG log messages|False| + +[^1]: The removal will only happen if OgreXMLConverter/OgreMeshTool finishes successfully +[^2]: This is useful for using with: `shareSkeletonInstanceWith()` + NOTE: The name of the.skeleton file will be that of the Armature +[^3]: Exported animation won't be affected by Inverse Kinematics, Drivers and modified F-Curves +[^4]: Useful for hiding IK-Bones used in Blender. + NOTE: Any bone with deformablechildren/descendants will be output as well +[^5]: If the animation has scale in it, the exported animation needs to be adjusted to account for the state of the inherit-scale flag in OGRE +[^6]: Submeshes can have optional "extremity points" stored with them to allow submeshes to be sorted with respect to each other in the case of transparency. + For some meshes with transparent materials (partial transparency) this can be useful +[^7]: Options: + '0': Do not export + '3': Generate + '4': Generate with parity +[^8]: See Vertex Buffers Options for more settings +[^9]: Available flags are: + p - converts POSITION to 16-bit floats. + q - converts normal tangent and bitangent (28-36 bytes) to QTangents (8 bytes). + u - converts UVs to 16-bit floats. + s - make shadow mapping passes have their own optimised buffers. Overrides existing ones if any. + S - strips the buffers for shadow mapping (consumes less space and memory) +[^10]: OgreMeshUpgrader/OgreMeshTool does LOD by removing edges, which allows only changing the index buffer and re-use the vertex-buffer (storage efficient). + Blenders decimate does LOD by collapsing vertices, which can result in a visually better LOD, but needs different vertex-buffers per LOD. + +### Exporter Script +This is an example exporting script with all the options and their default values + +```python +import bpy + +bpy.ops.ogre.export( +filepath="D:\\tmp\\NormalsExport\\blender2ogre.scene", + +# General +EX_SWAP_AXIS='xz-y', +# - 'xyz': No swapping +# - 'xz-y'OGRE Standard +# - '-xzy': Non standard +EX_V2_MESH_TOOL_VERSION='v2', +# - 'v1': Export the mesh as a v1 object +# - 'v2': Export the mesh as a v2 object +EX_XML_DELETE=True, + +# Scene +EX_SCENE=True, +EX_SELECTED_ONLY=True, +EX_EXPORT_HIDDEN=True, +EX_FORCE_CAMERA=True, +EX_FORCE_LAMPS=True, +EX_NODE_ANIMATION=True, + +# Materials +EX_MATERIALS=True, +EX_SEPARATE_MATERIALS=True, +EX_COPY_SHADER_PROGRAMS=True, + +# Textures +EX_DDS_MIPS=16, +EX_FORCE_IMAGE_FORMAT='NONE', + +# Armature +EX_ARMATURE_ANIMATION=True, +EX_SHARED_ARMATURE=False, +EX_ONLY_KEYFRAMES=False, +EX_ONLY_DEFORMABLE_BONES=False, +EX_ONLY_KEYFRAMED_BONES=False, +EX_OGRE_INHERIT_SCALE=False, +EX_TRIM_BONE_WEIGHTS=0.01, + +# Mesh Options +EX_MESH=True, +EX_MESH_OVERWRITE=True, +EX_ARRAY=True, +EX_V1_EXTREMITY_POINTS=0, +EX_Vx_GENERATE_EDGE_LISTS=False, +EX_GENERATE_TANGENTS='0', +# - '0': Do not export +# - '3': Generate +# - '4': Generate with parity +EX_Vx_OPTIMISE_ANIMATIONS=True, +EX_V2_OPTIMISE_VERTEX_BUFFERS=True, +EX_V2_OPTIMISE_VERTEX_BUFFERS_OPTIONS="puqs", + +# LOD +EX_LOD_LEVELS=0, +EX_LOD_DISTANCE=300, +EX_LOD_PERCENT=40, +EX_LOD_MESH_TOOLS=False, + +# Pose Animation +EX_SHAPE_ANIMATIONS=True, +EX_SHAPE_NORMALS=True, + +# Logging +EX_Vx_ENABLE_LOGGING=True, +EX_Vx_DEBUG_LOGGING=True +) +``` + +## Importer + +### Importer Options +Option|Name|Description|Default Value +|---|---|---|---| +|**General**| +|IM_SWAP_AXIS|Swap Axis|Axis swapping mode|'xyz'| +|IM_V2_MESH_TOOL_VERSION|Mesh Import Version|Specify Ogre version format to read|'v2'| +|IM_XML_DELETE|Clean up xml files|Remove the generated xml files after binary conversion.[^11]|True| +|**Mesh**| +|IM_IMPORT_NORMALS|Import Normals|Import custom mesh normals|True| +|IM_MERGE_SUBMESHES|Merge Submeshes|Whether to merge submeshes to form a single mesh with different materials|True| +|**Armature**| +|IM_IMPORT_ANIMATIONS|Import animation|Import animations as actions|True| +|IM_ROUND_FRAMES|Adjust frame rate|Adjust scene frame rate to match imported animation|True| +|IM_USE_SELECTED_SKELETON|Use selected skeleton|Link with selected armature object rather than importing a skeleton.[^12]|True| +|**Shape Keys**| +|IM_IMPORT_SHAPEKEYS|Import shape keys|Import shape keys (morphs)|True| +|**Logging**| +|IM_Vx_ENABLE_LOGGING|Write Importer Logs|Write Log file to the output directory (blender2ogre.log)|False| + +[^11]: The removal will only happen if OgreXMLConverter/OgreMeshTool finishes successfully +[^12]: Use this for importing skinned meshes that don't have their own skeleton. + Make sure you have the correct skeleton selected or the weight maps may get mixed up. + +### Importer Script +This is an example importing script with all the options and their default values + +```python +import bpy + +bpy.ops.ogre.import_mesh( +filepath="D:\\tmp\\NormalsExport\\Suzanne.mesh", + +# General +IM_SWAP_AXIS='xz-y', # Axis swapping mode +IM_V2_MESH_TOOL_VERSION='v2', # Specify Ogre version format to read +IM_XML_DELETE=True, # Remove the generated xml files after binary conversion. + +# Mesh +IM_IMPORT_NORMALS=True, # Import custom mesh normals +IM_MERGE_SUBMESHES=True, # Whether to merge submeshes to form a single mesh with different materials + +# Armature +IM_IMPORT_ANIMATIONS=True, # Import animations as actions +IM_ROUND_FRAMES=True, # Adjust scene frame rate to match imported animation +IM_USE_SELECTED_SKELETON=True, # Link with selected armature object rather than importing a skeleton + +# Shape Keys +IM_IMPORT_SHAPEKEYS=True, # Import shape keys (morphs) + +# Logging +IM_Vx_ENABLE_LOGGING=True # Write Log file to the output directory (blender2ogre.log) +) +``` diff --git a/assets/blender/scripts/blender2ogre/ParticleSystem.md b/assets/blender/scripts/blender2ogre/ParticleSystem.md new file mode 100644 index 0000000..a982e5f --- /dev/null +++ b/assets/blender/scripts/blender2ogre/ParticleSystem.md @@ -0,0 +1,124 @@ + +# Exporting Particle Systems + +## Introduction +A common technique for laying out random objects on a scene in Blender is to use the Particle System. +(That is, before Geometry Nodes in Blender 2.92) + +In this tutorial, we are going to explore how to lay out objects in a scene randomly and export that scene for use with OGRE. + +This tutorial is based on the ideas shown in this "CG Geek" video: +[Create Realistic Grass in Blender 2.8 in 15 minutes](https://www.youtube.com/watch?v=-GAm-7_3N6g) + +Terminology: + - *Base Object*: an object with a mesh that represents a terrain or some other form on which we want to place random objects on. + - *Dupli Objects*: an object with a mesh that represents random objects (like grass, trees, rocks, etc.) that we want to place on the Base object. + +The scene in the images is available [here](examples/particle-system.blend), it was made with the A.N.T Landscape Plugin that comes bundled with Blender (River preset) and very simple "Earth" and "Water" materials. + +## Creating and setting up the Particle System +Select the mesh where you want to place your foliage or debris (the *Base Object*). + +Go to the `Particles` tab and click on new to add a new particle system. + +Select type: `Hair` and click on `Advanced` to show the advanced options. + +By default `Hair Length` is set to 4 which makes the debris show 4 x larger so set the value to 1. + +With `Number` it is possible to control the number of objects that will appear randomly on the mesh surface. + +![particle-system1.png](images/particle-sys/particle-system1.png) + +Click on `Rotation` and select `Initial Orientation: Normal`. + +You can change the `Phase` and `Random` values to have the debris show random rotations around the Z axis (tangent space), this way if you have trees they will be showing in different orientations. + +![particle-system2.png](images/particle-sys/particle-system2.png) + +In the `Render` setting change the type to `Object` and in `Dupli Object` select the object you want to duplicate randomly over the mesh. +Check the `Rotation` and `Scale` options so the Particle System will take the *Dupli Objects* rotation and scale into account. +Set `Size` to 1 and choose a `Random Size` value to control the amount of randomness in the size distribution of the *Dupli Objects*. + +![particle-system3.png](images/particle-sys/particle-system3.png) + +At this point, you should see the object appearing randomly in the places where there were hair strands. +But, the objects appear rotated -90 degrees over the Y axis. +To solve this it is necessary to rotate the Dupli Object 90 degrees over the Y-axis to counter this rotation. +Don't apply this rotation to the Dupli Object, otherwise the `blender2ogre` add-on will export the mesh with this rotation applied. +The add-on will automatically apply this rotation to the nodes in the exported scene. + +Another thing to take into account is that the Dupli Object origin should be at its base, otherwise it will appear to be buried halfway. + +## Extras +It is also possible to paint certain parts of the Base Objects mesh to modify the *Dupli Objects* density by using weight painting. +Select the *Base Object*, go into weight paint mode and start painting the areas where you want to increase the *Dupli Objects* density. + +Remember that the *Base Object* should have some tesselation for this to work well, otherwise there might be few vertices to paint. +One way to tesselate is to go into Edit mode and subdivide or activate Dynotopo in Sculpt mode. + +After painting the areas go back to the `Particles` tab and in the `Vertex Groups` section choose the vertex group in `Density` and `Length` (the default vertex group is `Default`). + +![particle-system4.png](images/particle-sys/particle-system4.png) + +## Troubleshooting +Some tips for troubleshooting: + - Don't apply the 90-degree rotation on the Y axis to the Dupli Object, that rotation is just to see the object well placed in Blender + - Make sure that the origin of the Dupli Object is centered in the base + - Apply any scale used on the Dupli and Base Objects + - Check the logs for any errors or warnings (in Windows: Window > Toggle System Console) + +## Automation +After adding the second particle system you will probably be asking for a way to automate the process of adding more foliage/debris layers to the scene. + +That is possible with a very simple Blender script, switch to the `Scripting` layout and copy the following script: +``` +import bpy + +def add_debris_layer( object_name, seed=1, number=100, phase=1, random=1, size=1, size_rnd=1 ): + + bpy.ops.object.particle_system_add() + + index = len(bpy.context.object.particle_systems) - 1 + particle_system = bpy.context.object.particle_systems[index] + + particle_system_settings = particle_system.settings + + particle_system.name = object_name + particle_system_settings.name = object_name + + particle_system_settings.use_advanced_hair = True + + particle_system_settings.type = "HAIR" + particle_system_settings.hair_length = 1 + particle_system_settings.count = number + + particle_system.seed = seed + + particle_system_settings.use_rotations = True + particle_system_settings.rotation_mode = "NOR" + particle_system_settings.phase_factor = phase + particle_system_settings.phase_factor_random = random + + particle_system_settings.render_type = "OBJECT" + particle_system_settings.use_rotation_dupli = True + + particle_system_settings.dupli_object = bpy.data.objects[object_name] + particle_system_settings.particle_size = size + particle_system_settings.size_random = size_rnd + + bpy.ops.object.vertex_group_add() + + index = len(bpy.context.object.vertex_groups) - 1 + vertex_group = bpy.context.object.vertex_groups[index] + + vertex_group.name = object_name + + particle_system.vertex_group_density = object_name + particle_system.vertex_group_length = object_name + +# Example usage: +add_debris_layer("Bush", 1) +add_debris_layer("Grass 1", 2) +add_debris_layer("Stone 1", 5) +add_debris_layer("Tree 1", 10) +``` diff --git a/assets/blender/scripts/blender2ogre/Physics.md b/assets/blender/scripts/blender2ogre/Physics.md new file mode 100644 index 0000000..dcbdf46 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/Physics.md @@ -0,0 +1,101 @@ + +# Exporting for Physics Engines + +## Index + - [Introduction](#introduction) + - [Creating a Convex Hull](#creating-a-convex-hull) + - [Using the Decimate Modifier](#using-the-decimate-modifier) + +## Introduction +Since OGRE3D is a graphics engine, if you are interested in making a game then you are probably also using a physics engine. +Some of the most popular physics engines are: + - [Havok Physics](https://www.havok.com/) + - [NVIDIA PhysX SDK](https://github.com/NVIDIAGameWorks/PhysX) + - [Bullet Physics](https://pybullet.org/wordpress/) + +In the case of Bullet Physics, there is even an official OGRE Component that connects OGRE objects to the Bullet Physics world ([Bullet-Physics to Ogre connection](https://ogrecave.github.io/ogre/api/13/group___bullet.html#details)). + +One common issue when using physics engines that relates to `blender2ogre` is the creation of collision meshes or hulls. + +For the physics engine to operate at maximum efficiency the collision shapes in the world should be [Convex Hulls](https://en.wikipedia.org/wiki/Convex_hull). + +So when creating a level in Blender and then exporting the mesh to render into OGRE3D, the same mesh or the same arrangement of vertices would in most cases be very inefficient to use as a collision mesh. + +## Creating a Convex Hull +> **WARNING**: This section covers a feature only available since Blender version 3.0 (Geometry Nodes + Convex Hull) + +In general for collision testing, it is most efficient to use the physics engines' primitive shapes, so if the asset can be put within one of these shapes it will consume fewer CPU resources (for example a barrel within a Cylinder Collision Shape). + +Primitive shapes: + - Sphere Shape + - Plane Shape + - Box Shape + - Cylinder Shape + - Capsule Shape + - Cone Shape + +Complex shapes: + - Compound Shape (Collection of primitive shapes) + - Convex Hull Shape (A good analogy for a convex hull is an elastic membrane or balloon under pressure which is placed around a given set of vertices) + - Triangle Mesh Shape (A triangle mesh shape is similar to the convex hull shape, except that it is not restricted to convex geometry) + - Heightfield Shape (Used for terrain, because it can take elevation data from an image) + - Soft Body Shape + +Primitive shapes don't concern us in this document, since they can be easily created in OGRE. +For example `Ogre::Bullet::createSphereCollider( const MovableObject * mo )` would create a Sphere Shape for the provided OGRE Movable Object (like an Entity). + +Our concern is regarding more complex objects that require a more detailed collision shape that better fits the shape of the object. + +An example could be some rocks that the player can climb on top of. +None of the primitive shapes approximate the random shape of a rock and if we tried the mismatch between the visual and the physical would be awkward for the player. + +For this example, we are going to use the [Mossy Stone Rock](https://opengameart.org/content/mossy-stone-rock) from OpenGameArt. + +![Mossy Stone Rock](images/physics/stone-mesh.png) + +To create a collision mesh of type Convex Hull for this object first we are going to duplicate it with `Shift-D`. + +Rename the cloned object to "Stone_collision" (usually collision meshes are named like this to distinguish from the visual mesh). + +With the cloned object selected go to Modifiers and add the `Geometry Nodes Modifer`. + +![Geometry Nodes Modifer](images/physics/geometry-nodes1.png) + +Create a new Geometry Node and then change the Layout to `Geometry Nodes` (layouts are in the top bar in Blender) + +Now add a `Geometry Node` of type Convex Hull (Add (Shift-A) -> Geometry -> Convex Hull) and put the node between the `Group Input` and the `Group Output` + +![Geometry Nodes Layout](images/physics/geometry-nodes2.png) + +This will give you a simplified convex version of the mesh that can be exported by blender2ogre and then used as a collision shape in Bullet or even PhysX. + +> NOTE: Very important! +Before exporting the mesh remember to set the shading to `Shade Smooth` (Object -> Shade Smooth), this is because `Flat Shading` makes `blender2ogre` export more vertices that would be necessary for this case. +Also, remember to remove the material since the collision mesh won't need it. + +## Using the Decimate Modifier +But what if you want to use a model like this one: + +[Stronghold](https://opengameart.org/content/stronghold) from OpenGameArt. + +The object looks like this in Blender: + +![Stronghold Original model](images/physics/stronghold1.png) + +If we use a `Geometry Node` to create a Convex Hull, the result looks like this: + +![Stronghold Original model](images/physics/stronghold2.png) + +So, this cannot be used as a place where the player can go around going into the Stronghold and picking up loot. + +In this case, a better approach is to duplicate the object and then add the `Decimate Modifier`. +Use the `Collapse` option and play with the `Ratio` lowering it as much as possible until the geometry starts collapsing too much. +Also: + - Use the `Clean Up` menu (`Edit Mode`, Mesh -> Clean Up) to clean up the mesh to avoid as much degenerate geometry as possible. + - Set the shading to `Shade Smooth` (Object -> Shade Smooth), this is because `Flat Shading` makes `blender2ogre` export more vertices that would be necessary for this case. + - Remove the materials since the collision mesh won't need it. + - This model has some ornaments that can be removed, like the window and door ledges which also reduces the complexity of the model. + +The result is an object that looks bad but has a lower vertex count which will improve performance for the physics engine when calculating collisions. + +![Stronghold Original model](images/physics/stronghold3.png) diff --git a/assets/blender/scripts/blender2ogre/README.md b/assets/blender/scripts/blender2ogre/README.md new file mode 100644 index 0000000..349683b --- /dev/null +++ b/assets/blender/scripts/blender2ogre/README.md @@ -0,0 +1,315 @@ + +# blender2ogre # +* License: [GNU LGPL](http://www.gnu.org/licenses/lgpl.html) +* [Ogre forum thread](https://forums.ogre3d.org/viewtopic.php?f=8&t=61485) + +**This versions requires Blender 2.8+.** For Blender 2.7x: [use the 2.7x-support branch](https://github.com/OGRECave/blender2ogre/tree/2.7x-support) + +## Index + - [Installing](#installing) + - [Updating to new versions](#updating-to-new-versions) + - [Video Tutorials](#video-tutorials) + - [Exporting Meshes](#exporting-meshes) + - [Materials](#materials) + - [Blender Modifiers Support](#blender-modifiers-support) + - [Mesh triangulation issues](#mesh-triangulation-issues) + - [OgreNext Tips](#ogrenext-tips) + - [Importing Meshes](#importing-meshes) + - [Additional Features](#additional-features) + - [Merge Objects on export](#merge-objects-on-export) + - [External OGRE Materials](#external-ogre-materials) + - [Console Export](#console-export) + - [Exporting Custom Vertex Groups](#exporting-custom-vertex-groups) + - [Exporting Custom Normals](#exporting-custom-normals) + - [Exporting Skeletal Animations](#exporting-skeletal-animations) + - [Exporting Particle Systems](#exporting-particle-systems) + - [Exporting Shape (or Pose) Animations](#exporting-shape-animations) + - [Exporting Node Animations](#exporting-node-animations) + - [Exporting for Physics](#exporting-for-physics) + - [Exporting Vertex Colors](#exporting-vertex-colors) + - [Exporting SkyBoxes](#exporting-skyboxes) + - [Level of Detail (LOD)](#level-of-detail-lod) + - [Mesh Previewer](#mesh-previewer) + - [About](#about) + - [Authors](#authors) + +## Installing +1. Copy the [io_ogre](io_ogre) folder into the [$BLENDER_DIR](https://docs.blender.org/manual/en/latest/advanced/blender_directory_layout.html)`/scripts/addons` folder. +2. Enable the addon in Blender: `Edit menu > Preferences > Add-ons`. Search for `'ogre'` and click the box up the top left. +3. Configure the plugin before the first run. + - Set the correct path to `OGRETOOLS_XML_CONVERTER` + - for Ogre (v1): path should point to `OgreXMLConverter.exe`. This can be found in the [Ogre SDK](https://www.ogre3d.org/download/sdk/sdk-ogre) + - for OgreNext (v2): path should point to `OgreMeshTool.exe`. This can be found in the [OgreNext SDK](https://www.ogre3d.org/download/sdk/sdk-ogre-next) + - *OPTIONAL* Set `MESH_PREVIEWER` to a path pointed to `ogre-meshviewer.bat`. This can be found in [OGRECave/ogre-meshviewer](https://github.com/OGRECave/ogre-meshviewer/releases) + - Make sure that `USER_MATERIALS` isn't set to a directory like "C:\\\". The addon scans this path recursively and will crash when it hits a path it doesn't have permissions for. + +> **NOTE**: Installing Blender using Ubuntu Snap package or Fedora Flatpak will lead to the following error: `cp: cannot create directory '/snap/blender/3132/3.4/scripts/addons/io_ogre': Read-only file system +` (see: [Installing on Ubuntu 20.04 and Blender 3.4.1. #169](https://github.com/OGRECave/blender2ogre/issues/169)) + +There are two possible solutions: + - After downloading the `blender2ogre` repo, uncompress it somwhere and then compress the [io_ogre](io_ogre) folder as a .zip file. Go to `Edit` -> `Preferences (CTRL-ALT-U)` -> `Add-ons` -> `Install...` and then select the file `io_ogre.zip` + - Copy the [io_ogre](io_ogre) folder into the folder: `~/.config/blender/3.4/scripts/addons/` (where 3.4 is the Blender version) + + +## Updating to new versions ## +If you are upgrading from a previous version of blender2ogre and having problems, you may want to delete your old .json config file from +[$BLENDER_DIR](https://docs.blender.org/manual/en/latest/advanced/blender_directory_layout.html)`/config/scripts/io_ogre.json` and restart blender. + +## Video Tutorials +* [General Usage](http://www.youtube.com/watch?feature=player_embedded&v=3EpwEsB0_kk) +* [Animations](http://www.youtube.com/watch?feature=player_embedded&v=5oVM0Lmeb68) +* [Meshmoon: Video and text instructions on how to install and use blender2ogre addon](http://doc.meshmoon.com/index.html?page=from-blender-to-meshmoon-part-1) + +## Exporting Meshes +To export a blender model: `File Menu > Export > Ogre3D (.scene & .mesh)`. + +If the menu button is greyed out (or you get this error: `RuntimeError: Operator bpy.ops.ogre.export.poll() failed, context is incorrect`), then make sure there is an active object selection in the blender Node tree (Scene collections) first. +The active object selection is when there is an object with a yellow outline (in contrast to the orange outline of the passive selected objects) + +- If you have `OGRETOOLS_XML_CONVERTER` set to "OgreXMLConverter.exe" path, then the export dialogue will display options relevant to the Ogre (v1) mesh format. +- If you have `OGRETOOLS_XML_CONVERTER` set to "OgreMeshTool.exe" path, then the export dialogue will display options relevant to the OgreNext (v2) mesh format. + +Check out all the exporter and importer options in the [Options Document](Options.md) + +### Materials +Materials will be exported as OGRE 1.x material files or as OGRE Next material.json files as required. + +#### OGRE 1.x Materials +Materials are exported as RTSS OGRE 1.x materials (unless "Fixed Function Parameters" is selected). +The following textures are exported: Base Color, Metallic and Roughness, Normal Map and Emission. Baked Ambient Occlusion is not supported for the moment. + +Your material will be best exported if you follow the GLTF2 guidelines: [glTF 2.0 - Exported Materials](https://docs.blender.org/manual/en/2.80/addons/io_scene_gltf2.html#exported-materials). +Except for the Emission texture, where the Emission input of the Principled BSDF node is used as the Emission texture. + +A good example of how the material should be setup for best results is the "Damaged Helmet" model found here: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/DamagedHelmet + +#### OGRE Next JSON Materials +The current OGRE Next JSON exporter only explicitly supports a metalness workflow, however it will attempt to export materials not following that workflow regardless and *may* produce passable results. + +For materials using metalness it expects a single image with the blue channel containing the metalness map and the green channel containing the roughness map fed into the Principled BSDF via a Separate RGB node. Not following this convention will print a warning to the console and the exported materials will likely not appear correctly when rendered. + +There are numerous features in the Ogre Next JSON format that are not directly supported, see the [Materials JSON](MaterialsJSON.md) notes for details. + +### Blender Modifiers Support +Blender has some very useful modifiers, and most of them are supported by `blender2ogre` but not all of them. +Check out the [Blender Modifiers Support Page](Modifiers.md) to check out the list and also some recommendations about them. + +### Mesh triangulation issues +![Cube with broken shading](images/triangulate/broken-shading.png) + +If you are seeing some issues with the mesh triangulation (like visible triangles in your shading), check the [Mesh Triangulation README](MeshTriangulation.md) to learn more about the subject and how to work around it. + +### OgreNext tips +If you do want to export in the OgreNext (v2.) format, make sure in the `Export dialogue > General Settings > Mesh Export Version` is set to V2. The following parameters are a good starting point to get a model exported to an Ogre mesh: +* General + - Mesh export version: v2 +* Materials + - Export materials: ticked +* Armature + - Armature animation: ticked +* Mesh + - Export mesh: ticked + - Edge lists: un-ticked + - If your model's materials contain normal mapping: + - Tangents: "generate with parity" + - Else Tangents: "none" + - Optimise Vertex buffers for shaders: ticked + - Vertex buffer options: **puqs** + +You can check the arguments passed to `OgreMeshTool.exe` in the Blender console. (`Window Menu > Toggle System Console`) + +Blender will export the material format in an Ogre (V1) format. This is not compatible with OgreNext (V2.*). You should manually convert them to a material.json file. See the [Ogre Wiki: HLMS Materials](https://wiki.ogre3d.org/HLMS+Materials) for more information. + +## Importing Meshes +As of `blender2ogre` version *0.8.2*, the Kenshi Importer has been integrated into `blender2ogre` with the following features: + - Import mesh from `.xml` as well as `.mesh` files + - Import whole `.scene` files (as of version *0.8.5*) with all its objects + - Option to be able to merge imported submeshes or keep them separate + - Parsing/Conversion of materials into Blender (just create a Principled BSDF material and add a texture with the proper UV mapping) + - Importing of Poses + - Importing of Skeletons works for the most part, but Ogre skeletons conventions are not like Blenders (see: [How to get bone's vector and bone's length?](https://forums.ogre3d.org/viewtopic.php?t=49689)) + - Importing of Animations work, but depends on the skeleton which sometimes doesn't get correctly imported + - As of Blender 4.1+, it is now possible to drag and drop files of types: `.xml`, `.mesh` or `.scene` and import them automatically. (https://docs.blender.org/api/4.1/bpy.types.FileHandler.html) + +> **NOTE:** Orientation of the imported mesh is assumed to be `xz-y` (Ogre default), the `blender2ogre` Axis Swapping option does not work for the importing process. + +## Additional Features + +### Merge Objects on export +You might have hundreds of objects, which you want to keep separate but have them in one `.mesh` on export. +For this create a new collection (M) named `merge.`. The output will be a single `.mesh` file. Alternatively link the collection. + +> **NOTE:** The origin of the resulting merged object will be that of the *last* object you added to the collection (although when reloading the blend file, this order will be lost). +To have control over the precise location of where the merged objects' origin will be, use the `dupli_offset` property of the collection. +Setting any value other than the default `(0, 0, 0)` will result in a mesh with the origin set to that value. For example: + +![dupli-offset.png](images/dupli-offset.png) + +### Instancing and DotScene Plugin +As of OGRE 1.13 a new feature has been added to the DotScene Plugin where it now accepts the static / instanced keywords for entities. +(for more information read the [DotScene Plugin README](https://github.com/OGRECave/ogre/blob/master/PlugIns/DotScene/README.md)). + +To use this feature create a new collection (M) names as `static.` or `instanced.` and blender2ogre will automatically add the corresponding attribute to the exported entities in the Scene. +This feature goes hand in hand with [Exporting Particle Systems](#exporting-particle-systems) to create vegetation, debris and other static objects in your scene. + +### External OGRE Materials +You might already have some materials in OGRE that you do not want to export. +Prefix them with `extern.` and the sub-entity will have the material name set, but the material is not exported. +The following material 'vertexcolor' can be defined in your OGRE project: + +![extern-material.png](images/extern-material.png) + +### Console Export +You might have several blender files in your project you want to export to Ogre. Do this by hand? NO! You can do better! +Here is how you can export a scene with blender2ogre. + +```sh +blender test.blend -b --python-expr "import bpy;bpy.ops.ogre.export(filepath='test.scene')" +``` + +### Exporting Custom Vertex Groups +As shown in the picture below, you can now export SubEntities that contain a user-defined amount of faces. + +![blender-vertex-group.png](images/blender-vertex-group.png) + +You simply call your vertex group with the +prefix `ogre.vertex.group.` and access it in Ogre similar to the following: + +```cpp +void example(const Ogre::Entity * entity) +{ + auto collision = entity->getSubEntity("collision"); + auto mesh = collision->getSubMesh(); + VertexData * data = nullptr; + if (!mesh->useSharedVertices) { + data = mesh->vertexData; + } else { + data = mesh->parent->sharedVertexData; + } + auto pos_cursor = data->vertexDeclaration->findElementBySemantic(Ogre::VES_POSITION); + auto vbuffer = data->vertexBufferBinding->getBuffer(pos_cursor->getSource()); + auto ibuffer = mesh->indexData->indexBuffer; + + uint16_t * indices = static_cast(ibuffer->lock(Ogre::HardwareBuffer::HBL_READ_ONLY)); + float * vertices = static_cast(vbuffer->lock(Ogre::HardwareBuffer::HBL_READ_ONLY)); + float * v; + int count = mesh->indexData->indexCount; + int stride = vbuffer->getVertexSize() / 4; + for (int i = 0; i < count; i+=3) { + uint16_t a = indices[i], b = indices[i+1], c = indices[i+2]; + pos_cursor->baseVertexPointerToElement(vertices + a * stride, &v); + Vector3 va(v); + pos_cursor->baseVertexPointerToElement(vertices + b * stride, &v); + Vector3 vb(v); + pos_cursor->baseVertexPointerToElement(vertices + c * stride, &v); + Vector3 vc(v); + // do something with your triangle here + } + ibuffer->unlock(); + vbuffer->unlock(); +} +``` +The vertex group will get the material name 'none' so you might want to add the following script: + +``` +material none { + technique { + pass { + // ... + } + } +} +``` + +### Exporting Custom Normals +![Cube with Filled Edges](images/normals/fillet-edges.png) + +Custom Split Normals is a way to tweak the mesh shading by pointing normals towards directions other than the default, auto-computed ones. +It is mostly used in game development, where it helps counterbalance some issues generated by low-poly objects +(the most common examples are low-poly trees, bushes, grass, etc. and the ‘rounded’ corners). +Check out the [Custom Normals README](CustomSplitNormals.md) to learn more about Custom Normals in Blender. + +### Exporting Skeletal Animations +![skeletal-animations.png](images/skeletal-animations.png) + +Skeletal Animation refers to the technique of using bones to deform a mesh as if the mesh were the skin. +This kind of animation is commonly used to animate characters in video games. +Check out the [Skeletal Animations README](SkeletalAnimation.md) to see how to create and export an animated mesh. + +### Exporting Particle Systems +![particle-system5.png](images/particle-sys/particle-system5.png) + +A common technique for laying out random objects on a scene in Blender is to use the Particle System. +Check out the [Particle System README](ParticleSystem.md) to see how to create and export a scene where the trees, foliage and rocks are distributed randomly using a particle system. + +### Exporting Shape Animations +![shape-animations4.png](images/shape-anim/shape-animations4.png) + +Shape (or Pose) Animations allow animating different poses, a technique commonly used to do face animations. +Check out the [Shape Animations](ShapeAnimations.md) tutorial to see how to create some poses and animate them. +Then you can use `blender2ogre` to export the poses and animations into a `.mesh` file. + +### Exporting Node Animations +Node Animations are a way to have scripted node animations in your Ogre application. +Check out the [Node Animations](NodeAnimations.md) tutorial to see how to create some animations for a couple of different scenarios. + +### Exporting for Physics +Check out the [Exporting for Physics](Physics.md) tutorial to see some techniques and optimizations when exporting collision meshes for Physics Engines + +### Exporting Vertex Colors +![vertex-colors.png](images/vertex_colors/vertex_colors1.png) + +`blender2ogre` can import and export Vertex Colors, a way to assign colors to your mesh by vertex. It is also possible to asign alpha values to each vertex for transparency or other uses. +Check out the [Vertex Colors](VertexColors.md) tutorial to see how to create the Vertex Colors in Blender and how to export them. + +### Exporting SkyBoxes +![skyboxes1.jpg](images/skyboxes/skyboxes1.jpg) + +`blender2ogre` can generate SkyBoxes from HDRi Maps and export them in a format that can be used in OGRE, that is a Cube Map. +Check out the [SkyBoxes](SkyBoxes.md) tutorial to see how to create import the Environment Map in Blender and how to export it. + + +### Level of Detail (LOD) +Level of Detail or LOD is an optimization technique supported by OGRE, where meshes that are far away from the camera are replaced by meshes with lower vertex count. +Because you can get away with less detailed models when they are in the distance this optimizacion technique is very common, especially in games. +With `blender2ogre` there are three ways to generate LOD levels for a mesh: + * `OgreMesh Tools`: This method uses the tool `OgreMeshUpgrader` to automatically generate the LOD levels for the mesh by removing edges. This allows only changing the index buffer and re-use the vertex-buffer (storage efficient) + * `Blender`: This method uses Blenders "Decimate" Modifier to automatically generate the LOD Leves by collapsing vertices. This can result in a visually better LOD, but needs different vertex-buffers per LOD + * `Manual`: This metod requires that the user manually creates the additional LOD levels for the mesh. The meshes should be called [base mesh]_LOD_1, [base mesh]_LOD_2, etc. This method gives better control over the resulting LODs. + +For the `Manual` method it is currently not possible to manually set the distances for each LOD, but it is possible to have different materials for each LOD level. + + +### Mesh Previewer +If `MESH_PREVIEWER` is set, a button will appear allowing you to preview your mesh in Ogre3D. If the button isn't there, the path is invalid. This only works for Ogre (V1) meshes. +The button is located here: + +![Preview mesh button location](images/mesh-preview-button.png) + + +## About +[The original version of this](https://code.google.com/archive/p/blender2ogre/) was a *single* monolithic Python file. +This is not maintainable and contains a tremendous amount of bugs. There was the need to export Blender model to OGRE from +the console, thus I rewrote the whole script and split it into several files. +It has been well-tested on Linux 64-bit and should work with others. + +## Authors +This Blender addon was made possible by the following list of people. +Anyone can contribute to the project by sending bug reports and feature requests [here](https://github.com/OGRECave/blender2ogre/issues). +Naturally, the most welcome contribution is actual code via [pull requests](https://github.com/OGRECave/blender2ogre/pulls). +If you are planning to implement something "big", it's a good practice to discuss it in the issue tracker first with other authors. +So that there is no overlap with other developers or the overall roadmap. + +* [Git Contributors](https://github.com/OGRECave/blender2ogre/graphs/contributors) +* [Brett](http://pyppet.blogspot.fi/) +* S. Rombauts +* F00bar +* Waruck +* [Mind Calamity](https://bitbucket.org/MindCalamity) +* Mr.Magne +* [Jonne Nauha](https://bitbucket.org/jonnenauha) aka Pforce +* vax456 +* Sybren Stüvel + +Additionally, the following companies have supported/sponsored the development efforts. +* [Adminotech Ltd.](http://www.meshmoon.com/) diff --git a/assets/blender/scripts/blender2ogre/ShapeAnimations.md b/assets/blender/scripts/blender2ogre/ShapeAnimations.md new file mode 100644 index 0000000..6c02fd5 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/ShapeAnimations.md @@ -0,0 +1,121 @@ + +# Shape Animations + +## Index + - [Introduction](#introduction) + - [Set up](#set-up) + - [Baking complex animations into Shape Animations](#baking-complex-animations-into-shape-animations) + - [Using Shape Animations in Ogre](#using-shape-animations-in-ogre) + +## Introduction +A common technique for creating face animations is to use Poses or Shapes for the different phonemes that the character should go through when talking. + +In this tutorial, we are going to explore how to create a couple of poses and animate them. + +Terminology: + - *Shape Keys*: a certain pose defined in Blender. This pose can be blended with other poses to different degrees to achieve new poses + +## Set up +To make this tutorial simple, the Animation is going to be applied only to the default cube. + +To see a better representation of face animations, take a look at [this](examples/shape-animation.blend) example. + +Create a new scene in Blender and split it into two, in the second view select `Dope Sheet` and `Shape Key Editor`. +![shape-animations1.png](images/shape-anim/shape-animations1.png) + +Select the default cube and go to the `Vertex` or `Data` tab. Under `Shape Keys` press the `+` button three times to create three Shape Keys. +![shape-animations2.png](images/shape-anim/shape-animations2.png) + +The Basis is the base shape and the other shape keys are going to be the poses that we apply over the base. + +Select `Key 1` and go into `Edit Mode` (press tab). + +Once you are in `Edit Mode` press `s` (scale), 2 and `Enter` to scale the cube two times. + +Then select `Key 2` and press again `s`, write 0.5 and `Enter` to scale the cube to half size. + +Now you have three shapes to animate. + +Go into `Object Mode` by pressing tab and at the bottom you should see that the current frame is 1. +![shape-animations3.png](images/shape-anim/shape-animations3.png) + +Select `Key 1` and in `value` set it all the way to 1. + +Hover the mouse over `value` and press `i` to insert a keyframe at frame 1. + +Then, select `Key 2` and leave `value` as it is, Hover the mouse over `value` and press `i` to insert a keyframe at frame 1 for `Key 2`. + +You should see in the `Dope Sheet` view that now there is a new "Key Action", press `F` next to it to save the action by associating it with a fake user. + +Now, set the current frame to 60 and repeat the above process but setting `value` to 0 for `Key 1` and 1 for `Key 2`, remember to insert the keyframes (the `value`s should turn yellow). + +Set the `End` frame to 60 and press play to see the cube changing size. Of course, this is a simple animation but with some imagination, something much more complex can be achieved like face animations. + +Last but not least for the `blender2ogre` add-on to properly export the animation it is necessary to turn it into an NLA Track, and select the `Push Down` button next to the action name. +![shape-animations1.png](images/shape-anim/shape-animations1.png) + +You can now go into the `NLA Editor` view and change the name of the NLA Track that name is the one that is going to be exported. + +## Baking complex animations into Shape Animations +[How to Bake Modifier Animation in Blender / 1. Wave Modifier Animation to Shape Keys!](https://www.youtube.com/watch?v=KMIkOhTSP1U) +https://docs.blender.org/manual/en/latest/addons/import_export/shape_mdd.html + +Blender can perform some complex vertex animations (`Wave Modifier` being an example). +However, it is not possible to just export these animations into OGRE directly. +But there is a trick to baking these animations into Shape Key Animations and then it is possible to export into OGRE. +The trick consists of exporting the animation using the `NewTek MDD` format and then importing it, the resulting mesh will have the vertex animations baked as a Shape Key Animation + +> NOTE: Care must be taken if the animation has too many frames since there will be one Shape Key for every frame and that makes the exported mesh heavier. + +The steps are the following (for example using `Wave Modifier`): +1) Add a plane mesh (Shift-A -> Mesh -> Plane), then enter `Edit Mode` (Tab) and subdivide the mesh (Ctrl-E -> Subdivide) 5 times so the `Wave Modifier` has some geometry to work with +2) Set the object shading to smooth (Object -> Shade Smooth) +3) Add the `Wave Modifier` to the "Plane" Object and rename the "Plane" to "Wave" +4) Set the starting and ending frames of the animation (lower right corner) +5) Enable the `NewTek MDD` Add-On (go to Edit -> Preferences -> Add-ons -> `NewTek MDD` and enable the Add-on) +6a) Make sure the Plane/Wave object is selected +6b) Export the animation to an .mdd file: File -> Export -> Lightwave Point Cache (.mdd) and set a proper filename like wave.mdd +7) Duplicate the Plane object (Shift-D) and set a name like Wave2 +8) On the Duplicate Plane object, remove the Wave modifier +9a) Make sure the duplicate object "Wave2" is selected +9b) Now import the recently exported .mdd file: File -> Import -> Lightwave Point Cache (.mdd) +Now the duplicate object "Wave2" has a number of Shape Keys, each for every frame that was exported in step 6b) +Besides a new action `KeyAction` is created, which you can see in the `Shape Key Editor` of the `Dope Sheet` (Shift-F12) +10) Now to get blender2ogre to export the animation we need to create a NLA track, go to the `Animation` and in the upper left corner change the view to `Nonlinear Animation` +11) Perform a push-down of the animation toward an NLA Track +12) Set the name of the NLA Track, which will be the name of the Shape/Pose Animation in OGRE +13) Now use `blender2ogre` to export the animation, make sure the option `SHAPE_ANIMATIONS` is set to `True` + + +## Using Shape Animations in Ogre +Create an Entity and attach it to a SceneNode +``` +Ogre::Entity* cube = mSceneMgr->createEntity("Cube", "Cube.mesh"); +Ogre::SceneNode* cubeNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("Cube"); +cubeNode->attachObject(cube); +``` + +Get the AnimationState, enable it and set the starting time position +``` +auto animationState = cube->getAnimationState("KeyAction"); +animationState->setEnabled(true); +animationState->setTimePosition(0); +``` + +Then you need to `addTime()` to the *AnimationState*, we will use a controller for that. +``` +auto& controllerMgr = Ogre::ControllerManager::getSingleton(); + +// Create a controller to pass the frame time to the Animation State, otherwise the animation won't play +// (this is a better method than using animationState->addTime() in your main loop) +controllerMgr.createFrameTimePassthroughController(Ogre::AnimationStateControllerValue::create(animationState, true)); +``` + +For more information, please take a look at section [Vertex-Animation](https://ogrecave.github.io/ogre/api/latest/_animation.html#Vertex-Animation) in the manual. + +And also consult the Ogre API manual: + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_manager.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_animation_state.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_node.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_controller_manager.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_controller.html diff --git a/assets/blender/scripts/blender2ogre/SkeletalAnimation.md b/assets/blender/scripts/blender2ogre/SkeletalAnimation.md new file mode 100644 index 0000000..2f31edf --- /dev/null +++ b/assets/blender/scripts/blender2ogre/SkeletalAnimation.md @@ -0,0 +1,219 @@ + +# Exporting Skeletal Animations + +## Index + - [Introduction](#introduction) + - [Creating a basic rig](#creating-a-basic-rig) + - [How does the exporter work?](#how-does-the-exporter-work-) + - [Smooth Corrective Modifier](#smooth-corrective-modifier) + - [Exporter Options](#exporter-options) + - [Using Skeletal Animations in Ogre](#using-skeletal-animations-in-ogre) + - [Tips/Troubleshooting](#tips-troubleshooting) + - [Human Top+Base Animations](#human-top-base-animations) + - [Character clothing](#character-clothing) + - [Shared Skeleton](#shared-skeleton) + - [Base Mesh/Skin clipping](#base-mesh-skin-clipping) + - [Random Tips from the forum](#random-tips-from-the-forum) + - [Automation](#automation) + +## Introduction +Skeletal Animation refers to the technique of using bones to deform a mesh as if the mesh were the skin. +The bones deform different parts of the mesh based on vertex weights that specify how much any bone should influence a certain part of the mesh. +This kind of animation is commonly used to animate characters in video games, but it's not limited to that. + +Refer to the Blender Manual to know more about rigging: https://docs.blender.org/manual/en/2.79/rigging/index.html + +## Creating a basic rig +To simplify the tutorial we will use the default cube just to show the basic concepts, for more complex tutorials check YouTube, for example: + - [Blender 2.8 Tutorial : Rig ANY Character for Animation in 10 Minutes!](https://www.youtube.com/watch?v=SBYb1YmaOMY) + - [Blender 2.82 Rigging Tutorial (In 2 Minutes!!!)](https://www.youtube.com/watch?v=PFaqjwpGxOc) + +Start a new Blender scene with `Ctrl-N`. + +Press `Shift-A -> Armature -> Single Bone` to add an Armature consisting of a single bone. + +Select the Cube, then select the Armature (in that order) and press `Ctrl-P -> Armature Deform (With Automatic Weights)`. + +Now the default cube is a child of the Armature and Blender has automatically created a Vertex Group named *Bone* on the Cube. + +Select the Cube and go to the `Data` tab (the one that looks like an inverted triangle), there you will see a Vertex Group named *Bone*. + +If you change the interaction mode to `Weight Paint` you will see that the cube suddenly becomes red, that color represents the strength of the weights assigned to the bone *Bone* where red is the strongest weight and blue is the weakest. + +Split the Blender viewport to open a second view, bring up the Dopesheet editor and select the Action Editor context. +Click "New" to add a new action and set a name for it, also don't forget to press the `F` button next to it (in Blender 2.7x, it is a shield in Blender 2.8+). +The purpose of that button is to avoid getting into a situation where the Action you create to animate your object has 0 users and the next time you open your .blend file the animation is gone. +One way for this to happen is if you press the `X` button then the action is no longer associated with the Armature and you will see a 0 next to it (it has 0 users) so Blender does not save it to disk. + +Select the Armature and then set the mode to `Pose Mode`, this mode allows you to animate the bones. +Select the bone and now each time you press `I` a Keyframe is inserted and the proper keys for that bone are set. +Blender will ask each time which keys you want to insert, to make this process less tedious there is a box underneath the timeline where you can set `LocRot` for Blender to automatically insert location and rotation keys for each Keyframe you insert. + +So, make sure that the current frame is `1` and insert a Keyframe. You will see that in the dopesheet now there is a Keyframe in the action. + +Another interesting tidbit about actions is that they are composed of F-Curves and there is an F-Curve for each attribute of the bone being animated (location.x, location.y,... rotation.x, etc.). +And these F-Curves reference a vertex group by name (*Bone* in our case), an interesting thing is that if you have another Armature with bones with the same names and select the action in the dope sheet with that Armature selected then your animation will apply to that Armature as well. Of course, if the bones in the second Armature have the same names but different positions and orientations then the animation will look all wrong. +Transferring one set of animations from one Rig (a particular bone layout) to another is called Animation Retargeting, this is commonly used to transfer motion capture animations. +There is an official Blender plugin for Animation Retargeting but it is no longer being maintained, it is best to look up the subject in Google or YouTube. + +OK, now go to frame `60` and move the bone to a different position/rotation of your choosing, the Cube should move with the bone since the weights have maximum influence. Insert another Keyframe with `I`. + +![skeletal-animation.png](images/skeletal-animation.png) + +Now there are two keyframes and you have a basic animation, set the `End:` frame to 60 and play the animation with `Alt-A` the cube should move and/or rotate from its original position to the one you set. + +After finishing with the animation, press `Push Down` next to the action to add the action to the NLA Stack. This will indicate to the exporter that you want to export this action. + +Change the Dopesheet editor to the NLA editor view and check that the action has an NLA Track, is advisable to change the NLA Track name since that will be the name of the animation in Ogre. + +You can also play with an already made model to see how the process goes: [Stacy-rigged](https://www.turbosquid.com/FullPreview/Index.cfm/ID/535459). +It is advisable to try an export your model at every step of the process of making it more complex, to avoid getting into a situation where you put a lot of work into it and there are problems with the export. + +## How does the exporter work? +To accommodate for complex animations with Inverse Kinematics, Drivers and modified F-Curves the exporter cannot simply export the Keyframes to Ogre because all the influences of these modifiers would be lost. +So what the exporter does is go frame by frame and have Blender calculate the bone transforms and export that information to the skeleton.xml file. +This means that your animation in Ogre has a keyframe for every single frame from the start of the animation to the end. +As a result of this setting `IM_SPLINE` for frame interpolation in Ogre would make no difference and might even slow down the skeletal animation. + +If you only require key frames exported, then make sure the "Only Keyframes" option is ticked in the exporter properties. + +## Smooth Corrective Modifier +https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/corrective_smooth.html +The Smooth Corrective modifier is used to reduce highly distorted areas of a mesh by smoothing the deformations. +This is typically useful after an Armature modifier, where distortion around joints may be hard to avoid, even with careful weight painting. + +## Exporter Options + - *EX_ARMATURE_ANIMATION* (Armature Animation) : Export armature animations (updates the .skeleton file), enable this option to export the armature animations. + - *EX_SHARED_ARMATURE* (Shared Armature) : Export a single .skeleton file for objects that have the same Armature parent (useful for: `shareSkeletonInstanceWith()`). NOTE: The name of the .skeleton file will be that of the Armature", + - *EX_ONLY_KEYFRAMES* (Only Keyframes) : Exports only the keyframes. Influences that Inverse Kinematics, Drivers and modified F-Curves have on the animation will be lost. + - *EX_ONLY_DEFORMABLE_BONES* (Only Deformable Bones) : Only exports bones that are deformable. Useful for hiding IK-Bones used in Blender. NOTE: Any bone with deformable children/descendants will be output as well + - *EX_ONLY_KEYFRAMED_BONES* (Only Keyframed Bones) : Only exports bones that have been keyframed for a given animation. Useful to limit the set of bones on a per-animation basis + - *EX_OGRE_INHERIT_SCALE* (OGRE Inherit Scale) : Whether the OGRE bones have the 'inherit scale' flag on. If the animation has scale in it, the exported animation needs to be adjusted to account for the state of the inherit-scale flag in OGRE. + - *EX_TRIM_BONE_WEIGHTS* (Trim Weights) : Ignore bone weights below this value (Ogre supports 4 bones per vertex) + +## Using Skeletal Animations in Ogre +Create an Entity and attach it to a SceneNode +``` +Ogre::Entity* cube = mSceneMgr->createEntity("Cube", "Cube.mesh"); +Ogre::SceneNode* cubeNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("Cube"); +cubeNode->attachObject(cube); +``` + +Get the AnimationState, enable it and set the starting time position +``` +auto animationState = cube->getAnimationState("ArmatureAction"); +animationState->setEnabled(true); +animationState->setTimePosition(0); +``` + +Then you need to `addTime()` to the *AnimationState*, we will use a controller for that. +``` +auto& controllerMgr = Ogre::ControllerManager::getSingleton(); + +// Create a controller to pass the frame time to the Animation State, otherwise the animation won't play +// (this is a better method than using animationState->addTime() in your main loop) +controllerMgr.createFrameTimePassthroughController(Ogre::AnimationStateControllerValue::create(animationState, true)); +``` + +If you are going to blend between animations +``` +SkeletonInstance* skel = cube->getSkeleton(); +skel->setBlendMode( ANIMBLEND_CUMULATIVE ); +``` + +For more information, please take a look at section [Vertex-Animation](https://ogrecave.github.io/ogre/api/latest/_animation.html#Vertex-Animation) in the manual. + +And also consult the Ogre API manual: + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_animation_state.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_controller.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_controller_manager.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_entity.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_manager.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_node.html + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_skeleton.html + +## Tips/Troubleshooting +Some useful tips to avoid problems or diagnose issues: + - Before parenting a mesh to an armature remember to apply location, rotation and scale to avoid problems. + - Remember to add your actions as NLA strips, otherwise they won't be recognized by the exporter. + - If you're using IK bones, you need to keyframe the bones it is affecting as well otherwise the animation won't work in Ogre. + +## Human Top+Base Animations +It is common for humanoid models to separate animations into top and bottom animations, +that is for example to have a running animation separated into the legs animation (Base) and the torso + head animation (Top). +The objective is that if there is another animation (shooting an arrow, for example which involves only the Top bones) +then it is possible to combine the animations: `RunningBase` and `ShootingTop` to have the character running and shooting arrows at the same time. + +One good example of this comes with the Ogre SDK: Sinbad.zip. The Sinbad model has a `RunBase` and `RunTop` running animations. +Both animations have to be performed at the same time for the Sinbad character model to run using both arms and legs. + +This requires on the Blender side creating the `RunningBase` and `RunningTop` animations, and for each one to *only* have keyframes the bones that correspond to the Top or Base of the model. + +## Character clothing + +### Shared Skeleton +If you want your game characters to have clothing a common thing to do is have a base mesh with the Player/Non-Player skin and then sharing the same Armature (or Skeleton) other meshes for the clothing or armor. All these meshes will be children of the same Armature with the same actions applied to all of them. + +When exporting the Armature, if you select the option: `EX_SHARED_ARMATURE` (Shared Armature), then `blender2ogre` will export the Armature as a single .skeleton file and all the exported OGRE meshes will use the same Skeleton for their animations. (The normal behaviour is for `blender2ogre` to export one skeleton for each mesh that is a child of the Armature) + +Then from you OGRE code use: +``` +// Share Skeleton +Ogre::Entity* baseMesh = mSceneMgr->getEntity("Player_Skin"); +Ogre::Entity* shirtMesh = mSceneMgr->getEntity("Player_Shirt") +shirtMesh->shareSkeletonInstanceWith(baseMesh); + +// Attach Entities to Scene Nodes +Ogre::SceneNode* playerNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("PlayerNode"); +playerNode->attachObject(baseMesh); +playerNode->attachObject(shirtMesh); +``` + +Now, when animating the `baseMesh`, the `shirtMesh` will animate in the same manner, giving the illusion that the character has clothing/armor. + +### Base Mesh/Skin clipping +Usually in some poses or animations, the base mesh (skin) might clip on top of the clothes, which looks pretty bad. + - One solution to this problem is to use shape keys to make the base model thinner and apply the shape key when the character has the clothes on since then the shape won't be visible. + - Another option is to use the Blender `Mask` modifier: create a vertex group for every part of the base mesh that will be visible with clothes on (like arms, head and ankles perhaps) and select that vertex group as the mask. With the mask active, only those parts of the mesh will be exported. + - A third option is to use vertex groups. Create one vertex group for each part of the base mesh/skin that you want to hide and then you can reference these vertex groups within OGRE to hide them. More details in: [Exporting Custom Vertex Groups](README.md#exporting-custom-vertex-groups) + +## Random Tips from the forum +Here are gathered some tips lifted from the forum: [Blender26 Ogre Exporter](https://forums.ogre3d.org/viewtopic.php?f=8&t=61485) + +For combining animations at the same time, you may need to call `setUseBaseKeyFrame()` on the animations to tell OGRE which pose each of the animations should be relative to. By default, Ogre uses the skeleton bind pose. +The "base keyframe" should be a pose where the bones that should be untouched (like the top half of the character for the bottom animation) match exactly for ALL frames of the animation. Otherwise, you'll get unwanted extra rotations. So if you aren't setting the base key frame explicitly, then your animations should be authored such that the bones that should be untouched match the bind pose. The top half of the character should be in bind pose for the bottom animation. For details, see [setUseBaseKeyFrame()](https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_animation.html#a22780aed1bf68e2cd67e6dd62ac5c287) + +The editor only exports the local coordinates and ignores any global transformation, unfortunately in Blender a skeleton and the mesh it is attached to can have different global transformations and take these into account when deforming the mesh. The easy solution to this is moving the skeleton in object-mode until it 'looks like' it is placed correctly, then press `Ctrl-A` to apply location, rotation and if necessary scale. This will set the global transform to zero and recalculate every local transformation to match the view. (apply this to both, skeleton and mesh). + +"An object is not allowed to be scaled/rotated/translated before export": since I read that, I asked myself whether this would mean that all objects need to be default size and located in the origin, as created by "Add", which would be of somewhat reduced use. As it turned out, that meant the object is not allowed to have any LOCAL transformations to it. For example, instead of having a cube with dimensions (8, 2, 4) and a scale (0.5, 1.5, 2), one needs a cube of dimensions (4, 3, 8 ) and of scale (1, 1, 1). The same for rotation and location. This can be achieved by selecting the object, pressing `Ctrl-A`, and then selecting first "Location", followed by (after pressing `Ctrl-A` again) "Rotation & Scale". By doing so, all local transformations are applied to the object and reset to safe values. + +Every time you have a problem with skeletons, make sure that the mesh and the skeleton has the scale and rotation applied `Ctrl+A` and share the same position (origin in the same pos). For that you can select the skeleton, `Shift+S -> Cursor to selected`, then select the mesh, `Ctrl+Alt+Chift+C -> Origin to 3D Cursor`. Maybe it will mess up the envelope, so maybe is the other way around (first mesh then skeleton). + +In very few cases, it has happened to me that if I apply an armature to a mesh, and later do the reset to that mesh, the animation in Ogre is a complete mess, in those cases what I had to do is unparent all bones from the mesh, erase the automatic Armature modifier in the "Objects Modifiers" window, and erase the Vertex Groups in the "Object Data" window, just mentioned if this happens to you. + +The standard way of attaching equipment to a character is to attach it to a bone. Either with a constant offset to an existing bone (maybe the spine) or by creating extra non-deformable bones just for the purpose of attaching equipment to them which then can be moved in your animation to move the attached equipment in any way you like. The Sindbad character for example has extra bones at the sheaths on his back to properly place the swords in them when they are not drawn. + +## Automation +You might get excited animating your models and ending up with a lot of actions that have no corresponding NLA Track. + +Going through every action, pushing it down into the NLA Track and renaming it is a very tedious process, so here is a simple script to automate that process. +``` +import bpy + +def add_NLA_strips(object): + + for action in bpy.data.actions: + track = object.animation_data.nla_tracks.new() + track.strips.new(action.name, action.frame_range[0], action) + track.name = action.name + +# Example for a single armature: +add_NLA_strips(bpy.data.objects['Armature']) + +# If there are many armatures in the scene +for obj in bpy.data.objects: + if obj.type != 'ARMATURE': + continue + + add_NLA_strips(obj) +``` diff --git a/assets/blender/scripts/blender2ogre/SkyBoxes.md b/assets/blender/scripts/blender2ogre/SkyBoxes.md new file mode 100644 index 0000000..6008a58 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/SkyBoxes.md @@ -0,0 +1,66 @@ + +# Exporting SkyBoxes + +![skyboxes1.jpg](images/skyboxes/skyboxes1.jpg) + +## Documentation: + - https://en.wikipedia.org/wiki/Skybox_(video_games) + - https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_manager.html#a370c23fff9cc351b150632c707a46700 + - https://docs.blender.org/manual/en/latest/render/lights/world.html + +## Introduction +It is possible to export an HDRi map as a Cube Map that can be used in OGRE for SkyBoxes or environment maps. + +## Setting the Environment Map in Blender +1) Go to `World Properties` Tab \ +![skyboxes2.png](images/skyboxes/skyboxes2.png) + +2) Then under "Surface", make sure that `Use Nodes` is checked. Surface should say "Background". + +3) Then, where it says `Color` click on the white dot and a menu should appear to select the texture for the environment map. \ +![skyboxes3.png](images/skyboxes/skyboxes3.png) + +4) Open a Picture or select one from the Blender cache. \ +![skyboxes4.png](images/skyboxes/skyboxes4.png) + +5) Then select `File`->`Export`->`Ogre3D (.scene and .mesh)` \ +![skyboxes5.png](images/skyboxes/skyboxes5.png) + +6) In the `General` Settings, make sure to enable the option `Export SkyBox` and set the resolution to some power of 2 value (e.g.: 512, 1024, 2048, etc.). \ +The higher the resolution the more time it will take for the exporting process. \ +![skyboxes6.png](images/skyboxes/skyboxes6.png) + +The exporter will create six images, one for each of the faces of the "SkyBox": +- front: "_fr.png" +- back: "_bk.png" +- right: "_rt.png" +- left: "_lf.png" +- top: "_up.png" +- bottom: "_dn.png" + \ +![skyboxes7.png](images/skyboxes/skyboxes7.png) + +And a material with the same name as the environment texture. + +## Using the SkyBox in OGRE +To load the SkyBox in OGRE it can be done with the DotScene Plugin, where you can use the .scene exported by `blender2ogre`. + \ +The exported scene has the following section: +``` +... + + + + +... +``` + +To use it in code you can do the following: +``` +String material = "symmetrical_garden_02"; +Real distance = 5000; +bool drawFirst = true; +bool active = true; +String groupName = "Scene"; +mSceneMgr->setSkyBox(true, material, distance, drawFirst, rotation, groupName); +``` diff --git a/assets/blender/scripts/blender2ogre/VertexColors.md b/assets/blender/scripts/blender2ogre/VertexColors.md new file mode 100644 index 0000000..3b4fec7 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/VertexColors.md @@ -0,0 +1,171 @@ + +# Exporting Vertex Colors + +## Introduction +It is possible to export the vertex color properties of objects in Blender. + +## Creating Vertex Colors in Blender +1) Select the object you want to create Vertex Colors for + +2) (Blender version < 3.2) Go the the `Object Data Properties` section and expand the Vertex Colors section \ +![vertex_colors2a.png](images/vertex_colors/vertex_colors2a.png) + +3) (Blender version >= 3.2) Go the the `Object Data Properties` section and expand the Color Attributes section \ +![vertex_colors2b.png](images/vertex_colors/vertex_colors2b.png) + +4) Press the + button to create the Vertex Color data + +5) (Blender version < 3.2) \ +![vertex_colors3a.png](images/vertex_colors/vertex_colors3a.png) + +6) (Blender version >= 3.2) You have to select `Face Corner` and `Byte Color` so that the Color data is compatible with the exporter \ +![vertex_colors3b.png](images/vertex_colors/vertex_colors3b.png) + +7) Now that we have created the data block, it is time to fill it with something useful. +Change the mode to `Vertex Paint` and paint the vertices as you please. \ +![vertex_colors4.png](images/vertex_colors/vertex_colors4.png) + +8) To see the results in Blender you have to create a Material that takes as input the Vertex Colors. +Go to the `Shading` tab and create a Material as follows: \ +![vertex_colors5.png](images/vertex_colors/vertex_colors5.png) + +> **NOTE**: In Blender version >= 3.2 the input node is called `Color Attribute` + +9) One last step is to modify the Material so that it is able to render transparency with EEVEE. +Go to the `Material Properties` section and set `Blend Mode: Alpha Hashed` and `Shadow Mode: None` for best results \ +![vertex_colors6.png](images/vertex_colors/vertex_colors6.png) + +## About exporting and importing into Ogre +The exported vertex color property shows in the resulting mesh XML as follows: +``` +... + + + + + + + + +... +``` +The option `colours_diffuse="True"` is an indication to OGRE and also to the blender2ogre importer that the mesh has vertex colors. +The value shows as `` as a float4 with RGB values for color and a fourth value for the alpha. + +To view the results in OGRE, the RTSS follows the normal diffuse colour flow, which can track vertex colours and contain alpha. +See (vertexcolour): https://ogrecave.github.io/ogre/api/latest/_material-_scripts.html#autotoc_md128 + +If you're using your own shaders, then the vertex colour is passed to the vertex shader as the VES_COLOUR (5th element) and format: Colour, typically VET_UBYTE4. +https://ogrecave.github.io/ogre/api/latest/group___render_system.html#gac7ecb5ad110f918f709b3c5d5cbae655 + +Minimal example `vertex_color.vs`: +``` +#version 330 core + +layout (location = 0) in vec4 vertex; +layout (location = 2) in vec3 normal; +layout (location = 3) in vec4 colour; + +out vec3 FragPos; +out vec3 Normal; +out vec4 vertexColor; + +uniform mat4 world; +uniform mat4 worldViewProj; + +void main() +{ + FragPos = vec3(world * vertex); + Normal = normal; + vertexColor = colour; + + gl_Position = worldViewProj * vertex; +} +``` + +Minimal example `vertex_color.fs`: +``` +#version 330 core + +in vec3 FragPos; +in vec4 vertexColor; +in vec3 vertexNormal; + +out vec4 FragColor; + +uniform vec3 lightPos; +uniform vec3 lightColor; +uniform vec3 objectColor; + +void main() +{ + // ambient + float ambientStrength = 0.3; + vec3 ambient = ambientStrength * lightColor; + + // diffuse + vec3 norm = normalize(vertexNormal); + vec3 lightDir = normalize(lightPos - FragPos); + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = diff * lightColor; + + vec3 result = (ambient + diffuse) * vec3(vertexColor); + FragColor = vec4(result, vertexColor.a); +} +``` + +Program: +``` +vertex_program Vertex-ColorVS glsl +{ + source "vertex_color.vs" + + default_params + { + param_named_auto world world_matrix + param_named_auto worldViewProj worldviewproj_matrix + } +} + +fragment_program Vertex-ColorFS glsl +{ + source "vertex_color.fs" + + default_params + { + param_named_auto lightPos light_position 0 + param_named lightColor float3 1.0 1.0 1.0 + } +} +``` + +Material: +``` +material VertexColor { + receive_shadows on + technique { + pass { + lighting on + ambient 0.8 0.8 0.8 1.0 + diffuse 0.64 0.64 0.64 1.0 + specular 0.5 0.5 0.5 1.0 12.5 + emissive 0.0 0.0 0.0 1.0 + + //scene_blend one zero + + vertex_program_ref Vertex-ColorVS + { + } + + fragment_program_ref Vertex-ColorFS + { + } + } + } +} +``` + +## Troubleshooting +Some tips for troubleshooting: + - For Blender versions >= 3.2 make sure that the color data is set to `Face Corner` and `Byte Color`. + - Beware of selecting an object and directly changing the mode to `Vertex Paint` because the color data is automatically created. diff --git a/assets/blender/scripts/blender2ogre/archived_code/game_logic.py b/assets/blender/scripts/blender2ogre/archived_code/game_logic.py new file mode 100644 index 0000000..364805c --- /dev/null +++ b/assets/blender/scripts/blender2ogre/archived_code/game_logic.py @@ -0,0 +1,201 @@ +## Archived on the 22/09/2021 +## Original game_logic.py lived at io_ogre/game_logic.py + +_game_logic_intro_doc_ = ''' +Hijacking the BGE + +Blender contains a fully functional game engine (BGE) that is highly useful for learning the concepts of game programming by breaking it down into three simple parts: Sensor, Controller, and Actuator. An Ogre based game engine will likely have similar concepts in its internal API and game logic scripting. Without a custom interface to define game logic, very often game designers may have to resort to having programmers implement their ideas in purely handwritten script. This is prone to breakage because object names then end up being hard-coded. Not only does this lead to non-reusable code, its also a slow process. Why should we have to resort to this when Blender already contains a very rich interface for game logic? By hijacking a subset of the BGE interface we can make this workflow between game designer and game programmer much better. + +The OgreDocScene format can easily be extened to include extra game logic data. While the BGE contains some features that can not be easily mapped to other game engines, there are many are highly useful generic features we can exploit, including many of the Sensors and Actuators. Blender uses the paradigm of: 1. Sensor -> 2. Controller -> 3. Actuator. In pseudo-code, this can be thought of as: 1. on-event -> 2. conditional logic -> 3. do-action. The designer is most often concerned with the on-events (the Sensors), and the do-actions (the Actuators); and the BGE interface provides a clear way for defining and editing those. Its a harder task to provide a good interface for the conditional logic (Controller), that is flexible enough to fit everyones different Ogre engine and requirements, so that is outside the scope of this exporter at this time. A programmer will still be required to fill the gap between Sensor and Actuator, but hopefully his work is greatly reduced and can write more generic/reuseable code. + +The rules for which Sensors trigger which Actuators is left undefined, as explained above we are hijacking the BGE interface not trying to export and reimplement everything. BGE Controllers and all links are ignored by the exporter, so whats the best way to define Sensor/Actuator relationships? One convention that seems logical is to group Sensors and Actuators by name. More complex syntax could be used in Sensor/Actuators names, or they could be completely ignored and instead all the mapping is done by the game programmer using other rules. This issue is not easily solved so designers and the engine programmers will have to decide upon their own conventions, there is no one size fits all solution. +''' + +_ogre_logic_types_doc_ = ''' +Supported Sensors: + . Collision + . Near + . Radar + . Touching + . Raycast + . Message + +Supported Actuators: + . Shape Action* + . Edit Object + . Camera + . Constraint + . Message + . Motion + . Sound + . Visibility + +*note: Shape Action +The most common thing a designer will want to do is have an event trigger an animation. The BGE contains an Actuator called "Shape Action", with useful properties like: start/end frame, and blending. It also contains a property called "Action" but this is hidden because the exporter ignores action names and instead uses the names of NLA strips when exporting Ogre animation tracks. The current workaround is to hijack the "Frame Property" attribute and change its name to "animation". The designer can then simply type the name of the animation track (NLA strip). Any custom syntax could actually be implemented here for calling animations, its up to the engine programmer to define how this field will be used. For example: "*.explode" could be implemented to mean "on all objects" play the "explode" animation. +''' + +# UI panels + +@UI +class PANEL_Physics(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Physics" + + @classmethod + def poll(cls, context): + if context.active_object: + return True + else: + return False + + def draw(self, context): + layout = self.layout + ob = context.active_object + game = ob.game + + if ob.type != 'MESH': + return + elif ob.subcollision == True: + box = layout.box() + if ob.parent: + box.label(text='object is a collision proxy for: %s' %ob.parent.name) + else: + box.label(text='WARNING: collision proxy missing parent') + return + + box = layout.box() + box.prop(ob, 'physics_mode') + if ob.physics_mode != 'NONE': + box.prop(game, 'mass', text='Mass') + box.prop(ob, 'physics_friction', text='Friction', slider=True) + box.prop(ob, 'physics_bounce', text='Bounce', slider=True) + + box.label(text="Damping:") + box.prop(game, 'damping', text='Translation', slider=True) + box.prop(game, 'rotation_damping', text='Rotation', slider=True) + + box.label(text="Velocity:") + box.prop(game, "velocity_min", text="Minimum") + box.prop(game, "velocity_max", text="Maximum") + +@UI +class PANEL_Collision(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Collision" + + @classmethod + def poll(cls, context): + if context.active_object: + return True + else: + return False + + def draw(self, context): + layout = self.layout + ob = context.active_object + game = ob.game + + if ob.type != 'MESH': + return + elif ob.subcollision == True: + box = layout.box() + if ob.parent: + box.label(text='object is a collision proxy for: %s' %ob.parent.name) + else: + box.label(text='WARNING: collision proxy missing parent') + return + + mode = ob.collision_mode + if mode == 'NONE': + box = layout.box() + op = box.operator( 'ogre.set_collision', text='Enable Collision', icon='PHYSICS' ) + op.MODE = 'PRIMITIVE:%s' %game.collision_bounds_type + else: + prim = game.collision_bounds_type + + box = layout.box() + op = box.operator( 'ogre.set_collision', text='Disable Collision', icon='X' ) + op.MODE = 'NONE' + box.prop(game, "collision_margin", text="Collision Margin", slider=True) + + box = layout.box() + if mode == 'PRIMITIVE': + box.label(text='Primitive: %s' %prim) + else: + box.label(text='Primitive') + + row = box.row() + _icons = { + 'BOX':'MESH_CUBE', 'SPHERE':'MESH_UVSPHERE', 'CYLINDER':'MESH_CYLINDER', + 'CONE':'MESH_CONE', 'CAPSULE':'META_CAPSULE'} + for a in 'BOX SPHERE CYLINDER CONE CAPSULE'.split(): + if prim == a and mode == 'PRIMITIVE': + op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=True ) + op.MODE = 'PRIMITIVE:%s' %a + else: + op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=False ) + op.MODE = 'PRIMITIVE:%s' %a + + box = layout.box() + if mode == 'MESH': box.label(text='Mesh: %s' %prim.split('_')[0] ) + else: box.label(text='Mesh') + row = box.row() + row.label(text='- - - - - - - - - - - - - -') + _icons = {'TRIANGLE_MESH':'MESH_ICOSPHERE', 'CONVEX_HULL':'SURFACE_NCURVE'} + for a in 'TRIANGLE_MESH CONVEX_HULL'.split(): + if prim == a and mode == 'MESH': + op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=True ) + op.MODE = 'MESH:%s' %a + else: + op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=False ) + op.MODE = 'MESH:%s' %a + + box = layout.box() + if mode == 'DECIMATED': + box.label(text='Decimate: %s' %prim.split('_')[0] ) + row = box.row() + mod = _get_proxy_decimate_mod( ob ) + assert mod # decimate modifier is missing + row.label(text='Faces: %s' %mod.face_count ) + box.prop( mod, 'ratio', text='' ) + else: + box.label(text='Decimate') + row = box.row() + row.label(text='- - - - - - - - - - - - - -') + + _icons = {'TRIANGLE_MESH':'MESH_ICOSPHERE', 'CONVEX_HULL':'SURFACE_NCURVE'} + for a in 'TRIANGLE_MESH CONVEX_HULL'.split(): + if prim == a and mode == 'DECIMATED': + op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=True ) + op.MODE = 'DECIMATED:%s' %a + else: + op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=False ) + op.MODE = 'DECIMATED:%s' %a + + box = layout.box() + if mode == 'TERRAIN': + terrain = get_subcollisions( ob )[0] + if ob.collision_terrain_x_steps != terrain.collision_terrain_x_steps or ob.collision_terrain_y_steps != terrain.collision_terrain_y_steps: + op = box.operator( 'ogre.set_collision', text='Rebuild Terrain', icon='MESH_GRID' ) + op.MODE = 'TERRAIN' + else: + box.label(text='Terrain:') + row = box.row() + row.prop( ob, 'collision_terrain_x_steps', 'X' ) + row.prop( ob, 'collision_terrain_y_steps', 'Y' ) + #box.prop( terrain.modifiers[0], 'offset' ) # gets normalized away + box.prop( terrain.modifiers[0], 'cull_face', text='Cull' ) + box.prop( terrain, 'location' ) # TODO hide X and Y + else: + op = box.operator( 'ogre.set_collision', text='Terrain Collision', icon='MESH_GRID' ) + op.MODE = 'TERRAIN' + + box = layout.box() + if mode == 'COMPOUND': + op = box.operator( 'ogre.set_collision', text='Compound Collision', icon='ROTATECOLLECTION' ) + else: + op = box.operator( 'ogre.set_collision', text='Compound Collision', icon='ROTATECOLLECTION' ) + op.MODE = 'COMPOUND' + diff --git a/assets/blender/scripts/blender2ogre/archived_code/materials.py b/assets/blender/scripts/blender2ogre/archived_code/materials.py new file mode 100644 index 0000000..5d182a7 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/archived_code/materials.py @@ -0,0 +1,216 @@ +## Archived on the 22/09/2021 +## Original materials.py lived at io_ogre/ui/materials.py + +import bpy +from .. import shader +from bpy.props import IntProperty +from ..ogre.material import OgreMaterialGenerator +from ..util import wordwrap + + +def ogre_register(register): + yield PANEL_properties_window_ogre_material + yield MatPass1 + yield MatPass2 + yield MatPass3 + yield MatPass4 + yield MatPass5 + yield MatPass6 + yield MatPass7 + yield MatPass8 + yield MT_preview_material_text + yield CreateMaterialLayerOperator + yield SetupMaterialPassesOperator + +class MT_preview_material_text(bpy.types.Menu): + """ Preview the outputted material in a menu in the top header """ + bl_label = 'preview' + + @classmethod + def poll(self,context): + if context.active_object and context.active_object.active_material: + return True + + def draw(self, context): + layout = self.layout + mat = context.active_object.active_material + if mat: + preview = OgreMaterialGenerator( mat ).generate() + for line in preview.splitlines(): + if line.strip(): + for ww in wordwrap( line ): + layout.label(text=ww) + + +class CreateMaterialLayerOperator(bpy.types.Operator): + '''helper to create new material layer''' + bl_idname = "ogre.helper_create_attach_material_layer" + bl_label = "creates and assigns new material to layer" + bl_options = {'REGISTER'} + INDEX = IntProperty(name="material layer index", description="index", default=0, min=0, max=8) + + @classmethod + def poll(cls, context): + if context.active_object and context.active_object.active_material\ + and context.active_object.active_material.use_material_passes: + return True + + def execute(self, context): + mat = context.active_object.active_material + nodes = shader.get_or_create_material_passes( mat ) + node = nodes[ self.INDEX ] + node.material = bpy.data.materials.new( name='%s.LAYER%s'%(mat.name,self.INDEX) ) + node.material.offset_z = (self.INDEX*2) + 2 # nudge each pass by 2 + return {'FINISHED'} + +class SetupMaterialPassesOperator(bpy.types.Operator): + '''operator: enables material nodes (workaround for not having IDPointers in pyRNA)''' + bl_idname = "ogre.force_setup_material_passes" + bl_label = "force bpyShaders" + bl_options = {'REGISTER'} + + @classmethod + def poll(cls, context): + if context.active_object and context.active_object.active_material: return True + + def invoke(self, context, event): + mat = context.active_object.active_material + mat.use_material_passes = True + shader.create_material_passes( mat ) + return {'FINISHED'} + +class PANEL_properties_window_ogre_material( bpy.types.Panel ): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "material" + bl_label = "Ogre Material (base pass)" + + @classmethod + def poll( self, context ): + if not hasattr(context, "material"): return False + if not context.active_object: return False + if not context.active_object.active_material: return False + return True + + def draw(self, context): + mat = context.material + ob = context.object + slot = context.material_slot + layout = self.layout + if not mat.use_material_passes: + box = layout.box() + box.operator( 'ogre.force_setup_material_passes', text="Ogre Material Layers", icon='SCENE_DATA' ) + + ogre_material_panel( layout, mat ) + ogre_material_panel_extra( layout, mat ) + +class _OgreMatPass( object ): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "material" + + @classmethod + def poll(cls, context): + if context.active_object and context.active_object.active_material and context.active_object.active_material.use_material_passes: + return True + + def draw(self, context): + if not hasattr(context, "material"): + return + if not context.active_object: + return + if not context.active_object.active_material: + return + + mat = context.material + ob = context.object + slot = context.material_slot + layout = self.layout + #layout.label(text=str(self.INDEX)) + if mat.use_material_passes: + db = layout.box() + nodes = shader.get_or_create_material_passes( mat ) + node = nodes[ self.INDEX ] + split = db.row() + if node.material: split.prop( node.material, 'use_in_ogre_material_pass', text='' ) + split.prop( node, 'material' ) + if not node.material: + op = split.operator( 'ogre.helper_create_attach_material_layer', icon="PLUS", text='' ) + op.INDEX = self.INDEX + if node.material and node.material.use_in_ogre_material_pass: + dbb = db.box() + ogre_material_panel( dbb, node.material, parent=mat ) + ogre_material_panel_extra( dbb, node.material ) + +# is there a better way to do this? +class MatPass1( _OgreMatPass, bpy.types.Panel ): INDEX = 0; bl_label = "Ogre Material (pass%s)"%str(INDEX+1) +class MatPass2( _OgreMatPass, bpy.types.Panel ): INDEX = 1; bl_label = "Ogre Material (pass%s)"%str(INDEX+1) +class MatPass3( _OgreMatPass, bpy.types.Panel ): INDEX = 2; bl_label = "Ogre Material (pass%s)"%str(INDEX+1) +class MatPass4( _OgreMatPass, bpy.types.Panel ): INDEX = 3; bl_label = "Ogre Material (pass%s)"%str(INDEX+1) +class MatPass5( _OgreMatPass, bpy.types.Panel ): INDEX = 4; bl_label = "Ogre Material (pass%s)"%str(INDEX+1) +class MatPass6( _OgreMatPass, bpy.types.Panel ): INDEX = 5; bl_label = "Ogre Material (pass%s)"%str(INDEX+1) +class MatPass7( _OgreMatPass, bpy.types.Panel ): INDEX = 6; bl_label = "Ogre Material (pass%s)"%str(INDEX+1) +class MatPass8( _OgreMatPass, bpy.types.Panel ): INDEX = 7; bl_label = "Ogre Material (pass%s)"%str(INDEX+1) + +def ogre_material_panel_extra( parent, mat ): + box = parent.box() + header = box.row() + header.prop(mat, 'use_ogre_advanced_options', text='---guru options---' ) + + if mat.use_ogre_advanced_options: + box.prop(mat, 'offset_z') + for tag in 'ogre_colour_write ogre_normalise_normals ogre_light_clip_planes ogre_light_scissor ogre_alpha_to_coverage ogre_depth_check'.split(): + box.prop(mat, tag) + for tag in 'ogre_polygon_mode ogre_shading ogre_transparent_sorting ogre_illumination_stage ogre_depth_func ogre_scene_blend_op'.split(): + box.prop(mat, tag) + +def ogre_material_panel( layout, mat, parent=None, show_programs=True ): + if not parent: + return # only allow on pass1 and higher + + box = layout.box() + header = box.row() + + header.prop(mat, 'use_ogre_parent_material', icon='FILE_SCRIPT', text='') + + if mat.use_ogre_parent_material: + row = box.row() + row.prop(mat, 'ogre_parent_material', text='') + + s = get_ogre_user_material( mat.ogre_parent_material ) # gets by name + if s and (s.vertex_programs or s.fragment_programs): + progs = s.get_programs() + split = box.row() + texnodes = None + + if parent: + texnodes = shader.get_texture_subnodes( parent, submaterial=mat ) + elif mat.node_tree: + texnodes = shader.get_texture_subnodes( mat ) # assume toplevel + + if not progs: + bx = split.box() + bx.label( text='(missing shader programs)', icon='ERROR' ) + elif s.texture_units and texnodes: + bx = split.box() + for i,name in enumerate(s.texture_units_order): + if i= 1.0: + texop = 'alpha_blend' + else: + texop = TextureUnit.colour_op_ex[ btype ] + ex = True + elif btype=='MIX' and slot.use_map_alpha and slot.use_stencil: + texop = 'blend_current_alpha'; ex=True + elif btype=='MIX' and not slot.use_map_alpha and slot.use_stencil: + texop = 'blend_texture_alpha'; ex=True + else: + texop = TextureUnit.colour_op[ btype ] + elif btype in TextureUnit.colour_op_ex: + texop = TextureUnit.colour_op_ex[ btype ] + ex = True + + box = layout.box() + row = box.row() + if texop: + if ex: + row.prop(slot, "blend_type", text=texop, icon='NEW') + else: + row.prop(slot, "blend_type", text=texop) + else: + row.prop(slot, "blend_type", text='(invalid option)') + + if btype == 'MIX': + row.prop(slot, "use_stencil", text="") + row.prop(slot, "use_map_alpha", text="") + if texop == 'blend_manual': + row = box.row() + row.label(text="Alpha:") + row.prop(slot, "diffuse_color_factor", text="") + + if hasattr(slot.texture, 'image') and slot.texture.image: + row = box.row() + n = '(invalid option)' + if slot.texture.extension in TextureUnit.tex_address_mode: + n = TextureUnit.tex_address_mode[ slot.texture.extension ] + row.prop(slot.texture, "extension", text=n) + if slot.texture.extension == 'CLIP': + row.prop(slot, "color", text="Border Color") + + row = box.row() + if slot.texture_coords == 'UV': + row.prop(slot, "texture_coords", text="", icon='GROUP_UVS') + row.prop(slot, "uv_layer", text='Layer') + elif slot.texture_coords == 'REFLECTION': + row.prop(slot, "texture_coords", text="", icon='MOD_UVPROJECT') + n = '(invalid option)' + if slot.mapping in 'FLAT SPHERE'.split(): n = '' + row.prop(slot, "mapping", text=n) + else: + row.prop(slot, "texture_coords", text="(invalid mapping option)") + + # Animation and offset options + split = layout.row() + box = split.box() + box.prop(slot, "offset", text="XY=offset, Z=rotation") + box = split.box() + box.prop(slot, "scale", text="XY=scale (Z ignored)") + + box = layout.box() + row = box.row() + row.label(text='scrolling animation') + + # Can't use if its enabled by default row.prop(slot, "use_map_density", text="") + row.prop(slot, "use_map_scatter", text="") + row = box.row() + row.prop(slot, "density_factor", text="X") + row.prop(slot, "emission_factor", text="Y") + + box = layout.box() + row = box.row() + row.label(text='rotation animation') + row.prop(slot, "emission_color_factor", text="") + row.prop(slot, "use_from_dupli", text="") + + ## Image magick + if hasattr(slot.texture, 'image') and slot.texture.image: + img = slot.texture.image + box = layout.box() + row = box.row() + row.prop( img, 'use_convert_format' ) + if img.use_convert_format: + row.prop( img, 'convert_format' ) + if img.convert_format == 'jpg': + box.prop( img, 'jpeg_quality' ) + + row = box.row() + row.prop( img, 'use_color_quantize', text='Reduce Colors' ) + if img.use_color_quantize: + row.prop( img, 'use_color_quantize_dither', text='dither' ) + row.prop( img, 'color_quantize', text='colors' ) + + row = box.row() + row.prop( img, 'use_resize_half' ) + if not img.use_resize_half: + row.prop( img, 'use_resize_absolute' ) + if img.use_resize_absolute: + row = box.row() + row.prop( img, 'resize_x' ) + row.prop( img, 'resize_y' ) + +## Ogre Documentation to UI + +class INFO_MT_ogre_shader_pass_attributes(bpy.types.Menu): + bl_label = "Shader-Pass" + + def draw(self, context): + layout = self.layout + for cls in _OGRE_SHADER_REF_: + layout.menu( cls.__name__ ) + +class INFO_MT_ogre_shader_texture_attributes(bpy.types.Menu): + bl_label = "Shader-Texture" + + def draw(self, context): + layout = self.layout + for cls in _OGRE_SHADER_REF_TEX_: + layout.menu( cls.__name__ ) + +class MeshMagick(object): + ''' Usage: MeshMagick [global_options] toolname [tool_options] infile(s) -- [outfile(s)] + Available Tools + =============== + info - print information about the mesh. + meshmerge - Merge multiple submeshes into a single mesh. + optimise - Optimise meshes and skeletons. + rename - Rename different elements of meshes and skeletons. + transform - Scale, rotate or otherwise transform a mesh. + ''' + + @staticmethod + def get_merge_group( ob ): + return get_merge_group( ob, prefix='magicmerge' ) + + @staticmethod + def merge( group, path='/tmp', force_name=None ): + print('-'*80) + print(' mesh magick - merge ') + exe = CONFIG['OGRETOOLS_MESH_MAGICK'] + if not os.path.isfile( exe ): + print( 'ERROR: can not find MeshMagick.exe' ) + print( exe ) + return + + files = [] + for ob in group.objects: + if ob.data.users == 1: # single users only + files.append( os.path.join( path, ob.data.name+'.mesh' ) ) + print( files[-1] ) + + opts = 'meshmerge' + if sys.platform == 'linux2': cmd = '/usr/bin/wine %s %s' %(exe, opts) + else: cmd = '%s %s' %(exe, opts) + if force_name: output = force_name + '.mesh' + else: output = '_%s_.mesh' %group.name + cmd = cmd.split() + files + ['--', os.path.join(path,output) ] + subprocess.call( cmd ) + print(' mesh magick - complete ') + print('-'*80) + +## Selector extras + +class INFO_MT_instances(bpy.types.Menu): + bl_label = "Instances" + + def draw(self, context): + layout = self.layout + inst = gather_instances() + for data in inst: + ob = inst[data][0] + op = layout.operator(INFO_MT_instance.bl_idname, text=ob.name) # operator has no variable for button name? + op.mystring = ob.name + layout.separator() + +class INFO_MT_instance(bpy.types.Operator): + '''select instance group''' + bl_idname = "ogre.select_instances" + bl_label = "Select Instance Group" + bl_options = {'REGISTER', 'UNDO'} # Options for this panel type + mystring= StringProperty(name="MyString", description="hidden string", maxlen=1024, default="my string") + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + print( 'invoke select_instances op', event ) + select_instances( context, self.mystring ) + return {'FINISHED'} + +class INFO_MT_groups(bpy.types.Menu): + bl_label = "Groups" + + def draw(self, context): + layout = self.layout + for group in bpy.data.collections: + op = layout.operator(INFO_MT_group.bl_idname, text=group.name) # operator no variable for button name? + op.mystring = group.name + layout.separator() + +class INFO_MT_group(bpy.types.Operator): + '''select group''' + bl_idname = "ogre.select_group" + bl_label = "Select Group" + bl_options = {'REGISTER'} # Options for this panel type + mystring= StringProperty(name="MyString", description="hidden string", maxlen=1024, default="my string") + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + select_group( context, self.mystring ) + return {'FINISHED'} + +## More UI + diff --git a/assets/blender/scripts/blender2ogre/archived_code/unused.py b/assets/blender/scripts/blender2ogre/archived_code/unused.py new file mode 100644 index 0000000..f7c9b88 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/archived_code/unused.py @@ -0,0 +1,159 @@ +## Archived on the 22/09/2021 +## Original unused.py lived at io_ogre/unused.py + +## Ogre Command Line Tools Documentation +## Pop up dialog for various info/error messages + +popup_message = "" + +class PopUpDialogOperator(bpy.types.Operator): + bl_idname = "object.popup_dialog_operator" + bl_label = "blender2ogre" + + def __init__(self): + print("dialog Start") + + def __del__(self): + print("dialog End") + + def execute(self, context): + print ("execute") + return {'RUNNING_MODAL'} + + def draw(self, context): + # todo: Make this bigger and center on screen. + # Blender UI stuff seems quite complex, would + # think that showing a dialog with a message thath + # does not hide when mouse is moved would be simpler! + global popup_message + layout = self.layout + col = layout.column() + col.label(popup_message, 'ERROR') + + def invoke(self, context, event): + wm = context.window_manager + wm.invoke_popup(self) + wm.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + # Close + if event.type == 'LEFTMOUSE': + print ("Left mouse") + return {'FINISHED'} + # Close + elif event.type in ('RIGHTMOUSE', 'ESC'): + print ("right mouse") + return {'FINISHED'} + + print("running modal") + return {'RUNNING_MODAL'} + +def show_dialog(message): + global popup_message + popup_message = message + bpy.ops.object.popup_dialog_operator('INVOKE_DEFAULT') + + +_ogre_command_line_tools_doc = ''' +Usage: OgreXMLConverter [options] sourcefile [destfile] + +Available options: +-i = interactive mode - prompt for options +(The next 4 options are only applicable when converting XML to Mesh) +-l lodlevels = number of LOD levels +-v lodvalue = value increment to reduce LOD +-s lodstrategy = LOD strategy to use for this mesh +-p lodpercent = Percentage triangle reduction amount per LOD +-f lodnumtris = Fixed vertex reduction per LOD +-e = DON'T generate edge lists (for stencil shadows) +-r = DON'T reorganise vertex buffers to OGRE recommended format. +-t = Generate tangents (for normal mapping) +-td [uvw|tangent] + = Tangent vertex semantic destination (default tangent) +-ts [3|4] = Tangent size (3 or 4 components, 4 includes parity, default 3) +-tm = Split tangent vertices at UV mirror points +-tr = Split tangent vertices where basis is rotated > 90 degrees +-o = DON'T optimise out redundant tracks & keyframes +-d3d = Prefer D3D packed colour formats (default on Windows) +-gl = Prefer GL packed colour formats (default on non-Windows) +-E endian = Set endian mode 'big' 'little' or 'native' (default) +-x num = Generate no more than num eXtremes for every submesh (default 0) +-q = Quiet mode, less output +-log filename = name of the log file (default: 'OgreXMLConverter.log') +sourcefile = name of file to convert +destfile = optional name of file to write to. If you don't + specify this OGRE works it out through the extension + and the XML contents if the source is XML. For example + test.mesh becomes test.xml, test.xml becomes test.mesh + if the XML document root is etc. +''' + +class CMesh(object): + + def __init__(self, data): + self.numVerts = N = len( data.vertices ) + self.numFaces = Nfaces = len(data.tessfaces) + + self.vertex_positions = (ctypes.c_float * (N * 3))() + data.vertices.foreach_get( 'co', self.vertex_positions ) + v = self.vertex_positions + + self.vertex_normals = (ctypes.c_float * (N * 3))() + data.vertices.foreach_get( 'normal', self.vertex_normals ) + + self.faces = (ctypes.c_uint * (Nfaces * 4))() + data.tessfaces.foreach_get( 'vertices_raw', self.faces ) + + self.faces_normals = (ctypes.c_float * (Nfaces * 3))() + data.tessfaces.foreach_get( 'normal', self.faces_normals ) + + self.faces_smooth = (ctypes.c_bool * Nfaces)() + data.tessfaces.foreach_get( 'use_smooth', self.faces_smooth ) + + self.faces_material_index = (ctypes.c_ushort * Nfaces)() + data.tessfaces.foreach_get( 'material_index', self.faces_material_index ) + + self.vertex_colors = [] + if len( data.vertex_colors ): + vc = data.vertex_colors[0] + n = len(vc.data) + # no colors_raw !!? + self.vcolors1 = (ctypes.c_float * (n * 3))() # face1 + vc.data.foreach_get( 'color1', self.vcolors1 ) + self.vertex_colors.append( self.vcolors1 ) + + self.vcolors2 = (ctypes.c_float * (n * 3))() # face2 + vc.data.foreach_get( 'color2', self.vcolors2 ) + self.vertex_colors.append( self.vcolors2 ) + + self.vcolors3 = (ctypes.c_float * (n * 3))() # face3 + vc.data.foreach_get( 'color3', self.vcolors3 ) + self.vertex_colors.append( self.vcolors3 ) + + self.vcolors4 = (ctypes.c_float * (n * 3))() # face4 + vc.data.foreach_get( 'color4', self.vcolors4 ) + self.vertex_colors.append( self.vcolors4 ) + + self.uv_textures = [] + if data.uv_textures.active: + for layer in data.uv_textures: + n = len(layer.data) * 8 + a = (ctypes.c_float * n)() + layer.data.foreach_get( 'uv_raw', a ) # 4 faces + self.uv_textures.append( a ) + + def save( blenderobject, path ): + cmesh = Mesh( blenderobject.data ) + start = time.time() + dotmesh( + path, + ctypes.addressof( cmesh.faces ), + ctypes.addressof( cmesh.faces_smooth ), + ctypes.addressof( cmesh.faces_material_index ), + ctypes.addressof( cmesh.vertex_positions ), + ctypes.addressof( cmesh.vertex_normals ), + cmesh.numFaces, + cmesh.numVerts, + ) + print('Mesh dumped in %s seconds' % (time.time()-start)) \ No newline at end of file diff --git a/assets/blender/scripts/blender2ogre/examples/armature-test.blend b/assets/blender/scripts/blender2ogre/examples/armature-test.blend new file mode 100644 index 0000000..d13aca2 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/armature-test.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3d6fcf0d964feb88ff5180df6659d6ae5e2f76e50d111c82dd7bdf5a164edb4 +size 443388 diff --git a/assets/blender/scripts/blender2ogre/examples/axis.blend b/assets/blender/scripts/blender2ogre/examples/axis.blend new file mode 100644 index 0000000..7050528 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/axis.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9372653b27daf358db290aabbadde27762a21d79a6999bd4cc331a0bd4396eb7 +size 400304 diff --git a/assets/blender/scripts/blender2ogre/examples/floating-bones.blend b/assets/blender/scripts/blender2ogre/examples/floating-bones.blend new file mode 100644 index 0000000..661463e --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/floating-bones.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f955721e757fda3badef17f412de02f74ce484428be55f4bfe17984f761e3b11 +size 484488 diff --git a/assets/blender/scripts/blender2ogre/examples/layered-materials.blend b/assets/blender/scripts/blender2ogre/examples/layered-materials.blend new file mode 100644 index 0000000..072fe57 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/layered-materials.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cde99c5fa7e9110e3791d52e99265e3c8f858645aa13745ece4eeeaf35ba8956 +size 478948 diff --git a/assets/blender/scripts/blender2ogre/examples/merge-group-linked-scene.blend b/assets/blender/scripts/blender2ogre/examples/merge-group-linked-scene.blend new file mode 100644 index 0000000..da403df --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/merge-group-linked-scene.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:627c3f41638f9f800b01bcc5b90588df54c49a79beb4bd95ec2ba894314a689e +size 312144 diff --git a/assets/blender/scripts/blender2ogre/examples/merge-group-linked-source.blend b/assets/blender/scripts/blender2ogre/examples/merge-group-linked-source.blend new file mode 100644 index 0000000..60c71e1 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/merge-group-linked-source.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39f22b9786d6188bb8b30e21ce1e67b832b790271288f88d2a33e6c3c8748b05 +size 457160 diff --git a/assets/blender/scripts/blender2ogre/examples/multi-material.blend b/assets/blender/scripts/blender2ogre/examples/multi-material.blend new file mode 100644 index 0000000..0aaae40 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/multi-material.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d42c6dfd2046d0911c5b2f58e3c6d64c22a8f7cab505405a0f2e2a3f2ee06e23 +size 413080 diff --git a/assets/blender/scripts/blender2ogre/examples/multiscene.blend b/assets/blender/scripts/blender2ogre/examples/multiscene.blend new file mode 100644 index 0000000..fa7aeab --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/multiscene.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90719ca032c38b303b52186072c873dbf0e7e2deac21c9383731c95c3953ca6f +size 717004 diff --git a/assets/blender/scripts/blender2ogre/examples/multitrack-advanced.blend b/assets/blender/scripts/blender2ogre/examples/multitrack-advanced.blend new file mode 100644 index 0000000..f1606e3 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/multitrack-advanced.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f89c4b13b51f2e70a55be69c4b83ff0580406d06bbf938e2f9f5815c9615ae41 +size 548248 diff --git a/assets/blender/scripts/blender2ogre/examples/multitrack-simple.blend b/assets/blender/scripts/blender2ogre/examples/multitrack-simple.blend new file mode 100644 index 0000000..aa5d03b --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/multitrack-simple.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae2cab4d57e0579e5eb0c93573009345e2f85e1b15dc78e19a36b795b028a8b8 +size 543448 diff --git a/assets/blender/scripts/blender2ogre/examples/object-links.blend b/assets/blender/scripts/blender2ogre/examples/object-links.blend new file mode 100644 index 0000000..9630d45 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/object-links.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:361eb888428b93cbd8c8025eb69ce377f747da9c1c9ed426311f35a0513f72d9 +size 432976 diff --git a/assets/blender/scripts/blender2ogre/examples/shape-animation.blend b/assets/blender/scripts/blender2ogre/examples/shape-animation.blend new file mode 100644 index 0000000..85e66da --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/shape-animation.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b34be3d1abdd4dc64539ebaedade9f674c473fc76d0f0835300ce119aaed38f7 +size 620512 diff --git a/assets/blender/scripts/blender2ogre/examples/smooth-and-sharp-normals.blend b/assets/blender/scripts/blender2ogre/examples/smooth-and-sharp-normals.blend new file mode 100644 index 0000000..99eb22b --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/smooth-and-sharp-normals.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8279ef04c961eb89850c383a5a98e6e0895fcacc810bcd608f3478592ea9959a +size 434480 diff --git a/assets/blender/scripts/blender2ogre/examples/texture.jpg b/assets/blender/scripts/blender2ogre/examples/texture.jpg new file mode 100644 index 0000000..17a058a Binary files /dev/null and b/assets/blender/scripts/blender2ogre/examples/texture.jpg differ diff --git a/assets/blender/scripts/blender2ogre/examples/texture2.jpg b/assets/blender/scripts/blender2ogre/examples/texture2.jpg new file mode 100644 index 0000000..7856d62 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/examples/texture2.jpg differ diff --git a/assets/blender/scripts/blender2ogre/examples/vertex-colored-alpha.blend b/assets/blender/scripts/blender2ogre/examples/vertex-colored-alpha.blend new file mode 100644 index 0000000..4072084 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/vertex-colored-alpha.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:070ba280037888783d44f57313389355eb52374603a0b229ddc2556f82c45ef8 +size 434788 diff --git a/assets/blender/scripts/blender2ogre/examples/vertex-colored.blend b/assets/blender/scripts/blender2ogre/examples/vertex-colored.blend new file mode 100644 index 0000000..266a996 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/examples/vertex-colored.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75d3422168fd05202e42cac3943074fe34976ca7ea177e3238bdf0c3f061a307 +size 426380 diff --git a/assets/blender/scripts/blender2ogre/feature_tests/nodeanimation.blend b/assets/blender/scripts/blender2ogre/feature_tests/nodeanimation.blend new file mode 100644 index 0000000..53bb491 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/feature_tests/nodeanimation.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8aed1ac562d18abe0b50db007a1260199591b6a0c7ae1b934490b53f028ce4fd +size 531516 diff --git a/assets/blender/scripts/blender2ogre/images/blender-vertex-group.png b/assets/blender/scripts/blender2ogre/images/blender-vertex-group.png new file mode 100644 index 0000000..0e227b3 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/blender-vertex-group.png differ diff --git a/assets/blender/scripts/blender2ogre/images/cube-action.png b/assets/blender/scripts/blender2ogre/images/cube-action.png new file mode 100644 index 0000000..96374d2 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/cube-action.png differ diff --git a/assets/blender/scripts/blender2ogre/images/dupli-offset.png b/assets/blender/scripts/blender2ogre/images/dupli-offset.png new file mode 100644 index 0000000..46bdaf9 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/dupli-offset.png differ diff --git a/assets/blender/scripts/blender2ogre/images/extern-material.png b/assets/blender/scripts/blender2ogre/images/extern-material.png new file mode 100644 index 0000000..90413a0 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/extern-material.png differ diff --git a/assets/blender/scripts/blender2ogre/images/follow-path-const.png b/assets/blender/scripts/blender2ogre/images/follow-path-const.png new file mode 100644 index 0000000..835da15 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/follow-path-const.png differ diff --git a/assets/blender/scripts/blender2ogre/images/mesh-preview-button.png b/assets/blender/scripts/blender2ogre/images/mesh-preview-button.png new file mode 100644 index 0000000..b9261e7 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/mesh-preview-button.png differ diff --git a/assets/blender/scripts/blender2ogre/images/modifiers/fail.png b/assets/blender/scripts/blender2ogre/images/modifiers/fail.png new file mode 100644 index 0000000..bc76be6 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/modifiers/fail.png differ diff --git a/assets/blender/scripts/blender2ogre/images/modifiers/ok.png b/assets/blender/scripts/blender2ogre/images/modifiers/ok.png new file mode 100644 index 0000000..7154bfc Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/modifiers/ok.png differ diff --git a/assets/blender/scripts/blender2ogre/images/modifiers/warning.png b/assets/blender/scripts/blender2ogre/images/modifiers/warning.png new file mode 100644 index 0000000..2602942 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/modifiers/warning.png differ diff --git a/assets/blender/scripts/blender2ogre/images/normals/auto-smooth-and-split-normals.png b/assets/blender/scripts/blender2ogre/images/normals/auto-smooth-and-split-normals.png new file mode 100644 index 0000000..ee23a19 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/normals/auto-smooth-and-split-normals.png differ diff --git a/assets/blender/scripts/blender2ogre/images/normals/fillet-edges-modifiers.png b/assets/blender/scripts/blender2ogre/images/normals/fillet-edges-modifiers.png new file mode 100644 index 0000000..2c08fc5 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/normals/fillet-edges-modifiers.png differ diff --git a/assets/blender/scripts/blender2ogre/images/normals/fillet-edges.png b/assets/blender/scripts/blender2ogre/images/normals/fillet-edges.png new file mode 100644 index 0000000..6e74b61 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/normals/fillet-edges.png differ diff --git a/assets/blender/scripts/blender2ogre/images/normals/flat-vs-smooth-vs-auto-smooth.png b/assets/blender/scripts/blender2ogre/images/normals/flat-vs-smooth-vs-auto-smooth.png new file mode 100644 index 0000000..8e0d62a Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/normals/flat-vs-smooth-vs-auto-smooth.png differ diff --git a/assets/blender/scripts/blender2ogre/images/normals/normals-menu.png b/assets/blender/scripts/blender2ogre/images/normals/normals-menu.png new file mode 100644 index 0000000..4bf9bb5 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/normals/normals-menu.png differ diff --git a/assets/blender/scripts/blender2ogre/images/normals/sharp-edges-marked.png b/assets/blender/scripts/blender2ogre/images/normals/sharp-edges-marked.png new file mode 100644 index 0000000..38f3337 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/normals/sharp-edges-marked.png differ diff --git a/assets/blender/scripts/blender2ogre/images/normals/show-custom-split-normals.png b/assets/blender/scripts/blender2ogre/images/normals/show-custom-split-normals.png new file mode 100644 index 0000000..d09cc8f Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/normals/show-custom-split-normals.png differ diff --git a/assets/blender/scripts/blender2ogre/images/normals/smooth-vs-flat-shading.png b/assets/blender/scripts/blender2ogre/images/normals/smooth-vs-flat-shading.png new file mode 100644 index 0000000..ea05554 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/normals/smooth-vs-flat-shading.png differ diff --git a/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system1.png b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system1.png new file mode 100644 index 0000000..0b9b200 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system1.png differ diff --git a/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system2.png b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system2.png new file mode 100644 index 0000000..6f536a2 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system2.png differ diff --git a/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system3.png b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system3.png new file mode 100644 index 0000000..799530b Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system3.png differ diff --git a/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system4.png b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system4.png new file mode 100644 index 0000000..a84719b Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system4.png differ diff --git a/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system5.png b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system5.png new file mode 100644 index 0000000..1c32b0d Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/particle-sys/particle-system5.png differ diff --git a/assets/blender/scripts/blender2ogre/images/physics/geometry-nodes1.png b/assets/blender/scripts/blender2ogre/images/physics/geometry-nodes1.png new file mode 100644 index 0000000..4a94a1c Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/physics/geometry-nodes1.png differ diff --git a/assets/blender/scripts/blender2ogre/images/physics/geometry-nodes2.png b/assets/blender/scripts/blender2ogre/images/physics/geometry-nodes2.png new file mode 100644 index 0000000..b289920 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/physics/geometry-nodes2.png differ diff --git a/assets/blender/scripts/blender2ogre/images/physics/stone-mesh.png b/assets/blender/scripts/blender2ogre/images/physics/stone-mesh.png new file mode 100644 index 0000000..1b917b3 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/physics/stone-mesh.png differ diff --git a/assets/blender/scripts/blender2ogre/images/physics/stronghold1.png b/assets/blender/scripts/blender2ogre/images/physics/stronghold1.png new file mode 100644 index 0000000..3c9c6be Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/physics/stronghold1.png differ diff --git a/assets/blender/scripts/blender2ogre/images/physics/stronghold2.png b/assets/blender/scripts/blender2ogre/images/physics/stronghold2.png new file mode 100644 index 0000000..daf9000 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/physics/stronghold2.png differ diff --git a/assets/blender/scripts/blender2ogre/images/physics/stronghold3.png b/assets/blender/scripts/blender2ogre/images/physics/stronghold3.png new file mode 100644 index 0000000..74ab66b Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/physics/stronghold3.png differ diff --git a/assets/blender/scripts/blender2ogre/images/physics/stronghold4.png b/assets/blender/scripts/blender2ogre/images/physics/stronghold4.png new file mode 100644 index 0000000..7cf89e4 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/physics/stronghold4.png differ diff --git a/assets/blender/scripts/blender2ogre/images/push-down.png b/assets/blender/scripts/blender2ogre/images/push-down.png new file mode 100644 index 0000000..1290286 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/push-down.png differ diff --git a/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations1.png b/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations1.png new file mode 100644 index 0000000..e8b89e5 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations1.png differ diff --git a/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations2.png b/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations2.png new file mode 100644 index 0000000..9be90d7 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations2.png differ diff --git a/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations3.png b/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations3.png new file mode 100644 index 0000000..02541e8 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations3.png differ diff --git a/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations4.png b/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations4.png new file mode 100644 index 0000000..47eb7c2 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/shape-anim/shape-animations4.png differ diff --git a/assets/blender/scripts/blender2ogre/images/skeletal-animation.png b/assets/blender/scripts/blender2ogre/images/skeletal-animation.png new file mode 100644 index 0000000..b8f3982 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skeletal-animation.png differ diff --git a/assets/blender/scripts/blender2ogre/images/skeletal-animations.png b/assets/blender/scripts/blender2ogre/images/skeletal-animations.png new file mode 100644 index 0000000..a2feaab Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skeletal-animations.png differ diff --git a/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes1.jpg b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes1.jpg new file mode 100644 index 0000000..d115f3b Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes1.jpg differ diff --git a/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes2.png b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes2.png new file mode 100644 index 0000000..cb09733 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes2.png differ diff --git a/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes3.png b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes3.png new file mode 100644 index 0000000..137ff92 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes3.png differ diff --git a/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes4.png b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes4.png new file mode 100644 index 0000000..5bf4e5b Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes4.png differ diff --git a/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes5.png b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes5.png new file mode 100644 index 0000000..00518a1 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes5.png differ diff --git a/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes6.png b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes6.png new file mode 100644 index 0000000..6808017 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes6.png differ diff --git a/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes7.png b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes7.png new file mode 100644 index 0000000..929831e Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/skyboxes/skyboxes7.png differ diff --git a/assets/blender/scripts/blender2ogre/images/transform-panel.png b/assets/blender/scripts/blender2ogre/images/transform-panel.png new file mode 100644 index 0000000..01801af Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/transform-panel.png differ diff --git a/assets/blender/scripts/blender2ogre/images/triangulate/beveled-broken-cube.png b/assets/blender/scripts/blender2ogre/images/triangulate/beveled-broken-cube.png new file mode 100644 index 0000000..cc50f61 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/triangulate/beveled-broken-cube.png differ diff --git a/assets/blender/scripts/blender2ogre/images/triangulate/beveled-cube.png b/assets/blender/scripts/blender2ogre/images/triangulate/beveled-cube.png new file mode 100644 index 0000000..84ee527 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/triangulate/beveled-cube.png differ diff --git a/assets/blender/scripts/blender2ogre/images/triangulate/broken-shading.png b/assets/blender/scripts/blender2ogre/images/triangulate/broken-shading.png new file mode 100644 index 0000000..0299482 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/triangulate/broken-shading.png differ diff --git a/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-distortion.png b/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-distortion.png new file mode 100644 index 0000000..2b52bfa Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-distortion.png differ diff --git a/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-original.png b/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-original.png new file mode 100644 index 0000000..e0582f4 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-original.png differ diff --git a/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-triangulated.png b/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-triangulated.png new file mode 100644 index 0000000..ec2139e Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-triangulated.png differ diff --git a/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-worried.png b/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-worried.png new file mode 100644 index 0000000..d5f5799 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/triangulate/suzanne-worried.png differ diff --git a/assets/blender/scripts/blender2ogre/images/triangulate/viewport-overlays.png b/assets/blender/scripts/blender2ogre/images/triangulate/viewport-overlays.png new file mode 100644 index 0000000..d2e46d7 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/triangulate/viewport-overlays.png differ diff --git a/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors1.png b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors1.png new file mode 100644 index 0000000..fcb2d02 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors1.png differ diff --git a/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors2a.png b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors2a.png new file mode 100644 index 0000000..58ceed2 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors2a.png differ diff --git a/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors2b.png b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors2b.png new file mode 100644 index 0000000..1e93d6f Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors2b.png differ diff --git a/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors3a.png b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors3a.png new file mode 100644 index 0000000..a486383 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors3a.png differ diff --git a/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors3b.png b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors3b.png new file mode 100644 index 0000000..0974967 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors3b.png differ diff --git a/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors4.png b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors4.png new file mode 100644 index 0000000..dd03825 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors4.png differ diff --git a/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors5.png b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors5.png new file mode 100644 index 0000000..4f4bd15 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors5.png differ diff --git a/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors6.png b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors6.png new file mode 100644 index 0000000..f0e8089 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/images/vertex_colors/vertex_colors6.png differ diff --git a/assets/blender/scripts/blender2ogre/installer/blender2ogre-auto-updater.txt b/assets/blender/scripts/blender2ogre/installer/blender2ogre-auto-updater.txt new file mode 100644 index 0000000..719a8b1 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/installer/blender2ogre-auto-updater.txt @@ -0,0 +1,29 @@ +;aiu; + +[0.5.8] +Name=blender2ogre 0.5.8 +ProductVersion=0.5.8.0 +URL=http://blender2ogre.googlecode.com/files/blender2ogre-0.5.8.exe +Size=2766806 +MD5=2ce6e423116b245636ceda0529d8948f +ServerFileName=blender2ogre-0.5.8.exe +Flags=NoCache +RegistryKey=HKCR\Software\blender2ogre\Version +Version=0.5.8.0 +Feature=* Added silent auto update checks if blender2ogre was installed using +Feature1= the .exe installer. This will keep people up to date when new versions are out. +Feature2=* Fix tracker issue 48: Needs to check if outputting to /tmp or ~/.wine/drive_c/tmp on Linux. Thanks to vax456 for providing the patch, added him to contributors. Preview mesh's are now placed under /tmp on Linux systems if the OgreMeshy executable ends with .exe +Feature3=* Fix tracker issue 46: add operationtype to +Feature4=* Implement a modal dialog that reports if material names have invalid characters and cant be saved on disk. This small popup will show until user presses left or right mouse (anywhere). +Feature5=* Fix tracker issue 44: XML Attributes not properly escaped in .scene file +Feature6=* Implemented reading OgreXmlConverter path from windows registry. The .msi installer will ship with certain tools so we can stop guessing and making the user install tools separately and setting up paths. +Feature7=* Fix bug that .mesh files were not generated while doing a .txml export. This was result of the late 2.63 mods that forgot to update object facecount before determining if mesh should be exported. +Feature8=* Fix bug that changed settings in the export dialog were forgotten when you re-exported without closing blender. Now settings should persist always from the last export. They are also stored to disk so the same settings are in use when if you restart Blender. +Feature9=* Fix bug that once you did a export, the next time the export location was forgotten. Now on sequential exports, the last export path is remembered in the export dialog. +Feature10=* Remove all local:// from asset refs and make them relative in .txml export. Having relative refs is the best for local preview and importing the txml to existing scenes. +Feature11=* Make .material generate what version of this plugins was used to generate the material file. Will be helpful in production to catch things. Added pretty printing line endings so the raw .material data is easier to read. +Feature12=* Improve console logging for the export stages. Run Blender from cmd prompt to see this information. +Feature13=* Clean/fix documentation in code for future development +Feature14=* Add todo to code for future development +Feature15=* Restructure/move code for easier readability +Feature16=* Remove extra white spaces and convert tabs to space diff --git a/assets/blender/scripts/blender2ogre/installer/blender2ogre-intall.rtf b/assets/blender/scripts/blender2ogre/installer/blender2ogre-intall.rtf new file mode 100644 index 0000000..f5f4bf8 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/installer/blender2ogre-intall.rtf differ diff --git a/assets/blender/scripts/blender2ogre/installer/blender2ogre-script-install.bat b/assets/blender/scripts/blender2ogre/installer/blender2ogre-script-install.bat new file mode 100644 index 0000000..26f8b9c --- /dev/null +++ b/assets/blender/scripts/blender2ogre/installer/blender2ogre-script-install.bat @@ -0,0 +1,17 @@ +@echo off +echo. + +IF %2=="" GOTO :ERROR +IF %2=="-" GOTO :ERROR +IF %2=="--\scripts\addons\" GOTO ERROR + +echo Copying blender2ogre addon to Blender +echo from %1 +echo to %2 +copy /Y %1 %2 + +GOTO :EOF + +:ERROR +echo Input parameter for install location is invalid. Please copy %1 manually to Blender\\scripts\addons. +pause diff --git a/assets/blender/scripts/blender2ogre/installer/blender2ogre-script-uninstall.bat b/assets/blender/scripts/blender2ogre/installer/blender2ogre-script-uninstall.bat new file mode 100644 index 0000000..e9dfac9 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/installer/blender2ogre-script-uninstall.bat @@ -0,0 +1,15 @@ +@echo off +echo. + +IF %1=="" GOTO :ERROR +IF %1=="-" GOTO :ERROR +IF %1=="--\scripts\addons\io_export_ogreDotScene.py" GOTO ERROR + +echo Removing addon from %1 +del /Q %1 + +GOTO :EOF + +:ERROR +echo Input parameter for install location is invalid. Please remove io_export_ogreDotScene.py manually from Blender\\scripts\addons. +pause diff --git a/assets/blender/scripts/blender2ogre/installer/installer-banner-big.jpg b/assets/blender/scripts/blender2ogre/installer/installer-banner-big.jpg new file mode 100644 index 0000000..1e4a541 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/installer/installer-banner-big.jpg differ diff --git a/assets/blender/scripts/blender2ogre/installer/installer-banner-small.jpg b/assets/blender/scripts/blender2ogre/installer/installer-banner-small.jpg new file mode 100644 index 0000000..2c4b9b5 Binary files /dev/null and b/assets/blender/scripts/blender2ogre/installer/installer-banner-small.jpg differ diff --git a/assets/blender/scripts/blender2ogre/io_ogre/__init__.py b/assets/blender/scripts/blender2ogre/io_ogre/__init__.py new file mode 100644 index 0000000..23cf6d3 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/__init__.py @@ -0,0 +1,129 @@ +# Copyright (C) 2010 Brett Hartshorn +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +bl_info = { + "name": "OGRE Importer-Exporter (.scene, .mesh, .skeleton)", + "author": "Brett, S.Rombauts, F00bar, Waruck, Mind Calamity, Mr.Magne, Jonne Nauha, vax456, Richard Plangger, Pavel Rojtberg, Guillermo Ojea Quintana", + "version": (0, 9, 0), + "blender": (2, 80, 0), + "location": "File > Import and Export...", + "description": "Import and Export to and from Ogre xml and binary formats", + "wiki_url": "https://github.com/OGRECave/blender2ogre", + "tracker_url": "https://github.com/OGRECave/blender2ogre/issues", + "category": "Import-Export" +} + +# https://blender.stackexchange.com/questions/2691/is-there-a-way-to-restart-a-modified-addon +# https://blender.stackexchange.com/questions/28504/blender-ignores-changes-to-python-scripts/28505 +# When bpy is already in local, we know this is not the initial import... +if "bpy" in locals(): + import importlib + #print("Reloading modules: config, mesh_preview, properties, xml, ui, util") + importlib.reload(config) + importlib.reload(mesh_preview) + importlib.reload(properties) + importlib.reload(xml) + importlib.reload(ui) + importlib.reload(util) + +import bpy +import os, sys, logging +from pprint import pprint +from . import config, properties, ui + +# Import the plugin directory and setup the plugin +class Blender2OgreAddonPreferences(bpy.types.AddonPreferences): + bl_idname = __name__ + + def apply_preferences_to_config(self, context): + config.update_from_addon_preference(context) + ui.update_meshpreview_button_visibility(True) + + IMAGE_MAGICK_CONVERT : bpy.props.StringProperty( + name="IMAGE_MAGICK_CONVERT", + subtype='FILE_PATH', + default=config.CONFIG['IMAGE_MAGICK_CONVERT'], + update=apply_preferences_to_config + ) + OGRETOOLS_XML_CONVERTER : bpy.props.StringProperty( + name="OGRETOOLS_XML_CONVERTER", + subtype='FILE_PATH', + default=config.CONFIG['OGRETOOLS_XML_CONVERTER'], + update=apply_preferences_to_config + ) + OGRETOOLS_MESH_UPGRADER : bpy.props.StringProperty( + name="OGRETOOLS_MESH_UPGRADER", + subtype='FILE_PATH', + default=config.CONFIG['OGRETOOLS_MESH_UPGRADER'], + update=apply_preferences_to_config + ) + MESH_PREVIEWER : bpy.props.StringProperty( + name="MESH_PREVIEWER", + subtype='FILE_PATH', + default=config.CONFIG['MESH_PREVIEWER'], + update=apply_preferences_to_config + ) + USER_MATERIALS : bpy.props.StringProperty( + name="USER_MATERIALS", + subtype='FILE_PATH', + default=config.CONFIG['USER_MATERIALS'], + update=apply_preferences_to_config + ) + SHADER_PROGRAMS : bpy.props.StringProperty( + name="SHADER_PROGRAMS", + subtype='FILE_PATH', + default=config.CONFIG['SHADER_PROGRAMS'], + update=apply_preferences_to_config + ) + + def draw(self, context): + layout = self.layout + layout.prop(self, "OGRETOOLS_XML_CONVERTER") + layout.prop(self, "OGRETOOLS_MESH_UPGRADER") + layout.prop(self, "MESH_PREVIEWER") + layout.prop(self, "IMAGE_MAGICK_CONVERT") + layout.prop(self, "USER_MATERIALS") + layout.prop(self, "SHADER_PROGRAMS") + +def register(): + logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='[%(levelname)5s] %(message)s', datefmt='%H:%M:%S') + + logging.info('Starting io_ogre %s', bl_info["version"]) + + # The UI modules define auto_register functions that + # return classes that should be loaded by the plugin + for clazz in ui.auto_register(True): + bpy.utils.register_class(clazz) + + bpy.utils.register_class(Blender2OgreAddonPreferences) + + # Read user preferences + config.update_from_addon_preference(bpy.context) + +def unregister(): + logging.info('Unloading io_ogre %s', bl_info["version"]) + + # Save the config + config.save_config() + + # Unregister classes + for clazz in ui.auto_register(False): + bpy.utils.unregister_class(clazz) + + bpy.utils.unregister_class(Blender2OgreAddonPreferences) + +if __name__ == "__main__": + register() diff --git a/assets/blender/scripts/blender2ogre/io_ogre/api.py b/assets/blender/scripts/blender2ogre/io_ogre/api.py new file mode 100644 index 0000000..ece5114 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/api.py @@ -0,0 +1,7 @@ +from .ogre.mesh import dot_mesh +from .ogre.skeleton import dot_skeleton +from .ogre.material import dot_material +from .ogre.materialv2json import dot_materialsv2json +from .ogre.scene import dot_scene + +# import various functions that can be used to export objects diff --git a/assets/blender/scripts/blender2ogre/io_ogre/config.py b/assets/blender/scripts/blender2ogre/io_ogre/config.py new file mode 100644 index 0000000..4158edc --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/config.py @@ -0,0 +1,253 @@ +import bpy, os, sys, logging, mathutils, json +from pprint import pprint +from bpy.props import * + +logger = logging.getLogger('config') + +AXIS_MODES = [ + ('xyz', 'xyz', 'No Axis swapping'), + ('xz-y', 'xz-y', 'Ogre standard'), + ('-xzy', '-xzy', 'Non standard'), +] + +MESH_TOOL_VERSIONS = [ + ('v1', 'v1', 'Export the mesh as a v1 object'), + ('v2', 'v2', 'Export the mesh as a v2 object') +] + +TANGENT_MODES = [ + ('0', 'none', 'Do not export tangents'), + ('3', 'generate', 'Generate tangents'), + ('4', 'generate with parity', 'Generate with parity') +] + +LOD_METHODS = [ + ('0', 'meshtools', 'Generate LODs using OgreMesh Tools: does LOD by removing edges, which allows only changing the index buffer and re-use the vertex-buffer (storage efficient)'), + ('1', 'blender', 'Generate LODs using Blenders "Decimate" Modifier: does LOD by collapsing vertices, which can result in a visually better LOD, but needs different vertex-buffers per LOD'), + ('2', 'manual', 'Generate LODs by manually crafting the lower LODs: needs different vertex-buffers per LOD') +] + +CONFIG_PATH = bpy.utils.user_resource('CONFIG', path='scripts', create=True) +CONFIG_FILENAME = 'io_ogre.json' +CONFIG_FILEPATH = os.path.join(CONFIG_PATH, CONFIG_FILENAME) + +CONFIG = {} + +_CONFIG_DEFAULTS_ALL = { + # General + 'SWAP_AXIS' : 'xz-y', + 'MESH_TOOL_VERSION' : 'v2', + 'EXPORT_XML_DELETE' : True, + + # Scene + 'SCENE' : True, + 'SELECTED_ONLY' : True, + 'EXPORT_HIDDEN' : True, + #'EXPORT_USER' : True, + 'FORCE_CAMERA' : True, + 'FORCE_LIGHTS' : True, + 'NODE_ANIMATION' : True, + #'NODE_KEYFRAMES' : False, + 'EXPORT_SKYBOX': False, + 'SKYBOX_RESOLUTION': 2048, + + # Materials + 'MATERIALS' : True, + 'COPY_SHADER_PROGRAMS' : True, + 'SEPARATE_MATERIALS' : True, + 'USE_FFP_PARAMETERS': False, + + # Textures + 'MAX_TEXTURE_SIZE' : 4096, + 'FORCE_IMAGE_FORMAT' : 'NONE', + 'TOUCH_TEXTURES' : True, + 'DDS_MIPS' : 16, + + # Armature + 'ONLY_DEFORMABLE_BONES' : False, + 'ONLY_KEYFRAMED_BONES' : False, + 'OGRE_INHERIT_SCALE' : False, + 'ARMATURE_ANIMATION' : True, + 'TRIM_BONE_WEIGHTS' : 0.01, + 'ONLY_KEYFRAMES' : False, + 'SHARED_ARMATURE' : False, + + # Mesh + 'MESH' : True, + 'MESH_OVERWRITE' : True, + 'ARRAY' : True, + 'EXTREMITY_POINTS' : 0, + 'GENERATE_EDGE_LISTS' : False, + 'GENERATE_TANGENTS' : '0', + 'PACK_INT_10_10_10_2': False, + 'OPTIMISE_ANIMATIONS' : True, + 'INTERFACE_TOGGLE': False, + 'OPTIMISE_VERTEX_CACHE' : False, + 'OPTIMISE_VERTEX_BUFFERS' : True, + 'OPTIMISE_VERTEX_BUFFERS_OPTIONS' : 'puqs', + + # LOD + 'LOD_GENERATION': '0', + 'LOD_LEVELS' : 0, + 'LOD_DISTANCE' : 300, + 'LOD_PERCENT' : 40, + + # Pose Animation + 'SHAPE_ANIMATIONS' : True, + 'SHAPE_NORMALS' : True, + + # Logging + 'ENABLE_LOGGING' : False, + 'DEBUG_LOGGING' : False, + #'SHOW_LOG_NAME' : False, + + # Import + 'IMPORT_XML_DELETE' : False, + 'IMPORT_NORMALS' : True, + 'MERGE_SUBMESHES' : True, + 'IMPORT_ANIMATIONS' : True, + 'ROUND_FRAMES' : True, + 'USE_SELECTED_SKELETON' : True, + 'IMPORT_SHAPEKEYS' : True, +} + +_CONFIG_TAGS_ = 'OGRETOOLS_XML_CONVERTER OGRETOOLS_MESH_UPGRADER MESH_PREVIEWER IMAGE_MAGICK_CONVERT USER_MATERIALS SHADER_PROGRAMS'.split() + +''' todo: Change pretty much all of these windows ones. Make a smarter way of detecting + Ogre tools from various default folders. Also consider making a installer that + ships Ogre cmd line tools to ease the setup steps for end users. ''' + +_CONFIG_DEFAULTS_WINDOWS = { + 'OGRETOOLS_XML_CONVERTER' : 'C:\\OgreCommandLineTools\\OgreXMLConverter.exe', + 'OGRETOOLS_MESH_UPGRADER' : 'C:\\OgreCommandLineTools\\OgreMeshUpgrader.exe', + 'MESH_PREVIEWER' : 'ogre-meshviewer.bat', + 'IMAGE_MAGICK_CONVERT' : 'C:\\Program Files\\ImageMagick\\convert.exe', + 'USER_MATERIALS' : '', + 'SHADER_PROGRAMS' : 'C:\\' +} + +_CONFIG_DEFAULTS_UNIX = { + # do not use absolute paths like /usr/bin/exe_name. some distris install to /usr/local/bin ... + # just trust the env PATH variable + 'IMAGE_MAGICK_CONVERT' : 'convert', + 'OGRETOOLS_XML_CONVERTER' : 'OgreXMLConverter', + 'OGRETOOLS_MESH_UPGRADER' : 'OgreMeshUpgrader', + 'MESH_PREVIEWER' : 'ogre-meshviewer', + 'USER_MATERIALS' : '', + 'SHADER_PROGRAMS' : '~/', + #'USER_MATERIALS' : '~/ogre_src_v1-7-3/Samples/Media/materials', + #'SHADER_PROGRAMS' : '~/ogre_src_v1-7-3/Samples/Media/materials/programs', +} + +# Unix: Replace ~ with absolute home dir path +if sys.platform.startswith('linux') or sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'): + for tag in _CONFIG_DEFAULTS_UNIX: + path = _CONFIG_DEFAULTS_UNIX[ tag ] + if path.startswith('~'): + _CONFIG_DEFAULTS_UNIX[ tag ] = os.path.expanduser( path ) + elif tag.startswith('OGRETOOLS') and not os.path.isfile( path ): + _CONFIG_DEFAULTS_UNIX[ tag ] = os.path.join( '/usr/bin', os.path.split( path )[-1] ) + del tag + del path + + +## PUBLIC API continues + +def load_config(): + global CONFIG + logger.info('* Loading config: %s' % CONFIG_FILEPATH) + config_dict = {} + + # Check if the config file exists and load it + if os.path.isfile( CONFIG_FILEPATH ): + try: + with open( os.path.join(CONFIG_FILEPATH), 'r' ) as f: + config_dict = json.load(f) + except EOFError: + logger.error('Config file: %s is empty' % CONFIG_FILEPATH) + except Exception as e: + logger.error('Can not read config from: %s' % CONFIG_FILEPATH) + logger.error('Exception: %s' % e) + else: + logger.error('Config file: %s does not exist' % CONFIG_FILEPATH) + + # Load default values from _CONFIG_DEFAULTS_ALL if they don't exist after loading config from file + for tag in _CONFIG_DEFAULTS_ALL: + if tag not in config_dict: + config_dict[ tag ] = _CONFIG_DEFAULTS_ALL[ tag ] + + # Load default values from _CONFIG_DEFAULTS_WINDOWS or _CONFIG_DEFAULTS_UNIX if they don't exist after loading config from file + for tag in _CONFIG_TAGS_: + if tag not in config_dict: + if sys.platform.startswith('win'): + config_dict[ tag ] = _CONFIG_DEFAULTS_WINDOWS[ tag ] + elif sys.platform.startswith('linux') or sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'): + config_dict[ tag ] = _CONFIG_DEFAULTS_UNIX[ tag ] + else: + logger.error('Unknown platform: %s' % sys.platform) + assert 0 + + # Setup temp hidden RNA to expose the file paths + for tag in _CONFIG_TAGS_: + default = config_dict[ tag ] + #func = eval( 'lambda self,con: config_dict.update( {"%s" : self.%s} )' %(tag,tag) ) + func = lambda self,con: config_dict.update( {tag : getattr(self,tag,default)} ) + if type(default) is bool: + prop = BoolProperty( name=tag, + description='updates bool setting', + default=default, + options={'SKIP_SAVE'}, + update=func) + else: + prop = StringProperty( name=tag, + description='updates path setting', + maxlen=128, + default=default, + options={'SKIP_SAVE'}, + update=func) + setattr( bpy.types.WindowManager, tag, prop ) + + return config_dict + +def get(name, default=None): + global CONFIG + if name in CONFIG: + return CONFIG[name] + else: + logger.error("Config option %s does not exist!" % name) + return default + +# Global CONFIG dictionary +CONFIG = load_config() + +def update(**kwargs): + global CONFIG + for key,value in kwargs.items(): + if key not in _CONFIG_DEFAULTS_ALL: + logger.warn("Trying to set CONFIG['%s'] = %s, but it is not a known config setting" % (key, value)) + #print("update() :: key: %s, value: %s" % (key, value)) + CONFIG[key] = value + save_config() + +def save_config(): + global CONFIG + logger.info('* Saving config to: %s' % CONFIG_FILEPATH) + #for key in CONFIG: print( '%s = %s' %(key, CONFIG[key]) ) + if os.path.isdir( CONFIG_PATH ): + try: + with open( os.path.join(CONFIG_FILEPATH), 'w' ) as f: + f.write(json.dumps(CONFIG, indent=4)) + except Exception as e: + logger.error('Can not write to %s' % CONFIG_FILEPATH) + logger.error('Exception: %s' % e) + else: + logger.error('Config directory %s does not exist' % CONFIG_PATH) + +def update_from_addon_preference(context): + global CONFIG + addon_preferences = context.preferences.addons["io_ogre"].preferences + + for key in _CONFIG_TAGS_: + addon_pref_value = getattr(addon_preferences,key,None) + if addon_pref_value is not None: + CONFIG[key] = addon_pref_value diff --git a/assets/blender/scripts/blender2ogre/io_ogre/mesh_preview.py b/assets/blender/scripts/blender2ogre/io_ogre/mesh_preview.py new file mode 100644 index 0000000..f7595dc --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/mesh_preview.py @@ -0,0 +1,106 @@ +# Code that handles the "Ogre Mesh Preview" button +# Exports current model to temp folder and loads Ogre viewer from commandline + +import bpy, sys, os, subprocess, logging +from bpy.props import BoolProperty +from .report import Report +from . import config +from .ogre.mesh import dot_mesh +from .ogre.material import dot_materials +from .util import objects_merge_materials, merge_objects + +## This class is called when the "Preview Mesh" button is pressed. It opens the Ogre mesh previewer +## The button is created int ui/__init__.py > auto_register() function. +class OGREMESH_OT_preview(bpy.types.Operator): + '''helper to open ogremesh''' + bl_idname = 'ogremesh.preview' + bl_label = "opens mesh viewer in a subprocess" + bl_options = {'REGISTER'} + preview : BoolProperty(name="preview", description="fast preview", default=True) + groups : BoolProperty(name="preview merge groups", description="use merge groups", default=False) + mesh : BoolProperty(name="update mesh", description="update mesh (disable for fast material preview", default=True) + + @classmethod + def poll(cls, context): + if context.active_object and context.active_object.type in ('MESH','EMPTY') and context.mode != 'EDIT_MESH': + if context.active_object.type == 'EMPTY' and context.active_object.instance_type != 'COLLECTION': + return False + else: + return True + + def execute(self, context): + Report.reset() + Report.messages.append('Running: "%s"' % config.get('MESH_PREVIEWER')) + + if sys.platform.startswith('linux') or sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'): + path = os.path.expanduser("~/io_blender2ogre") # use $HOME so snap can access it + if not os.path.exists(path): + os.makedirs(path) + else: + path = 'C:\\tmp' + + mat = None + mgroup = merged = None + + if context.active_object.type == 'MESH': + mat = context.active_object.active_material + elif context.active_object.type == 'EMPTY': # assume group + obs = [] + for e in context.selected_objects: + if e.type != 'EMPTY' and e.instance_collection: continue + grp = e.instance_collection + subs = [] + for o in grp.objects: + if o.type=='MESH': subs.append( o ) + if subs: + m = merge_objects( subs, transform=e.matrix_world ) + obs.append( m ) + if obs: + merged = merge_objects( obs ) + umaterials = dot_mesh( merged, path=path, force_name='preview' ) + for o in obs: context.scene.objects.unlink(o) + + if not self.mesh: + for ob in context.selected_objects: + if ob.type == 'MESH': + for mat in ob.data.materials: + if mat and mat not in umaterials: umaterials.append( mat ) + + if not merged: + mgroup = False # TODO relevant? MeshMagick.get_merge_group( context.active_object ) + if not mgroup and self.groups: + group = get_merge_group( context.active_object ) + if group: + print('--------------- has merge group ---------------' ) + merged = merge_group( group ) + else: + print('--------------- NO merge group ---------------' ) + elif len(context.selected_objects)>1 and context.selected_objects: + merged = merge_objects( context.selected_objects ) + + if mgroup: + for ob in mgroup.objects: + nmats = dot_mesh( ob, path=path ) + for m in nmats: + if m not in umaterials: umaterials.append( m ) + MeshMagick.merge( mgroup, path=path, force_name='preview' ) + else: + dot_mesh( merged or context.active_object, path=path, force_name='preview', overwrite=True ) + + mats = objects_merge_materials([merged or context.active_object]) + dot_materials(mats, path, False, "preview") + + if merged: context.scene.objects.unlink( merged ) + + try: + os.environ["OGRE_MIN_LOGLEVEL"] = "3" # only warnings and up + if sys.platform.startswith('linux') or sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'): + subprocess.Popen([config.get('MESH_PREVIEWER'), path + '/preview.mesh']) + else: + subprocess.Popen([config.get('MESH_PREVIEWER'), 'C:\\tmp\\preview.mesh'] ) + except: + Report.errors.append('Viewer not found at "%s"' % config.get('MESH_PREVIEWER')) + + Report.show() + return {'FINISHED'} + diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/__init__.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/material.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/material.py new file mode 100644 index 0000000..7a98800 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/material.py @@ -0,0 +1,837 @@ +# When bpy is already in local, we know this is not the initial import... +if "bpy" in locals(): + import importlib + #print("Reloading modules: shader") + importlib.reload(shader) + +import os, shutil, tempfile, math, logging +from .. import config +from .. import shader +from .. import util +from .. import bl_info +from ..report import Report +from ..util import * +from .program import OgreProgram +from bpy.props import EnumProperty +from bpy_extras import io_utils +from bpy_extras import node_shader_utils +from datetime import datetime +from itertools import chain +from mathutils import Vector +from os.path import join, split, splitext + +logger = logging.getLogger('material') + +def _write_b2o_ver(fd): + b2o_ver = ".".join(str(i) for i in bl_info["version"]) + fd.write(bytes('// generated by blender2ogre %s on %s\n' % (b2o_ver, datetime.now().replace(microsecond=0)), 'utf-8')) + +def dot_materials(materials, path=None, separate_files=True, prefix='mats', **kwargs): + """ + generate material files, or copy them into a single file + + path: string - or None if one must use a temp file + separate_files: bool - each material gets it's own filename + """ + if not materials: + logger.warn('No materials, not writting .material script') + return [] + + if not path: + path = tempfile.mkdtemp(prefix='ogre_io') + + if separate_files: + for mat in materials: + dot_material(mat, path) + else: + mat_file_name = prefix + target_file = os.path.join(path, '%s.material' % mat_file_name) + with open(target_file, 'wb') as fd: + _write_b2o_ver(fd) + include_missing = False + for mat in materials: + if mat is None: + include_missing = True + continue + Report.materials.append( material_name(mat) ) + generator = OgreMaterialGenerator(mat, path) + # Generate before copying textures to collect images first + material_text = generator.generate() + if kwargs.get('copy_programs', config.get('COPY_SHADER_PROGRAMS')): + generator.copy_programs() + if kwargs.get('touch_textures', config.get('TOUCH_TEXTURES')): + generator.copy_textures() + fd.write(bytes(material_text + "\n", 'utf-8')) + + if include_missing: + fd.write(bytes(MISSING_MATERIAL + "\n", 'utf-8')) + +def dot_material(mat, path, **kwargs): + """ + write the material file of a + mat: a blender material + path: target directory to save the file to + + kwargs: + * prefix - string. The prefix name of the file. default '' + * copy_programs - bool. default False + * touch_textures - bool. Copy the images along to the material files. + """ + prefix = kwargs.get('prefix', '') + generator = OgreMaterialGenerator(mat, path, prefix=prefix) + # Generate before copying textures to collect images first + material_text = generator.generate() + if kwargs.get('copy_programs', config.get('COPY_SHADER_PROGRAMS')): + generator.copy_programs() + if kwargs.get('touch_textures', config.get('TOUCH_TEXTURES')): + generator.copy_textures() + try: + mat_file_name = join(path, clean_object_name(generator.material_name) + ".material") + with open(mat_file_name, 'wb') as fd: + _write_b2o_ver(fd) + fd.write(bytes(material_text, 'utf-8')) + except Exception as e: + logger.error("Unable to create material file: %s" % mat_file_name) + logger.error(e) + Report.errors.append("Unable to create material file: %s" % mat_file_name) + + return generator.material_name + +class OgreMaterialGenerator(object): + # Texture wrapper attribute names + TEXTURE_KEYS = [ + "base_color_texture", + "specular_texture", + "roughness_texture", + "alpha_texture", + "normalmap_texture", + "metallic_texture", + "emission_color_texture" + ] + + def __init__(self, material, target_path, prefix=''): + self.material = material + self.target_path = target_path + self.w = util.IndentedWriter() + self.passes = [] + if self.material is None: + self.material_name = "_missing_material_" + else: + self.material_name = material_name(self.material, prefix=prefix) + self.images = set() + + if (self.material is not None and + material.node_tree is not None): + nodes = shader.get_subnodes( self.material.node_tree, type='MATERIAL_EXT' ) + for node in nodes: + if node.material: + self.passes.append( node.material ) + + def generate(self): + if self.material is None: + return MISSING_MATERIAL + + self.generate_header() + with self.w.iword('material').word(self.material_name).embed(): + if self.material['visible_shadow']: + self.w.iline('receive_shadows on') + else: + self.w.iline('receive_shadows off') + with self.w.iword('technique').embed(): + self.generate_passes() + + text = self.w.text + self.w.text = '' + return text + + def generate_header(self): + for mat in self.passes: + if mat.use_ogre_parent_material and mat.ogre_parent_material: + usermat = get_ogre_user_material( mat.ogre_parent_material ) + self.w.iline( '// user material: %s' %usermat.name ) + # TODO: fix what is r +# for prog in usermat.get_programs(): +# r.append( prog.data ) + self.w.iline( '// abstract passes //' ) + for line in usermat.as_abstract_passes(): + self.w.iline(line) + + def generate_passes(self): + self.generate_pass(self.material) + for mat in self.passes: + if mat.use_in_ogre_material_pass: # submaterials + self.generate_pass(mat) + + def generate_pass( self, mat, pass_name="" ): + usermat = texnodes = None + if mat.use_ogre_parent_material: + usermat = get_ogre_user_material( mat.ogre_parent_material ) + texnodes = shader.get_texture_subnodes( self.material, mat ) + + if usermat: + self.w.iword('pass %s : %s/PASS0' %(pass_name,usermat.name)) + else: + self.w.iword('pass') + if pass_name: self.w.word(pass_name) + + with self.w.embed(): + # Texture wrappers + textures = {} + mat_wrapper = node_shader_utils.PrincipledBSDFWrapper(mat) + for tex_key in self.TEXTURE_KEYS: + texture = getattr(mat_wrapper, tex_key, None) + # In the case of the Metallic and Roughness textures, they cannot be obtained using "node_shader_utils" + # https://docs.blender.org/manual/en/2.80/addons/io_scene_gltf2.html#metallic-and-roughness + if tex_key == 'roughness_texture': + texture = gather_metallic_roughness_texture(mat_wrapper) + if texture and texture.image: + textures[tex_key] = texture + # adds image to the list for later copy + self.images.add(texture.image) + + color = mat_wrapper.base_color + alpha = 1.0 + if mat.blend_method == "CLIP": + alpha = mat_wrapper.alpha + self.w.iword('alpha_rejection greater_equal').round(255*mat.alpha_threshold).nl() + elif mat.blend_method == "BLEND": + alpha = mat_wrapper.alpha + self.w.iword('scene_blend alpha_blend').nl() + if mat.show_transparent_back: + self.w.iword('cull_hardware none').nl() + self.w.iword('depth_write off').nl() + + if config.get('USE_FFP_PARAMETERS') is True: + # arbitrary bad translation from PBR to Blinn Phong + # derive proportions from metallic + bf = 1.0 - mat_wrapper.metallic + mf = max(0.04, mat_wrapper.metallic) + # derive specular color + sc = mathutils.Color(color[:3]) * mf + (1.0 - mf) * mathutils.Color((1, 1, 1)) * (1.0 - mat_wrapper.roughness) + si = (1.0 - mat_wrapper.roughness) * 128 + + self.w.iword('diffuse').round(color[0]*bf).round(color[1]*bf).round(color[2]*bf).round(alpha).nl() + self.w.iword('specular').round(sc[0]).round(sc[1]).round(sc[2]).round(alpha).round(si, 3).nl() + else: + self.w.iword('diffuse').round(color[0]).round(color[1]).round(color[2]).round(alpha).nl() + self.w.iword('specular').round(mat_wrapper.roughness).round(mat_wrapper.metallic).real(0).real(0).real(0).nl() + self.generate_rtshader_system(textures) + + for name in dir(mat): #mat.items() - items returns custom props not pyRNA: + if name.startswith('ogre_') and name != 'ogre_parent_material': + var = getattr(mat,name) + op = name.replace('ogre_', '') + val = var + if type(var) == bool: + if var: val = 'on' + else: val = 'off' + self.w.iword(op).word(val).nl() + + if texnodes and usermat.texture_units: + for i,name in enumerate(usermat.texture_units_order): + if i has a non-UV mapping type (%s) and not picked a proper projection type of: Sphere or Flat' % (texture.name, slot.mapping)) + + x,y = texture.translation[0:2] + if x or y: + self.w.iword('scroll').round(x).round(y).nl() + if texture.rotation.z: + # Radians to degrees + self.w.iword('rotate').round(math.degrees(texture.rotation.z), 2).nl() + + btype = 'modulate' + if key == "emission_color_texture": + btype = "add" + + self.w.iword('colour_op').word(btype).nl() + + def copy_textures(self): + for image in self.images: + self.copy_texture(image) + + def copy_texture(self, image): + origin_filepath = image.filepath + target_filepath = split(origin_filepath or image.name)[1] + target_filepath = self.change_ext(target_filepath, image) + target_filepath = join(self.target_path, target_filepath) + + if image.packed_file: + # packed in .blend file, save image as target file + image.filepath = target_filepath + image.save() + image.filepath = origin_filepath + logger.info("Writing texture: (%s)", target_filepath) + else: + image_filepath = bpy.path.abspath(image.filepath, library=image.library) + image_filepath = os.path.normpath(image_filepath) + + # Should we update the file + update = False + if os.path.isfile(target_filepath): + src_stat = os.stat(target_filepath) + dst_stat = os.stat(image_filepath) + update = src_stat.st_size != dst_stat.st_size \ + or src_stat.st_mtime != dst_stat.st_mtime + else: + update = True + + if update: + if is_image_postprocessed(image): + logger.info("ImageMagick: (%s) -> (%s)", image_filepath, target_filepath) + util.image_magick(image, image_filepath, target_filepath) + else: + # copy2 tries to copy all metadata (modification date included), to keep update decision consistent + shutil.copy2(image_filepath, target_filepath) + logger.info("Copying image: (%s)", origin_filepath) + else: + logger.info("Skip copying (%s). Texture is already up to date.", origin_filepath) + + def get_active_programs(self): + r = [] + for mat in self.passes: + if mat.use_ogre_parent_material and mat.ogre_parent_material: + usermat = get_ogre_user_material( mat.ogre_parent_material ) + for prog in usermat.get_programs(): r.append( prog ) + return r + + def copy_programs(self): + for prog in self.get_active_programs(): + if prog.source: + prog.save(self.target_path) + else: + logger.warn('Uses program %s which has no source' % prog.name) + + def change_ext( self, name, image ): + name_no_ext, _ = splitext(name) + if image.file_format != 'NONE': + name = name_no_ext + "." + image.file_format.lower() + if config.get('FORCE_IMAGE_FORMAT') != 'NONE': + name = name_no_ext + "." + config.get('FORCE_IMAGE_FORMAT') + return name + +# Make default material for missing materials: +# * Red flags for users so they can quickly see what they forgot to assign a material to. +# * Do not crash if no material on object - thats annoying for the user. +TEXTURE_COLOUR_OP = { + 'MIX' : 'modulate', # Ogre Default - was "replace" but that kills lighting + 'ADD' : 'add', + 'MULTIPLY' : 'modulate', + #'alpha_blend' : '', +} +TEXTURE_COLOUR_OP_EX = { + 'MIX' : 'blend_manual', + 'SCREEN' : 'modulate_x2', + 'LIGHTEN' : 'modulate_x4', + 'SUBTRACT' : 'subtract', + 'OVERLAY' : 'add_signed', + 'DIFFERENCE': 'dotproduct', # best match? + 'VALUE' : 'blend_diffuse_colour', +} + +TEXTURE_ADDRESS_MODE = { + 'REPEAT' : 'wrap', + 'EXTEND' : 'clamp', + 'CLIP' : 'border', + 'CHECKER' : 'mirror' +} + +MISSING_MATERIAL = ''' +material _missing_material_ +{ + receive_shadows off + technique + { + pass + { + ambient 0.1 0.1 0.1 1.0 + diffuse 0.8 0.0 0.0 1.0 + specular 0.5 0.5 0.5 1.0 12.5 + emissive 0.3 0.3 0.3 1.0 + } + } +} +''' + +def load_user_materials(): + # I think this is solely used for realXtend... + # the config of USER_MATERIAL points to a subdirectory of tundra by default. + # In this case all parsing can be moved to the tundra subfolder + + # Exit this function if the path is empty. Allows 'USER_MATERIALS' to be blank and not affect anything. + # If 'USER_MATERIALS' is something too broad like "C:\\" it recursively scans it and might crash if it + # hits directories it doesn't have access too + if config.get('USER_MATERIALS') == '': + return + + try: + if os.path.isdir( config.get('USER_MATERIALS') ): + scripts,progs = update_parent_material_path( config.get('USER_MATERIALS') ) + for prog in progs: + logger.info('Ogre shader program: %s' % prog.name) + except Exception as e: + logger.warn("Unable to parse 'USER_MATERIALS' directory: %s" % config.get('USER_MATERIALS')) + logger.warn(e) + + +def material_name( mat, clean = False, prefix='' ): + """ + returns the material name. + + materials from a library might be exported several times for multiple objects. + there is no need to have those textures + material scripts several times. thus + library materials are prefixed with the material filename. (e.g. test.blend + diffuse + should result in "test_diffuse". special chars are converted to underscore. + + clean: deprecated. do not use! + """ + if type(mat) is str: + return prefix + clean_object_name(mat, invalid_chars=invalid_chars_in_name) + name = clean_object_name(mat.name, invalid_chars=invalid_chars_in_name) + if mat.library: + _, filename = split(mat.library.filepath) + prefix, _ = splitext(filename) + return prefix + "_" + name + else: + return prefix + name + +def get_shader_program( name ): + if name in OgreProgram.PROGRAMS: + return OgreProgram.PROGRAMS[ name ] + else: + logger.warn('No shader program named: %s' % name) + +def get_shader_programs(): + return OgreProgram.PROGRAMS.values() + +def parse_material_and_program_scripts( path, scripts, progs, missing ): # recursive + + for name in os.listdir(path): + url = os.path.join(path, name) + if os.path.isdir( url ): + parse_material_and_program_scripts( url, scripts, progs, missing ) + + elif os.path.isfile( url ): + if name.endswith('.material'): + logger.debug(' %s' % url ) + scripts.append( MaterialScripts( url ) ) + + if name.endswith('.program'): + logger.debug(' %s' % url ) + data = open( url, 'rb' ).read().decode('utf-8') + + chk = []; chunks = [ chk ] + for line in data.splitlines(): + line = line.split('//')[0] + if line.startswith('}'): + chk.append( line ) + chk = []; chunks.append( chk ) + elif line.strip(): + chk.append( line ) + + for chk in chunks: + if not chk: continue + p = OgreProgram( data='\n'.join(chk) ) + if p.source: + ok = p.reload() + if not ok: missing.append( p ) + else: progs.append( p ) + +def get_ogre_user_material( name ): + if name in MaterialScripts.ALL_MATERIALS: + return MaterialScripts.ALL_MATERIALS[ name ] + +class OgreMaterialScript(object): + def get_programs(self): + progs = [] + for name in list(self.vertex_programs.keys()) + list(self.fragment_programs.keys()): + p = get_shader_program( name ) # OgreProgram.PROGRAMS + if p: progs.append( p ) + return progs + + def __init__(self, txt, url): + self.url = url + self.data = txt.strip() + self.parent = None + self.vertex_programs = {} + self.fragment_programs = {} + self.texture_units = {} + self.texture_units_order = [] + self.passes = [] + + line = self.data.splitlines()[0] + assert line.startswith('material') + if ':' in line: + line, self.parent = line.split(':') + self.name = line.split()[-1] + logger.debug('New ogre material: %s' % self.name ) + + brace = 0 + self.techniques = techs = [] + prog = None # pick up program params + tex = None # pick up texture_unit options, require "texture" ? + for line in self.data.splitlines(): + #logger.debug( line ) + rawline = line + line = line.split('//')[0] + line = line.strip() + if not line: continue + + if line == '{': brace += 1 + elif line == '}': brace -= 1; prog = None; tex = None + + if line.startswith('technique'): + tech = {'passes':[]}; techs.append( tech ) + if len(line.split()) > 1: tech['technique-name'] = line.split()[-1] + elif techs: + if line.startswith('pass'): + P = {'texture_units':[], 'vprogram':None, 'fprogram':None, 'body':[]} + tech['passes'].append( P ) + self.passes.append( P ) + + elif tech['passes']: + P = tech['passes'][-1] + P['body'].append( rawline ) + + if line == '{' or line == '}': continue + + if line.startswith('vertex_program_ref'): + prog = P['vprogram'] = {'name':line.split()[-1], 'params':{}} + self.vertex_programs[ prog['name'] ] = prog + elif line.startswith('fragment_program_ref'): + prog = P['fprogram'] = {'name':line.split()[-1], 'params':{}} + self.fragment_programs[ prog['name'] ] = prog + + elif line.startswith('texture_unit'): + prog = None + tex = {'name':line.split()[-1], 'params':{}} + if tex['name'] == 'texture_unit': # ignore unnamed texture units + logger.warn('Material %s contains unnamed texture_units' % self.name) + logger.warn('--- Unnamed texture units will be ignored ---') + else: + P['texture_units'].append( tex ) + self.texture_units[ tex['name'] ] = tex + self.texture_units_order.append( tex['name'] ) + + elif prog: + p = line.split()[0] + if p=='param_named': + items = line.split() + if len(items) == 4: p, o, t, v = items + elif len(items) == 3: + p, o, v = items + t = 'class' + elif len(items) > 4: + o = items[1]; t = items[2] + v = items[3:] + + opt = { 'name': o, 'type':t, 'raw_value':v } + prog['params'][ o ] = opt + if t=='float': opt['value'] = float(v) + elif t in 'float2 float3 float4'.split(): opt['value'] = [ float(a) for a in v ] + else: logger.debug('Unknown type: %s' % t) + + elif tex: # (not used) + tex['params'][ line.split()[0] ] = line.split()[ 1 : ] + + for P in self.passes: + lines = P['body'] + while lines and ''.join(lines).count('{')!=''.join(lines).count('}'): + if lines[-1].strip() == '}': lines.pop() + else: break + P['body'] = '\n'.join( lines ) + assert P['body'].count('{') == P['body'].count('}') # if this fails, the parser choked + + #logger.debug( self.techniques ) + self.hidden_texture_units = rem = [] + for tex in self.texture_units.values(): + if 'texture' not in tex['params']: + rem.append( tex ) + for tex in rem: + logger.warn('Not using texture_unit <%s> because it lacks a "texture" parameter' % tex['name']) + self.texture_units.pop( tex['name'] ) + + if len(self.techniques)>1: + logger.warn('User material %s has more than one technique' % self.url) + + def as_abstract_passes( self ): + r = [] + for i,P in enumerate(self.passes): + head = 'abstract pass %s/PASS%s' %(self.name,i) + r.append( head + '\n' + P['body'] ) + return r + +class MaterialScripts(object): + ALL_MATERIALS = {} + ENUM_ITEMS = [] + + def __init__(self, url): + self.url = url + self.data = '' + data = open( url, 'rb' ).read() + try: + self.data = data.decode('utf-8') + except: + self.data = data.decode('latin-1') + + self.materials = {} + ## chop up .material file, find all material defs #### + mats = [] + mat = [] + skip = False # for now - programs must be defined in .program files, not in the .material + for line in self.data.splitlines(): + if not line.strip(): continue + a = line.split()[0] #NOTE ".split()" strips white space + if a == 'material': + mat = []; mats.append( mat ) + mat.append( line ) + elif a in ('vertex_program', 'fragment_program', 'abstract'): + skip = True + elif mat and not skip: + mat.append( line ) + elif skip and line=='}': + skip = False + + ########################## + for mat in mats: + omat = OgreMaterialScript('\n'.join( mat ), url ) + if omat.name in self.ALL_MATERIALS: + logger.warn('Material %s redefined' % omat.name ) + #logger.debug('--- OLD MATERIAL ---') + #logger.debug( self.ALL_MATERIALS[ omat.name ].data ) + #logger.debug('--- NEW MATERIAL ---') + #logger.debug( omat.data ) + self.materials[ omat.name ] = omat + self.ALL_MATERIALS[ omat.name ] = omat + if omat.vertex_programs or omat.fragment_programs: # ignore materials without programs + self.ENUM_ITEMS.append( (omat.name, omat.name, url) ) + + @classmethod # only call after parsing all material scripts + def reset_rna(self, callback=None): + bpy.types.Material.ogre_parent_material = EnumProperty( + name="Script Inheritence", + description='OGRE parent material class', + items=self.ENUM_ITEMS, + #update=callback + ) + +IMAGE_FORMATS = [ + ('NONE','NONE', 'Do not convert image'), + ('bmp', 'bmp', 'Bitmap format'), + ('jpg', 'jpg', 'JPEG format'), + ('gif', 'gif', 'GIF format'), + ('png', 'png', 'PNG format'), + ('tga', 'tga', 'Targa format'), + ('dds', 'dds', 'DDS format'), +] + +def is_image_postprocessed( image ): + if config.get('FORCE_IMAGE_FORMAT') != 'NONE': + return True + else: + return False + + +def update_parent_material_path( path ): + ''' updates RNA ''' + logger.debug('>> SEARCHING FOR OGRE MATERIALS: %s' % path ) + scripts = [] + progs = [] + missing = [] + parse_material_and_program_scripts( path, scripts, progs, missing ) + + if missing: + logger.warn('Missing shader programs:') + for p in missing: logger.debug(p.name) + if missing and not progs: + logger.warn('No shader programs were found - set "SHADER_PROGRAMS" to your path') + + MaterialScripts.reset_rna( callback=shader.on_change_parent_material ) + return scripts, progs + +class ShaderImageTextureWrapper(): + """ + This class imitates the namesake Class from the library: node_shader_utils. + The objective is that the Metallic Roughness Texture follows the same codepath as the other textures + """ + + def __init__(self, node_image): + self.image = node_image.image + self.extension = node_image.extension + self.node_image = node_image + #self.name = ?? + self.texcoords = 'UV' + self.projection = node_image.projection + self.scale = self.get_mapping_input('Scale') + self.translation = self.get_mapping_input('Location') + self.rotation = self.get_mapping_input('Rotation') + + # Esta funcion obtiene los datos de un nodo de tipo: Mapping + def get_mapping_input(self, input): + if len(self.node_image.inputs['Vector'].links) > 0: + node_mapping = self.node_image.inputs['Vector'].links[0].from_node + + if node_mapping.type == 'MAPPING': + return node_mapping.inputs[input].default_value + else: + logger.warn("Connected node: %s is not of type 'MAPPING'" % node_mapping.name) + return None + else: + return None + +def gather_metallic_roughness_texture(mat_wrapper): + """ + For a given material, retrieve the corresponding metallic roughness texture according to glTF2 guidelines. + (https://docs.blender.org/manual/en/2.80/addons/io_scene_gltf2.html#metallic-and-roughness) + :param blender_material: a blender material for which to get the metallic roughness texture + :return: a blender Image + """ + material = mat_wrapper.material + + logger.debug("Getting Metallic roughness texture of material: '%s'" % material.name) + + separate_name = None + #image_texture = None + node_image = None + + for input_name in ['Roughness', 'Metallic']: + logger.debug(" + Processing input: '%s'" % input_name) + + if material.use_nodes == False: + logger.warn("Material: '%s' does not use nodes" % material.name) + return None + + if 'Principled BSDF' not in material.node_tree.nodes: + logger.warn("Material: '%s' does not have a 'Principled BSDF' node" % material.name) + return None + + input = material.node_tree.nodes['Principled BSDF'].inputs[input_name] + + # Check that input is connected to a node + if len(input.links) > 0: + separate_node = input.links[0].from_node + else: + logger.warn("%s input is not connected" % input_name) + return None + + # Check that connected node is of type 'SEPARATE_COLOR' + if separate_node.type not in ['SEPARATE_COLOR', 'SEPRGB']: + logger.warn("Connected node '%s' is not of type 'SEPARATE_COLOR'" % separate_node.name) + return None + + # Check that both inputs are connected to the same 'SEPARATE_COLOR' node (node names are unique) + if separate_name == None: + separate_name = separate_node.name + elif separate_name != separate_node.name: + logger.warn("Connected node '%s' is different between 'Roughness' and 'Metallic' inputs" % separate_node.name) + return None + + # Check that 'Roughness' is connected to 'Green' output and 'Metallic' is connected to 'Blue' output + if input_name == 'Roughness' and input.links[0].from_socket.name not in ['Green', 'G']: + logger.warn("'Roughness' input connected to wrong output of node: '%s'" % separate_node.name) + return None + elif input_name == 'Metallic' and input.links[0].from_socket.name not in ['Blue', 'B']: + logger.warn("'Metallic' input connected to wrong output of node: '%s'" % separate_node.name) + return None + + # Check that input is connected to a node + if len(separate_node.inputs[0].links) == 0: + logger.warn("node '%s' has no input texture" % separate_node.name) + return None + + # Get the image texture + node_image = separate_node.inputs[0].links[0].from_node + if node_image.type != 'TEX_IMAGE': + logger.warn("Node connected to '%s' is not of type: 'TEX_IMAGE'" % separate_node.name) + return None + + return ShaderImageTextureWrapper(node_image) diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/material_parser.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/material_parser.py new file mode 100644 index 0000000..ef783c9 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/material_parser.py @@ -0,0 +1,534 @@ + +import bpy +import os +import ast +import logging + +logger = logging.getLogger('material_parser') + +class ScriptToken: + TID_LBRACKET = 0 + TID_RBRACKET = 1 + TID_COLON = 2 + TID_VARIABLE = 3 + TID_WORD = 4 + TID_QUOTE = 5 + TID_NEWLINE = 6 + TID_UNKNOWN = 7 + TID_END = 8 + + types = ["TID_LBRACKET", "TID_RBRACKET", "TID_COLON", "TID_VARIABLE", "TID_WORD", "TID_QUOTE", "TID_NEWLINE", "TID_UNKNOWN", "TID_END"] + + def __init__(self, line): + self.line = line + self.type = -1 + self.lexeme = "" + + def __str__(self): + return("line: %s, type: %s, lexeme: %s" % (self.line, self.types[self.type], self.lexeme)) + +class ScriptLexer: + + error = "" + + def tokenize(self, str, source): + + # States + READY = 0 + COMMENT = 1 + MULTICOMMENT = 2 + WORD = 3 + QUOTE = 4 + VAR = 5 + POSSIBLECOMMENT = 6 + + # Set up some constant characters of interest + varopener = '$' + quote = '"' + slash = '/' + backslash = '\\' + openbrace = '{' + closebrace = '}' + colon = ':' + star = '*' + cr = '\r' + lf = '\n' + + c = 0 + lastc = 0 + + lexeme = "" + line = 1 + state = READY + lastQuote = 0 + firstOpenBrace = 0 + braceLayer = 0 + tokens = [] + + # Iterate over the input + for i in str: + + lastc = c; + c = i; + + if c == quote: + lastQuote = line; + + if state == READY or state == WORD or state == VAR: + if c == openbrace: + if braceLayer == 0: + firstOpenBrace = line; + braceLayer += 1 + + elif c == closebrace: + if braceLayer == 0: + self.error = "no matching open bracket '{' found for close bracket '}' at %s:%s" % (source, line) + return tokens; + braceLayer -= 1 + + if state == READY: + if c == slash and lastc == slash: + # Comment start, clear out the lexeme + lexeme = "" + state = COMMENT + + elif c == star and lastc == slash: + lexeme = "" + state = MULTICOMMENT + + elif c == quote: + # Clear out the lexeme ready to be filled with quotes! + lexeme = c + state = QUOTE + + elif c == varopener: + # Set up to read in a variable + lexeme = c + state = VAR + + elif self.isNewline(c): + lexeme = c + self.setToken(lexeme, line, tokens) + + elif not self.isWhitespace(c): + lexeme = c + if c == slash: + state = POSSIBLECOMMENT + else: + state = WORD + + elif state == COMMENT: + if self.isNewline(c): + lexeme = c + self.setToken(lexeme, line, tokens) + state = READY + + elif state == MULTICOMMENT: + if c == slash and lastc == star: + state = READY + + elif state == POSSIBLECOMMENT: + if c == slash and lastc == slash: + lexeme = "" + state = COMMENT + + elif c == star and lastc == slash: + lexeme = "" + state = MULTICOMMENT + + else: + state = WORD + # OGRE_FALLTHROUGH; + + elif state == WORD: + if self.isNewline(c): + self.setToken(lexeme, line, tokens) + lexeme = c + self.setToken(lexeme, line, tokens) + state = READY + + elif self.isWhitespace(c): + self.setToken(lexeme, line, tokens) + state = READY + + elif c == openbrace or c == closebrace or c == colon: + self.setToken(lexeme, line, tokens) + lexeme = c + self.setToken(lexeme, line, tokens) + state = READY + + else: + lexeme += c + + elif state == QUOTE: + if c != backslash: + # Allow embedded quotes with escaping + if c == quote and lastc == backslash: + lexeme += c + + elif c == quote: + lexeme += c + self.setToken(lexeme, line, tokens) + state = READY + + else: + # Backtrack here and allow a backslash normally within the quote + if lastc == backslash: + lexeme = lexeme + "\\" + c + else: + lexeme += c + + elif state == VAR: + if self.isNewline(c): + self.setToken(lexeme, line, tokens) + lexeme = c + self.setToken(lexeme, line, tokens) + state = READY + + elif self.isWhitespace(c): + self.setToken(lexeme, line, tokens) + state = READY + + elif c == openbrace or c == closebrace or c == colon: + self.setToken(lexeme, line, tokens) + lexeme = c + self.setToken(lexeme, line, tokens) + state = READY + + else: + lexeme += c + + # Separate check for newlines just to track line numbers + if (c == cr or (c == lf and lastc != cr)): + line += 1 + + # Check for valid exit states + if state == WORD or state == VAR: + if lexeme != "": + self.setToken(lexeme, line, tokens) + + else: + if state == QUOTE: + self.error = "no matching \" found for \" at %s:%s" % (source, lastQuote) + return tokens; + + # Check that all opened brackets have been closed + if braceLayer == 1: + self.error = "no matching closing bracket '}' for open bracket '{' at %s:%s" % (source, firstOpenBrace) + + elif braceLayer > 1: + self.error = "too many open brackets (%d) '{' without matching closing bracket '}' in %s" % (braceLayer, source) + + return tokens; + + def setToken(self, lexeme, line, tokens): + openBracket = '{' + closeBracket = '}' + colon = ':' + quote = '\"' + var = '$' + + token = ScriptToken(line) + token.line = line + ignore = False + + # Check the user token map first + if(len(lexeme) == 1 and self.isNewline(lexeme[0])): + token.type = ScriptToken.TID_NEWLINE + + if(len(tokens) != 0 and tokens[-1].type == ScriptToken.TID_NEWLINE): + ignore = True + + elif(len(lexeme) == 1 and lexeme[0] == openBracket): + token.type = ScriptToken.TID_LBRACKET + + elif(len(lexeme) == 1 and lexeme[0] == closeBracket): + token.type = ScriptToken.TID_RBRACKET + + elif(len(lexeme) == 1 and lexeme[0] == colon): + token.type = ScriptToken.TID_COLON + + else: + token.lexeme = lexeme + + # This is either a non-zero length phrase or quoted phrase + if(len(lexeme) >= 2 and lexeme[0] == quote and lexeme[-1] == quote): + token.type =ScriptToken.TID_QUOTE + + elif(len(lexeme) > 1 and lexeme[0] == var): + token.type = ScriptToken.TID_VARIABLE + + else: + token.type = ScriptToken.TID_WORD + + if(not ignore): + tokens.append(token) + + def isWhitespace(self, c): + return (c == ' ' or c == '\r' or c == '\t') + + def isNewline(self, c): + return (c == '\n' or c == '\r') + + +class MaterialParser: + + def unquote(string): + return (string[1:-1]) + + + def parameters(i, tokens): + parameters = [] + j = 1 + while tokens[i + j].type == ScriptToken.TID_WORD: + + try: + lexeme = ast.literal_eval(tokens[i + j].lexeme) + + except ValueError: + lexeme = tokens[i + j].lexeme + + except SyntaxError: + lexeme = tokens[i + j].lexeme + + parameters.append(lexeme) + + j += 1 + + return parameters + + + def xParseMaterial(meshMaterials, materialFile, folder): + logger.info("* Parsing material file: %s" % materialFile) + + try: + filein = open(materialFile) + except Exception: + logger.warning("Material: File %s not found!" % materialFile) + return None + + data = filein.read() + filein.close() + + sl = ScriptLexer() + tokens = sl.tokenize(data, materialFile) + + if sl.error != "": + logger.error("ERROR: Material Script tokenizer failed with error: %s" % sl.error) + return None + + SID_NONE = -1 + SID_MATERIAL = 0 + SID_TECHNIQUE = 1 + SID_PASS = 2 + SID_TEXTURE_UNIT = 3 + + state = SID_NONE + + # TODO: + # - RTSS Support + + for i in range(0, len(tokens)): + token = tokens[i] + + # Find Material + if state == SID_NONE: + if token.type == ScriptToken.TID_WORD and token.lexeme == "import": + logger.warning("Script importing and material inheritance are not supported") + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "vertex_program": + logger.warning("Vertex programs not supported, only Fixed Function Pipeline style materials") + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "fragment_program": + logger.warning("Fragment programs not supported, only Fixed Function Pipeline style materials") + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "geometry_program": + logger.warning("Geometry programs not supported, only Fixed Function Pipeline style materials") + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "material": + technique_nr = 0 + pass_nr = 0 + name = "" + Material = {} + + state = SID_MATERIAL + + if tokens[i + 1].type == ScriptToken.TID_WORD: + name = tokens[i + 1].lexeme + + elif tokens[i + 1].type == ScriptToken.TID_QUOTE: + name = MaterialParser.unquote(tokens[i + 1].lexeme) + + else: + logger.error("ERROR: Material name not found %s" % token) + continue + + logger.debug("Material name: %s" % name) + + # Parse Material + elif state == SID_MATERIAL: + if token.type == ScriptToken.TID_WORD and token.lexeme == "technique": + state = SID_TECHNIQUE + + technique_nr += 1 + + if technique_nr > 1: + logger.warning("Only one technique is supported") + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "receive_shadows": + if tokens[i + 1].type == ScriptToken.TID_WORD and tokens[i + 1].lexeme == "on": + logger.debug("receive_shadows: %s" % tokens[i + 1].lexeme) + Material['receive_shadows'] = True + # context.material.use_shadows = True + + elif tokens[i + 1].type == ScriptToken.TID_WORD and tokens[i + 1].lexeme == "off": + logger.debug("receive_shadows: %s" % tokens[i + 1].lexeme) + Material['receive_shadows'] = False + # context.material.use_shadows = False + + else: + logger.error("Parsing receive_shadows %s" % token) + + elif token.type == ScriptToken.TID_RBRACKET: + state = SID_NONE + + if len(Material) > 0: + meshMaterials[name] = Material + else: + logger.warning("Material %s is empty" % name) + + # Parse Technique + elif state == SID_TECHNIQUE: + if technique_nr > 1 and token.type != ScriptToken.TID_RBRACKET: + continue + + if token.type == ScriptToken.TID_WORD and token.lexeme == "pass": + state = SID_PASS + + pass_nr += 1 + + if pass_nr > 1: + logger.warning("Only one pass is supported") + + elif token.type == ScriptToken.TID_RBRACKET: + state = SID_MATERIAL + + # Parse Pass + elif state == SID_PASS: + if pass_nr > 1 and token.type != ScriptToken.TID_RBRACKET: + continue + + if token.type == ScriptToken.TID_WORD and token.lexeme == "texture_unit": + state = SID_TEXTURE_UNIT + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "ambient": + params = MaterialParser.parameters(i, tokens) + + if len(params) == 1 and tokens[i + 1].lexeme == "vertexcolour": + Material['vertexcolour'] = True + #context.material.use_vertex_color_paint = True + + elif len(params) == 3 or len(params) == 4: + Material['ambient'] = params + + else: + logger.error("Parsing ambient: %s" % token) + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "diffuse": + params = MaterialParser.parameters(i, tokens) + + if len(params) == 1 and tokens[i + 1].lexeme == "vertexcolour": + Material['vertexcolour'] = True + #context.material.use_vertex_color_paint = True + + elif len(params) == 3 or len(params) == 4: + Material['diffuse'] = params + + else: + logger.error("Parsing diffuse: %s" % token) + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "specular": + params = MaterialParser.parameters(i, tokens) + + if len(params) == 1 and tokens[i + 1].lexeme == "vertexcolour": + Material['vertexcolour'] = True + #context.material.use_vertex_color_paint = True + + elif len(params) == 3 or len(params) == 4 or len(params) == 5: + Material['specular'] = params + #print("\t\tspecular: %s" % params) + + else: + logger.error("Parsing specular: %s" % token) + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "emissive": + params = MaterialParser.parameters(i, tokens) + + if len(params) == 1 and tokens[i + 1].lexeme == "vertexcolour": + Material['vertexcolour'] = True + #context.material.use_vertex_color_paint = True + + elif len(params) == 3 or len(params) == 4: + Material['emissive'] = params + #emit_intensity = (color[0] * 0.2126 + color[1] * 0.7152 + color[2] * 0.0722) * color[3]; + #emit_intensity = color[0] * 0.2126 + color[1] * 0.7152 + color[2] * 0.0722; + else: + logger.error("Parsing emissive: %s" % token) + + elif token.type == ScriptToken.TID_WORD and token.lexeme == "depth_bias": + params = MaterialParser.parameters(i, tokens) + + if len(params) == 1: + Material['depth_bias'] = params + # mat.offset_z = tokens[i + 1].lexeme + else: + logger.error("Parsing depth_bias: %s" % token) + + elif token.type == ScriptToken.TID_RBRACKET: + state = SID_TECHNIQUE + + elif state == SID_TEXTURE_UNIT: + if token.type == ScriptToken.TID_WORD and token.lexeme == "texture": + if tokens[i + 1].type == ScriptToken.TID_WORD: + imageName = tokens[i + 1].lexeme + + elif tokens[i + 1].type == ScriptToken.TID_QUOTE: + imageName = self.unquote(tokens[i + 1].lexeme) + + else: + logger.error("Texture name not found: %s" % token) + continue + + file = os.path.join(folder, imageName) + if(not os.path.isfile(file)): + # Just force to use .dds if there isn't a file specified in the material file + file = os.path.join(folder, os.path.splitext(imageName)[0] + ".dds") + if(os.path.isfile(file)): + Material['texture'] = file + Material['imageNameOnly'] = imageName + else: + logger.warning("Referenced texture '%s' not found" % imageName) + else: + Material['texture'] = file + Material['imageNameOnly'] = imageName + + elif token.type == ScriptToken.TID_RBRACKET: + state = SID_PASS + + + def xCollectMaterialData(meshData, onlyName, folder): + + meshMaterials = {} + nameDotMaterial = onlyName + ".material" + pathMaterial = os.path.join(folder, nameDotMaterial) + if not os.path.isfile(pathMaterial): + # Search directory for .material + for filename in os.listdir(folder): + if ".material" in filename: + # Material file + pathMaterial = os.path.join(folder, filename) + MaterialParser.xParseMaterial(meshMaterials, pathMaterial, folder) + else: + MaterialParser.xParseMaterial(meshMaterials, pathMaterial, folder) + + meshData['materials'] = meshMaterials diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/materialv2json.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/materialv2json.py new file mode 100644 index 0000000..bda5e03 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/materialv2json.py @@ -0,0 +1,312 @@ +import logging, os, shutil, tempfile, json +from .. import config +from .. import util +from ..report import Report +from .material import material_name, ShaderImageTextureWrapper, gather_metallic_roughness_texture +from bpy_extras import node_shader_utils +import bpy.path + +logger = logging.getLogger('materialv2json') + +def dot_materialsv2json(materials, path=None, separate_files=True, prefix="mats", **kwargs): + """Output v2 .material.json files""" + if not materials: + logger.warn("No materials, not writing .materials.json") + + if not path: + path = tempfile.mkdtemp(prefix="ogre_io") + + generator = OgreMaterialv2JsonGenerator(materials, path, separate_files, prefix) + generator.process_materials() + + +class OgreMaterialv2JsonGenerator(object): + """Generator for v2 Json materials""" + + def __init__(self, materials, target_path, separate_files=True, prefix=''): + self.materials = materials + self.target_path = target_path + self.separate_files = separate_files + self.prefix = prefix + self.convert_set = set() + self.copy_set = set() + self.remove_set = set() + + def process_materials(self): + """Process all the materials, create the output json and copy textures""" + if self.separate_files: + for mat in self.materials: + datablock, blendblocks = self.generate_pbs_datablock(mat) + dst_filename = os.path.join(self.target_path, "{}.material.json".format(material_name(mat))) + logger.info("Writing material '{}'".format(dst_filename)) + try: + with open(dst_filename, 'w') as fp: + json.dump({"pbs": {material_name(mat): datablock}, "blendblocks": blendblocks}, fp, indent=2, sort_keys=True) + #json.dump({"pbs": {"blendblocks": blendblocks}}, fp, indent=2, sort_keys=True) + Report.materials.append(material_name(mat)) + except Exception as e: + logger.error("Unable to create material file '{}'".format(dst_filename)) + Report.errors.append("Unable to create material file '{}'".format(dst_filename)) + logger.error(e) + else: + dst_filename = os.path.join(self.target_path, "{}.material.json".format(self.prefix)) + fileblock = {"pbs": {}} + for mat in self.materials: + logger.info("Preparing material '{}' for file '{}".format(material_name(mat), dst_filename)) + fileblock["pbs"][material_name(mat)], fileblock["blendblocks"] = self.generate_pbs_datablock(mat) + try: + with open(dst_filename, 'w') as fp: + json.dump(fileblock, fp, indent=2, sort_keys=True) + except Exception as e: + logger.error("Unable to create material file '{}'".format(dst_filename)) + Report.errors.append("Unable to create material file '{}'".format(dst_filename)) + logger.error(e) + + self.copy_textures() + + def generate_pbs_datablock(self, material): + """Generate a PBS datablock for a material. + + # PBS datablock generator + based on the Ogre Next documentation. + doc: https://ogrecave.github.io/ogre-next/api/latest/hlmspbsdatablockref.html + + ## Metallic Workflow + Metalness texture fetching expects a single image with the metal + texture in the Blue channel and the roughness texture in the Green + channel. This is in line with the glTF standard setup. + + ## Specular Workflow + Unsupported. + + + ## Unsupported features + + ### fresnel + This is used in the Specular workflows supported by Ogre. Right now we + only support the metallic workflow. + + ### blendblock + Blendblocks are used for advanced effects and don't fit into the + standard Blender workflow. One commmon use would be to have better + alpha blending on complex textures. Limit of 32 blend blocks at + runtime also means we shouldn't "just generate them anyway." + doc: https://ogrecave.github.io/ogre-next/api/latest/hlmsblendblockref.html + + ### macroblock + Macroblocks are used for advanced effects and don't fit into the + standard Blender workflow. One common use would be to render a skybox + behind everything else in a scene. Limit of 32 macroblocks at runtime + also means we shouldn't "just generate them anyway." + doc: https://ogrecave.github.io/ogre-next/api/latest/hlmsmacroblockref.html + + ### sampler + Samplerblocks are used for advanced texture handling like filtering, + addressing, LOD, etc. These settings have signifigant visual and + performance effects. Limit of 32 samplerblocks at runtime also means + we shouldn't "just generate them anyway." + + ### recieve_shadows + No receive shadow setting in Blender 2.8+ but was available in 2.79. + We leave this unset which defaults to true. Maybe add support in + the 2.7 branch? + See: https://docs.blender.org/manual/en/2.79/render/blender_render/materials/properties/shadows.html#shadow-receiving-object-material + ### shadow_const_bias + Leave shadow const bias undefined to default. It is usually used to + fix specific self-shadowing issues and is an advanced feature. + + ### brdf + Leave brdf undefined to default. This setting has huge visual and + performance impacts and is for specific use cases. + doc: https://ogrecave.github.io/ogre-next/api/latest/hlmspbsdatablockref.html#dbParamBRDF + + ### reflection + Leave reflection undefined to default. In most cases for reflections + users will want to use generated cubemaps in-engine. + + ### detail_diffuse[0-3] + Layered diffuse maps for advanced effects. + + ### detail_normal[0-3] + Layered normal maps for advanced effects. + + ### detail_weight + Texture acting as a mask for the detail maps. + """ + + logger.debug("Generating PBS datablock for '{}'".format(material.name)) + bsdf = node_shader_utils.PrincipledBSDFWrapper(material) + + # Initialize datablock + datablock = {} + logger.debug("Diffuse params") + # Set up the diffuse paramenters + datablock["diffuse"] = { + "value": bsdf.base_color[0:3] + } + tex_filename = self.prepare_texture(bsdf.base_color_texture) + if tex_filename: + datablock["diffuse"]["texture"] = tex_filename + + # Set up emissive parameters + tex_filename = self.prepare_texture(bsdf.emission_color_texture) + if tex_filename: + logger.debug("Emissive params") + datablock["emissive"] = { + "lightmap": False, # bsdf.emission_strength_texture not supported in Blender < 2.9.0 + "value": bsdf.emission_color[0:3] + } + datablock["emissive"]["texture"] = tex_filename + + # Set up metalness parameters + tex_filename = self.prepare_texture(gather_metallic_roughness_texture(bsdf), channel=2) + logger.debug("Metallic params") + datablock["metalness"] = { + "value": bsdf.metallic + } + if tex_filename: + datablock["metalness"]["texture"] = tex_filename + else: # Support for standalone metallic texture + tex_filename = self.prepare_texture(bsdf.metallic_texture) + if tex_filename: + datablock["metalness"]["texture"] = tex_filename + + # Set up normalmap parameters, only if texture is present + tex_filename = self.prepare_texture(bsdf.normalmap_texture) + if tex_filename: + logger.debug("Normalmap params") + datablock["normal"] = { + "value": bsdf.normalmap_strength + } + datablock["normal"]["texture"] = tex_filename + + # Set up roughness parameters + tex_filename = self.prepare_texture(gather_metallic_roughness_texture(bsdf), channel=1) + logger.debug("Roughness params") + datablock["roughness"] = { + "value": bsdf.roughness + } + if tex_filename: + datablock["roughness"]["texture"] = tex_filename + else: # Support for standalone roughness texture + tex_filename = self.prepare_texture(bsdf.roughness_texture) + if tex_filename: + datablock["roughness"]["texture"] = tex_filename + + # Set up specular parameters + logger.debug("Specular params") + datablock["specular"] = { + "value": material.specular_color[0:3] + } + tex_filename = self.prepare_texture(bsdf.specular_texture) + if tex_filename: + datablock["specular"]["texture"] = tex_filename + + # Set up transparency parameters, only if texture is present + logger.debug("Transparency params") + # Initialize blendblock + blendblocks = {} + tex_filename = self.prepare_texture(bsdf.alpha_texture) + # Give blendblock specific settings + if material.blend_method == "OPAQUE": # OPAQUE will pass for now + pass + elif material.blend_method == "CLIP": # CLIP enables alpha_test (alpha rejection) + datablock["alpha_test"] = ["greater_equal", material.alpha_threshold, False] + elif material.blend_method == "BLEND": + datablock["transparency"] = { + "mode": "Transparent", + "use_alpha_from_textures": tex_filename != None, # DEFAULT + "value": bsdf.alpha + } + # Give blendblock common settings + datablock["blendblock"] = ["blendblock_name", "blendblock_name_for_shadows"] + blendblocks["blendblock_name"] = {} + blendblocks["blendblock_name"]["alpha_to_coverage"] = False + blendblocks["blendblock_name"]["blendmask"] = "rgba" + blendblocks["blendblock_name"]["separate_blend"] = False + blendblocks["blendblock_name"]["blend_operation"] = "add" + blendblocks["blendblock_name"]["blend_operation_alpha"] = "add" + blendblocks["blendblock_name"]["src_blend_factor"] = "one" + blendblocks["blendblock_name"]["dst_blend_factor"] = "one_minus_src_colour" # using "dst_colour" give an even clearer result than BLEND + blendblocks["blendblock_name"]["src_alpha_blend_factor"] = "one" + blendblocks["blendblock_name"]["dst_alpha_blend_factor"] = "one_minus_src_colour" + + # Backface culling + datablock["two_sided"] = not material.use_backface_culling + + # TODO: workflow for specular_fresnel, specular_ogre (default) + if datablock.get("metalness", None): + datablock["workflow"] = "metallic" + try: + datablock.pop("fresnel") # No fresnel if workflow is metallic + except KeyError: pass + + return datablock, blendblocks + + def prepare_texture(self, tex, channel=None): + """Prepare a texture for use + + channel is None=all channels, 0=red 1=green 2=blue + """ + if not (tex and tex.image): + return None + + src_filename = bpy.path.abspath(tex.image.filepath or tex.image.name) + dst_filename = bpy.path.basename(src_filename) + dst_filename = os.path.splitext(dst_filename)[0] + if channel is not None : + dst_filename="{}_c{}".format(dst_filename, channel) + + # pick target file format, prefer image format unless forced + src_format = tex.image.file_format.lower() + dst_format = src_format + if config.get("FORCE_IMAGE_FORMAT") != "NONE": + dst_format = config.get("FORCE_IMAGE_FORMAT") + dst_filename = "{}.{}".format(dst_filename, dst_format) + dst_filename = os.path.join(self.target_path, dst_filename) + + if tex.image.packed_file: + # save the image out to a temporary file + src_filename = "{}_{}".format(dst_filename, os.path.split(src_filename)[-1]) + self.remove_set.add(src_filename) + orig_filepath = tex.image.filepath + tex.image.filepath = src_filename + tex.image.save() + tex.image.filepath = orig_filepath + + if not os.path.isfile(src_filename): + logger.error("Cannot find source image: '{}'".format(src_filename)) + Report.errors.append("Cannot find source image: '{}'".format(src_filename)) + return None + + if src_format != dst_format or channel is not None: + # using extensions to determine filetype? gross + self.convert_set.add((tex.image, src_filename, dst_filename, channel)) + else: + self.copy_set.add((src_filename, dst_filename)) + + return os.path.split(dst_filename)[-1] + + def copy_textures(self): + """Copy and/or convert textures from previous prepare_texture() calls""" + for image, src_filename, dst_filename, channel in self.convert_set: + logger.info("ImageMagick: {} -> {}".format(src_filename, dst_filename)) + util.image_magick(image, src_filename, dst_filename, separate_channel=channel) + self.convert_set.clear() + + for src_filename, dst_filename in self.copy_set: + if os.path.isfile(dst_filename): + src_stat = os.stat(src_filename) + dst_stat = os.stat(dst_filename) + if src_stat.st_size == dst_stat.st_size and \ + src_stat.st_mtime == dst_stat.st_mtime: + logger.info("Skipping '{}', file is up to date".format(dst_filename)) + continue + logger.info("Copying: {} -> {}".format(src_filename, dst_filename)) + shutil.copy2(src_filename, dst_filename) + self.copy_set.clear() + + for filename in self.remove_set: + os.unlink(filename) + self.remove_set.clear() + diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/mesh.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/mesh.py new file mode 100644 index 0000000..9072a7f --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/mesh.py @@ -0,0 +1,930 @@ +import os, time, sys, logging +import bmesh, mathutils, math + +from ..report import Report +from ..util import * +from ..xml import * +from .. import util, config +from .material import * +from .skeleton import Skeleton + +logger = logging.getLogger('mesh') + +class VertexColorLookup: + def __init__(self, mesh): + self.__colors = None + + color_names = ["col", "color", "colour", "attribute"] + + vertex_colors = None + + # In Blender version 3.2, vertex colors have been refactored into generic color attributes + # https://developer.blender.org/docs/release_notes/3.2/sculpt/ + if bpy.app.version >= (3, 2, 0): + vertex_colors = mesh.color_attributes + else: + vertex_colors = mesh.vertex_colors + + if len(vertex_colors) > 0: + logger.debug("* Mesh has vertex colors") + + for key, colors in vertex_colors.items(): + if (self.__colors is None) and (key.lower() in color_names): + self.__colors = colors + + if (bpy.app.version >= (3, 2, 0)): + if colors.domain != 'CORNER': + Report.warnings.append( 'Mesh "%s" with color attribute "%s" has wrong color domain: "%s" (should be: "CORNER")' % \ + (mesh.name, key, colors.domain) ) + if colors.data_type != 'BYTE_COLOR': + Report.warnings.append( 'Mesh "%s" with color attribute "%s" has wrong color data type: "%s" (should be: "BYTE_COLOR")' % \ + (mesh.name, key, colors.data_type) ) + + if self.__colors is None: + # No color found by name, assume that the only vertex color data is actual color data + logger.debug("No color found by name, assume that the only vertex color data is actual color data") + self.__colors = colors + + if self.__colors: + self.__colors = [x.color for x in self.__colors.data] + #self.__colors = [x.color_srgb for x in self.__colors.data] + + @property + def has_color_data(self): + return self.__colors is not None + + def get(self, item): + if self.__colors: + color = self.__colors[item] + else: + color = [1.0] * 4 + return color + +def dot_mesh(ob, path, force_name=None, ignore_shape_animation=False, normals=True, tangents=4, isLOD=False, **kwargs): + """ + export the vertices of an object into a .mesh file + + ob: the blender object + path: the path to save the .mesh file to. path MUST exist + force_name: force a different name for this .mesh + kwargs: + * material_prefix - string. (optional) + * overwrite - bool. (optional) default False + """ + obj_name = force_name or ob.data.name + obj_name = clean_object_name(obj_name) + target_file = os.path.join(path, '%s.mesh.xml' % obj_name ) + + material_prefix = kwargs.get('material_prefix', '') + overwrite = kwargs.get('overwrite', False) + + # Don't export hidden or unselected objects unless told to + if not isLOD and ( + (config.get('LOD_GENERATION') == '2' and "_LOD_" in ob.name) or + ((config.get("EXPORT_HIDDEN") is False) and ob not in bpy.context.visible_objects) or + ((config.get("SELECTED_ONLY") is True) and not ob.select_get()) + ): + logger.debug("Skip exporting hidden/non-selected object: %s" % ob.data.name) + return [] + + if os.path.isfile(target_file) and not overwrite: + return [] + + if not os.path.isdir( path ): + os.makedirs( path ) + + start = time.time() + + if ob.modifiers != None: + # Disable Armature and Array modifiers before `to_mesh()` collapse + # NOTE: We need to disable the modifiers on the original object itself, we'll enable them again later + # If we try to remove the unwanted modifiers from the copy object, then none of the modifiers will be applied when doing `to_mesh()` + + # If we want to optimise array modifiers as instances, then the Array Modifier should be disabled + if config.get("ARRAY") is True: + disable_mods = ['ARMATURE', 'ARRAY'] + else: + disable_mods = ['ARMATURE'] + + for mod in ob.modifiers: + if mod.type in disable_mods and mod.show_viewport is True: + logger.debug("Disabling Modifier: %s" % mod.name) + mod.show_viewport = False + + # Without this, modifiers won't be applied by `to_mesh()` + depsgraph = bpy.context.evaluated_depsgraph_get() + object_eval = ob.evaluated_get(depsgraph) + + copy = object_eval.copy() + #bpy.context.scene.collection.objects.link(copy) + else: + copy = ob + + # bake mesh + mesh = copy.to_mesh() + mesh.update() + + # Blender by default does not calculate these. + # When querying the quads/tris of the object blender would crash if calc_tessface was not updated + mesh.calc_loop_triangles() + + Report.meshes.append( obj_name ) + Report.faces += len( mesh.loop_triangles ) + Report.orig_vertices += len( mesh.vertices ) + + logger.info('* Generating: %s.mesh.xml' % obj_name) + logger.info(" - Vertices: %s" % len( mesh.vertices )) + logger.info(" - Loop triangles: %s" % len( mesh.loop_triangles )) + + try: + with open(target_file, 'w') as f: + f.flush() + except Exception as e: + logger.error("Unable to create mesh file: %s" % target_file) + logger.error(e) + Report.errors.append("Unable to create mesh file: %s" % target_file) + return [] + + with open(target_file, 'w') as f: + doc = SimpleSaxWriter(f, 'mesh', {}) + + # Very ugly, have to replace number of vertices later + doc.start_tag('sharedgeometry', {'vertexcount' : '__TO_BE_REPLACED_VERTEX_COUNT__'}) + + logger.info('* Writing shared geometry') + + # Print a warning if there are no UV Maps created for the object + # and the user requested to have tangents generated + # (they won't be without a UV Map) + if int(config.get("GENERATE_TANGENTS")) != 0 and len(mesh.uv_layers) == 0: + logger.warning("No UV Maps were created for this object: <%s>, tangents won't be exported." % ob.name) + Report.warnings.append( 'Object "%s" has no UV Maps, tangents won\'t be exported.' % ob.name ) + + # Textures + dotextures = False + if mesh.uv_layers: + dotextures = True + else: + tangents = 0 + + doc.start_tag('vertexbuffer', { + 'positions':'true', + 'normals':'true', + 'tangents': str(bool(tangents)), + 'tangent_dimensions': str(tangents), + 'colours_diffuse' : str(bool( mesh.vertex_colors )), + 'texture_coords' : '%s' % len(mesh.uv_layers) * dotextures + }) + + # Materials + # saves tuples of material name and material obj (or None) + materials = [] + # a material named 'vertex.color.' will overwrite + # the diffuse color in the mesh file! + + for mat in ob.data.materials: + mat_name = "_missing_material_" + if mat is not None: + mat_name = mat.name + mat_name = material_name(mat_name, prefix=material_prefix) + extern = False + if mat_name.startswith("extern."): + mat_name = mat_name[len("extern."):] + extern = True + if mat: + materials.append( (mat_name, extern, mat) ) + else: + logger.warn('Bad material data in: %s' % ob.name) + materials.append( ('_missing_material_', True, None) ) # fixed dec22, keep proper index + if not materials: + materials.append( ('_missing_material_', True, None) ) + vertex_groups = {} + material_faces = [] + for matidx, mat in enumerate(materials): + material_faces.append([]) + + shared_vertices = {} + _remap_verts_ = [] + _face_indices_ = [] + + numverts = 0 + + # Create bmesh to help obtain custom vertex normals + bm = bmesh.new() + bm.from_mesh(mesh) + bm.verts.ensure_lookup_table() + + if mesh.has_custom_normals: + logger.debug("* Mesh has custom normals") + else: + logger.debug("* Mesh has NO custom normals") + + # Ogre only supports triangles + bmesh_return = bmesh.ops.triangulate(bm, faces=bm.faces, quad_method='FIXED') + bm.to_mesh(mesh) + + # Map the original face indices to the tesselated ones + face_map = bmesh_return['face_map'] + + _tess_polygon_face_map_ = {} + + for tess_face in face_map: + #print("tess_face.index[%s] <---> polygon_face.index[%s]" % (tess_face.index, face_map[tess_face].index)) + _tess_polygon_face_map_[tess_face.index] = face_map[tess_face].index + + # Vertex colors + vertex_color_lookup = VertexColorLookup(mesh) + + if tangents != 0: + mesh.calc_tangents(uvmap=mesh.uv_layers.active.name) + else: + # calc_tangents() already calculates split normals for us + if bpy.app.version < (4, 1, 0): + mesh.calc_normals_split() + + progressbar = util.ProgressBar("Faces", len(mesh.polygons)) + + # Process mesh after triangulation + for F in mesh.polygons: + progressbar.update(F.index) + + tri = (F.vertices[0], F.vertices[1], F.vertices[2]) + face = [] + for loop_idx, idx in zip(F.loop_indices, tri): + v = mesh.vertices[ idx ] + + if bpy.app.version < (3, 6, 0): + nx,ny,nz = swap( mesh.loops[ loop_idx ].normal ) + else: + nx,ny,nz = swap( mesh.corner_normals[ loop_idx ].vector ) + + if tangents != 0: + tx,ty,tz = swap( mesh.loops[ loop_idx ].tangent ) + tw = mesh.loops[ loop_idx ].bitangent_sign + + r,g,b,ra = vertex_color_lookup.get(loop_idx) + + # Texture maps + vert_uvs = [] + if dotextures: + for layer in mesh.uv_layers: + vert_uvs.append( layer.data[ loop_idx ].uv ) + + ''' Check if we already exported that vertex with same normal, do not export in that case, + (flat shading in blender seems to work with face normals, so we copy each flat face' + vertices, if this vertex with same normals was already exported, + todo: maybe not best solution, check other ways (let blender do all the work, or only + support smooth shading, what about seems, smoothing groups, materials, ...) + ''' + vert = VertexNoPos(numverts, nx, ny, nz, r, g, b, ra, vert_uvs) + alreadyExported = False + if idx in shared_vertices: + for vert2 in shared_vertices[idx]: + #does not compare ogre_vidx (and position at the moment) + if vert == vert2: + face.append(vert2.ogre_vidx) + alreadyExported = True + #print(idx, numverts, nx,ny,nz, r,g,b,ra, vert_uvs, "already exported") + break + if not alreadyExported: + face.append(vert.ogre_vidx) + shared_vertices[idx].append(vert) + #print(numverts, nx,ny,nz, r,g,b,ra, vert_uvs, "appended") + else: + face.append(vert.ogre_vidx) + shared_vertices[idx] = [vert] + #print(idx, numverts, nx,ny,nz, r,g,b,ra, vert_uvs, "created") + + if alreadyExported: + continue + + numverts += 1 + _remap_verts_.append( v ) + + # Use mapping from tesselated face to polygon face if the mapping exists + #if F.index in _tess_polygon_face_map_: + # _face_indices_.append( _tess_polygon_face_map_[F.index] ) + #else: + # _face_indices_.append( F.index ) + _face_indices_.append( F.index ) + + x,y,z = swap(v.co) # xz-y is correct! + + doc.start_tag('vertex', {}) + doc.leaf_tag('position', { + 'x' : '%6f' % x, + 'y' : '%6f' % y, + 'z' : '%6f' % z + }) + + doc.leaf_tag('normal', { + 'x' : '%6f' % nx, + 'y' : '%6f' % ny, + 'z' : '%6f' % nz + }) + + if tangents != 0: + doc.leaf_tag('tangent', { + 'x' : '%6f' % tx, + 'y' : '%6f' % ty, + 'z' : '%6f' % tz, + 'w' : '%6f' % tw + }) + + if vertex_color_lookup.has_color_data: + doc.leaf_tag('colour_diffuse', {'value' : '%6f %6f %6f %6f' % (r,g,b,ra)}) + + # Texture maps + if dotextures: + for uv in vert_uvs: + doc.leaf_tag('texcoord', { + 'u' : '%6f' % uv[0], + 'v' : '%6f' % (1.0-uv[1]) + }) + + doc.end_tag('vertex') + + append_triangle_in_vertex_group(mesh, ob, vertex_groups, face, tri) + + try: + material_faces[F.material_index].append(face) + except: + failure = 'FAILED to assign material to face - you might be using a Boolean Modifier between objects with different materials!' + failure += '[ mesh : %s ]' % mesh.name + Report.warnings.append( failure ) + logger.error( failure ) + break + + Report.vertices += numverts + + doc.end_tag('vertexbuffer') + doc.end_tag('sharedgeometry') + + sys.stdout.write("\n") + + logger.info('- Done at %s seconds' % util.timer_diff_str(start)) + logger.info('* Writing submeshes') + + doc.start_tag('submeshes', {}) + for matidx, (mat_name, extern, mat) in enumerate(materials): + if len(material_faces[matidx]) == 0: + Report.warnings.append('BAD SUBMESH "%s": material %r, has not been applied to any faces - not exporting as submesh.' % (obj_name, mat_name) ) + continue # fixes corrupt unused materials + + submesh_attributes = { + 'usesharedvertices' : 'true', + # Maybe better look at index of all faces, if one over 65535 set to true; + # Problem: we know it too late, postprocessing of file needed + "use32bitindexes" : str(bool(numverts > 65535)), + "operationtype" : "triangle_list" + } + if mat_name != "_missing_material_": + submesh_attributes['material'] = mat_name + + doc.start_tag('submesh', submesh_attributes) + doc.start_tag('faces', { + 'count' : str(len(material_faces[matidx])) + }) + for fidx, (v1, v2, v3) in enumerate(material_faces[matidx]): + doc.leaf_tag('face', { + 'v1' : str(v1), + 'v2' : str(v2), + 'v3' : str(v3) + }) + doc.end_tag('faces') + doc.end_tag('submesh') + Report.triangles += len(material_faces[matidx]) + + for name, ogre_indices in vertex_groups.items(): + if len(ogre_indices) <= 0: + continue + submesh_attributes = { + 'usesharedvertices' : 'true', + "use32bitindexes" : str(bool(numverts > 65535)), + "operationtype" : "triangle_list", + "material": "none", + } + doc.start_tag('submesh', submesh_attributes) + doc.start_tag('faces', { + 'count' : len(ogre_indices) + }) + for (v1, v2, v3) in ogre_indices: + doc.leaf_tag('face', { + 'v1' : str(v1), + 'v2' : str(v2), + 'v3' : str(v3) + }) + doc.end_tag('faces') + doc.end_tag('submesh') + + del material_faces + del shared_vertices + doc.end_tag('submeshes') + + # Submesh names + # todo: why is the submesh name taken from the material + # when we have the blender object name available? + doc.start_tag('submeshnames', {}) + for matidx, (mat_name, extern, mat) in enumerate(materials): + doc.leaf_tag('submesh', { + 'name' : mat_name, + 'index' : str(matidx) + }) + idx = len(materials) + for name in vertex_groups.keys(): + name = name[len('ogre.vertex.group.'):] + doc.leaf_tag('submesh', {'name': name, 'index': idx}) + idx += 1 + doc.end_tag('submeshnames') + + logger.info('- Done at %s seconds' % util.timer_diff_str(start)) + + # Generate LOD levels for manual LOD meshes + if isLOD == False and ob.type == 'MESH' and config.get('LOD_LEVELS') > 0 and config.get('LOD_GENERATION') == '2': + lod_levels = config.get('LOD_LEVELS') + lod_distance = config.get('LOD_DISTANCE') + + lod_generated = [] + lod_current_distance = lod_distance + + for level in range(lod_levels + 1)[1:]: + lod_ob_name = obj_name + '_LOD_' + str(level) + lod_manual_ob = bpy.context.scene.objects.get(lod_ob_name) + + if lod_manual_ob: + logger.info("- Found LOD Manual object: %s" % lod_ob_name) + lod_generated.append({ 'level': level, 'distance': lod_current_distance, 'lod_manual_ob': lod_manual_ob }) + lod_current_distance += lod_distance + + else: + failure = 'FAILED to manually create LOD levels, manual LOD with name %s NOT FOUND!' % lod_ob_name + Report.warnings.append( failure ) + logger.error( failure ) + break + + # Create lod .mesh files and generate LOD XML to the original .mesh.xml + if len(lod_generated) > 0: + # 'manual' means if the geometry gets loaded from a different file than this LOD list references + doc.start_tag('levelofdetail', { + 'strategy' : 'default', + 'numlevels' : str(len(lod_generated) + 1), # The main mesh is + 1 (kind of weird Ogre logic) + 'manual' : "true" + }) + + logger.info('- Generating: %s LOD meshes. Original: vertices %s, faces: %s' % (len(lod_generated), len(mesh.vertices), len(mesh.loop_triangles))) + for lod in lod_generated: + lod_manual_ob = lod['lod_manual_ob'] + + logger.info("- Writing LOD %s for distance %s, with %s vertices and %s faces" % + (lod['level'], lod['distance'], len(lod_manual_ob.data.vertices), len(lod_manual_ob.data.loop_triangles))) + + dot_mesh(lod_manual_ob, path, lod_manual_ob.data.name, ignore_shape_animation, normals, tangents, isLOD=True) + + # 'value' is the distance this LOD kicks in for the 'Distance' strategy. + doc.leaf_tag('lodmanual', { + 'value' : str(lod['distance']), + 'meshname' : lod_manual_ob.data.name + ".mesh" + }) + + doc.end_tag('levelofdetail') + + # Generate LOD levels automatically using Blenders "Decimate" Modifier + if isLOD == False and ob.type == 'MESH' and config.get('LOD_LEVELS') > 0 and config.get('LOD_GENERATION') == '1': + lod_levels = config.get('LOD_LEVELS') + lod_distance = config.get('LOD_DISTANCE') + lod_ratio = config.get('LOD_PERCENT') / 100.0 + lod_pre_mesh_count = len(bpy.data.meshes) + + # Cap lod levels to something sensible (what is it?) + if lod_levels > 10: + lod_levels = 10 + + def duplicate_object(scene, name, copyobj): + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Create new object associated with the mesh + ob_new = bpy.data.objects.new(name, mesh) + + # Copy data block from the old object into the new object + ob_new.data = copyobj.data.copy() + ob_new.location = copyobj.location + ob_new.rotation_euler = copyobj.rotation_euler + ob_new.scale = copyobj.scale + + # Link new object to the given scene and select it + scene.collection.objects.link(ob_new) + ob_new.select_set(True) + + return ob_new, mesh + + # Create a temporary duplicate + ob_copy, ob_copy_mesh = duplicate_object(bpy.context.scene, obj_name + "_LOD_TEMP_COPY", ob) + ob_copy_meshes = [ ob_copy.data, ob_copy_mesh ] + + # Activate clone for modifier manipulation + decimate = ob_copy.modifiers.new(name="Ogre-LOD_Decimate", type='DECIMATE') + if decimate is not None: + decimate.decimate_type = 'COLLAPSE' + decimate.show_viewport = True + decimate.show_render = True + + lod_generated = [] + lod_ratio_multiplier = 1.0 - lod_ratio + lod_current_ratio = 1.0 * lod_ratio_multiplier + lod_current_distance = lod_distance + lod_current_vertice_count = len(mesh.vertices) + lod_min_vertice_count = 12 + + for level in range(lod_levels + 1)[1:]: + decimate.ratio = lod_current_ratio + + # https://docs.blender.org/api/current/bpy.types.Depsgraph.html + depsgraph = bpy.context.evaluated_depsgraph_get() + object_eval = ob_copy.evaluated_get(depsgraph) + lod_mesh = bpy.data.meshes.new_from_object(object_eval) + + ob_copy_meshes.append(lod_mesh) + + # Check min vertice count and that the vertice count got reduced from last iteration + lod_mesh_vertices = len(lod_mesh.vertices) + + if lod_mesh_vertices < lod_min_vertice_count: + logger.info('- LOD level: %s, vertice count: %s too small. Ignoring LOD.' % (level, lod_mesh_vertices)) + break + if lod_mesh_vertices >= lod_current_vertice_count: + logger.info('- LOD level: %s, vertice count: %s cannot be decimated any longer. Ignoring LOD.' % (level - 1, lod_mesh_vertices)) + break + # todo: should we check if the ratio gets too small? although its up to the user to configure from the export panel + + lod_generated.append({ 'level': level, 'distance': lod_current_distance, 'ratio': lod_current_ratio, 'mesh': lod_mesh }) + lod_current_distance += lod_distance + lod_current_vertice_count = lod_mesh_vertices + lod_current_ratio *= lod_ratio_multiplier + + # Create lod .mesh files and generate LOD XML to the original .mesh.xml + if len(lod_generated) > 0: + # 'manual' means if the geometry gets loaded from a + # different file that this LOD list references + # NOTE: This is the approach at the moment. Another option would be to + # references to the same vertex indexes in the shared geometry. But the + # decimate approach wont work with this as it generates a fresh geometry. + doc.start_tag('levelofdetail', { + 'strategy' : 'default', + 'numlevels' : str(len(lod_generated) + 1), # The main mesh is + 1 (kind of weird Ogre logic) + 'manual' : "true" + }) + + logger.info('- Generating: %s LOD meshes. Original: vertices %s, faces: %s' % (len(lod_generated), len(mesh.vertices), len(mesh.loop_triangles))) + for lod in lod_generated: + ratio_percent = round(lod['ratio'] * 100.0, 0) + logger.info("- Writing LOD %s for distance %s and ratio %s/100, with %s vertices, %s faces" % + (lod['level'], lod['distance'], str(ratio_percent), len(lod['mesh'].vertices), len(lod['mesh'].loop_triangles))) + lod_ob_temp = bpy.data.objects.new(obj_name, lod['mesh']) + lod_ob_temp.data.name = obj_name + '_LOD_' + str(lod['level']) + dot_mesh(lod_ob_temp, path, lod_ob_temp.data.name, ignore_shape_animation, normals, tangents, isLOD=True) + + # 'value' is the distance this LOD kicks in for the 'Distance' strategy. + doc.leaf_tag('lodmanual', { + 'value' : str(lod['distance']), + 'meshname' : lod_ob_temp.data.name + ".mesh" + }) + + # Delete temporary LOD object. + # The clone meshes will be deleted later. + lod_ob_temp.user_clear() + logger.debug("Removing temporary LOD object: %s" % lod_ob_temp.name) + bpy.data.objects.remove(lod_ob_temp, do_unlink=True) + del lod_ob_temp + + doc.end_tag('levelofdetail') + + # Delete temporary LOD object + logger.debug("Removing temporary LOD object: %s" % ob_copy.name) + bpy.data.objects.remove(ob_copy, do_unlink=True) + del ob_copy + + # Delete temporary data/mesh objects + bpy.context.evaluated_depsgraph_get().update() + for mesh_iter in ob_copy_meshes: + mesh_iter.user_clear() + logger.debug("Removing temporary LOD mesh: %s" % mesh_iter.name) + bpy.data.meshes.remove(mesh_iter) + del mesh_iter + ob_copy_meshes = [] + + if lod_pre_mesh_count != len(bpy.data.meshes): + logger.warn('- After LOD generation, cleanup failed to erase all temporary data!') + + arm = ob.find_armature() + if arm: + skeleton_name = obj_name + if config.get('SHARED_ARMATURE') is True: + skeleton_name = arm.data.name + skeleton_name = util.clean_object_name(skeleton_name) + + doc.leaf_tag('skeletonlink', { + 'name' : '%s.skeleton' % skeleton_name + }) + doc.start_tag('boneassignments', {}) + boneOutputEnableFromName = {} + boneIndexFromName = {} + for bone in arm.pose.bones: + boneOutputEnableFromName[ bone.name ] = True + if config.get('ONLY_DEFORMABLE_BONES') is True: + # if we found a deformable bone, + if bone.bone.use_deform: + # visit all ancestor bones and mark them "output enabled" + parBone = bone.parent + while parBone: + boneOutputEnableFromName[ parBone.name ] = True + parBone = parBone.parent + else: + # non-deformable bone, no output + boneOutputEnableFromName[ bone.name ] = False + boneIndex = 0 + for bone in arm.pose.bones: + boneIndexFromName[ bone.name ] = boneIndex + if boneOutputEnableFromName[ bone.name ]: + boneIndex += 1 + badverts = 0 + for vidx, v in enumerate(_remap_verts_): + check = 0 + for vgroup in v.groups: + if vgroup.weight > config.get('TRIM_BONE_WEIGHTS'): + groupIndex = vgroup.group + if groupIndex < len(copy.vertex_groups): + vg = copy.vertex_groups[ groupIndex ] + if vg.name in boneIndexFromName: # allows other vertex groups, not just armature vertex groups + bnidx = boneIndexFromName[ vg.name ] # find_bone_index(copy,arm,vgroup.group) + doc.leaf_tag('vertexboneassignment', { + 'vertexindex' : str(vidx), + 'boneindex' : str(bnidx), + 'weight' : '%6f' % vgroup.weight + }) + check += 1 + else: + logger.warn('Mesh: %s vertex groups not in sync with armature %s (groupIndex = %s)' % (mesh.name, arm.name, groupIndex)) + if check > 4: + badverts += 1 + logger.warn('<%s> vertex %s is in more than 4 vertex groups (bone weights). This maybe Ogre incompatible' % (mesh.name, vidx)) + if badverts: + Report.warnings.append( 'Mesh "%s" has %s vertices weighted to too many bones (Ogre limits a vertex to 4 bones). Try increasing the Trim-Weights threshold option' % (mesh.name, badverts) ) + doc.end_tag('boneassignments') + + # Updated June3 2011 - shape animation works + if (config.get('SHAPE_ANIMATIONS') is True) and mesh.shape_keys and len(mesh.shape_keys.key_blocks) > 0: + logger.info('* Writing shape keys') + + if ob.active_shape_key_index != 0 and ob.show_only_shape_key == True: + warning = "Object \"%s\" mesh will look like selected shape key: '%s', not 'Basis'" % (ob.name, ob.active_shape_key.name) + logger.warn(warning) + Report.warnings.append(warning) + + doc.start_tag('poses', {}) + for sidx, skey in enumerate(mesh.shape_keys.key_blocks): + # Skip the basis Shape Key + if sidx == 0: + continue + + if len(skey.data) != len( ob.data.vertices ): + failure = 'FAILED to save shape animation - you can not use a modifier that changes the vertex count! ' + failure += '[ mesh : %s ]' % mesh.name + Report.warnings.append( failure ) + logger.error( failure ) + break + + doc.start_tag('pose', { + 'name' : skey.name, + # If target is 'mesh', no index needed, if target is submesh then submesh identified by 'index' + #'index' : str(sidx-1), + #'index' : '0', + 'target' : 'mesh' + }) + + normals_data = skey.normals_split_get() + # Separate normals_split_get() data into 3-tuples + snormals = tuple(normals_data[i:i + 3] for i in range(0, len(normals_data), 3)) + shape_data = skey.data + + # Create mapping between objects original mesh (polygons,vertices) and normals from "normals_split_get()" + normal_idx = 0 + _remap_normals_ = {} + for poly in ob.data.polygons: + for loop_idx in poly.loop_indices: + vertex_idx = ob.data.loops[loop_idx].vertex_index + _remap_normals_[(poly.index,vertex_idx)] = normal_idx + normal_idx = normal_idx + 1 + + # Go through _remap_verts_ (array of vertices that are going into OGRE mesh) + for vidx, v in enumerate(_remap_verts_): + pv = skey.data[ v.index ] + x,y,z = swap( pv.co - v.co ) + + if config.get('SHAPE_NORMALS') is True: + vertex_idx = v.index + + # Try to get original polygon loop index (before tesselation) + if _face_indices_[vidx] in _tess_polygon_face_map_: + loop_idx = _tess_polygon_face_map_[_face_indices_[vidx]] + # If not in _tess_polygon_face_map_, then we can get the original + else: + loop_idx = _face_indices_[vidx] + + # Index of normal into snormals array + normal_idx = _remap_normals_[(loop_idx, vertex_idx)] + + pn = mathutils.Vector( snormals[normal_idx] ) + nx,ny,nz = swap( pn ) + + if config.get('SHAPE_NORMALS') is True: + doc.leaf_tag('poseoffset', { + 'x' : '%6f' % x, + 'y' : '%6f' % y, + 'z' : '%6f' % z, + 'nx' : '%6f' % nx, + 'ny' : '%6f' % ny, + 'nz' : '%6f' % nz, + 'index' : str(vidx) + }) + else: + doc.leaf_tag('poseoffset', { + 'x' : '%6f' % x, + 'y' : '%6f' % y, + 'z' : '%6f' % z, + 'index' : str(vidx) + }) + + del _remap_normals_ + + doc.end_tag('pose') + doc.end_tag('poses') + + logger.info('- Done at %s seconds' % util.timer_diff_str(start)) + + if mesh.shape_keys.animation_data and len(mesh.shape_keys.animation_data.nla_tracks) > 0: + logger.info('* Writing shape animations') + doc.start_tag('animations', {}) + _fps = float( bpy.context.scene.render.fps ) + for nla in mesh.shape_keys.animation_data.nla_tracks: + for idx, strip in enumerate(nla.strips): + doc.start_tag('animation', { + 'name' : strip.name, + 'length' : str((strip.frame_end-strip.frame_start)/_fps) + }) + doc.start_tag('tracks', {}) + doc.start_tag('track', { + 'type' : 'pose', + 'target' : 'mesh' + # If target is 'mesh', no index needed, if target is submesh then submesh identified by 'index' + #'index' : str(idx) + #'index' : '0' + }) + doc.start_tag('keyframes', {}) + for frame in range( int(strip.frame_start), int(strip.frame_end)+1, bpy.context.scene.frame_step):#thanks to Vesa + bpy.context.scene.frame_set(frame) + doc.start_tag('keyframe', { + 'time' : str((frame-strip.frame_start)/_fps) + }) + for sidx, skey in enumerate( mesh.shape_keys.key_blocks ): + if sidx == 0: continue + doc.leaf_tag('poseref', { + 'poseindex' : str(sidx-1), + 'influence' : str(skey.value) + }) + doc.end_tag('keyframe') + doc.end_tag('keyframes') + doc.end_tag('track') + doc.end_tag('tracks') + doc.end_tag('animation') + doc.end_tag('animations') + logger.info('- Done at %s seconds' % util.timer_diff_str(start)) + + ## If we made a copy of the object, clean it up + if ob != copy: + #bpy.context.collection.objects.unlink(copy) # Blender 2.7x + #bpy.context.scene.collection.objects.unlink(copy) # Blender 2.8+ + copy.user_clear() + logger.debug("Removing temporary object: %s" % copy.name) + bpy.data.objects.remove(copy) + del copy + + # Reenable disabled modifiers + if ob.modifiers != None: + for mod in ob.modifiers: + if mod.type in disable_mods and mod.show_viewport == False: + logger.debug("Enabling Modifier: %s" % mod.name) + mod.show_viewport = True + + # Release BMesh resources + bm.free() + del bm + + del _remap_verts_ + del _face_indices_ + doc.close() # reported by Reyn + f.close() + + logger.info('- Created %s.mesh.xml at %s seconds' % (obj_name, util.timer_diff_str(start))) + + # todo: Very ugly, find better way + def replaceInplace(f,searchExp,replaceExp): + import fileinput + + with fileinput.FileInput(f, inplace=True, encoding="utf-8") as file: + for line in file: + if searchExp in line: + line = line.replace(searchExp,replaceExp) + sys.stdout.write(line) + + replaceInplace(target_file, '__TO_BE_REPLACED_VERTEX_COUNT__' + '"', str(numverts) + '"' )#+ ' ' * (ls - lr)) + + # Start .mesh.xml to .mesh convertion tool + util.xml_convert(target_file, has_uvs=dotextures) + + logger.info('- Created %s.mesh in total time %s seconds' % (obj_name, util.timer_diff_str(start))) + + # If requested by the user, generate LOD levels / Edge Lists / Vertex buffer optimization through OgreMeshUpgrader + if ((config.get('LOD_LEVELS') > 0 and config.get('LOD_GENERATION') == '0') or + (config.get('GENERATE_EDGE_LISTS') is True) or + (config.get('PACK_INT_10_10_10_2') is True) or + (config.get('OPTIMISE_VERTEX_CACHE') is True)): + target_mesh_file = os.path.join(path, '%s.mesh' % obj_name ) + util.mesh_upgrade_tool(target_mesh_file) + + # Note that exporting the skeleton does not happen here anymore + # It was moved to the function dot_skeleton in its own module (skeleton.py) + + mats = [] + for mat_name, extern, mat in materials: + # _missing_material_ is marked as extern + if not extern: + mats.append(mat_name) + else: + logger.info("Extern material: %s" % mat_name) + + return mats + +def triangle_list_in_group(mesh, shared_vertices, group_index): + faces = [] + for face in mesh.data.loop_triangles: + vertices = [mesh.data.vertices[v] for v in face.vertices] + match_group = lambda g, v: g in [x.group for x in v.groups] + all_in_group = all([match_group(group_index, v) for v in vertices]) + if not all_in_group: + continue + assert len(face.vertices) == 3 + entry = [shared_vertices[v][0].ogre_vidx for v in face.vertices] + faces.append(tuple(entry)) + return faces + +def append_triangle_in_vertex_group(mesh, obj, vertex_groups, ogre_indices, blender_indices): + vertices = [mesh.vertices[i] for i in blender_indices] + names = set() + for v in vertices: + for g in v.groups: + if g.group >= len(obj.vertex_groups): + return + group = obj.vertex_groups[g.group] + if not group.name.startswith("ogre.vertex.group."): + return + names.add(group.name) + match_group = lambda name, v: name in [obj.vertex_groups[x.group].name for x in v.groups] + for name in names: + all_in_group = all([match_group(name, v) for v in vertices]) + if not all_in_group: + continue + if name not in vertex_groups: + vertex_groups[name] = [] + vertex_groups[name].append(ogre_indices) + +class VertexNoPos(object): + def __init__(self, ogre_vidx, nx,ny,nz, r,g,b,ra, vert_uvs): + self.ogre_vidx = ogre_vidx + self.nx = nx + self.ny = ny + self.nz = nz + self.r = r + self.g = g + self.b = b + self.ra = ra + self.vert_uvs = vert_uvs + + '''does not compare ogre_vidx (and position at the moment) [ no need to compare position ]''' + def __eq__(self, o): + if not math.isclose(self.nx, o.nx): return False + if not math.isclose(self.ny, o.ny): return False + if not math.isclose(self.nz, o.nz): return False + if not math.isclose(self.r, o.r): return False + if not math.isclose(self.g, o.g): return False + if not math.isclose(self.b, o.b): return False + if not math.isclose(self.ra, o.ra): return False + if len(self.vert_uvs) != len(o.vert_uvs): return False + if self.vert_uvs: + for i, uv1 in enumerate( self.vert_uvs ): + uv2 = o.vert_uvs[ i ] + if uv1 != uv2: return False + return True + + def __repr__(self): + return 'vertex(%d)' % self.ogre_vidx diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/node_anim.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/node_anim.py new file mode 100644 index 0000000..9a906a7 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/node_anim.py @@ -0,0 +1,179 @@ +import bpy, mathutils, logging, time +from .. import config +from ..report import Report +from ..xml import RDocument +from .. import util +from os.path import join + +logger = logging.getLogger('node_anim') + +# Node Animation, based on the work done in Easy Ogre Exporter (3D Studio Max exporter) +# https://github.com/OGRECave/EasyOgreExporter/blob/master/source/ExScene.cpp#L104 +# The idea is that the data exported by blender2ogre would have the same format as the one exported by Easy Ogre Exporter which would be the standard by being the first ones to implement it +# There seems to be some support for animation in the Maya Ogre Exporter, but for the camera so it is possible to animate values like FOV +# https://github.com/bitgate/maya-ogre3d-exporter/blob/master/src/ogreExporter.cpp#L373 + +def dot_nodeanim(ob, doc, xmlnode): + """ + Create the node animation for this object. + This is only possible if the object has any animation data + + ob: the blender object + doc: the parent xml node to attach the animation data + """ + + # Do not process node animations for Armatures (to avoid setting spurious rotations on the armature which causes problems with SeletalAnimation) + # To have a node animation in combination with an Armature, it should be parented to an Empty and have the Empty animated + if ob.type == 'ARMATURE': + return + + anim = ob.animation_data + + if anim is None or anim.nla_tracks is None: + return + + savedUseNla = anim.use_nla + savedAction = anim.action + anim.use_nla = False + if not len( anim.nla_tracks ): + Report.warnings.append('You must assign an NLA strip to object (%s) that defines the start and end frames' % ob.name) + + logger.info('* Generating node animation for: %s' % ob.name) + + start = time.time() + + actions = {} # actions by name + # the only thing NLA is used for is to gather the names of the actions + # it doesn't matter if the actions are all in the same NLA track or in different tracks + for nla in anim.nla_tracks: # NLA required, lone actions not supported + logger.info('+ NLA track: %s' % nla.name) + + for strip in nla.strips: + action = strip.action + actions[ action.name ] = [action, strip.action_frame_start, strip.action_frame_end] + logger.info(' - Action name: %s' % action.name) + logger.info(' - Strip name: %s' % strip.name) + + actionNames = sorted( actions.keys() ) # output actions in alphabetical order + for actionName in actionNames: + actionData = actions[ actionName ] + action = actionData[0] + anim.action = action # set as the current action + write_animation( ob, action, actionData[1], actionData[2], doc, xmlnode ) + + # restore these to what they originally were + anim.action = savedAction + anim.use_nla = savedUseNla + + logger.info('- Done at %s seconds' % util.timer_diff_str(start)) + +# A note about the option: NODE_KEYFRAMES +# If NODE_KEYFRAMES is False, then this function processess the objects animation frame by frame, instead of using its keyframes +# The advantage of this method is that it respects the users tuning of the node animation curves +# The disadvantage is that it generates more data than just processing the keyframes and it might clutter the .scene file. +# Since the .scene file is not a binary file it might take longer to process with this method + +# If NODE_KEYFRAMES is True, then this function processess the objects keyframes one by one by going through the F-Curves +# The advantage of this method is that it is very fast and produces less data (only the keyframes) +# The disadvantage is that if the user did an extensive tuning of the node animation curves that tuning is lost since only IM_LINEAR / IM_SPLINE at the global level is supported by Ogre +# Another disadvantage is that it is very difficult if not impossible to choose between IM_LINEAR or IM_SPLINE for the resulting animation since the user might be choosing between different interpolation types for each F-Curve +def write_animation(ob, action, frame_start, frame_end, doc, xmlnode): + + _fps = float( bpy.context.scene.render.fps ) + + # Actually in Blender this does not make sense because there is only one possible animation per object, + # but lets maintain compatibility with Easy Ogre Exporter + aa = doc.createElement('animations') + xmlnode.appendChild(aa) + + a = doc.createElement('animation') + a.setAttribute("name", "%s" % action.name) + a.setAttribute("enable", "false") + a.setAttribute("loop", "false") + a.setAttribute("interpolationMode", "linear") + a.setAttribute("rotationInterpolationMode", "linear") + a.setAttribute("length", '%6f' % ((frame_end) / _fps)) + aa.appendChild(a) + + frame_current = bpy.context.scene.frame_current + + initial_location = mathutils.Vector((0, 0, 0)) + initial_rotation = mathutils.Quaternion((1, 0, 0, 0)) + initial_scale = mathutils.Vector((1, 1, 1)) + + frames = range(int(frame_start), int(frame_end) + 1) + + # If NODE_KEYFRAMES is True, then use only the keyframes to export the animation + #if config.get('NODE_KEYFRAMES') is True: + # frames = get_keyframes(action) + + for frame in frames: + + kf = doc.createElement('keyframe') + kf.setAttribute("time", '%6f' % (frame / _fps)) + a.appendChild(kf) + + bpy.context.scene.frame_set(frame) + + translation = mathutils.Vector((0, 0, 0)) + rotation_quat = mathutils.Quaternion((1, 0, 0, 0)) + scale = mathutils.Vector((1, 1, 1)) + + if frame == frame_start: + initial_location = util.swap( ob.matrix_local.to_translation() ) + initial_rotation = util.swap( ob.matrix_local.to_quaternion() ) + initial_scale = calc_scale( ob.matrix_local ) + + else: + translation = util.swap( ob.matrix_local.to_translation() ) - initial_location + rotation_quat = initial_rotation.rotation_difference( util.swap( ob.matrix_local.to_quaternion() ) ) + current_scale = calc_scale( ob.matrix_local ) + scale.x = current_scale.x / initial_scale.x + scale.y = current_scale.y / initial_scale.y + scale.z = current_scale.z / initial_scale.z + + t = doc.createElement('position') + t.setAttribute("x", '%6f' % translation.x) + t.setAttribute("y", '%6f' % translation.y) + t.setAttribute("z", '%6f' % translation.z) + kf.appendChild(t) + + q = doc.createElement('rotation') + q.setAttribute("qw", '%6f' % rotation_quat.w) + q.setAttribute("qx", '%6f' % rotation_quat.x) + q.setAttribute("qy", '%6f' % rotation_quat.y) + q.setAttribute("qz", '%6f' % rotation_quat.z) + kf.appendChild(q) + + s = doc.createElement('scale') + s.setAttribute("x", '%6f' % scale.x) + s.setAttribute("y", '%6f' % scale.y) + s.setAttribute("z", '%6f' % scale.z) + kf.appendChild(s) + + bpy.context.scene.frame_set(frame_current) + +def calc_scale(matrix_local): + # Scale is different in Ogre from blender - rotation is removed + ri = matrix_local.to_quaternion().inverted().to_matrix() + scale = ri.to_4x4() * matrix_local + v = util.swap( scale.to_scale() ) + + return mathutils.Vector((abs(v.x), abs(v.y), abs(v.z))) + +def get_keyframes(action): + + keyframes = {} + + for fcurve in action.fcurves: + + for keyframe in fcurve.keyframe_points: + + frame, value = keyframe.co + + # Add Keyframe if it does not exist + if frame not in keyframes: + keyframes[frame] = frame + + return sorted(keyframes) + diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/ogre_import.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/ogre_import.py new file mode 100644 index 0000000..1843c32 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/ogre_import.py @@ -0,0 +1,1749 @@ +#!BPY + +""" +Name: 'OGRE for Kenshi (*.MESH)' +Blender: 2.80 +Group: 'Import/Export' +Tooltip: 'Import/Export Kenhi OGRE mesh files' + +Author: Someone + +Based on the Torchlight Impost/Export script by 'Dusho' + +Thanks goes to 'goatman' for his port of Ogre export script from 2.49b to 2.5x, +and 'CCCenturion' for trying to refactor the code to be nicer (to be included) +""" + +__author__ = "someone" +__version__ = "0.9.1 13-Sep-2019" + +__bpydoc__ = """\ +This script imports/exports Kenshi Ogre models into/from Blender. + +Supported:
+ * import/export of basic meshes + * import/export of skeleton + * import/export of animations + * import/export of vertex weights (ability to import characters and adjust rigs) + * import/export of vertex colour (RGB) + * import/export of vertex alpha (Uses second vertex colour layer called Alpha) + * import/export of shape keys + * Calculation of tangents and binormals for export + +Known issues:
+ * imported materials will lose certain informations not applicable to Blender when exported + +History:
+ * v0.9.1 (13-Sep-2019) - Fixed importing skeletons + * v0.9.0 (07-May-2019) - Switched to Blender 2.80 API + * v0.8.15 (17-Jul-2019) - Added option to import normals + * v0.8.14 (14-May-2019) - Fixed blender deleting zero length bones + * v0.8.13 (19-Mar-2019) - Exporting material files is optional + * v0.8.12 (14-Mar-2019) - Fixed error exporting animation scale keyframes + * v0.8.11 (26-Feb-2019) - Fixed tangents and binormals for mirrorred uvs + * v0.8.10 (32-Jan-2019) - Fixed export when mesh has multiple uv sets + * v0.8.9 (08-Mar-2018) - Added import option to match weight maps and link with a previously imported skeleton + * v0.8.8 (26-Feb-2018) - Fixed export triangulation and custom normals + * v0.8.7 (01-Feb-2018) - Scene frame rate adjusted on import, Fixed quatenion normalisation + * v0.8.6 (31-Jan-2018) - Fixed crash exporting animations in blender 2.79 + * v0.8.5 (02-Jan-2018) - Optimisation: Use hashmap for duplicate vertex detection + * v0.8.4 (20-Nov-2017) - Fixed animation quaternion interpolation + * v0.8.3 (06-Nov-2017) - Warning when linked skeleton file not found + * v0.8.2 (25-Sep-2017) - Fixed bone translations in animations + * v0.8.1 (28-Jul-2017) - Added alpha component to vertex colour + * v0.8.0 (30-Jun-2017) - Added animation and shape key support. Rewritten skeleton export + * v0.7.2 (08-Dec-2016) - fixed divide by 0 error calculating tangents + * v0.7.1 (07-Sep-2016) - bug fixes + * v0.7.0 (02-Sep-2016) - Implemented changes needed for Kenshi: Persistant Ogre bone IDs, Export vertex colours. Generates tangents and binormals. + * v0.6.2 (09-Mar-2013) - bug fixes (working with materials+textures), added 'Apply modifiers' and 'Copy textures' + * v0.6.1 (27-Sep-2012) - updated to work with Blender 2.63a + * v0.6 (01-Sep-2012) - added skeleton import + vertex weights import/export + * v0.5 (06-Mar-2012) - added material import/export + * v0.4.1 (29-Feb-2012) - flag for applying transformation, default=true + * v0.4 (28-Feb-2012) - fixing export when no UV data are present + * v0.3 (22-Feb-2012) - WIP - started cleaning + using OgreXMLConverter + * v0.2 (19-Feb-2012) - WIP - working export of geometry and faces + * v0.1 (18-Feb-2012) - initial 2.59 import code (from .xml) + * v0.0 (12-Feb-2012) - file created +""" + +""" +When importing: (x)-Blender, (x')-Ogre +vectors: x=x', y=-z', z=y' +UVtex: u=u', v = -v'+1 + +Inner data representation: +MESHDATA: +['sharedgeometry']: {} + ['positions'] - vectors with [x,y,z] + ['normals'] - vectors with [x,y,z] + ['vertexcolors'] - vectors with [r,g,b,a] + ['texcoordsets'] - integer (number of UV sets) + ['uvsets'] - vectors with [u,v] * number or UV sets for vertex [[u,v]][[u,v]]... + ['boneassignments']: {[boneName]} - for every bone name: + [[vertexNumber], [weight]], [[vertexNumber], [weight]], .. +['submeshes'][idx] + [material] - string (material name) + [materialOrig] - original material name - for searching in the shared materials file + [faces] - vectors with faces [v1,v2,v3] + [geometry] - identical to 'sharedgeometry' data content +['poses'] + ['pose'] - name, target, index (if target = submesh) + ['poseoffset'] - vectors with index, nx, ny, nz, x, y, z +['materials'] + [(matID)]: {} + ['texture'] - full path to texture file + ['imageNameOnly'] - only image name from material file +['skeleton']: {[boneName]} for each bone + ['name'] - bone name + ['id'] - bone ID + ['position'] - bone position [x,y,z] + ['rotation'] - bone rotation [x,y,z,angle] + ['parent'] - bone name of parent bone + ['children'] - list with names if children ([child1, child2, ...]) +['boneIDs']: {[bone ID]:[bone Name]} - dictionary with ID to name +['skeletonName'] - name of skeleton +[animations] - vector of animation tuples + [animation] - tuple: ([tracks], name, length) + [tracks] - vector of track tuples + [track] - tuple: ([keyframes], type, target, index (>=0 if submesh, else: -1)) + [keyframes] - vector of keyframe tuples + [keyframe] - tuple: ([poserefs], frame) + [poserefs] - vector of poseref tuples + poseref: tuple: (poseindex, influence) + +Note: Bones store their OGREID as a custom variable so they are consistent when a mesh is exported +""" + +# When bpy is already in local, we know this is not the initial import... +#if "bpy" in locals(): +# import importlib +# print("RELOADING: material_parser.MaterialParser") +# importlib.reload(material_parser) + +import bpy +from xml.dom import minidom +from mathutils import Vector, Matrix, Quaternion +import math, os, subprocess, json +from .material_parser import MaterialParser +from .. import config, util +from ..util import * +from ..report import Report +from bpy_extras.io_utils import unpack_list + +logger = logging.getLogger('ogre_import') + +# Script internal options: +SHOW_IMPORT_DUMPS = False +SHOW_IMPORT_TRACE = False +MIN_BONE_LENGTH = 0.00001 # Prevent automatic removal of bones + +# Makes sure name doesn't exceed Blenders naming limits +# Also keeps after name (as Torchlight uses names to identify types -boots, chest, ...- with names) +# TODO: This is not needed for Blender 2.62 and above +def GetValidBlenderName(name): + + newname = name.strip() + + maxChars = 20 + if bpy.app.version >= (2, 62, 0): + maxChars = 63 + + if(len(name) > maxChars): + if(name.find("/") >= 0): + if(name.find("Material") >= 0): + # Replace 'Material' string with only 'Mt' + newname = name.replace("Material", "Mt") + # Check if it's still above 20 + if(len(newname) > maxChars): + suffix = newname[newname.find("/"):] + prefix = newname[0:(maxChars+1-len(suffix))] + newname = prefix + suffix + else: + newname = name[0 : maxChars + 1] + + if(newname != name): + logger.warning("Name truncated (%s -> %s)" % (name, newname)) + Report.warnings.append("Name truncated (%s -> %s)" % (name, newname)) + + return newname + +def xOpenFile(filename): + logger.info("* Parsing file: %s ..." % filename) + start = time.time() + + xml_file = open(filename) + try: + xml_doc = minidom.parse(xml_file) + output = xml_doc + except Exception: + logger.error("File %s is not valid XML!" % filename) + Report.errors.append("File %s is not valid XML!" % filename) + output = 'None' + xml_file.close() + + logger.info('- Done at %s seconds' % util.timer_diff_str(start)) + + return output + + +def xCollectFaceData(facedata): + faces = [] + for face in facedata.childNodes: + if face.localName == 'face': + v1 = int(face.getAttributeNode('v1').value) + v2 = int(face.getAttributeNode('v2').value) + v3 = int(face.getAttributeNode('v3').value) + faces.append([v1, v2, v3]) + + return faces + + +def xCollectVertexData(data): + vertexdata = {} + vertices = [] + normals = [] + vertexcolors = [] + + for vb in data.childNodes: + if vb.localName == 'vertexbuffer': + if vb.hasAttribute('positions'): + progressbar = util.ProgressBar("Vertices", len(vb.getElementsByTagName('vertex'))) + index = 0 + + for vertex in vb.getElementsByTagName('vertex'): + progressbar.update(index) + index = index + 1 + + for vp in vertex.childNodes: + if vp.localName == 'position': + x = float(vp.getAttributeNode('x').value) + y = -float(vp.getAttributeNode('z').value) + z = float(vp.getAttributeNode('y').value) + vertices.append([x, y, z]) + vertexdata['positions'] = vertices + + sys.stdout.write("\n") + + if vb.hasAttribute('normals') and config.get('IMPORT_NORMALS') is True: + for vertex in vb.getElementsByTagName('vertex'): + for vn in vertex.childNodes: + if vn.localName == 'normal': + x = float(vn.getAttributeNode('x').value) + y = -float(vn.getAttributeNode('z').value) + z = float(vn.getAttributeNode('y').value) + normals.append([x, y, z]) + vertexdata['normals'] = normals + + if vb.hasAttribute('colours_diffuse'): + for vertex in vb.getElementsByTagName('vertex'): + for vcd in vertex.childNodes: + if vcd.localName == 'colour_diffuse': + rgba = vcd.getAttributeNode('value').value + r = float(rgba.split()[0]) + g = float(rgba.split()[1]) + b = float(rgba.split()[2]) + a = float(rgba.split()[3]) + vertexcolors.append([r, g, b, a]) + vertexdata['vertexcolors'] = vertexcolors + + if vb.hasAttribute('texture_coord_dimensions_0'): + texcosets = int(vb.getAttributeNode('texture_coords').value) + vertexdata['texcoordsets'] = texcosets + uvcoordset = [] + for vertex in vb.getElementsByTagName('vertex'): + uvcoords = [] + for vt in vertex.childNodes: + if vt.localName == 'texcoord': + u = float(vt.getAttributeNode('u').value) + v = -float(vt.getAttributeNode('v').value) + 1.0 + uvcoords.append([u, v]) + + if len(uvcoords) > 0: + uvcoordset.append(uvcoords) + vertexdata['uvsets'] = uvcoordset + + return vertexdata + + +def xCollectMeshData(meshData, xmldoc, meshname, dirname): + logger.info("* Collecting mesh data...") + + #global has_skeleton + faceslist = [] + subMeshData = [] + allObjs = [] + isSharedGeometry = False + sharedGeom = [] + hasSkeleton = 'boneIDs' in meshData + + # Collect shared geometry + if(len(xmldoc.getElementsByTagName('sharedgeometry')) > 0): + isSharedGeometry = True + for subnodes in xmldoc.getElementsByTagName('sharedgeometry'): + meshData['sharedgeometry'] = xCollectVertexData(subnodes) + + if hasSkeleton: + for subnodes in xmldoc.getElementsByTagName('boneassignments'): + meshData['sharedgeometry']['boneassignments'] = xCollectBoneAssignments(meshData, subnodes) + + # Collect submeshes data + for submeshes in xmldoc.getElementsByTagName('submeshes'): + for submesh in submeshes.childNodes: + if submesh.localName == 'submesh': + materialOrig = "Material" + if submesh.getAttributeNode('material') is not None: + materialOrig = str(submesh.getAttributeNode('material').value) + # To avoid Blender naming limit problems + material = GetValidBlenderName(materialOrig) + sm = {} + sm['material'] = material + sm['materialOrig'] = materialOrig + for subnodes in submesh.childNodes: + if subnodes.localName == 'faces': + facescount = int(subnodes.getAttributeNode('count').value) + sm['faces'] = xCollectFaceData(subnodes) + + if len(xCollectFaceData(subnodes)) != facescount: + logger.debug("FacesCount doesn't match!") + break + + if (subnodes.localName == 'geometry'): + vertexcount = int(subnodes.getAttributeNode('vertexcount').value) + sm['geometry'] = xCollectVertexData(subnodes) + + if hasSkeleton and subnodes.localName == 'boneassignments' and isSharedGeometry==False: + sm['geometry']['boneassignments'] = xCollectBoneAssignments(meshData, subnodes) + + subMeshData.append(sm) + + meshData['submeshes'] = subMeshData + + return meshData + + +def xCollectBoneAssignments(meshData, xmldoc): + boneIDtoName = meshData['boneIDs'] + + VertexGroups = {} + for vg in xmldoc.childNodes: + if vg.localName == 'vertexboneassignment': + VG = str(vg.getAttributeNode('boneindex').value) + if VG in boneIDtoName.keys(): + VGNew = boneIDtoName[VG] + else: + VGNew = 'Group ' + VG + if VGNew not in VertexGroups.keys(): + VertexGroups[VGNew] = [] + + for vg in xmldoc.childNodes: + if vg.localName == 'vertexboneassignment': + + VG = str(vg.getAttributeNode('boneindex').value) + if VG in boneIDtoName.keys(): + VGNew = boneIDtoName[VG] + else: + VGNew = VG + verti = int(vg.getAttributeNode('vertexindex').value) + weight = float(vg.getAttributeNode('weight').value) + #logger.debug("bone=%s, vert=%s, weight=%s" % (VGNew, verti, weight)) + VertexGroups[VGNew].append([verti, weight]) + + return VertexGroups + + +def xCollectPoseData(meshData, xmldoc): + logger.info("* Collecting pose data...") + + poses = xmldoc.getElementsByTagName('poses') + if(len(poses) == 0): + return + + meshData['poses'] = [] + + for pose in poses[0].getElementsByTagName('pose'): + name = pose.getAttribute('name') + target = pose.getAttribute('target') + + poseData = {} + poseData['name'] = name + if target == 'submesh': + index = pose.getAttribute('index') + poseData['submesh'] = int(index) + logger.info("+ Pose: %s, index: %s, target: %s" % (name, index, target)) + else: + logger.info("+ Pose: %s, target: %s" % (name, target)) + poseData['data'] = data = [] + meshData['poses'].append(poseData) + for value in pose.getElementsByTagName('poseoffset'): + index = int(value.getAttribute('index')) + x = float(value.getAttribute('x')) + y = float(value.getAttribute('y')) + z = float(value.getAttribute('z')) + data.append((index, x, -z, y)) + + # Let's see if there are Pose animations as well + # https://ogrecave.github.io/ogre/api/13/_animation.html#Pose-Animation + animations_tag = xmldoc.getElementsByTagName('animations') + if(len(animations_tag) == 0): + return + + animations = [] + + fps = bpy.context.scene.render.fps + + for animation_tag in animations_tag[0].getElementsByTagName('animation'): + name = animation_tag.getAttribute('name') + length = animation_tag.getAttribute('length') + + logger.info("+ Animation: %s, length: %s" % (name, length)) + + tracks = [] + + for track_tag in animation_tag.getElementsByTagName('track'): + target = track_tag.getAttribute('target') + type = track_tag.getAttribute('type') + + # If the type of animations were not pose after all... bail + if type != "pose": + return + + if target == 'submesh': + submesh_index = track_tag.getAttribute('index') + logger.info("+ Track: %s, index: %s, target: %s" % (name, submesh_index, target)) + else: + submesh_index = -1 + logger.info("+ Track: %s, target: %s" % (name, target)) + + for keyframes_tag in track_tag.getElementsByTagName('keyframes'): + + keyframes = [] + + for keyframe_tag in keyframes_tag.getElementsByTagName('keyframe'): + + time = float(keyframe_tag.getAttribute('time')) + frame = time * fps + 1 + #print("frame: %s, time: %s" % (frame, time)) + + if config.get('ROUND_FRAMES') is True: + frame = round(frame) + + poserefs = [] + + for poseref_tag in keyframe_tag.getElementsByTagName('poseref'): + influence = poseref_tag.getAttribute('influence') + poseindex = poseref_tag.getAttribute('poseindex') + + poseref = (poseindex, influence) + poserefs.append(poseref) + + keyframe = (poserefs, frame) + keyframes.append(keyframe) + + track = (keyframes, type, target, submesh_index) + tracks.append(track) + + animation = (tracks, name, length) + animations.append(animation) + + meshData['pose_animations'] = animations + + +def bCreatePoseAnimations(ob, meshData, subMeshIndex): + if 'pose_animations' in meshData and len(meshData['pose_animations']) > 0: + logger.info("+ Creating pose animations...") + + # Create animation data for the shape animations + shape_keys = ob.data.shape_keys + + if shape_keys.animation_data is None: + shape_keys.animation_data_create() + + shape_key_names = [] + for pose in meshData['poses']: + shape_key_names.append(pose['name']) + + for animation in meshData['pose_animations']: + tracks = animation[0] + name = animation[1] + length = float(animation[2]) + + logger.debug("- Animation: %s, length: %s" % (name, length)) + + fcurves = {} + + # The way we are iterating over submeshes the action might already exist + if name in bpy.data.actions: + action = bpy.data.actions[name] + logger.debug("- Action: %s already in 'bpy.data.actions'" % name) + + # Get the FCurves for each pose/shape key + for shape_key_name in shape_key_names: + fcurves[shape_key_name] = action.fcurves.find(data_path=f'key_blocks["{shape_key_name}"].value') + else: + action = bpy.data.actions.new(name) + # action.use_fake_user = True # Dont need this as we are adding them to the nla editor + shape_keys.animation_data.action = action + Report.shape_animations.append(name) + + # Add action to NLA tracks + track = shape_keys.animation_data.nla_tracks.new() + track.name = name + track.mute = True + track.strips.new(name, 0, action) + + # Create the FCurves for each pose/shape key + for shape_key_name in shape_key_names: + fcurve = action.fcurves.new(data_path=f'key_blocks["{shape_key_name}"].value') + fcurves[shape_key_name] = fcurve + + logger.info("+ Created action: %s" % name) + + for track in tracks: + keyframes = track[0] + type = track[1] + target = track[2] + submesh_index = int(track[3]) + logger.debug("- Track -- type: %s, target: %s, submesh_index: %s" % (type, target, submesh_index)) + + # If we are not dealing with shared geometry and this track does not apply to this submesh... skip + if(submesh_index >= 0 and submesh_index != subMeshIndex): + logger.debug("Skipping submesh_index: %s" % submesh_index) + continue + + # Create fcurves + for keyframe in keyframes: + poserefs = keyframe[0] + frame = keyframe[1] + + # We have to account for the shape keys not referenced in the poserefs + referenced_shape_keys = [] + + for poseref in poserefs: + pose_index = int(poseref[0]) + influence = float(poseref[1]) + shape_key_name = meshData['poses'][pose_index]['name'] + referenced_shape_keys.append(shape_key_name) + + fcurve = fcurves[shape_key_name] + fcurve.keyframe_points.insert(frame, influence) + + # Set influence to 0 for shape keys not referenced in the poserefs + # Otherwise the animation will look bad + for shape_key_name in [item for item in shape_key_names if item not in referenced_shape_keys]: + fcurve = fcurves[shape_key_name] + fcurve.keyframe_points.insert(frame, 0) + + +def xGetSkeletonLink(xmldoc, folder): + skeletonFile = "None" + if(len(xmldoc.getElementsByTagName("skeletonlink")) > 0): + # Get the skeleton link of the mesh + skeletonLink = xmldoc.getElementsByTagName("skeletonlink")[0] + skeletonName = skeletonLink.getAttribute("name") + skeletonFile = os.path.join(folder, skeletonName) + # Check for existence of skeleton file + if not os.path.isfile(skeletonFile): + logger.warning("Ogre skeleton missing: %s" % skeletonFile) + Report.warnings.append("Cannot find linked skeleton file '%s'\nIt must be in the same directory as the mesh file." % skeletonName) + skeletonFile = "None" + + return skeletonFile + + +#def xCollectBoneData(meshData, xDoc, name, folder): +def xCollectBoneData(meshData, xDoc): + logger.info("* Collecting bone data...") + + OGRE_Bones = {} + BoneIDToName = {} + meshData['skeleton'] = OGRE_Bones + meshData['boneIDs'] = BoneIDToName + + for bones in xDoc.getElementsByTagName('bones'): + for bone in bones.childNodes: + OGRE_Bone = {} + if bone.localName == 'bone': + boneName = str(bone.getAttributeNode('name').value) + boneID = int(bone.getAttributeNode('id').value) + OGRE_Bone['name'] = boneName + OGRE_Bone['id'] = boneID + BoneIDToName[str(boneID)] = boneName + + for b in bone.childNodes: + if b.localName == 'position': + x = float(b.getAttributeNode('x').value) + y = float(b.getAttributeNode('y').value) + z = float(b.getAttributeNode('z').value) + OGRE_Bone['position'] = [x, y, z] + if b.localName == 'rotation': + angle = float(b.getAttributeNode('angle').value) + axis = b.childNodes[1] + axisx = float(axis.getAttributeNode('x').value) + axisy = float(axis.getAttributeNode('y').value) + axisz = float(axis.getAttributeNode('z').value) + OGRE_Bone['rotation'] = [axisx, axisy, axisz, angle] + + OGRE_Bones[boneName] = OGRE_Bone + + for bonehierarchy in xDoc.getElementsByTagName('bonehierarchy'): + for boneparent in bonehierarchy.childNodes: + if boneparent.localName == 'boneparent': + Bone = str(boneparent.getAttributeNode('bone').value) + Parent = str(boneparent.getAttributeNode('parent').value) + OGRE_Bones[Bone]['parent'] = Parent + + # Update Ogre bones with list of children + calcBoneChildren(OGRE_Bones) + + # Helper bones + calcHelperBones(OGRE_Bones) + calcZeroBones(OGRE_Bones) + + # Update Ogre bones with head positions + calcBoneHeadPositions(OGRE_Bones) + + # Update Ogre bones with rotation matrices + calcBoneRotations(OGRE_Bones) + + return OGRE_Bones + + +def calcBoneChildren(BonesData): + for bone in BonesData.keys(): + childlist = [] + for key in BonesData.keys(): + if 'parent' in BonesData[key]: + parent = BonesData[key]['parent'] + if parent == bone: + childlist.append(key) + BonesData[bone]['children'] = childlist + + +def calcHelperBones(BonesData): + count = 0 + helperBones = {} + for bone in BonesData.keys(): + if ((len(BonesData[bone]['children']) == 0) or + (len(BonesData[bone]['children']) > 1)): + HelperBone = {} + HelperBone['position'] = [0.2, 0.0, 0.0] + HelperBone['parent'] = bone + HelperBone['rotation'] = [1.0, 0.0, 0.0, 0.0] + HelperBone['flag'] = 'helper' + HelperBone['name'] = 'Helper' + str(count) + HelperBone['children'] = [] + helperBones['Helper' + str(count)] = HelperBone + count += 1 + for hBone in helperBones.keys(): + BonesData[hBone] = helperBones[hBone] + + +def calcZeroBones(BonesData): + zeroBones = {} + for bone in BonesData.keys(): + pos = BonesData[bone]['position'] + if (math.sqrt(pos[0]**2 + pos[1]**2 + pos[2]**2)) == 0: + ZeroBone = {} + ZeroBone['position'] = [0.2, 0.0, 0.0] + ZeroBone['rotation'] = [1.0, 0.0, 0.0, 0.0] + if 'parent' in BonesData[bone]: + ZeroBone['parent'] = BonesData[bone]['parent'] + ZeroBone['flag'] = 'zerobone' + ZeroBone['name'] = 'Zero' + bone + ZeroBone['children'] = [] + zeroBones['Zero' + bone] = ZeroBone + if 'parent' in BonesData[bone]: + BonesData[BonesData[bone]['parent']]['children'].append('Zero' + bone) + for hBone in zeroBones.keys(): + BonesData[hBone] = zeroBones[hBone] + + +def calcBoneHeadPositions(BonesData): + for key in BonesData.keys(): + + start = 0 + thisbone = key + posh = BonesData[key]['position'] + #print("SetBonesASPositions: bone=%s, org. position=%s" % (key, posh)) + while start == 0: + if 'parent' in BonesData[thisbone]: + parentbone = BonesData[thisbone]['parent'] + prot = BonesData[parentbone]['rotation'] + ppos = BonesData[parentbone]['position'] + + #protmat = RotationMatrix(math.degrees(prot[3]), 3, 'r', Vector(prot[0], prot[1], prot[2])).invert() + protmat = Matrix.Rotation(math.degrees(prot[3]), 3, Vector([prot[0], prot[1], prot[2]])).inverted() + #print ("SetBonesASPositions: bone=%s, protmat=%s" % (key, protmat)) + #print(protmat) + #newposh = protmat * Vector([posh[0], posh[1], posh[2]]) + #newposh = protmat * Vector([posh[2], posh[1], posh[0]]) #02 + newposh = protmat.transposed() @ Vector([posh[0], posh[1], posh[2]]) #03 + #print("SetBonesASPositions: bone=%s, newposh=%s" % (key, newposh)) + positionh = VectorSum(ppos,newposh) + + posh = positionh + + thisbone = parentbone + else: + start = 1 + + BonesData[key]['posHAS'] = posh + #print("SetBonesASPositions: bone=%s, posHAS=%s" % (key, posh)) + + +def calcBoneRotations(BonesDic): + objDic = {} + scn = bpy.context.scene + #scn = Scene.GetCurrent() + for bone in BonesDic.keys(): + #obj = Object.New('Empty',bone) + obj = bpy.data.objects.new(bone, None) + objDic[bone] = obj + scn.collection.objects.link(obj) + #print("all objects created") + #print(bpy.data.objects) + for bone in BonesDic.keys(): + if 'parent' in BonesDic[bone]: + #Parent = Object.Get(BonesDic[bone]['parent']) + #print(BonesDic[bone]['parent']) + Parent = objDic.get(BonesDic[bone]['parent']) + object = objDic.get(bone) + object.parent = Parent + #Parent.makeParent([object]) + #print("all parents linked") + for bone in BonesDic.keys(): + obj = objDic.get(bone) + rot = BonesDic[bone]['rotation'] + loc = BonesDic[bone]['position'] + #print ("CreateEmptys:bone=%s, rot=%s" % (bone, rot)) + #print ("CreateEmptys:bone=%s, loc=%s" % (bone, loc)) + euler = Matrix.Rotation(rot[3], 3, Vector([rot[0], -rot[2], rot[1]])).to_euler() + obj.location = [loc[0], -loc[2], loc[1]] + #print ("CreateEmptys:bone=%s, euler=%s" % (bone, euler)) + #print ("CreateEmptys:bone=%s, obj.rotation_euler=%s" % (bone,[math.radians(euler[0]),math.radians(euler[1]),math.radians(euler[2])])) + #obj.rotation_euler = [math.radians(euler[0]),math.radians(euler[1]),math.radians(euler[2])] + #print ("CreateEmptys:bone=%s, obj.rotation_euler=%s" % (bone,[euler[0],euler[1],euler[2]])) # 02 + obj.rotation_euler = [euler[0], euler[1], euler[2]] # 02 + # Redraw() + bpy.context.view_layer.update() + # print("all objects rotated") + for bone in BonesDic.keys(): + obj = objDic.get(bone) + # TODO: need to get rotation matrix out of objects rotation + # loc, rot, scale = obj.matrix_local.decompose() + loc, rot, scale = obj.matrix_world.decompose() #02 + rotmatAS = rot.to_matrix() + # print(rotmatAS) + #obj.rotation_quaternion. + #rotmatAS = Matrix(.matrix_local..getMatrix().rotationPart() + BonesDic[bone]['rotmatAS'] = rotmatAS + #print ("CreateEmptys:bone=%s, rotmatAS=%s" % (bone, rotmatAS)) + # print("all matrices stored") + + #for bone in BonesDic.keys(): + # obj = objDic.get(bone) + # scn.collection.objects.unlink(obj) + # del obj + + # TODO cyclic + for bone in BonesDic.keys(): + obj = objDic.get(bone) + #obj.select = True + #bpy.ops.object.select_name(bone, False) + #bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + scn.collection.objects.unlink(obj) # TODO: cyclic message in console + #del obj + #bpy.context.scene.objects.unlink(obj) + bpy.data.objects.remove(obj) + + bpy.context.view_layer.update() + #removedObj = {} + #children=1 + #while children > 0: + # children = 0 + # for bone in BonesDic.keys(): + # if('children' in BonesDic[bone].keys() and bone not in removedObj): + # if len(BonesDic[bone]['children'])==0: + # obj = objDic.get(bone) + # scn.objects.unlink(obj) + # del obj + # removedObj[bone]=True + # else: + # children+=1 + + #print("all objects removed") + +def VectorSum(vec1, vec2): + vecout = [0, 0, 0] + vecout[0] = vec1[0] + vec2[0] + vecout[1] = vec1[1] + vec2[1] + vecout[2] = vec1[2] + vec2[2] + + return vecout + +## =========================================================================================== ## +def quaternionFromAngleAxis(angle, x, y, z): + r = angle * 0.5 + s = math.sin(r) + c = math.cos(r) + return (c, x*s, y*s, z*s) + + +def xGetChild(node, tag): + for n in node.childNodes: + if n.nodeType == 1 and n.tagName == tag: + return n + return None + + +def xAnalyseFPS(xDoc): + fps = 0 + lastTime = 1e8 + samples = 0 + for container in xDoc.getElementsByTagName('animations'): + for animation in container.childNodes: + if animation.nodeType == 1 and animation.tagName == 'animation': + tracks = xGetChild(animation, 'tracks') + for track in tracks.childNodes: + if track.nodeType == 1: + for keyframe in xGetChild(track, 'keyframes').childNodes: + if keyframe.nodeType == 1: + time = float(keyframe.getAttribute('time')) + if time > lastTime: + fps = max(fps, 1 / (time - lastTime)) + lastTime = time + samples = samples + 1 + if samples > 100: + return round(fps, 2) # stop here + return round(fps, 2) + + +def xCollectAnimations(meshData, xDoc): + logger.info("* Collecting animation data...") + + if 'animations' not in meshData: + meshData['animations'] = {} + for container in xDoc.getElementsByTagName('animations'): + for animation in container.childNodes: + if animation.nodeType == 1 and animation.tagName == 'animation': + name = animation.getAttribute('name') + + # read action data + action = {} + tracks = xGetChild(animation, 'tracks') + xReadAnimation(action, tracks.childNodes) + meshData['animations'][name] = action + + +def xReadAnimation(action, tracks): + fps = bpy.context.scene.render.fps + for track in tracks: + if track.nodeType != 1: + continue + target = track.getAttribute('bone') + action[target] = trackData = [[] for i in range(3)] # pos, rot, scl + for keyframe in xGetChild(track, 'keyframes').childNodes: + if keyframe.nodeType != 1: + continue + time = float(keyframe.getAttribute('time')) + frame = time * fps + if config.get('ROUND_FRAMES') is True: + frame = round(frame) + for key in keyframe.childNodes: + if key.nodeType != 1: + continue + if key.tagName == 'translate': + x = float(key.getAttribute('x')) + y = float(key.getAttribute('y')) + z = float(key.getAttribute('z')) + trackData[0].append([frame, (x, y, z)]) + elif key.tagName == 'rotate': + axis = xGetChild(key, 'axis') + angle = key.getAttribute('angle') + x = axis.getAttribute('x') + y = axis.getAttribute('y') + z = axis.getAttribute('z') + # skip if axis contains #INF or #IND + if '#' not in x and '#' not in y and '#' not in z: + quat = quaternionFromAngleAxis(float(angle), float(z), float(x), float(y)) + trackData[1].append([frame, quat]) + elif key.tagName == 'scale': + x = float(key.getAttribute('x')) + y = float(key.getAttribute('y')) + z = float(key.getAttribute('z')) + trackData[2].append([frame, (-x, z, y)]) + + +def bCreateAnimations(meshData): + path_id = ['location', 'rotation_quaternion', 'scale'] + + if 'animations' in meshData: + logger.info("+ Creating animations...") + + rig = meshData['rig'] + rig.animation_data_create() + animdata = rig.animation_data + + # calculate transformation matrices for translation + mat = {} + fix1 = Matrix([(1, 0, 0), (0, 0, 1), (0, -1, 0)]) + fix2 = Matrix([(0, 1, 0), (0, 0, 1), (1, 0, 0)]) + for bone in rig.pose.bones: + if bone.parent: + mat[bone.name] = fix2 @ bone.parent.matrix.to_3x3().transposed() @ bone.matrix.to_3x3() + else: + mat[bone.name] = fix1 @ bone.matrix.to_3x3() + + for name in sorted(meshData['animations'].keys(), reverse=True): + action = bpy.data.actions.new(name) + Report.armature_animations.append(name) + + # action.use_fake_user = True # Dont need this as we are adding them to the nla editor + logger.info("+ Created action: %s" % name) + + # Iterate target bones + for target in meshData['animations'][name]: + data = meshData['animations'][name][target] + bone = rig.pose.bones[target] + if not bone: + continue # error + bone.rotation_mode = 'QUATERNION' + + # Fix rotation inversions + for i in range(1, len(data[1])): + a = data[1][i-1][1] + b = data[1][i][1] + dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] + if dot < -0.8: + #print('fix inversion', name, target, i) + data[1][i][1] = (-b[0], -b[1], -b[2], -b[3]) + + # Fix translation keys - rotate by inverse rest orientation + m = mat[target].transposed() + for i in range(0, len(data[0])): + v = Vector(data[0][i][1]) + data[0][i][1] = m @ v + + # Create fcurves + for i in range(3): + if data[i]: + path = bone.path_from_id(path_id[i]) + for channel in range(len(data[i][0][1])): + curve = action.fcurves.new(path, index=channel, action_group=bone.name) + for key in data[i]: + curve.keyframe_points.insert(key[0], key[1][channel]) + + # Add action to NLA track + track = animdata.nla_tracks.new() + track.name = name + track.mute = True + track.strips.new(name, 0, action) + + +## =========================================================================================== ## + + +def bCreateMesh(meshData, folder, name, filepath): + if 'skeleton' in meshData: + skeletonName = meshData['skeletonName'] + bCreateSkeleton(meshData, skeletonName) + + logger.info("+ Creating mesh: %s" % name) + + # From collected data create all sub meshes + subObjs = bCreateSubMeshes(meshData, name) + # Skin submeshes + #bSkinMesh(subObjs) + + # Move to parent skeleton if there + if 'armature' in meshData: + arm = meshData['armature'] + for obj in subObjs: + logger.debug('Move to %s' % arm.location) + obj.location = arm.location + obj.rotation_euler = arm.rotation_euler + obj.rotation_axis_angle = arm.rotation_axis_angle + obj.rotation_quaternion = arm.rotation_quaternion + + bpy.ops.object.select_all(action='DESELECT') + + # Temporarily select all imported objects + for subOb in subObjs: + subOb.select_set(True) + + # TODO: Try to merge everything into the armature object + if config.get('MERGE_SUBMESHES') is True: + bpy.ops.object.join() + ob = bpy.context.view_layer.objects.active + ob.name = name + ob.data.name = name + + Report.meshes.append( name ) + + # Dump import structure + if SHOW_IMPORT_DUMPS: + import_dump = filepath.replace(".xml", ".json") + with open( import_dump, 'w' ) as f: + json.dump(meshData, f, indent=4) + + +def bCreateSkeleton(meshData, name): + if 'skeleton' not in meshData: + return + bonesData = meshData['skeleton'] + + logger.info("+ Creating skeleton: %s" % name) + + # Create Armature + amt = bpy.data.armatures.new(name) + rig = bpy.data.objects.new(name, amt) + meshData['rig'] = rig + #rig.location = origin + rig.show_in_front = True + #amt.show_names = True + # Link object to scene + scn = bpy.context.scene + scn.collection.objects.link(rig) + bpy.context.view_layer.objects.active = rig + bpy.context.view_layer.update() + + # Chose default length of bones with no children + averageBone = 0 + for b in bonesData.values(): + childLength = b['position'][0] + averageBone += childLength + averageBone /= len(bonesData) + if averageBone == 0: + averageBone = 0.2 + logger.debug("Default bone length: %s" % averageBone) + + bpy.ops.object.mode_set(mode='EDIT') + for bone in bonesData.keys(): + boneData = bonesData[bone] + boneName = boneData['name'] + + children = boneData['children'] + boneObj = amt.edit_bones.new(boneName) + + # Store Ogre bone id to match when exporting + if 'id' in boneData: + boneObj['OGREID'] = boneData['id'] + logger.debug("BoneID: %s, BoneName: %s" % (boneData['id'], boneName)) + + #boneObj.head = boneData['posHAS'] + #headPos = boneData['posHAS'] + headPos = boneData['posHAS'] + tailVector = 0 + if len(children) > 0: + for child in children: + tailVector = max(tailVector, bonesData[child]['position'][0]) + if tailVector < MIN_BONE_LENGTH: + tailVector = averageBone + + #boneObj.head = Vector([headPos[0], -headPos[2], headPos[1]]) + #boneObj.tail = Vector([headPos[0], -headPos[2], headPos[1] + tailVector]) + + #print("bCreateSkeleton: bone=%s, boneObj.head=%s" % (bone, boneObj.head)) + #print("bCreateSkeleton: bone=%s, boneObj.tail=%s" % (bone, boneObj.tail)) + #boneObj.matrix = + rotmat = boneData['rotmatAS'] + #print(rotmat[1].to_tuple()) + #boneObj.matrix = Matrix(rotmat[1],rotmat[0],rotmat[2]) + if bpy.app.version <= (2, 62, 0): + r0 = [rotmat[0].x] + [rotmat[0].y] + [rotmat[0].z] + r1 = [rotmat[1].x] + [rotmat[1].y] + [rotmat[1].z] + r2 = [rotmat[2].x] + [rotmat[2].y] + [rotmat[2].z] + boneRotMatrix = Matrix((r1, r0, r2)) + elif bpy.app.version > (2, 62, 0): + # this is fugly way of flipping matrix + r0 = [rotmat.col[0].x] + [rotmat.col[0].y] + [rotmat.col[0].z] + r1 = [rotmat.col[1].x] + [rotmat.col[1].y] + [rotmat.col[1].z] + r2 = [rotmat.col[2].x] + [rotmat.col[2].y] + [rotmat.col[2].z] + tmpR = Matrix((r1, r0, r2)) + boneRotMatrix = Matrix((tmpR.col[0], tmpR.col[1], tmpR.col[2])) + + #pos = Vector([headPos[0],-headPos[2],headPos[1]]) + #axis, roll = mat3_to_vec_roll(boneRotMatrix.to_3x3()) + + #boneObj.head = pos + #boneObj.tail = pos + axis + #boneObj.roll = roll + + #print("bCreateSkeleton: bone=%s, newrotmat=%s" % (bone, Matrix((r1,r0,r2)))) + #print(r1) + #mtx = Matrix.to_3x3()Translation(boneObj.head) # Matrix((r1,r0,r2)) + #boneObj.transform(Matrix((r1,r0,r2))) + #print("bCreateSkeleton: bone=%s, matrix_before=%s" % (bone, boneObj.matrix)) + #boneObj.use_local_location = False + #boneObj.transform(Matrix((r1,r0,r2)) , False, False) + #print("bCreateSkeleton: bone=%s, matrix_after=%s" % (bone, boneObj.matrix)) + boneObj.head = Vector([0, 0, 0]) + #boneObj.tail = Vector([0,0,tailVector]) + boneObj.tail = Vector([0, tailVector, 0]) + #matx = Matrix.Translation(Vector([headPos[0],-headPos[2],headPos[1]])) + + boneObj.transform(boneRotMatrix) + #bpy.context.view_layer.update() + boneObj.translate(Vector([headPos[0], -headPos[2], headPos[1]])) + #bpy.context.view_layer.update() + #boneObj.translate(Vector([headPos[0],-headPos[2],headPos[1]])) + #boneObj.head = Vector([headPos[0],-headPos[2],headPos[1]]) + #boneObj.tail = Vector([headPos[0],-headPos[2],headPos[1]]) + (Vector([0,0, tailVector]) * Matrix((r1,r0,r2))) + + #amt.bones[bone] = boneObj + #amt.update_tag(refresh) + + # Only after all bones are created we can link parents + for bone in bonesData.keys(): + boneData = bonesData[bone] + parent = None + if 'parent' in boneData.keys(): + parent = boneData['parent'] + # get bone obj + boneData = bonesData[bone] + boneName = boneData['name'] + boneObj = amt.edit_bones[boneName] + boneObj.parent = amt.edit_bones[parent] + + # Need to refresh armature before removing bones + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + + # Delete helper/zero bones + for bone in amt.bones.keys(): + # print("keys of bone=%s" % bonesData[bone].keys()) + if 'flag' in bonesData[bone].keys(): + # print ("deleting bone=%s" % bone) + bpy.context.object.data.edit_bones.remove(amt.edit_bones[bone]) + + bpy.ops.object.mode_set(mode='OBJECT') + #for (bname, pname, vector) in boneTable: + # bone = amt.edit_bones.new(bname) + # if pname: + # parent = amt.edit_bones[pname] + # bone.parent = parent + # bone.head = parent.tail + # bone.use_connect = False + # (trans, rot, scale) = parent.matrix.decompose() + # else: + # bone.head = (0,0,0) + # rot = Matrix.Translation((0,0,0)) # identity matrix + # bone.tail = rot * Vector(vector) + bone.head + #bpy.ops.object.mode_set(mode='OBJECT') + +def bMergeVertices(subMesh): + # This sort of works, but leaves all uv seams as sharp. + geometry = subMesh['geometry'] + vertices = geometry['positions'] + normals = geometry['normals'] + uvsets = geometry['uvsets'] if 'uvsets' in geometry else None + lookup = {} + map = [i for i in range(len(vertices))] + for i in range(len(vertices)): + vert = vertices[i] + norm = normals[i] + uv = tuple(uvsets[i][0]) if uvsets else None + item = (tuple(vert), tuple(norm), uv) + target = lookup.get(item) + if target is None: + lookup[item] = i + else: + map[i] = target + # update faces + faces = subMesh['faces'] + for face in faces: + for i in range(len(face)): + face[i] = map[face[i]] + + +def bCreateSubMeshes(meshData, meshName): + allObjects = [] + submeshes = meshData['submeshes'] + scene = bpy.context.scene + layer = bpy.context.view_layer + + for subMeshIndex in range(len(submeshes)): + subMeshData = submeshes[subMeshIndex] + subMeshName = subMeshData['material'] + + # Create mesh and object + me = bpy.data.meshes.new(subMeshName) + ob = bpy.data.objects.new(subMeshName, me) + + # Link object to scene + scene.collection.objects.link(ob) + layer.objects.active = ob + layer.update() + + # Check for submesh geometry, or take the shared one + if 'geometry' in subMeshData.keys(): + geometry = subMeshData['geometry'] + else: + geometry = meshData['sharedgeometry'] + + verts = geometry['positions'] + faces = subMeshData['faces'] + + hasNormals = False + if 'normals' in geometry.keys(): + normals = geometry['normals'] + hasNormals = True + + # Mesh vertices and faces + VertLength = len(verts) + FaceLength = len(faces) + me.vertices.add(VertLength) + me.loops.add(FaceLength * 3) + me.polygons.add(FaceLength) + + me.vertices.foreach_set("co", unpack_list(verts)) + + if hasNormals: + me.vertices.foreach_set("normal", unpack_list(normals)) + + me.polygons.foreach_set("loop_start", [i for i in range(0, FaceLength * 3, 3)]) + me.polygons.foreach_set("loop_total", [3] * (FaceLength)) + me.loops.foreach_set("vertex_index", unpack_list(faces)) + + hasTexture = False + # Material for the submesh + # Create image texture from image. + if subMeshName in meshData['materials']: + matInfo = meshData['materials'][subMeshName] # material data + if subMeshName not in Report.materials: + Report.materials.append(subMeshName) + + # Create shadeless material and MTex + mat = None + if subMeshName not in bpy.data.materials: + mat = bpy.data.materials.new(subMeshName) + else: + mat = bpy.data.materials.get(subMeshName) + + mat.use_nodes = True + + output = mat.node_tree.nodes["Material Output"] + output.location = 0, 0 + + bsdf = mat.node_tree.nodes["Principled BSDF"] + bsdf.location = -300, 0 + + if 'texture' in matInfo: + texturePath = matInfo['texture'] + if texturePath: + hasTexture = True + Report.textures.append( matInfo['imageNameOnly'] ) + + # Try to find among already loaded images + image = None + for lImage in bpy.data.images: + if lImage.name == matInfo['imageNameOnly']: + image = lImage + break + if not image: + image = bpy.data.textures.new(matInfo['imageNameOnly'], type = 'IMAGE') + image = bpy.data.images.load(texturePath) + + texImage = mat.node_tree.nodes.new(type= "ShaderNodeTexImage") + texImage.location = -600, 0 + texImage.image = image + inp = mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'] + outp = texImage.outputs['Color'] + mat.node_tree.links.new(inp, outp) + + mapping = mat.node_tree.nodes.new(type="ShaderNodeMapping") + mapping.location = -800, 0 + inp = texImage.inputs['Vector'] + outp = mat.node_tree.nodes['Mapping'].outputs['Vector'] + mat.node_tree.links.new(inp, outp) + + texcoord = mat.node_tree.nodes.new(type="ShaderNodeTexCoord") + texcoord.location = -1000, 0 + inp = mat.node_tree.nodes['Mapping'].inputs['Vector'] + outp = mat.node_tree.nodes['Texture Coordinate'].outputs['UV'] + mat.node_tree.links.new(inp, outp) + + # Add material to object + ob.data.materials.append(mat) + #logger.debug(me.uv_textures[0].data.values()[0].image) + else: + logger.warning("Definition of material: \"%s\" not found!" % subMeshName) + Report.warnings.append("Definition of material: \"%s\" not found!" % subMeshName) + # create default material if it could not be imported to preserve submesh material reference + mat = bpy.data.materials.new(name=subMeshName) + ob.data.materials.append(mat) + + # Texture coordinates + if 'texcoordsets' in geometry and 'uvsets' in geometry: + uvsets = geometry['uvsets'] + for j in range(geometry['texcoordsets']): + uvData = me.uv_layers.new(name='UVLayer'+str(j)).data + loopIndex = 0 + for face in faces: + for v in face: + uvData[loopIndex].uv = uvsets[v][j] + loopIndex += 1 + + # Vertex colors + if 'vertexcolors' in geometry and len(geometry['vertexcolors']) > 0: + colourData = None + if (bpy.app.version >= (3, 2, 0)): + colourData = me.color_attributes.new(name='Color', domain='CORNER', type='BYTE_COLOR').data + else: + colourData = me.vertex_colors.new(name='Color').data + + vcolors = geometry['vertexcolors'] + loopIndex = 0 + for face in faces: + for v in face: + colourData[loopIndex].color = vcolors[v] + loopIndex += 1 + + # Bone assignments: + if 'boneIDs' in meshData: + if 'boneassignments' in geometry.keys(): + vgroups = geometry['boneassignments'] + for vgname, vgroup in vgroups.items(): + #logger.debug("creating VGroup %s" % vgname) + grp = ob.vertex_groups.new(name=vgname) + for (v, w) in vgroup: + #grp.add([v], w, 'REPLACE') + grp.add([v], w, 'ADD') + + # Give mesh object an armature modifier, using vertex groups but not envelopes + if 'skeleton' in meshData: + skeletonName = meshData['skeletonName'] + Report.armatures.append(skeletonName) + + mod = ob.modifiers.new('OgreSkeleton', 'ARMATURE') + mod.object = bpy.data.objects[skeletonName] # gets the rig object + mod.use_bone_envelopes = False + mod.use_vertex_groups = True + + elif 'armature' in meshData: + mod = ob.modifiers.new('OgreSkeleton', 'ARMATURE') + mod.object = meshData['armature'] + mod.use_bone_envelopes = False + mod.use_vertex_groups = True + + # Shape keys (poses) + if 'poses' in meshData: + # Must have base shape + base = ob.shape_key_add(name='Basis') + + for pose in meshData['poses']: + if('submesh' not in pose or pose['submesh'] == subMeshIndex): + name = pose['name'] + Report.shape_keys.append(name) + + logger.info('* Creating pose: %s' % name) + shape = ob.shape_key_add(name=name) + for vkey in pose['data']: + b = base.data[vkey[0]].co + me.shape_keys.key_blocks[name].data[vkey[0]].co = [vkey[1] + b[0], vkey[2] + b[1], vkey[3] + b[2]] + + bCreatePoseAnimations(ob, meshData, subMeshIndex) + + # Update mesh with new data + #me.calc_loop_triangles() + me.update(calc_edges=True) + + # Try to set custom normals + if hasNormals: + if bpy.app.version < (4, 1, 0): + me.use_auto_smooth = True + me.normals_split_custom_set_from_vertices(normals) + + Report.orig_vertices = len( me.vertices ) + Report.faces += len( me.polygons ) + + bpy.ops.object.editmode_toggle() + #bpy.ops.mesh.remove_doubles(threshold=0.001) + bpy.ops.mesh.tris_convert_to_quads() + bpy.ops.object.editmode_toggle() + + Report.triangles += len( me.polygons ) + Report.vertices += len( me.vertices ) + + allObjects.append(ob) + + return allObjects + + +def matchFace(face, vertices, mesh, index): + if index >= len(mesh.polygons): + return False # ?? err - what broke + loop = mesh.polygons[index].loop_start + for v in face: + vi = mesh.loops[loop].vertex_index + vx = mesh.vertices[vi].co + + if (vx-Vector(vertices[v])).length_squared > 1e-6: + return False + + # if vx != Vector(vertices[v]): + # return False # May need threshold ? + loop += 1 + return True + + +def getBoneNameMapFromArmature(arm): + # Get Ogre bone IDs - need to be in edit mode to access edit_bones + # Arm should already be the active object + boneMap = {} + bpy.context.view_layer.objects.active = arm + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + bpy.ops.object.mode_set(mode='EDIT', toggle=False) + for bone in arm.data.edit_bones: + if 'OGREID' in bone: + boneMap[ str(bone['OGREID']) ] = bone.name; + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + return boneMap + + +def load_scene(filepath): + logger.info("* Loading scene from: %s" % str(filepath)) + + folder = os.path.dirname(filepath) + + xDocSceneData = xOpenFile(filepath) + + scene = xDocSceneData.getElementsByTagName('scene') + if len(scene) == 0: + logger.error("No scene found!") + Report.errors.append("No scene found!") + return + + import_collection = bpy.data.collections.new("OgreImport") + bpy.context.scene.collection.children.link(import_collection) + + environment = scene[0].getElementsByTagName('environment') + if len(environment) > 0: + logger.info("+ Environment") + + backgnd_col = environment[0].getElementsByTagName('colourBackground')[0] + backgnd_col_r = float(backgnd_col.getAttribute('r')) + backgnd_col_g = float(backgnd_col.getAttribute('g')) + backgnd_col_b = float(backgnd_col.getAttribute('b')) + logger.info(f" - Background Color: r={backgnd_col_r}, g={backgnd_col_g}, b={backgnd_col_b}") + + world = bpy.context.scene.world + world.color = Vector((backgnd_col_r, backgnd_col_g, backgnd_col_b)) + + if len(environment[0].getElementsByTagName('fog')) > 0: + fog = environment[0].getElementsByTagName('fog')[0] + fog_falloff = fog.getAttribute('mode') + linear_start = float(fog.getAttribute('linearStart')) + linear_end = float(fog.getAttribute('linearEnd')) + exp_density = float(fog.getAttribute('expDensity')) + logger.info(f" - Fog: falloff={fog_falloff}, linear_start={linear_start}, linear_end={linear_end}, exp_density={exp_density}") + + world.mist_settings.use_mist = True + world.mist_settings.intensity = exp_density + world.mist_settings.start = linear_start + world.mist_settings.depth = linear_end - linear_start + + falloff_types = {'linear': 'LINEAR', 'exp': 'QUADRATIC', 'exp2': 'INVERSE_QUADRATIC'} + world.mist_settings.falloff = falloff_types[fog_falloff] + + nodes = scene[0].getElementsByTagName('nodes') + if len(nodes) > 0: + for node in nodes[0].getElementsByTagName('node'): + # Extract and print node name + node_name = node.getAttribute('name') + logger.info(f"+ Node: {node_name}") + + # Position + position = node.getElementsByTagName('position')[0] + pos_x = float(position.getAttribute('x')) + pos_y = float(position.getAttribute('y')) + pos_z = float(position.getAttribute('z')) + location = Vector((pos_x, -pos_z, pos_y)) + logger.info(f" - Position: x={pos_x}, y={pos_y}, z={pos_z}") + + # Rotation + rotation = node.getElementsByTagName('rotation')[0] + rot_qw = float(rotation.getAttribute('qw')) + rot_qx = float(rotation.getAttribute('qx')) + rot_qy = float(rotation.getAttribute('qy')) + rot_qz = float(rotation.getAttribute('qz')) + quaternion = Quaternion((rot_qw, rot_qx, -rot_qz, rot_qy)) + euler = quaternion.to_euler() + logger.info(f" - Rotation: qw={rot_qw}, qx={rot_qx}, qy={rot_qy}, qz={rot_qz}") + + # Scale + scale = node.getElementsByTagName('scale')[0] + scale_x = float(scale.getAttribute('x')) + scale_y = float(scale.getAttribute('y')) + scale_z = float(scale.getAttribute('z')) + scale_vector = Vector((scale_x, scale_z, scale_y)) + logger.info(f" - Scale: x={scale_x}, y={scale_y}, z={scale_z}") + + # Entity (if any) + entities = node.getElementsByTagName('entity') + if entities: + entity = entities[0] + meshFile = entity.getAttribute('meshFile') + entity_name = entity.getAttribute('name') + logger.info(f" * Entity: {entity_name}, Mesh File: {meshFile}") + + mesh_path = os.path.join(folder, meshFile) + load_mesh(mesh_path) + + entity = bpy.context.active_object + entity.name = node_name + entity.location = location + entity.rotation_euler = euler + entity.scale = scale_vector + # Unlink from all current collections + for col in entity.users_collection: + col.objects.unlink(entity) + import_collection.objects.link(entity) + + # Light (if any) + lights = node.getElementsByTagName('light') + if lights: + light = lights[0] + light_name = light.getAttribute('name') + light_type = light.getAttribute('type') + powerScale = float(light.getAttribute('powerScale')) + logger.info(f" * Light: {light_name}, Type: {light_type}, Power Scale: {powerScale}") + + diff_color = light.getElementsByTagName('colourDiffuse') + diff_color_r = float(diff_color[0].getAttribute('r')) + diff_color_g = float(diff_color[0].getAttribute('g')) + diff_color_b = float(diff_color[0].getAttribute('b')) + diffuse_color = Vector((diff_color_r, diff_color_g, diff_color_b)) + diffuse_factor = 1 # Impossible to obtain from the data + logger.info(f" * Light Diffuse: (color: r={diff_color_r}, g={diff_color_g}, b={diff_color_b}), factor: {diffuse_factor}") + + spec_color = light.getElementsByTagName('colourSpecular') + spec_color_r = float(spec_color[0].getAttribute('r')) + spec_color_g = float(spec_color[0].getAttribute('g')) + spec_color_b = float(spec_color[0].getAttribute('b')) + specular_color = Vector((spec_color_r, spec_color_g, spec_color_b)) + if (bpy.app.version >= (2, 93, 0)): + specular_factor = 1 # Impossible to obtain from the data + else: + if diff_color_r > 0.001: + specular_factor = spec_color_r / diff_color_r + elif diff_color_g > 0.001: + specular_factor = spec_color_g / diff_color_g + elif diff_color_b > 0.001: + specular_factor = spec_color_b / diff_color_b + else: + specular_factor = 1 # Impossible to know + logger.info(f" * Light Specular (color: r={spec_color_r}, g={spec_color_g}, b={spec_color_b}), factor: {specular_factor}") + + light_attn = light.getElementsByTagName('lightAttenuation') + light_attn_constant = float(light_attn[0].getAttribute('constant')) + light_attn_linear = float(light_attn[0].getAttribute('linear')) + light_attn_quadratic = float(light_attn[0].getAttribute('quadratic')) + light_attn_range = float(light_attn[0].getAttribute('range')) + logger.info(f" * Light Attenuation -- Constant: {light_attn_constant}, Linear: {light_attn_linear}, Range: {light_attn_range}") + + light_types = {'point': 'POINT', 'directional': 'SUN', 'spot': 'SPOT', 'rect': 'RECTANGLE'} + bpy.ops.object.light_add(type=light_types[light_type], location=location, rotation=euler) + light = bpy.context.active_object + + # Common attributes to all lights: color, energy, diffuse, specular, volume + light.name = light_name + + # Unlink from all current collections + for col in light.users_collection: + col.objects.unlink(light) + import_collection.objects.link(light) + + light.data.energy = powerScale + if (bpy.app.version >= (2, 93, 0)): + light.data.color = diffuse_color / diffuse_factor + light.data.diffuse_factor = diffuse_factor + else: + light.data.color = diffuse_color + light.data.specular_factor = specular_factor + + Report.lights.append( light_name ) + + # Point light sources give off light equally in all directions, so require only position not direction. + #if light_type == 'point': + # light.data.use_custom_distance = True + # light.data.cutoff_distance = light_attn_range + + #Directional lights simulate parallel light beams from a distant source, hence have direction but no position. + #if light_type == 'directional': + # light.data.angle = 0.0615403 + # pass + + #Spotlights simulate a cone of light from a source so require position and direction, plus extra values for falloff. + #if light_type == 'spot': + # light.data.use_custom_distance = True + # light.data.cutoff_distance = light_attn_range + # light.data.spot_size = 0.785398 + # light.data.spot_blend = 0.15 + #a.setAttribute('inner', str( ob.data.spot_size * (1.0 - ob.data.spot_blend))) + #a.setAttribute('outer', str(ob.data.spot_size)) + + #A rectangular area light, requires position, direction, width and height. + #if light_type == 'rect': + # light.data.shape = 'RECTANGLE' + # light.data.size = 0.25 + # light.data.size_y = 0.25 + + # Camera (if any) + cameras = node.getElementsByTagName('camera') + if cameras: + camera = cameras[0] + camera_name = camera.getAttribute('name') + fov = float(camera.getAttribute('fov')) + projection_type = camera.getAttribute('projectionType') + clipping = camera.getElementsByTagName('clipping')[0] + clipping_near = float(clipping.getAttribute('near')) + clipping_far = float(clipping.getAttribute('far')) + logger.info(f" * Camera: {camera_name}, FOV: {fov}, Projection Type: {projection_type}, Clipping: (near: {clipping_near}, far: {clipping_far})") + + bpy.ops.object.camera_add(location=location, rotation=euler) + + camera = bpy.context.active_object + camera.name = camera_name + + # Unlink from all current collections + for col in camera.users_collection: + col.objects.unlink(camera) + import_collection.objects.link(camera) + + Report.cameras.append( camera_name ) + + #aspx = bpy.context.scene.render.pixel_aspect_x + #aspy = bpy.context.scene.render.pixel_aspect_y + #sx = bpy.context.scene.render.resolution_x + #sy = bpy.context.scene.render.resolution_y + #if ob.data.type == "PERSP": + # fovY = 0.0 + # if (sx*aspx > sy*aspy): + # fovY = 2 * math.atan(sy * aspy * 16.0 / (ob.data.lens * sx * aspx)) + # else: + # fovY = 2 * math.atan(16.0 / ob.data.lens) + # # fov in radians - like OgreMax - requested by cyrfer + # fov = math.radians( fovY * 180.0 / math.pi ) + # c.setAttribute('projectionType', "perspective") + # c.setAttribute('fov', '%6f' % fov) + #else: # ob.data.type == "ORTHO": + # c.setAttribute('projectionType', "orthographic") + # c.setAttribute('orthoScale', '%6f' % ob.data.ortho_scale) + + # Perspective + camera_types = {'perspective': 'PERSP', 'orthographic': 'ORTHO'} + camera.data.type = camera_types[projection_type] + + # Field of View + camera.data.lens_unit = 'FOV' + camera.data.angle = fov * 2 + + # Clipping + camera.data.clip_start = clipping_near + camera.data.clip_end = clipping_far + + +def load_mesh(filepath): + logger.info("* Loading mesh from: %s" % str(filepath)) + + pathMeshXml = None + # Get the mesh as .xml file + if filepath.lower().endswith(".mesh"): + if mesh_convert(filepath): + pathMeshXml = filepath + ".xml" + else: + logger.error("Failed to convert .mesh file %s to .xml" % filepath) + Report.errors.append("Failed to convert .mesh file to .xml") + return {'CANCELLED'} + elif filepath.lower().endswith(".xml"): + pathMeshXml = filepath + else: + return {'CANCELLED'} + + folder = os.path.dirname(filepath) + nameDotMeshDotXml = os.path.split(pathMeshXml)[1] + nameDotMesh = os.path.splitext(nameDotMeshDotXml)[0] + onlyName = os.path.splitext(nameDotMesh)[0] + + # Material + meshMaterials = [] + nameDotMaterial = onlyName + ".material" + pathMaterial = os.path.join(folder, nameDotMaterial) + if not os.path.isfile(pathMaterial): + # Search directory for .material + for filename in os.listdir(folder): + if ".material" in filename: + # Material file + pathMaterial = os.path.join(folder, filename) + meshMaterials.append(pathMaterial) + #logger.info("alternative material file: %s" % pathMaterial) + else: + meshMaterials.append(pathMaterial) + + # Try to parse xml file + xDocMeshData = xOpenFile(pathMeshXml) + + meshData = {} + if xDocMeshData != "None": + # Skeleton data + # Get the mesh as .xml file + skeletonFile = xGetSkeletonLink(xDocMeshData, folder) + + # Use selected skeleton + selectedSkeleton = bpy.context.active_object \ + if (config.get('USE_SELECTED_SKELETON') is True + and bpy.context.active_object + and bpy.context.active_object.type == 'ARMATURE') else None + if selectedSkeleton: + map = getBoneNameMapFromArmature(selectedSkeleton) + if map: + meshData['boneIDs'] = map + meshData['armature'] = selectedSkeleton + else: + logger.warning("Selected armature has no OGRE data.") + Report.warnings.append("Selected armature has no OGRE data.") + + # There is valid skeleton link and existing file + elif skeletonFile != "None": + if mesh_convert(skeletonFile): + skeletonFileXml = skeletonFile + ".xml" + + # Parse .xml skeleton file + xDocSkeletonData = xOpenFile(skeletonFileXml) + if xDocSkeletonData != "None": + xCollectBoneData(meshData, xDocSkeletonData) + meshData['skeletonName'] = os.path.basename(skeletonFile[:-9]) + + # Parse animations + if config.get('IMPORT_ANIMATIONS') is True: + fps = xAnalyseFPS(xDocSkeletonData) + if(fps and (config.get('ROUND_FRAMES') is True)): + logger.info(" * Setting FPS to %s" % fps) + bpy.context.scene.render.fps = int(fps) + xCollectAnimations(meshData, xDocSkeletonData) + + else: + logger.warning("Failed to load linked skeleton") + Report.warnings.append("Failed to load linked skeleton") + + # Collect mesh data + xCollectMeshData(meshData, xDocMeshData, onlyName, folder) + MaterialParser.xCollectMaterialData(meshData, onlyName, folder) + + if config.get('IMPORT_SHAPEKEYS') is True: + xCollectPoseData(meshData, xDocMeshData) + + # After collecting is done, start creating stuff# + # Create skeleton (if any) and mesh from parsed data + bCreateMesh(meshData, folder, onlyName, pathMeshXml) + bCreateAnimations(meshData) + + if config.get('IMPORT_XML_DELETE') is True: + # Cleanup by deleting the XML file we created + os.unlink("%s" % pathMeshXml) + if 'skeleton' in meshData: + os.unlink("%s" % skeletonFileXml) + + return {'FINISHED'} diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/program.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/program.py new file mode 100644 index 0000000..90bf747 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/program.py @@ -0,0 +1,74 @@ +import os, logging +from .. import config + +logger = logging.getLogger('program') + +class OgreProgram(object): + ''' + parses .program scripts + saves bytes to copy later + + self.name = name of program reference + self.source = name of shader program (.cg, .glsl) + ''' + + def save( self, path ): + logger.info('Saving program to: %s' % path) + f = open( os.path.join(path,self.source), 'wb' ) + f.write(self.source_bytes ) + f.close() + for name in self.includes: + f = open( os.path.join(path,name), 'wb' ) + f.write( self.includes[name] ) + f.close() + + PROGRAMS = {} + + def reload(self): # only one directory is allowed to hold shader programs + if self.source not in os.listdir( config.get('SHADER_PROGRAMS') ): + logger.error( 'Ogre material %s is missing source: %s' % (self.name,self.source) ) + logger.error( config.get('SHADER_PROGRAMS') ) + return False + url = os.path.join( config.get('SHADER_PROGRAMS'), self.source ) + logger.info('Shader source: %s' % url) + self.source_bytes = open( url, 'rb' ).read()#.decode('utf-8') + logger.info('Shader source num bytes: %s' % len(self.source_bytes)) + data = self.source_bytes.decode('utf-8') + + for line in data.splitlines(): # only cg shaders use the include macro? + if line.startswith('#include') and line.count('"')==2: + name = line.split()[-1].replace('"','').strip() + logger.info('Shader includes: %s' % name) + url = os.path.join( config.get('SHADER_PROGRAMS'), name ) + self.includes[ name ] = open( url, 'rb' ).read() + return True + + def __init__(self, name='', data=''): + self.name=name + self.data = data.strip() + self.source = None + self.includes = {} # cg files may use #include something.cg + + if self.name in OgreProgram.PROGRAMS: + logger.info('<%s> --- Copy Ogre Program --- ' % self.name) + other = OgreProgram.PROGRAMS + self.source = other.source + self.data = other.data + self.entry_point = other.entry_point + self.profiles = other.profiles + + if data: self.parse( self.data ) + if self.name: OgreProgram.PROGRAMS[ self.name ] = self + + def parse( self, txt ): + self.data = txt + logger.info('<%s> -- Parsing Ogre Shader Program-- ' % self.name ) + for line in self.data.splitlines(): + line = line.split('//')[0] + line = line.strip() + if line.startswith('vertex_program') or line.startswith('fragment_program'): + a, self.name, self.type = line.split() + + elif line.startswith('source'): self.source = line.split()[-1] + elif line.startswith('entry_point'): self.entry_point = line.split()[-1] + elif line.startswith('profiles'): self.profiles = line.split()[1:] diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/scene.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/scene.py new file mode 100644 index 0000000..bf5f9a8 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/scene.py @@ -0,0 +1,898 @@ +# When bpy is already in local, we know this is not the initial import... +if "bpy" in locals(): + import importlib + #print("Reloading modules: material, materialv2json, node_anim, mesh, skeleton") + importlib.reload(material) + importlib.reload(materialv2json) + importlib.reload(node_anim) + importlib.reload(mesh) + importlib.reload(skeleton) + +import bpy, mathutils, os, getpass, math, logging, datetime +from os.path import join +from . import material, materialv2json, node_anim, mesh, skeleton +from .. import bl_info, config, util +from ..report import Report +from ..xml import * +from .material import * +from .materialv2json import * +from .mesh import * + +logger = logging.getLogger('scene') + +# Called by io_ogre/ui/exporter.py to start exporting the scene +def dot_scene(path, scene_name=None): + """ + path: string - target path to save the scene file and related files to + scene_name: string optional - the name of the scene file, defaults to the scene name of blender + """ + if not scene_name: + scene_name = bpy.context.scene.name + scene_file = scene_name + '.scene' + target_scene_file = join(path, scene_file) + + start = time.time() + + # Create target path if it does not exist + if not os.path.exists(path): + logger.info("Creating Directory: %s" % path) + os.mkdir(path) + + logger.info("* Processing Scene: %s, path: %s" % (scene_name, path)) + prefix = scene_name + + # If an object has an animation, then we want to export the position at the first frame (with the object at rest) + # Otherwise the objects position will be at an arbitrary place if current frame is different from frame_start + frame_start = bpy.context.scene.frame_start + frame_current = bpy.context.scene.frame_current + bpy.context.scene.frame_set(frame_start) + + # Nodes (objects) - gather because macros will change selection state + objects = [] + linkedgroups = [] + invalidnamewarnings = [] + for ob in bpy.context.scene.objects: + if ob.subcollision: + continue + if ((config.get("EXPORT_HIDDEN") is False) and (ob not in bpy.context.visible_objects)): + continue + if config.get("SELECTED_ONLY") and not ob.select_get(): + if ob.type == 'CAMERA' and (config.get("FORCE_CAMERA") is True): + pass + elif ob.type == 'LIGHT' and (config.get("FORCE_LIGHTS") is True): + pass + else: + continue + if ob.type == 'EMPTY' and ob.instance_collection and ob.instance_type == 'COLLECTION': + linkedgroups.append(ob) + else: + # Gather data of invalid names. Don't bother user with warnings on names + # that only get spaces converted to _, just do that automatically. + cleanname = clean_object_name(ob.name) + cleannamespaces = clean_object_name(ob.name, spaces = False) + logger.debug("ABABA %s" % ob.name) + if cleanname != ob.name: + if cleannamespaces != ob.name: + invalidnamewarnings.append(ob.name + " -> " + cleanname) + objects.append(ob) + + # Print invalid obj names so user can go and fix them. + if len(invalidnamewarnings) > 0: + logger.warning("The following object names have invalid characters for creating files. They will be automatically converted.") + for namewarning in invalidnamewarnings: + Report.warnings.append('Auto corrected Object name: "%s"' % namewarning) + logger.warning("+ - %s" % namewarning) + + # Linked groups - allows 3 levels of nested blender library linking + temps = [] + for e in linkedgroups: + grp = e.instance_collection + subs = [] + for o in grp.objects: + if o.type=='MESH': + subs.append( o ) # TOP-LEVEL + elif o.type == 'EMPTY' and o.instance_collection and o.instance_type == 'COLLECTION': + ss = [] # LEVEL2 + for oo in o.instance_collection.objects: + if oo.type=='MESH': + ss.append( oo ) + elif oo.type == 'EMPTY' and oo.instance_collection and oo.instance_type == 'COLLECTION': + sss = [] # LEVEL3 + for ooo in oo.instance_collection.objects: + if ooo.type=='MESH': + sss.append( ooo ) + if sss: + m = merge_objects( sss, name=oo.name, transform=oo.matrix_world ) + subs.append( m ) + temps.append( m ) + if ss: + m = merge_objects( ss, name=o.name, transform=o.matrix_world ) + subs.append( m ) + temps.append( m ) + if subs: + m = merge_objects( subs, name=e.name, transform=e.matrix_world ) + objects.append( m ) + temps.append( m ) + + # Track that we don't export same data multiple times + exported_meshes = [] + exported_armatures = [] + + # Find merge groups + mgroups = [] + mobjects = [] + for ob in objects: + group = get_merge_group( ob ) + if group: + for member in group.objects: + if member not in mobjects: mobjects.append( member ) + if group not in mgroups: mgroups.append( group ) + for rem in mobjects: + if rem in objects: + objects.remove( rem ) + exported_meshes.append( rem.data.name ) + + for group in mgroups: + merged = merge_group( group ) + objects.append( merged ) + temps.append( merged ) + + # If user has set an offset for the dupli_group, then use that to set the origin of the merged objects + if group.instance_offset != mathutils.Vector((0.0, 0.0, 0.0)): + logger.info("Change origin of merged object %s to: %s" % ( merged.name, group.instance_offset )) + + # Use the 3D cursor to set the object origin + merged.select_set(True) + saved_location = bpy.context.scene.cursor.location # Save 3D cursor location + bpy.context.scene.cursor.location = group.instance_offset + bpy.ops.object.origin_set(type='ORIGIN_CURSOR') # Set the origin on the current object to the 3D cursor location + bpy.context.scene.cursor.location = saved_location # Set 3D cursor location back to the stored location + + # Gather roots because ogredotscene supports parents and children + def _flatten( _c, _f ): + if _c.parent in objects: _f.append( _c.parent ) + if _c.parent: _flatten( _c.parent, _f ) + else: _f.append( _c ) + + roots = [] + meshes = [] + + for ob in objects: + flat = [] + _flatten( ob, flat ) + root = flat[-1] + if root not in roots: + roots.append(root) + if ob.type=='MESH': + meshes.append(ob) + + materials = [] + if config.get("MATERIALS") is True: + logger.info("* Processing Materials") + materials = util.objects_merge_materials(meshes) + + converter_type= detect_converter_type() + if converter_type == "OgreMeshTool": + dot_materialsv2json(materials, path, separate_files=config.get('SEPARATE_MATERIALS'), prefix=prefix) + elif converter_type == "OgreXMLConverter": + dot_materials(materials, path, separate_files=config.get('SEPARATE_MATERIALS'), prefix=prefix) + else: # Unknown converter type, error + logger.error("Unknown converter type '{}', will not generate materials".format(converter_type)) + Report.errors.append("Unknown converter type '{}', will not generate materials".format(converter_type)) + + doc = ogre_document(materials, path) + + mesh_collision_prims = {} + mesh_collision_files = {} + + # Export the objects in the scene + for root in roots: + logger.info("* Exporting root node: %s " % root.name) + dot_scene_node_export(root, path = path, doc = doc, + exported_meshes = exported_meshes, + meshes = meshes, + mesh_collision_prims = mesh_collision_prims, + mesh_collision_files = mesh_collision_files, + exported_armatures = exported_armatures, + prefix = prefix, + objects = objects, + xmlparent = doc._scene_nodes + ) + + # Create the .scene file + if config.get('SCENE') is True: + data = doc.toprettyxml() + try: + with open(target_scene_file, 'wb') as fd: + fd.write(bytes(data,'utf-8')) + logger.info("- Exported Ogre Scene: %s " % target_scene_file) + except Exception as e: + logger.error("Unable to create scene file: %s" % target_scene_file) + logger.error(e) + Report.errors.append("Unable to create scene file: %s" % target_scene_file) + + # Remove temporary objects/meshes + for ob in temps: + logger.debug("Removing temporary mesh: %s" % ob.data.name) + bpy.data.meshes.remove(ob.data) + #BQfix for 2.8 unable to find merged object in collection + #bpy.context.collection.objects.unlink( ob ) + #bpy.data.objects.remove(ob, do_unlink=True) + + # Restore the scene previous frame position + bpy.context.scene.frame_set(frame_current) + + logger.info('- Done at %s seconds' % util.timer_diff_str(start)) + +class _WrapLogic(object): + SwapName = { 'frame_property' : 'animation' } # custom name hacks + + def __init__(self, node): + self.node = node + self.name = node.name + self.type = node.type + + def widget(self, layout): + box = layout.box() + row = box.row() + row.label( text=self.type ) + row.separator() + row.prop( self.node, 'name', text='' ) + if self.type in self.TYPES: + for name in self.TYPES[ self.type ]: + if name in self.SwapName: + box.prop( self.node, name, text=self.SwapName[name] ) + else: + box.prop( self.node, name ) + + def xml( self, doc ): + g = doc.createElement( self.LogicType ) + g.setAttribute('name', self.name) + g.setAttribute('type', self.type) + if self.type in self.TYPES: + for name in self.TYPES[ self.type ]: + attr = getattr( self.node, name ) + if name in self.SwapName: name = self.SwapName[name] + a = doc.createElement( 'component' ) + g.appendChild(a) + a.setAttribute('name', name) + if attr is None: a.setAttribute('type', 'POINTER' ) + else: a.setAttribute('type', type(attr).__name__) + + if type(attr) in (float, int, str, bool): a.setAttribute('value', str(attr)) + elif not attr: a.setAttribute('value', '') # None case + elif hasattr(attr,'filepath'): a.setAttribute('value', attr.filepath) + elif hasattr(attr,'name'): a.setAttribute('value', attr.name) + elif hasattr(attr,'x') and hasattr(attr,'y') and hasattr(attr,'z'): + a.setAttribute('value', '%s %s %s' %(attr.x, attr.y, attr.z)) + else: + logger.error('Unknown type: %s' % attr) + return g + +class WrapSensor( _WrapLogic ): + LogicType = 'sensor' + TYPES = { + 'COLLISION': ['property'], + 'MESSAGE' : ['subject'], + 'NEAR' : ['property', 'distance', 'reset_distance'], + 'RADAR' : ['property', 'axis', 'angle', 'distance' ], + 'RAY' : ['ray_type', 'property', 'material', 'axis', 'range', 'use_x_ray'], + 'TOUCH' : ['material'], + } + +class WrapActuator( _WrapLogic ): + LogicType = 'actuator' + TYPES = { + 'CAMERA' : ['object', 'height', 'min', 'max', 'axis'], + 'CONSTRAINT' : ['mode', 'limit', 'limit_min', 'limit_max', 'damping'], + 'MESSAGE' : ['to_property', 'subject', 'body_message'], #skipping body_type + 'OBJECT' : 'damping derivate_coefficient force force_max_x force_max_y force_max_z force_min_x force_min_y force_min_z integral_coefficient linear_velocity mode offset_location offset_rotation proportional_coefficient reference_object torque use_local_location use_local_rotation use_local_torque use_servo_limit_x use_servo_limit_y use_servo_limit_z'.split(), + 'SOUND' : 'cone_inner_angle_3d cone_outer_angle_3d cone_outer_gain_3d distance_3d_max distance_3d_reference gain_3d_max gain_3d_min mode pitch rolloff_factor_3d sound use_sound_3d volume'.split(), # note .sound contains .filepath + 'VISIBILITY' : 'apply_to_children use_occlusion use_visible'.split(), + 'SHAPE_ACTION' : 'frame_blend_in frame_end frame_property frame_start mode property use_continue_last_frame'.split(), + 'EDIT_OBJECT' : 'dynamic_operation linear_velocity mass mesh mode object time track_object use_3d_tracking use_local_angular_velocity use_local_linear_velocity use_replace_display_mesh use_replace_physics_mesh'.split(), + } + +def _property_helper(doc, user, propname, propvalue): + prop = doc.createElement('property') + user.appendChild(prop) + prop.setAttribute('name', propname) + prop.setAttribute('data', str(propvalue)) + prop.setAttribute('type', type(propvalue).__name__) + +def _mesh_instance_helper(e, ob, type): + group = get_merge_group( ob, type ) + + # The 'static' / 'instanced' attribute indicates that the mesh will be instanced with either static geometry or instancing + # The static geometry / instancing manager name is given by the group: (static | instancing).MyGroup + if group != None: + e.setAttribute( type, group.name[len(type + "."):] ) + +def _mesh_entity_helper(doc, ob, o): + user = doc.createElement('userData') + o.appendChild(user) + + """ + nope - no more ".game" in 2.80 + + # # extended format - BGE Physics ## + _property_helper(doc, user, 'mass', ob.game.mass) + _property_helper(doc, user, 'mass_radius', ob.game.radius) + _property_helper(doc, user, 'physics_type', ob.game.physics_type) + _property_helper(doc, user, 'actor', ob.game.use_actor) + _property_helper(doc, user, 'ghost', ob.game.use_ghost) + _property_helper(doc, user, 'velocity_min', ob.game.velocity_min) + _property_helper(doc, user, 'velocity_max', ob.game.velocity_max) + _property_helper(doc, user, 'lock_trans_x', ob.game.lock_location_x) + _property_helper(doc, user, 'lock_trans_y', ob.game.lock_location_y) + _property_helper(doc, user, 'lock_trans_z', ob.game.lock_location_z) + _property_helper(doc, user, 'lock_rot_x', ob.game.lock_rotation_x) + _property_helper(doc, user, 'lock_rot_y', ob.game.lock_rotation_y) + _property_helper(doc, user, 'lock_rot_z', ob.game.lock_rotation_z) + _property_helper(doc, user, 'anisotropic_friction', ob.game.use_anisotropic_friction) + x, y, z = ob.game.friction_coefficients + _property_helper(doc, user, 'friction_x', x) + _property_helper(doc, user, 'friction_y', y) + _property_helper(doc, user, 'friction_z', z) + _property_helper(doc, user, 'damping_trans', ob.game.damping) + _property_helper(doc, user, 'damping_rot', ob.game.rotation_damping) + _property_helper(doc, user, 'inertia_tensor', ob.game.form_factor) + """ + + mesh = ob.data + # custom user props + for prop in mesh.items(): + propname, propvalue = prop + if not propname.startswith('_'): + _property_helper(doc, user, propname, propvalue) + +def _ogre_node_helper( doc, ob, prefix='', pos=None, rot=None, scl=None ): + + # Get the object transform matrix + mat = ob.matrix_local + + o = doc.createElement('node') + o.setAttribute('name', prefix + ob.name) + + if pos: + v = swap(pos) + else: + v = swap( mat.to_translation() ) + + p = doc.createElement('position') + p.setAttribute('x', '%6f' % v.x) + p.setAttribute('y', '%6f' % v.y) + p.setAttribute('z', '%6f' % v.z) + o.appendChild(p) + + if rot: + v = swap(rot) + else: + v = swap( mat.to_quaternion() ) + q = doc.createElement('rotation') #('quaternion') + q.setAttribute('qx', '%6f' % v.x) + q.setAttribute('qy', '%6f' % v.y) + q.setAttribute('qz', '%6f' % v.z) + q.setAttribute('qw', '%6f' % v.w) + o.appendChild(q) + + if scl: # this should not be used + v = swap(scl) + x=abs(v.x); y=abs(v.y); z=abs(v.z) + else: # scale is different in Ogre from blender - rotation is removed + ri = mat.to_quaternion().inverted().to_matrix() + scale = ri.to_4x4() @ mat + v = swap( scale.to_scale() ) + x=abs(v.x); y=abs(v.y); z=abs(v.z) + s = doc.createElement('scale') + s.setAttribute('x', '%6f' % x) + s.setAttribute('y', '%6f' % y) + s.setAttribute('z', '%6f' % z) + o.appendChild(s) + + return o + +def ogre_document(materials, path): + now = time.time() + doc = RDocument() + scn = doc.createElement('scene') + doc.appendChild( scn ) + time_format = "%a, %d %b %Y %H:%M:%S +0000" + doc.addComment('exporter: blender2ogre ' + ".".join(str(i) for i in bl_info["version"])) + doc.addComment('export_time: ' + time.strftime(time_format, time.gmtime(now))) + doc.addComment('blender_version: %s (%s; %s)' % (bpy.app.version_string, bpy.app.version_cycle, bpy.app.build_platform.decode('UTF-8'))) + + scn.setAttribute('formatVersion', '1.1') + bscn = bpy.context.scene + + if '_previous_export_time_' in bscn.keys(): + doc.addComment('previous_export_time: ' + time.strftime(time_format, time.gmtime(bscn['_previous_export_time_']))) + + bscn[ '_previous_export_time_' ] = now + scn.setAttribute('author', getpass.getuser()) + + nodes = doc.createElement('nodes') + doc._scene_nodes = nodes + external = doc.createElement('externals') + environment = doc.createElement('environment') + for n in (nodes, external, environment): + scn.appendChild( n ) + + # External files + for mat in materials: + if mat is None: continue + item = doc.createElement('item') + external.appendChild( item ) + item.setAttribute('type', 'material') + a = doc.createElement('file') + item.appendChild( a ) + a.setAttribute('name', '%s.material' % material.material_name(mat)) + + # Environment settings + world = bpy.context.scene.world + if world: # multiple scenes - other scenes may not have a world + _c = [ ('colourBackground', world.color)] + for ctag, color in _c: + a = doc.createElement(ctag) + environment.appendChild( a ) + a.setAttribute('r', '%3f' % color.r) + a.setAttribute('g', '%3f' % color.g) + a.setAttribute('b', '%3f' % color.b) + + if world and world.mist_settings.use_mist: + fog = doc.createElement('fog') + environment.appendChild( fog ) + fog.setAttribute('linearStart', '%6f' % world.mist_settings.start ) + mist_falloff = world.mist_settings.falloff + if mist_falloff == 'QUADRATIC': fog.setAttribute('mode', 'exp') # on DTD spec (none | exp | exp2 | linear) + elif mist_falloff == 'LINEAR': fog.setAttribute('mode', 'linear') + else: fog.setAttribute('mode', 'exp2') + #fog.setAttribute('mode', world.mist_settings.falloff.lower() ) # not on DTD spec + fog.setAttribute('linearEnd', '%6f' % (world.mist_settings.start + world.mist_settings.depth)) + fog.setAttribute('expDensity', world.mist_settings.intensity) + + c = doc.createElement('colourDiffuse') + fog.appendChild( c ) + c.setAttribute('r', '%3f' % color.r) + c.setAttribute('g', '%3f' % color.g) + c.setAttribute('b', '%3f' % color.b) + + skybox_name = dot_scene_skybox_export( path ) + if skybox_name is not None: + skybox = doc.createElement('skyBox') + environment.appendChild( skybox ) + skybox.setAttribute('material', skybox_name ) + #skybox.setAttribute('distance', '5000') + #skybox.setAttribute('drawFirst', 'true') + skybox.setAttribute('active', 'true') + + return doc + +def dot_scene_skybox_export( path ): + if config.get('EXPORT_SKYBOX') is False: + return None + + skybox_name = None + skybox_resolution = config.get('SKYBOX_RESOLUTION') + skybox_distance = 5000 + skybox_imagepath = None + collection_name = "OgreSkyBox" + #path = "D:\\tmp\\SkyBox" + + # Get the current scene + scene = bpy.context.scene + + # Get the world used by the scene + world = scene.world + + # Ensure that node use is enabled for the world + if world.use_nodes: + # Get the node tree of the world + nodes = world.node_tree.nodes + + # Find the Background node (usually named 'Background') + background_node = nodes.get('Background') + if background_node: + # Access the 'Color' input + color_input = background_node.inputs['Color'] + + # Check if there is a link and if it's from a valid node + if color_input.is_linked: + linked_node = color_input.links[0].from_node + # Check if the node is an environment texture + if linked_node.type == 'TEX_ENVIRONMENT': + # Output some information about the image + logger.debug("SkyBox: Image linked as background:") + logger.debug("SkyBox: - Image Name: %s" % linked_node.image.name) + logger.debug("SkyBox: - Image Filepath: %s" % linked_node.image.filepath) + else: + logger.warning("Unable to create SkyBox: Linked node is not an environment texture. Node type: %s" % linked_node.type) + Report.warnings.append("Unable to create SkyBox: Linked node is not an environment texture. Node type: %s" % linked_node.type) + return None + else: + # Retrieve the static color if no image is linked + background_color = color_input.default_value + logger.warning("Unable to create SkyBox: Found background color instead of environment texture") + Report.warnings.append("Unable to create SkyBox: Found background color instead of environment texture") + return None + else: + logger.warning("Unable to create SkyBox: No Background node found") + Report.warnings.append("Unable to create SkyBox: No Background node found") + return None + else: + logger.warning("Unable to create SkyBox: World nodes are not enabled.") + Report.warnings.append("Unable to create SkyBox: World nodes are not enabled.") + return None + + skybox_imagepath = linked_node.image.filepath + skybox_name = os.path.splitext(linked_node.image.name)[0] + + logger.info("* Generating SkyBox: %s" % skybox_name) + logger.info("+ From Image: %s" % skybox_name) + logger.info("+ With resolution: %s" % skybox_resolution) + + # Create a collection to render the SkyBox + skybox_collection = bpy.data.collections.new(collection_name) + bpy.context.scene.collection.children.link(skybox_collection) + + # Create camera to render the SkyBox + camera_name = "OgreSkyBox_CAM" + + camera = bpy.data.cameras.new(camera_name) + camera_ob = bpy.data.objects.new(camera_name, camera) + #bpy.context.scene.objects.link(camera_ob) + + #camera_ob = bpy.data.objects['CameraY'] + skybox_collection.objects.link(camera_ob) + + scene_camera_orig = scene.camera + scene.camera = camera_ob + + # Set SkyBox camera settings + camera_ob.data.lens_unit = 'FOV' + camera_ob.data.angle = math.radians(90) + + # Depth of Field + #camera_ob.data.dof.use_dof = True + #camera_ob.data.dof.focus_distance = 10.0 + + camera_ob.data.clip_start = 0.1 + camera_ob.data.clip_end = 1000 + + # Backup scene settings + scene_res_x_orig = scene.render.resolution_x + scene_res_y_orig = scene.render.resolution_y + scene_rdr_perc_orig = scene.render.resolution_percentage + scene_filepath_orig = scene.render.filepath + scene_use_nodes_orig = scene.use_nodes + scene_file_format_orig = scene.render.image_settings.file_format + + # Set scene settings for SkyBox rendering + scene.render.resolution_x = skybox_resolution + scene.render.resolution_y = skybox_resolution + scene.render.resolution_percentage = 100 + scene.use_nodes = True + scene.render.image_settings.file_format = 'PNG' + + # Create SkyBox camera orientations + front = mathutils.Euler((math.radians(90), 0, 0), 'XYZ') + back = mathutils.Euler((math.radians(90), 0, math.radians(180)), 'XYZ') + right = mathutils.Euler((math.radians(90), 0, -math.radians(90)), 'XYZ') + left = mathutils.Euler((math.radians(90), 0, math.radians(90)), 'XYZ') + top = mathutils.Euler((math.radians(180), 0, 0), 'XYZ') + bottom = mathutils.Euler((0, 0, 0), 'XYZ') + + orientations = {"fr": front, "bk": back, "rt": right, "lf": left, "up": top, "dn": bottom} + + # Render only the SkyBox collection + for collection in bpy.data.collections: + if collection != skybox_collection: + collection.hide_render = True + else: + collection.hide_render = False + + # Render one side of the skybox for each orientation + i = 0 + for orientation in orientations: + camera_ob.rotation_euler = orientations[orientation] + image_name = os.path.join(path, skybox_name + "_" + orientation) + scene.render.filepath = image_name + logger.info("Exporting SkyBox image: %s" % image_name) + scene.render.use_compositing = True + bpy.ops.render.render(write_still = True) + #bpy.ops.render.render(animation=False, write_still=False, use_viewport=False, layer='', scene='') + #progressbar.update(i) + i = i + 1 + percent = len(orientations) / i + bpy.context.window_manager.progress_update(percent * 100) + + # Restore scene settings + scene.render.resolution_x = scene_res_x_orig + scene.render.resolution_y = scene_res_y_orig + scene.render.resolution_percentage = scene_rdr_perc_orig + scene.render.filepath = scene_filepath_orig + scene.use_nodes = scene_use_nodes_orig + scene.camera = scene_camera_orig + scene.render.image_settings.file_format = scene_file_format_orig + + # Restore collection rendering + for collection in bpy.data.collections: + collection.hide_render = False + + # Remove SkyBox camera + bpy.data.cameras.remove(camera) + + # Destroy the collection created to render the SkyBox + bpy.context.scene.collection.children.unlink(skybox_collection) + bpy.data.collections.remove(skybox_collection) + + w = util.IndentedWriter() + with w.iword('material').word(skybox_name).embed(): + with w.iword('technique').embed(): + with w.iword('pass').embed(): + w.iline('lighting off') + w.iline('depth_write off') + with w.iword('texture_unit').embed(): + w.iword('texture').word(skybox_name + ".png").word("cubic").nl() + w.iline('tex_address_mode clamp') + material_text = w.text + + try: + mat_file_name = join(path, skybox_name + ".material") + with open(mat_file_name, 'wb') as fd: + logger.info("SkyBox: Exporting material to: %s" % mat_file_name) + b2o_ver = ".".join(str(i) for i in bl_info["version"]) + fd.write(bytes('// generated by blender2ogre %s on %s\n' % (b2o_ver, datetime.now().replace(microsecond=0)), 'utf-8')) + fd.write(bytes(material_text, 'utf-8')) + except Exception as e: + logger.error("Unable to create SkyBox material file: %s" % mat_file_name) + logger.error(e) + Report.errors.append("Unable to create SkyBox material file: %s" % mat_file_name) + return None + + return skybox_name + + +# Recursive Node export +def dot_scene_node_export( ob, path, doc=None, rex=None, + exported_meshes=[], meshes=[], mesh_collision_prims={}, mesh_collision_files={}, + exported_armatures=[], prefix='', objects=[], xmlparent=None ): + + o = _ogre_node_helper( doc, ob ) + xmlparent.appendChild(o) + + # if config.get('EXPORT_USER') is True: + # Custom user props + if len(ob.items()) > 0: + user = doc.createElement('userData') + o.appendChild(user) + + for prop in ob.items(): + propname, propvalue = prop + if not propname.startswith('_'): + _property_helper(doc, user, propname, propvalue) + + if ob.type == 'MESH': + # ob.data.tessfaces is empty. always until the following call + ob.data.update() + ob.data.calc_loop_triangles() + # if it has no faces at all, the object itself will not be exported, BUT + # it might have children + + if ob.type == 'MESH' and len(ob.data.loop_triangles): + collisionFile = None + collisionPrim = None + if ob.data.name in mesh_collision_prims: + collisionPrim = mesh_collision_prims[ ob.data.name ] + if ob.data.name in mesh_collision_files: + collisionFile = mesh_collision_files[ ob.data.name ] + + e = doc.createElement('entity') + o.appendChild(e); e.setAttribute('name', ob.name) + prefix = '' + e.setAttribute('meshFile', '%s%s.mesh' % (prefix, clean_object_name(ob.data.name)) ) + + # Set the instancing attribute if the object belongs to the correct group + _mesh_instance_helper(e, ob, "static") + _mesh_instance_helper(e, ob, "instanced") + + if not collisionPrim and not collisionFile: + for child in ob.children: + if child.subcollision and child.name.startswith('DECIMATE'): + collisionFile = '%s_collision_%s.mesh' % (prefix, ob.data.name) + break + if child.name.endswith("-collision") + collisionFile = '%s_collision_%s.mesh' % (prefix, ob.data.name) + if collisionFile: + mesh_collision_files[ ob.data.name ] = collisionFile + mesh.dot_mesh(child, path, force_name='%s_collision_%s' % (prefix, ob.data.name) ) + skeleton.dot_skeleton(child, path) + + if collisionPrim: + e.setAttribute('collisionPrim', collisionPrim ) + elif collisionFile: + e.setAttribute('collisionFile', collisionFile ) + + #if config.get('EXPORT_USER') is True: + _mesh_entity_helper( doc, ob, e ) + + # export mesh.xml file of this object + if (config.get('MESH') is True) and ob.data.name not in exported_meshes: + # Alert if scale or rotation are not uniform + if ob.scale != mathutils.Vector((1.0, 1.0, 1.0)) or \ + ob.rotation_quaternion != mathutils.Quaternion((1.0, 0.0, 0.0, 0.0)) or \ + ob.rotation_euler.to_quaternion() != mathutils.Quaternion((1.0, 0.0, 0.0, 0.0)): + logger.warning("Object \"%s\" has non uniform scale or rotation" % ob.name) + Report.warnings.append("Object \"%s\" has non uniform scale or rotation, exported mesh will look different" % ob.name) + + exists = os.path.isfile( join( path, '%s.mesh' % ob.data.name ) ) + overwrite = not exists or (exists and (config.get("MESH_OVERWRITE") is True)) + tangents = int(config.get("GENERATE_TANGENTS")) + mesh.dot_mesh(ob, path, overwrite=overwrite, tangents=tangents) + exported_meshes.append( ob.data.name ) + skeleton.dot_skeleton(ob, path, overwrite=overwrite, exported_armatures=exported_armatures) + + # Deal with Array modifier + vecs = [ ob.matrix_world.to_translation() ] + for mod in ob.modifiers: + if (config.get("ARRAY") is True) and (mod.type == 'ARRAY'): + if mod.fit_type != 'FIXED_COUNT': + logger.warning("<%s> Unsupported array-modifier type: %s, only 'Fixed Count' is supported" % (ob.name, mod.fit_type)) + Report.warnings.append("Object \"%s\" has unsupported array-modifier type: %s, only 'Fixed Count' is supported" % (ob.name, mod.fit_type)) + continue + if not mod.use_constant_offset: + logger.warning("<%s> Unsupported array-modifier mode, must be of 'Constant Offset' type" % ob.name) + Report.warnings.append("Object \"%s\" has unsupported array-modifier mode, must be of 'Constant Offset' type" % ob.name) + continue + else: + newvecs = [] + for prev in vecs: + for i in range( mod.count - 1 ): + count = len(vecs + newvecs) + + v = prev + (i + 1) * mod.constant_offset_displace + + newvecs.append( v ) + ao = _ogre_node_helper( doc, ob, prefix='_array_%s_' % count, pos=v ) + xmlparent.appendChild(ao) + + e = doc.createElement('entity') + ao.appendChild(e) + e.setAttribute('name', '_array_%s_%s' % (count, ob.data.name)) + e.setAttribute('meshFile', '%s.mesh' % clean_object_name(ob.data.name)) + + # Set the instancing attribute if the object belongs to the correct group + _mesh_instance_helper(e, ob, "static") + _mesh_instance_helper(e, ob, "instanced") + + if collisionPrim: e.setAttribute('collisionPrim', collisionPrim ) + elif collisionFile: e.setAttribute('collisionFile', collisionFile ) + vecs += newvecs + + # Deal with Particle Systems + z_rot = mathutils.Quaternion((0.0, 0.0, 1.0), math.radians(90.0)) + + degp = bpy.context.evaluated_depsgraph_get() + particle_systems = ob.evaluated_get(degp).particle_systems + + for partsys in particle_systems: + if partsys.settings.type == 'HAIR' and partsys.settings.render_type == 'OBJECT': + index = 0 + for particle in partsys.particles: + dupob = partsys.settings.instance_object + ao = _ogre_node_helper( doc, dupob, prefix='%s_particle_%s_' % (clean_object_name(ob.data.name), index), pos=particle.hair_keys[0].co, rot=(particle.rotation * z_rot), scl=(dupob.scale * particle.size) ) + o.appendChild(ao) + + e = doc.createElement('entity') + ao.appendChild(e); e.setAttribute('name', ('%s_particle_%s_%s' % (clean_object_name(ob.data.name), index, clean_object_name(dupob.data.name)))) + e.setAttribute('meshFile', '%s.mesh' % clean_object_name(dupob.data.name)) + + # Set the instancing attribute if the object belongs to the correct group + _mesh_instance_helper(e, dupob, "static") + _mesh_instance_helper(e, dupob, "instanced") + + index += 1 + else: + logger.warn("<%s> Particle System %s is not supported for export (should be of type: 'Hair' and render_type: 'Object')" % (ob.name, partsys.name)) + Report.warnings.append("Object \"%s\" has Particle System: \"%s\" not supported for export (should be of type: 'Hair' and render_type: 'Object')" % (ob.name, partsys.name)) + + elif ob.type == 'CAMERA': + Report.cameras.append( ob.name ) + c = doc.createElement('camera') + o.appendChild(c); c.setAttribute('name', ob.data.name) + aspx = bpy.context.scene.render.pixel_aspect_x + aspy = bpy.context.scene.render.pixel_aspect_y + sx = bpy.context.scene.render.resolution_x + sy = bpy.context.scene.render.resolution_y + if ob.data.type == "PERSP": + fovY = 0.0 + if (sx*aspx > sy*aspy): + fovY = 2 * math.atan(sy * aspy * 16.0 / (ob.data.lens * sx * aspx)) + else: + fovY = 2 * math.atan(16.0 / ob.data.lens) + # fov in radians - like OgreMax - requested by cyrfer + fov = math.radians( fovY * 180.0 / math.pi ) + c.setAttribute('projectionType', "perspective") + c.setAttribute('fov', '%6f' % fov) + else: # ob.data.type == "ORTHO": + c.setAttribute('projectionType', "orthographic") + c.setAttribute('orthoScale', '%6f' % ob.data.ortho_scale) + a = doc.createElement('clipping'); c.appendChild( a ) + a.setAttribute('near', '%6f' % ob.data.clip_start) # requested by cyrfer + a.setAttribute('far', '%6f' % ob.data.clip_end) + + elif ob.type == 'LIGHT' and ob.data.type in 'POINT SPOT SUN AREA'.split(): + Report.lights.append( ob.name ) + l = doc.createElement('light') + o.appendChild(l) + + if ob.data.type == 'POINT': + l.setAttribute('type', 'point') + elif ob.data.type == 'SPOT': + l.setAttribute('type', 'spot') + elif ob.data.type == 'SUN': + l.setAttribute('type', 'directional') + elif ob.data.type == 'AREA': + l.setAttribute('type', 'rect') + + if (bpy.app.version >= (2, 93, 0)): + a = doc.createElement('colourDiffuse'); l.appendChild(a) + a.setAttribute('r', '%3f' % (ob.data.color.r * ob.data.diffuse_factor)) + a.setAttribute('g', '%3f' % (ob.data.color.g * ob.data.diffuse_factor)) + a.setAttribute('b', '%3f' % (ob.data.color.b * ob.data.diffuse_factor)) + else: + a = doc.createElement('colourDiffuse'); l.appendChild(a) + a.setAttribute('r', '%3f' % ob.data.color.r) + a.setAttribute('g', '%3f' % ob.data.color.g) + a.setAttribute('b', '%3f' % ob.data.color.b) + + if ob.data.specular_factor > 0: + a = doc.createElement('colourSpecular'); l.appendChild(a) + a.setAttribute('r', '%3f' % (ob.data.color.r * ob.data.specular_factor)) + a.setAttribute('g', '%3f' % (ob.data.color.g * ob.data.specular_factor)) + a.setAttribute('b', '%3f' % (ob.data.color.b * ob.data.specular_factor)) + + if ob.data.type == 'SPOT': + a = doc.createElement('lightRange') + l.appendChild(a) + a.setAttribute('inner', str(ob.data.spot_size * (1.0 - ob.data.spot_blend))) + a.setAttribute('outer', str(ob.data.spot_size)) + a.setAttribute('falloff', '1.0') + + if ob.data.type == 'AREA': + a = doc.createElement('lightSourceSize') + l.appendChild(a) + a.setAttribute('width', str(ob.data.size)) + a.setAttribute('height', str(ob.data.size_y)) + + factor = 10 + l.setAttribute('name', ob.name ) + l.setAttribute('powerScale', str(ob.data.energy / factor)) + + a = doc.createElement('lightAttenuation'); l.appendChild( a ) + light_range = ob.data.cutoff_distance / factor + if light_range == 0: + light_range = 0.001 + a.setAttribute('range', light_range * factor) + a.setAttribute('constant', '1.0') + a.setAttribute('linear', '%6f' % (4.5 / light_range)) + #a.setAttribute('linear', '%6f' % (0 / light_range)) + a.setAttribute('quadratic', '%6f' % (75.0 / (light_range * light_range))) + #a.setAttribute('quadratic', '%6f' % (1 / (light_range * light_range))) + + # Node Animation + if config.get('NODE_ANIMATION') is True: + node_anim.dot_nodeanim(ob, doc, o) + + for child in ob.children: + dot_scene_node_export( child, + path, doc = doc, rex = rex, + exported_meshes = exported_meshes, + meshes = meshes, + mesh_collision_prims = mesh_collision_prims, + mesh_collision_files = mesh_collision_files, + exported_armatures = exported_armatures, + prefix = prefix, + objects=objects, + xmlparent=o + ) diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ogre/skeleton.py b/assets/blender/scripts/blender2ogre/io_ogre/ogre/skeleton.py new file mode 100644 index 0000000..e209e49 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ogre/skeleton.py @@ -0,0 +1,555 @@ +import bpy, mathutils, logging, time, sys +from .. import config +from ..report import Report +from ..xml import RDocument +from .. import util +from os.path import join + +logger = logging.getLogger('skeleton') + +# Function exports the Ogre .skeleton file +# Called from ogre/scene.py > dot_scene_node_export() function +def dot_skeleton(obj, path, **kwargs): + """ + Create the .skeleton file for this object. + This is only possible if the object has an armature attached. + + obj: the blender object + path: the path where to save this to. Never None and must exist. + kwargs: + * force_name - string: force another name. default None + * invoke_xml_converter - bool: invoke the xml to binary converter. default True + * overwrite - bool: wether to overwrite the xml file + * exported_armatures - array: list of already exported armatures as skeletons + + returns None if there is no skeleton exported, or the filename on success + """ + + arm = obj.find_armature() + if arm and (config.get('ARMATURE_ANIMATION') is True): + exported_armatures = kwargs.get('exported_armatures') + + name = kwargs.get('force_name') or obj.data.name + if config.get('SHARED_ARMATURE') is True: + name = kwargs.get('force_name') or arm.data.name + name = util.clean_object_name(name) + + # Lets export the Armature only once + if name in exported_armatures: + logger.debug("Skip exporting Armature for object: %s" % obj.data.name) + return None + + xmlfile = join(path, '%s.skeleton.xml' % name) + + logger.info('* Generating: %s.skeleton.xml' % name) + + start = time.time() + + skel = Skeleton( obj ) + + with open(xmlfile, 'wb') as fd: + logger.debug("Writing Armature to file: %s" % xmlfile) + fd.write( bytes(skel.to_xml(),'utf-8') ) + + if kwargs.get('invoke_xml_converter', True): + util.xml_convert( xmlfile ) + + logger.info('- Done at %s seconds' % util.timer_diff_str(start)) + + exported_armatures.append( name ) + + return name + '.skeleton' + + return None + +class Bone(object): + + def __init__(self, rbone, pbone, skeleton): + if config.get('SWAP_AXIS') == 'xyz': + self.fixUpAxis = False + else: + self.fixUpAxis = True + if config.get('SWAP_AXIS') == '-xzy': # 1.x + self.flipMat = mathutils.Matrix(((-1,0,0,0),(0,0,1,0),(0,1,0,0),(0,0,0,1))) + elif config.get('SWAP_AXIS') == 'xz-y': # 2.x current generation + #self.flipMat = mathutils.Matrix(((1,0,0,0),(0,0,1,0),(0,1,0,0),(0,0,0,1))) + self.flipMat = mathutils.Matrix(((1,0,0,0),(0,0,1,0),(0,-1,0,0),(0,0,0,1))) # thanks to Waruck + else: + logger.error( '- TODO: Axis swap mode not supported with armature animation' ) + assert 0 + + self.skeleton = skeleton + self.name = pbone.name + self.matrix = rbone.matrix_local.copy() # armature space + #self.matrix_local = rbone.matrix.copy() # space? + + self.bone = pbone # safe to hold pointer to pose bone, not edit bone! + self.shouldOutput = True + if (config.get('ONLY_DEFORMABLE_BONES') is True) and not pbone.bone.use_deform: + self.shouldOutput = False + + # todo: Test -> #if pbone.bone.use_inherit_scale: logger.warn('Bone <%s> is using inherit scaling, Ogre has no support for this' % self.name) + self.parent = pbone.parent + self.children = [] + + def update(self): # called on frame update + pbone = self.bone + pose = pbone.matrix.copy() + self._inverse_total_trans_pose = pose.inverted() + # calculate difference to parent bone + if self.parent: + pose = self.parent._inverse_total_trans_pose @ pose + elif self.fixUpAxis: + pose = self.flipMat @ pose + else: + pass + + self.pose_location = pose.to_translation() - self.ogre_rest_matrix.to_translation() + pose = self.inverse_ogre_rest_matrix @ pose + self.pose_rotation = pose.to_quaternion() + + #self.pose_location = pbone.location.copy() + #self.pose_scale = pbone.scale.copy() + #if pbone.rotation_mode == 'QUATERNION': + # self.pose_rotation = pbone.rotation_quaternion.copy() + #else: + # self.pose_rotation = pbone.rotation_euler.to_quaternion() + + if config.get('OGRE_INHERIT_SCALE') is True: + # special case workaround for broken Ogre nonuniform scaling: + # Ogre can't deal with arbitrary nonuniform scaling, but it can handle certain special cases + # The special case we are trying to handle here is when a bone has a nonuniform scale and it's + # child bones are not inheriting the scale. We should be able to do this without having to + # do any extra setup in Ogre (like turning off "inherit scale" on the Ogre bones) + # if Ogre is inheriting scale, we just output the scale relative to the parent + self.pose_scale = pose.to_scale() + self.ogreDerivedScale = self.pose_scale.copy() + if self.parent: + # this is how Ogre handles inheritance of scale + self.ogreDerivedScale[0] *= self.parent.ogreDerivedScale[0] + self.ogreDerivedScale[1] *= self.parent.ogreDerivedScale[1] + self.ogreDerivedScale[2] *= self.parent.ogreDerivedScale[2] + # if we don't want inherited scale, + # https://docs.blender.org/api/2.83/bpy.types.Bone.html#bpy.types.Bone.inherit_scale + if self.bone.bone.inherit_scale == 'NONE' or self.bone.bone.inherit_scale == 'NONE_LEGACY': + # cancel out the scale that Ogre will calculate + scl = self.parent.ogreDerivedScale + self.pose_scale = mathutils.Vector((1.0/scl[0], 1.0/scl[1], 1.0/scl[2])) + self.ogreDerivedScale = mathutils.Vector((1.0, 1.0, 1.0)) + else: + # if Ogre is not inheriting the scale, + # just output the scale directly + self.pose_scale = pbone.scale.copy() + # however, if Blender is inheriting the scale, + # https://docs.blender.org/api/2.83/bpy.types.Bone.html#bpy.types.Bone.inherit_scale + if self.parent and self.bone.bone.inherit_scale == 'AVERAGE': + # apply parent's scale (only works for uniform scaling) + self.pose_scale[0] *= self.parent.pose_scale[0] + self.pose_scale[1] *= self.parent.pose_scale[1] + self.pose_scale[2] *= self.parent.pose_scale[2] + + for child in self.children: + child.update() + + def clear_pose_transform( self ): + self.bone.location.zero() + self.bone.scale.Fill(3, 1.0) + self.bone.rotation_quaternion.identity() + self.bone.rotation_euler.zero() + #self.bone.rotation_axis_angle #ignore axis angle mode + + def save_pose_transform( self ): + self.savedPoseLocation = self.bone.location.copy() + self.savedPoseScale = self.bone.scale.copy() + self.savedPoseRotationQ = self.bone.rotation_quaternion + self.savedPoseRotationE = self.bone.rotation_euler + #self.bone.rotation_axis_angle #ignore axis angle mode + + def restore_pose_transform( self ): + self.bone.location = self.savedPoseLocation + self.bone.scale = self.savedPoseScale + self.bone.rotation_quaternion = self.savedPoseRotationQ + self.bone.rotation_euler = self.savedPoseRotationE + #self.bone.rotation_axis_angle #ignore axis angle mode + + def rebuild_tree( self ): # called first on all bones + if self.parent: + self.parent = self.skeleton.get_bone( self.parent.name ) + self.parent.children.append( self ) + if self.shouldOutput and not self.parent.shouldOutput: + # mark all ancestor bones as shouldOutput + parent = self.parent + while parent: + parent.shouldOutput = True + parent = parent.parent + + def compute_rest( self ): # called after rebuild_tree, recursive roots to leaves + if self.parent: + inverseParentMatrix = self.parent.inverse_total_trans + elif self.fixUpAxis: + inverseParentMatrix = self.flipMat + else: + inverseParentMatrix = mathutils.Matrix(((1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1))) + + #self.ogre_rest_matrix = self.skeleton.object_space_transformation @ self.matrix # ALLOW ROTATION? + self.ogre_rest_matrix = self.matrix.copy() + # store total inverse transformation + self.inverse_total_trans = self.ogre_rest_matrix.inverted() + # relative to OGRE parent bone origin + self.ogre_rest_matrix = inverseParentMatrix @ self.ogre_rest_matrix + self.inverse_ogre_rest_matrix = self.ogre_rest_matrix.inverted() + + # recursion + for child in self.children: + child.compute_rest() + +class Keyframe: + def __init__(self, time, pos, rot, scale): + self.time = time + self.pos = pos.copy() + self.rot = rot.copy() + self.scale = scale.copy() + + def isTransIdentity( self ): + return self.pos.length < 0.0001 + + def isRotIdentity( self ): + # if the angle is very close to zero + if abs(self.rot.angle) < 0.0001: + # treat it as a zero rotation + return True + return False + + def isScaleIdentity( self ): + scaleDiff = mathutils.Vector((1,1,1)) - self.scale + return scaleDiff.length < 0.0001 + + +# Bone_Track +# Encapsulates all of the key information for an individual bone within a single animation, +# and stores that information as XML. +class Bone_Track: + def __init__(self, bone): + self.bone = bone + self.keyframes = [] + + def is_pos_animated( self ): + # take note if any keyframe is anything other than the IDENTITY transform + for kf in self.keyframes: + if not kf.isTransIdentity(): + return True + return False + + def is_rot_animated( self ): + # take note if any keyframe is anything other than the IDENTITY transform + for kf in self.keyframes: + if not kf.isRotIdentity(): + return True + return False + + def is_scale_animated( self ): + # take note if any keyframe is anything other than the IDENTITY transform + for kf in self.keyframes: + if not kf.isScaleIdentity(): + return True + return False + + def add_keyframe( self, time ): + bone = self.bone + kf = Keyframe(time, bone.pose_location, bone.pose_rotation, bone.pose_scale) + self.keyframes.append( kf ) + + def write_track( self, doc, tracks_element ): + isPosAnimated = self.is_pos_animated() + isRotAnimated = self.is_rot_animated() + isScaleAnimated = self.is_scale_animated() + if not isPosAnimated and not isRotAnimated and not isScaleAnimated: + return + track = doc.createElement('track') + track.setAttribute('bone', self.bone.name) + keyframes_element = doc.createElement('keyframes') + track.appendChild( keyframes_element ) + for kf in self.keyframes: + keyframe = doc.createElement('keyframe') + keyframe.setAttribute('time', '%6f' % kf.time) + if isPosAnimated: + trans = doc.createElement('translate') + keyframe.appendChild( trans ) + trans.setAttribute('x', '%6f' % kf.pos.x) + trans.setAttribute('y', '%6f' % kf.pos.y) + trans.setAttribute('z', '%6f' % kf.pos.z) + + if isRotAnimated: + rotElement = doc.createElement( 'rotate' ) + keyframe.appendChild( rotElement ) + angle = kf.rot.angle + axis = kf.rot.axis + # if angle is near zero or axis is not unit magnitude, + if kf.isRotIdentity(): + angle = 0.0 # avoid outputs like "-0.00000" + axis = mathutils.Vector((0,0,0)) + rotElement.setAttribute('angle', '%6f' %angle ) + axisElement = doc.createElement('axis') + rotElement.appendChild( axisElement ) + axisElement.setAttribute('x', '%6f' %axis[0]) + axisElement.setAttribute('y', '%6f' %axis[1]) + axisElement.setAttribute('z', '%6f' %axis[2]) + + if isScaleAnimated: + scale = doc.createElement('scale') + keyframe.appendChild( scale ) + x,y,z = kf.scale + scale.setAttribute('x', '%6f' %x) + scale.setAttribute('y', '%6f' %y) + scale.setAttribute('z', '%6f' %z) + keyframes_element.appendChild( keyframe ) + tracks_element.appendChild( track ) + +# Skeleton +def findArmature( ob ): + arm = ob.find_armature() + # if this armature has no animation, + if not arm.animation_data: + # The old proxy system has been deprecated in Blender 3.0, and fully removed in Blender 3.2. + # https://docs.blender.org/manual/en/3.2/files/linked_libraries/library_proxies.html + if (bpy.app.version >= (3, 2, 0)): + return arm + + # Search for another armature that is a proxy for it + for ob2 in bpy.data.objects: + if ob2.type == 'ARMATURE' and ob2.proxy == arm: + logger.info( "- Proxy armature %s found" % ob2.name ) + return ob2 + return arm + +class Skeleton(object): + def get_bone( self, name ): + for b in self.bones: + if b.name == name: + return b + return None + + def __init__(self, ob ): + if ob.location.x != 0 or ob.location.y != 0 or ob.location.z != 0: + Report.warnings.append('Mesh (%s): is offset from Armature - zero transform is required' % ob.name) + if ob.scale.x != 1 or ob.scale.y != 1 or ob.scale.z != 1: + Report.warnings.append('Mesh (%s): has been scaled - scale(1,1,1) is required' % ob.name) + + self.object = ob + self.bones = [] + mats = {} + self.arm = arm = findArmature( ob ) + arm.hide_viewport = False + #arm.layers = [True]*20 # can not have anything hidden - REQUIRED? + + for pbone in arm.pose.bones: + mybone = Bone( arm.data.bones[pbone.name], pbone, self ) + self.bones.append( mybone ) + + if arm.name not in Report.armatures: + Report.armatures.append( arm.name ) + + ## bad idea - allowing rotation of armature, means vertices must also be rotated, + ## also a bug with applying the rotation, the Z rotation is lost + #x,y,z = arm.matrix_local.copy().inverted().to_euler() + #e = mathutils.Euler( (x,z,y) ) + #self.object_space_transformation = e.to_matrix().to_4x4() + x,y,z = arm.matrix_local.to_euler() + if x != 0 or y != 0 or z != 0: + Report.warnings.append('Armature: %s is rotated - (rotation is ignored)' % arm.name) + + ## setup bones for Ogre format ## + for b in self.bones: + b.rebuild_tree() + ## walk bones, convert them ## + self.roots = [] + ep = 0.0001 + for b in self.bones: + if not b.parent: + b.compute_rest() + loc,rot,scl = b.ogre_rest_matrix.decompose() + #if loc.x or loc.y or loc.z: + # Report.errors.append('Root bone has non-zero transform (location offset)') + #if rot.w > ep or rot.x > ep or rot.y > ep or rot.z < 1.0-ep: + # Report.errors.append('Root bone has non-zero transform (rotation offset)') + self.roots.append( b ) + + def write_animation( self, arm, actionName, frameBegin, frameEnd, doc, parentElement ): + _fps = float( bpy.context.scene.render.fps ) + #boneNames = sorted( [bone.name for bone in arm.pose.bones] ) + bone_tracks = [] + for bone in self.bones: + #bone = self.get_bone(boneName) + if bone.shouldOutput: + bone_tracks.append( Bone_Track(bone) ) + bone.clear_pose_transform() # clear out any leftover pose transforms in case this bone isn't keyframed + + # Decide keyframes to export: + # ONLY keyframes (Exported animation won't be affected by Inverse Kinematics, Drivers and modified F-Curves) + # OR export keyframe each frame over animation range (Exported animation will be affected by Inverse Kinematics, Drivers and modified F-Curves) + if config.get('ONLY_KEYFRAMES') is True: # Only keyframes + frame_range = [] # Holds a list of keyframes for export + action = bpy.data.actions[actionName] # actionName is the animation name (NLAtrack child) + # loops through all channels on the f-curve --> Code taken from: https://blender.stackexchange.com/questions/8387/how-to-get-keyframe-data-from-python + for fcu in action.fcurves: + for keyframe in fcu.keyframe_points: # Loops through all the keyframes in the channel + kf = int(keyframe.co[0]) # key frame number + if kf not in frame_range: # only add the key frames in once. Keyframes repeat in different channels + frame_range.append(kf) + else: # Keyframes each frame + frame_range = range( int(frameBegin), int(frameEnd) + 1, bpy.context.scene.frame_step) #thanks to Vesa, NOTE: frame_step is [( # of frames / FPS ) / # of frames ] -- I think?? + + logger.info(" - Exporting action: %s" % actionName) + + progressbar = util.ProgressBar("Frames", len(frame_range) ) + + # Add keyframes to export + for frame in frame_range: + progressbar.update( frame - frameBegin ) + + bpy.context.scene.frame_set(frame) + for bone in self.roots: + bone.update() + for track in bone_tracks: + track.add_keyframe((frame - frameBegin) / _fps) + + sys.stdout.write("\n") + + # Check to see if any animation tracks would be output + animationFound = False + for track in bone_tracks: + if track.is_pos_animated() or track.is_rot_animated() or track.is_scale_animated(): + animationFound = True + break + if not animationFound: + return + anim = doc.createElement('animation') + parentElement.appendChild( anim ) + tracks = doc.createElement('tracks') + anim.appendChild( tracks ) + + # Report and log + suffix_text = '' + if config.get('ONLY_KEYFRAMES') is True: + suffix_text = ' - Key frames: ' + str(frame_range) + logger.info('+ %s Key frames: %s' %(actionName,str(frame_range))) + Report.armature_animations.append( '%s : %s [start frame=%s end frame=%s]%s' %(arm.name, actionName, frameBegin, frameEnd, suffix_text) ) + + # Write stuff to skeleton.xml file + anim.setAttribute('name', actionName) # USE the action name + anim.setAttribute('length', '%6f' %( (frameEnd - frameBegin)/ _fps ) ) + + for track in bone_tracks: + # will only write a track if there is some kind of animation there + track.write_track( doc, tracks ) + + def to_xml( self ): + doc = RDocument() + root = doc.createElement('skeleton'); doc.appendChild( root ) + bones = doc.createElement('bones'); root.appendChild( bones ) + bh = doc.createElement('bonehierarchy'); root.appendChild( bh ) + boneId = 0 + for bone in self.bones: + if not bone.shouldOutput: + continue + b = doc.createElement('bone') + b.setAttribute('name', bone.name) + b.setAttribute('id', str(boneId) ) + boneId = boneId + 1 + bones.appendChild( b ) + mat = bone.ogre_rest_matrix.copy() + if bone.parent: + bp = doc.createElement('boneparent') + bp.setAttribute('bone', bone.name) + bp.setAttribute('parent', bone.parent.name) + bh.appendChild( bp ) + + pos = doc.createElement( 'position' ); b.appendChild( pos ) + x,y,z = mat.to_translation() + pos.setAttribute('x', '%6f' %x ) + pos.setAttribute('y', '%6f' %y ) + pos.setAttribute('z', '%6f' %z ) + rot = doc.createElement( 'rotation' ) # "rotation", not "rotate" + b.appendChild( rot ) + + q = mat.to_quaternion() + rot.setAttribute('angle', '%6f' %q.angle ) + axis = doc.createElement('axis'); rot.appendChild( axis ) + x,y,z = q.axis + axis.setAttribute('x', '%6f' %x ) + axis.setAttribute('y', '%6f' %y ) + axis.setAttribute('z', '%6f' %z ) + + # Ogre bones do not have initial scaling + + arm = self.arm + # remember some things so we can put them back later + savedFrame = bpy.context.scene.frame_current + # save the current pose + for b in self.bones: + b.save_pose_transform() + + anims = doc.createElement('animations') + root.appendChild( anims ) + if not arm.animation_data or (arm.animation_data and not arm.animation_data.nla_tracks): + # write a single animation from the blender timeline + self.write_animation( arm, 'my_animation', bpy.context.scene.frame_start, bpy.context.scene.frame_end, doc, anims ) + + elif arm.animation_data: + savedUseNla = arm.animation_data.use_nla + savedAction = arm.animation_data.action + arm.animation_data.use_nla = False + if not len( arm.animation_data.nla_tracks ): + Report.warnings.append('You must assign an NLA strip to armature (%s) that defines the start and end frames' % arm.name) + + # Log to console + if config.get('ONLY_KEYFRAMES') is True: + logger.info('+ Only exporting keyframes') + + actions = {} # actions by name + # the only thing NLA is used for is to gather the names of the actions + # it doesn't matter if the actions are all in the same NLA track or in different tracks + for nla in arm.animation_data.nla_tracks: # NLA required, lone actions not supported + logger.info('+ NLA track: %s' % nla.name) + + for strip in nla.strips: + action = strip.action + if action is None: + logger.error("NLA strip '%s' has no action" % strip.name) + continue + actions[ action.name ] = [action, strip.action_frame_start, strip.action_frame_end] + logger.info(' - Action name: %s' % action.name) + logger.info(' - Strip name: %s' % strip.name) + + actionNames = sorted( actions.keys() ) # output actions in alphabetical order + for actionName in actionNames: + actionData = actions[ actionName ] + action = actionData[0] + arm.animation_data.action = action # set as the current action + suppressedBones = [] + if config.get('ONLY_KEYFRAMED_BONES') is True: + keyframedBones = {} + for group in action.groups: + keyframedBones[ group.name ] = True + for b in self.bones: + if (not b.name in keyframedBones) and b.shouldOutput: + # suppress this bone's output + b.shouldOutput = False + suppressedBones.append( b.name ) + self.write_animation( arm, actionName, actionData[1], actionData[2], doc, anims ) + # restore suppressed bones + for boneName in suppressedBones: + bone = self.get_bone( boneName ) + bone.shouldOutput = True + # restore these to what they originally were + arm.animation_data.action = savedAction + arm.animation_data.use_nla = savedUseNla + + # restore + bpy.context.scene.frame_set( savedFrame ) + # restore the current pose + for b in self.bones: + b.restore_pose_transform() + + return doc.toprettyxml() + diff --git a/assets/blender/scripts/blender2ogre/io_ogre/properties.py b/assets/blender/scripts/blender2ogre/io_ogre/properties.py new file mode 100644 index 0000000..195097d --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/properties.py @@ -0,0 +1,168 @@ +import bpy +from bpy.props import BoolProperty, StringProperty, FloatProperty, IntProperty, EnumProperty +from .ogre.material import IMAGE_FORMATS, load_user_materials + +load_user_materials() + +## Rendering +bpy.types.Object.use_draw_distance = BoolProperty( + name='enable draw distance', + description='use LOD draw distance', + default=False) +bpy.types.Object.draw_distance = FloatProperty( + name='draw distance', + description='distance at which to begin drawing object', + default=0.0, min=0.0, max=10000.0) +bpy.types.Object.cast_shadows = BoolProperty( + name='cast shadows', + description='cast shadows', + default=False) +bpy.types.Object.use_multires_lod = BoolProperty( + name='Enable Multires LOD', + description='enables multires LOD', + default=False) +bpy.types.Object.multires_lod_range = FloatProperty( + name='multires LOD range', + description='far distance at which multires is set to base level', + default=30.0, min=0.0, max=10000.0) + +## Physics +_physics_modes = [ + ('NONE', 'NONE', 'no physics'), + ('RIGID_BODY', 'RIGID_BODY', 'rigid body'), + ('SOFT_BODY', 'SOFT_BODY', 'soft body'), +] +_collision_modes = [ + ('NONE', 'NONE', 'no collision'), + ('PRIMITIVE', 'PRIMITIVE', 'primitive collision type'), + ('MESH', 'MESH', 'triangle-mesh or convex-hull collision type'), + ('DECIMATED', 'DECIMATED', 'auto-decimated collision type'), + ('COMPOUND', 'COMPOUND', 'children primitive compound collision type'), + ('TERRAIN', 'TERRAIN', 'terrain (height map) collision type'), +] + +bpy.types.Object.physics_mode = EnumProperty( + items = _physics_modes, + name = 'physics mode', + description='physics mode', + default='NONE') +bpy.types.Object.physics_friction = FloatProperty( + name='Simple Friction', + description='physics friction', + default=0.1, min=0.0, max=1.0) +bpy.types.Object.physics_bounce = FloatProperty( + name='Simple Bounce', + description='physics bounce', + default=0.01, min=0.0, max=1.0) +bpy.types.Object.collision_terrain_x_steps = IntProperty( + name="Ogre Terrain: x samples", + description="resolution in X of height map", + default=64, min=4, max=8192) +bpy.types.Object.collision_terrain_y_steps = IntProperty( + name="Ogre Terrain: y samples", + description="resolution in Y of height map", + default=64, min=4, max=8192) +bpy.types.Object.collision_mode = EnumProperty( + items = _collision_modes, + name = 'primary collision mode', + description='collision mode', + default='NONE') +bpy.types.Object.subcollision = BoolProperty( + name="collision compound", + description="member of a collision compound", + default=False) + +## Sound +bpy.types.Speaker.play_on_load = BoolProperty( + name='play on load', + default=False) +bpy.types.Speaker.loop = BoolProperty( + name='loop sound', + default=False) +bpy.types.Speaker.use_spatial = BoolProperty( + name='3D spatial sound', + default=True) +bpy.types.Image.use_convert_format = BoolProperty( + name='use convert format', + default=False) +bpy.types.Image.convert_format = EnumProperty( + name='convert to format', + description='converts to image format using imagemagick', + items=IMAGE_FORMATS, + default='NONE') +bpy.types.Image.jpeg_quality = IntProperty( + name="jpeg quality", + description="quality of jpeg", + default=80, min=0, max=100) +bpy.types.Image.use_color_quantize = BoolProperty( + name='use color quantize', + default=False) +bpy.types.Image.use_color_quantize_dither = BoolProperty( + name='use color quantize dither', + default=True) +bpy.types.Image.color_quantize = IntProperty( + name="color quantize", + description="reduce to N colors (requires ImageMagick)", + default=32, min=2, max=256) +bpy.types.Image.use_resize_half = BoolProperty( + name='resize by 1/2', + default=False) +bpy.types.Image.use_resize_absolute = BoolProperty( + name='force image resize', + default=False) +bpy.types.Image.resize_x = IntProperty( + name='resize X', + description='only if image is larger than defined, use ImageMagick to resize it down', + default=256, min=2, max=4096) +bpy.types.Image.resize_y = IntProperty( + name='resize Y', + description='only if image is larger than defined, use ImageMagick to resize it down', + default=256, min=2, max=4096) + +## Materials +bpy.types.Material.use_material_passes = BoolProperty( + # hidden option - gets turned on by operator + # todo: What is a hidden option, is this needed? + name='use ogre extra material passes (layers)', + default=False) +bpy.types.Material.use_in_ogre_material_pass = BoolProperty( + name='Layer Toggle', + default=True) +bpy.types.Material.use_ogre_advanced_options = BoolProperty( + name='Show Advanced Options', + default=False) +bpy.types.Material.use_ogre_parent_material = BoolProperty( + name='Use Script Inheritance', + default=False) +bpy.types.Material.ogre_parent_material = EnumProperty( + name="Script Inheritence", + description='ogre parent material class', + items=[ ('none', 'none', 'NONE') ], + default='none') + +bpy.types.World.ogre_skyX = BoolProperty( + name="enable sky", description="ogre sky", + default=False) +bpy.types.World.ogre_skyX_time = FloatProperty( + name="Time Multiplier", + description="change speed of day/night cycle", + default=0.3, + min=0.0, max=5.0) +bpy.types.World.ogre_skyX_wind = FloatProperty( + name="Wind Direction", + description="change direction of wind", + default=33.0, + min=0.0, max=360.0) +bpy.types.World.ogre_skyX_volumetric_clouds = BoolProperty( + name="volumetric clouds", description="toggle ogre volumetric clouds", + default=True) +bpy.types.World.ogre_skyX_cloud_density_x = FloatProperty( + name="Cloud Density X", + description="change density of volumetric clouds on X", + default=0.1, + min=0.0, max=5.0) +bpy.types.World.ogre_skyX_cloud_density_y = FloatProperty( + name="Cloud Density Y", + description="change density of volumetric clouds on Y", + default=1.0, + min=0.0, max=5.0) diff --git a/assets/blender/scripts/blender2ogre/io_ogre/report.py b/assets/blender/scripts/blender2ogre/io_ogre/report.py new file mode 100644 index 0000000..af542a8 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/report.py @@ -0,0 +1,82 @@ +import bpy, logging + +logger = logging.getLogger('report') + +class ReportSingleton(object): + def __init__(self): + self.reset() + + def show(self): + if not bpy.app.background: + bpy.ops.wm.call_menu( name='OGRE_MT_mini_report' ) + + def reset(self): + self.materials = [] + self.meshes = [] + self.lights = [] + self.cameras = [] + self.armatures = [] + self.armature_animations = [] + self.shape_keys = [] + self.shape_animations = [] + self.textures = [] + self.vertices = 0 + self.orig_vertices = 0 + self.faces = 0 + self.triangles = 0 + self.warnings = [] + self.errors = [] + self.messages = [] + self.paths = [] + self.importing = False + + def report(self): + r = ['Report:'] + ex = ['\nExtended Report:'] + if self.errors: + r.append( ' ERRORS:' ) + for a in self.errors: r.append( ' - %s' %a ) + + #if not bpy.context.selected_objects: + # self.warnings.append('YOU DID NOT SELECT ANYTHING TO EXPORT') + if self.warnings: + r.append( ' WARNINGS:' ) + for a in self.warnings: r.append( ' - %s' %a ) + + if self.messages: + r.append( ' MESSAGES:' ) + for a in self.messages: r.append( ' - %s' %a ) + if self.paths: + r.append( ' PATHS:' ) + for a in self.paths: r.append( ' - %s' %a ) + + if self.vertices: + action_name = "Exported" + if self.importing: + action_name = "Imported" + + r.append( ' Original Vertices: %s' % self.orig_vertices ) + r.append( ' %s Vertices: %s' % (action_name, self.vertices) ) + r.append( ' Original Faces: %s' % self.faces ) + r.append( ' %s Triangles: %s' % (action_name, self.triangles) ) + + ## TODO report file sizes, meshes and textures + + for tag in 'meshes lights cameras armatures armature_animations shape_keys shape_animations materials textures'.split(): + attr = getattr(self, tag) + if attr: + name = tag.replace('_',' ').upper() + r.append( ' %s: %s' %(name, len(attr)) ) + if attr: + ex.append( ' %s:' %name ) + for a in attr: ex.append( ' . %s' %a ) + + txt = '\n'.join( r ) + ex = '\n'.join( ex ) # console only - extended report + print('_' * 80) + print(txt) + print(ex) + print('_' * 80) + return txt + +Report = ReportSingleton() diff --git a/assets/blender/scripts/blender2ogre/io_ogre/shader.py b/assets/blender/scripts/blender2ogre/io_ogre/shader.py new file mode 100644 index 0000000..3840c1e --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/shader.py @@ -0,0 +1,119 @@ + +def on_change_parent_material(mat,context): + print(mat,context) + print('callback', mat.ogre_parent_material) + +def get_subnodes(mat, type='TEXTURE'): + d = {} + for node in mat.nodes: + if node.type==type: d[node.name] = node + keys = list(d.keys()) + keys.sort() + r = [] + for key in keys: r.append( d[key] ) + return r + +def get_texture_subnodes( parent, submaterial=None ): + if not submaterial: submaterial = parent.active_node_material + d = {} + for link in parent.node_tree.links: + if link.from_node and link.from_node.type=='TEXTURE': + if link.to_node and link.to_node.type == 'MATERIAL_EXT': + if link.to_node.material: + if link.to_node.material.name == submaterial.name: + node = link.from_node + d[node.name] = node + keys = list(d.keys()) # this breaks if the user renames the node - TODO improve me + keys.sort() + r = [] + for key in keys: r.append( d[key] ) + return r + +def get_connected_input_nodes( material, node ): + r = [] + for link in material.node_tree.links: + if link.to_node and link.to_node.name == node.name: + r.append( link.from_node ) + return r + +def get_or_create_material_passes( mat, n=8 ): + if not mat.node_tree: + print('CREATING MATERIAL PASSES', n) + create_material_passes( mat, n ) + + d = {} # funky, blender259 had this in order, now blender260 has random order + for node in mat.node_tree.nodes: + if node.type == 'MATERIAL_EXT' and node.name.startswith('GEN.'): + d[node.name] = node + keys = list(d.keys()) + keys.sort() + r = [] + for key in keys: r.append( d[key] ) + return r + +def get_or_create_texture_nodes( mat, n=6 ): # currently not used + assert mat.node_tree # must call create_material_passes first + m = [] + for node in mat.node_tree.nodes: + if node.type == 'MATERIAL_EXT' and node.name.startswith('GEN.'): + m.append( node ) + if not m: + m = get_or_create_material_passes(mat) + print(m) + r = [] + for link in mat.node_tree.links: + print(link, link.to_node, link.from_node) + if link.to_node and link.to_node.name.startswith('GEN.') and link.from_node.type=='TEXTURE': + r.append( link.from_node ) + if not r: + print('--missing texture nodes--') + r = create_texture_nodes( mat, n ) + return r + +def create_material_passes( mat, n=8, textures=True ): + mat.use_nodes = True + tree = mat.node_tree # valid pointer now + + nodes = get_subnodes( tree, 'MATERIAL' ) # assign base material + if nodes and not nodes[0].material: + nodes[0].material = mat + + r = [] + x = 680 + for i in range( n ): + node = tree.nodes.new( type='ShaderNodeExtendedMaterial' ) + node.name = 'GEN.%s' %i + node.location.x = x; node.location.y = 640 + r.append( node ) + x += 220 + #mat.use_nodes = False # TODO set user material to default output + if textures: + texnodes = create_texture_nodes( mat ) + print( texnodes ) + return r + +def create_texture_nodes( mat, n=6, geoms=True ): + assert mat.node_tree # must call create_material_passes first + mats = get_or_create_material_passes( mat ) + r = {}; x = 400 + for i,m in enumerate(mats): + r['material'] = m; r['textures'] = []; r['geoms'] = [] + inputs = [] # other inputs mess up material preview # + for tag in ['Mirror', 'Ambient', 'Emit', 'SpecTra', 'Reflectivity', 'Translucency']: + inputs.append( m.inputs[ tag ] ) + for j in range(n): + tex = mat.node_tree.nodes.new( type='ShaderNodeTexture' ) + tex.name = 'TEX.%s.%s' %(j, m.name) + tex.location.x = x - (j*16) + tex.location.y = -(j*230) + input = inputs[j]; output = tex.outputs['Color'] + link = mat.node_tree.links.new( input, output ) + r['textures'].append( tex ) + if geoms: + geo = mat.node_tree.nodes.new( type='ShaderNodeGeometry' ) + link = mat.node_tree.links.new( tex.inputs['Vector'], geo.outputs['UV'] ) + geo.location.x = x - (j*16) - 250 + geo.location.y = -(j*250) - 1500 + r['geoms'].append( geo ) + x += 220 + return r diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ui/__init__.py b/assets/blender/scripts/blender2ogre/io_ogre/ui/__init__.py new file mode 100644 index 0000000..ded9100 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ui/__init__.py @@ -0,0 +1,67 @@ +# When bpy is already in local, we know this is not the initial import... +if "bpy" in locals(): + import importlib + #print("Reloading modules: export, importer") + importlib.reload(export) + importlib.reload(importer) + +import bpy +import shutil +from os.path import exists +from . import importer, export +from .. import config +from ..report import Report +from ..mesh_preview import OGREMESH_OT_preview + +# Variable to visibility state of the mesh preview button is displayed +meshpreviewButtonDisplayed = False + +# Function to update the visibility state of the Ogre mesh preview button. Only shows if 'MESH_PREVIEWER' points to a valid path +def update_meshpreview_button_visibility(show): + global meshpreviewButtonDisplayed + + if show: + meshpreviewerExists = shutil.which(config.get('MESH_PREVIEWER')) is not None # Check `MESH_PREVIEWER` path is valid. Should check PATH environment variables as well + if meshpreviewerExists: + if not meshpreviewButtonDisplayed: + # 19/09/2021 - oldmanauz: I don't think this is the proper way to do this. bpy.types.VIEW3D_PT_tools_active doesn't exist in the documentation here: https://docs.blender.org/api/current/bpy.types.html + # Does this mean it is poorly supported, undefined or depreciated? Possible solution for future implemtation: bpy.utils.register_tool() & bpy.types.WorkSpaceTool; + bpy.types.VIEW3D_PT_tools_active.append(add_preview_button) + meshpreviewButtonDisplayed = True + else: + if meshpreviewButtonDisplayed: + bpy.types.VIEW3D_PT_tools_active.remove(add_preview_button) + meshpreviewButtonDisplayed = False + + elif not show and meshpreviewButtonDisplayed: + bpy.types.VIEW3D_PT_tools_active.remove(add_preview_button) + meshpreviewButtonDisplayed = False + +def add_preview_button(self, context): + layout = self.layout + op = layout.operator( 'ogremesh.preview', text='', icon='VIEWZOOM' ) + if op is not None: + op.mesh = True + +def auto_register(register): + yield OGRE_MT_mini_report + yield OGREMESH_OT_preview + + # Tries to show the Ogre mesh preview button + update_meshpreview_button_visibility(register) + + yield from importer.auto_register(register) + yield from export.auto_register(register) + +""" +General purpose ui elements +""" + +class OGRE_MT_mini_report(bpy.types.Menu): + bl_label = "Mini-Report | (see console for full report)" + def draw(self, context): + layout = self.layout + txt = Report.report() + for line in txt.splitlines(): + layout.label(text=line) + diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ui/export.py b/assets/blender/scripts/blender2ogre/io_ogre/ui/export.py new file mode 100644 index 0000000..6c5afed --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ui/export.py @@ -0,0 +1,501 @@ +## When bpy is already in local, we know this is not the initial import... +if "bpy" in locals(): + import importlib + #print("Reloading modules: scene") + importlib.reload(material) + importlib.reload(mesh) + importlib.reload(scene) + importlib.reload(skeleton) + +import bpy, os, getpass, math, mathutils, logging, datetime + +from pprint import pprint +from bpy.props import EnumProperty, BoolProperty, FloatProperty, StringProperty, IntProperty +from .. import config +from ..report import Report +from ..util import * +from ..xml import * +from ..ogre import material, mesh, scene, skeleton + +logger = logging.getLogger('export') + +def auto_register(register): + yield OP_ogre_export + + if register: + bpy.types.TOPBAR_MT_file_export.append(menu_func) + else: + bpy.types.TOPBAR_MT_file_export.remove(menu_func) + +def menu_func(self, context): + """ invoked when export in drop down menu is clicked """ + op = self.layout.operator(OP_ogre_export.bl_idname, text="Ogre3D (.scene and .mesh)") + return op + +class _OgreCommonExport_(object): + + called_from_UI = False + + @classmethod + def poll(cls, context): + if context.active_object and context.mode != 'EDIT_MESH': + return True + + def __init__(self): + # Check that converter is setup + self.converter = detect_converter_type() + + def invoke(self, context, event): + # Update the interface with the config values + for key, value in config.CONFIG.items(): + for prefix in ["EX_", "EX_Vx_", "EX_V1_", "EX_V2_"]: + attr_name = prefix + key + if getattr(self, attr_name, None) is not None: + setattr(self, attr_name, value) + + if not self.filepath: + blend_filepath = context.blend_data.filepath + if not blend_filepath: + blend_filepath = "blender2ogre" + else: + blend_filepath = os.path.splitext(blend_filepath)[0] + + self.filepath = blend_filepath + ".scene" + + logger.debug("Context.blend_data: %s" % context.blend_data.filepath) + logger.debug("Context.scene.name: %s" % context.scene.name) + logger.debug("Self.filepath: %s" % self.filepath) + + wm = context.window_manager + fs = wm.fileselect_add(self) + + return {'RUNNING_MODAL'} + + def draw(self, context): + layout = self.layout + self.called_from_UI = True + + if self.converter == "unknown": + layout.label(text="No converter found! Please check your preferences.", icon='ERROR') + else: + layout.label(text="Using '%s'" % self.converter, icon='INFO') + + # The Sections are listed in an array to have them in this particular order + sections = ["General", "Scene", "Materials", "Textures", "Armature", "Mesh", "LOD", "Shape Animation", "Logging"] + + # Icons to use for the sections + section_icons = { + "General" : "WORLD", "Scene" : "SCENE_DATA", + "Materials" : "MATERIAL", "Textures" : "TEXTURE", + "Armature" : "ARMATURE_DATA", "Mesh" : "MESH_DATA", "LOD" : "LATTICE_DATA", "Shape Animation" : "POSE_HLT", + "Logging" : "TEXT" + } + + # Options associated with each section + section_options = { + "General" : ["EX_SWAP_AXIS", "EX_V2_MESH_TOOL_VERSION", "EX_EXPORT_XML_DELETE"], + "Scene" : ["EX_SCENE", "EX_SELECTED_ONLY", "EX_EXPORT_HIDDEN", "EX_FORCE_CAMERA", "EX_FORCE_LIGHTS", "EX_NODE_ANIMATION", "EX_EXPORT_SKYBOX", "EX_SKYBOX_RESOLUTION"], + "Materials" : ["EX_MATERIALS", "EX_SEPARATE_MATERIALS", "EX_COPY_SHADER_PROGRAMS", "EX_USE_FFP_PARAMETERS"], + "Textures" : ["EX_DDS_MIPS", "EX_FORCE_IMAGE_FORMAT"], + "Armature" : ["EX_ARMATURE_ANIMATION", "EX_SHARED_ARMATURE", "EX_ONLY_KEYFRAMES", "EX_ONLY_DEFORMABLE_BONES", "EX_ONLY_KEYFRAMED_BONES", "EX_OGRE_INHERIT_SCALE", "EX_TRIM_BONE_WEIGHTS"], + "Mesh" : ["EX_MESH", "EX_MESH_OVERWRITE", "EX_ARRAY", "EX_V1_EXTREMITY_POINTS", "EX_Vx_GENERATE_EDGE_LISTS", "EX_GENERATE_TANGENTS", "EX_Vx_PACK_INT_10_10_10_2", "EX_Vx_OPTIMISE_ANIMATIONS", "EX_Vx_OPTIMISE_VERTEX_CACHE", "EX_V2_OPTIMISE_VERTEX_BUFFERS", "EX_V2_OPTIMISE_VERTEX_BUFFERS_OPTIONS"], + "LOD" : ["EX_LOD_GENERATION", "EX_LOD_LEVELS", "EX_LOD_DISTANCE", "EX_LOD_PERCENT"], + "Shape Animation" : ["EX_SHAPE_ANIMATIONS", "EX_SHAPE_NORMALS"], + "Logging" : ["EX_Vx_ENABLE_LOGGING", "EX_Vx_DEBUG_LOGGING"] + } + + for section in sections: + row = layout.row() + box = row.box() + box.label(text=section, icon=section_icons[section]) + for prop in section_options[section]: + if prop.startswith('EX_V1_'): + if self.converter == "OgreXMLConverter": + box.prop(self, prop) + elif prop.startswith('EX_V2_'): + if self.converter == "OgreMeshTool": + box.prop(self, prop) + elif prop.startswith('EX_Vx_'): + if self.converter != "unknown": + box.prop(self, prop) + elif prop.startswith('EX_'): + box.prop(self, prop) + + def execute(self, context): + Report.reset() + + # Add warning about missing XML converter + if self.converter == "unknown": + Report.errors.append( + "Cannot find suitable OgreXMLConverter or OgreMeshTool executable.\n" + + "Exported XML mesh was NOT automatically converted to .mesh file.\n" + + "You MUST run the converter manually to create binary .mesh file.") + + # Load addonPreference in CONFIG + config.update_from_addon_preference(context) + + # Update saved defaults to new settings and also print export code + kw = {} + + print ("_" * 80,"\n") + + script_text = "# Blender Export Script:\n\n" + script_text += "import bpy\n" + script_text += "bpy.ops.ogre.export(\n" + script_text += " filepath='%s', \n" % os.path.abspath(self.filepath).replace('\\', '\\\\') + for name in dir(_OgreCommonExport_): + conf_name = "" + if name.startswith('EX_V1_') or \ + name.startswith('EX_V2_') or \ + name.startswith('EX_Vx_'): + conf_name = name[6:] + elif name.startswith('EX_'): + conf_name = name[3:] + if conf_name not in config.CONFIG.keys(): + continue + attribute = getattr(self, name) + kw[ conf_name ] = attribute + if config._CONFIG_DEFAULTS_ALL[ conf_name ] != attribute: + if type(attribute) == str: + script_text += " %s='%s', \n" % (name, attribute) + else: + script_text += " %s=%s, \n" % (name, attribute) + script_text += ")\n" + + print(script_text) + + print ("_" * 80,"\n") + + # Let's save the script in a text block if called from the UI + if self.called_from_UI: + text_block_name = "ogre_export-" + datetime.datetime.now().strftime("%Y%m%d%H%M") + logger.info("* Creating Text Block '%s' with export script" % text_block_name) + if text_block_name not in bpy.data.texts: + #text_block = bpy.data.texts[text_block_name] + text_block = bpy.data.texts.new(text_block_name) + text_block.from_string(script_text) + + config.update(**kw) + + target_path, target_file_name = os.path.split(os.path.abspath(self.filepath)) + target_file_name = clean_object_name(target_file_name) + target_file_name_no_ext = os.path.splitext(target_file_name)[0] + + file_handler = None + + # Add a file handler to all Logger instances + if config.get('ENABLE_LOGGING') is True: + log_file = os.path.join(target_path, "blender2ogre.log") + logger.info("* Writing log file to: %s" % log_file) + + try: + file_handler = logging.FileHandler(filename=log_file, mode='w', encoding='utf-8', delay=False) + + # Show the python file name from where each log message originated + SHOW_LOG_NAME = False + + if SHOW_LOG_NAME: + file_formatter = logging.Formatter(fmt='%(asctime)s %(name)9s.py [%(levelname)5s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + else: + file_formatter = logging.Formatter(fmt='%(asctime)s [%(levelname)5s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + + file_handler.setFormatter(file_formatter) + + if config.get('DEBUG_LOGGING') is True: + level = logging.DEBUG + else: + level = logging.INFO + + for logger_name in logging.Logger.manager.loggerDict.keys(): + logging.getLogger(logger_name).addHandler(file_handler) + logging.getLogger(logger_name).setLevel(level) + except Exception as e: + logger.warn("Unable to create log file: %s" % log_file) + logger.warn(e) + + logger.info("* Target path: %s" % target_path) + logger.info("* Target file name: %s" % target_file_name) + logger.debug("* Target file name (no ext): %s" % target_file_name_no_ext) + + # https://blender.stackexchange.com/questions/45528/how-to-get-blenders-version-number-from-python + logger.info("* Blender version: %s (%s; %s)" % (bpy.app.version_string, bpy.app.version_cycle, bpy.app.build_platform.decode('UTF-8'))) + logger.debug(" + Binary Path: %s" % bpy.app.binary_path) + logger.debug(" + Build Date: %s %s" % (bpy.app.build_date.decode('UTF-8'), bpy.app.build_time.decode('UTF-8'))) + logger.debug(" + Build Hash: %s" % bpy.app.build_hash.decode('UTF-8')) + logger.debug(" + Build Branch: %s" % bpy.app.build_branch.decode('UTF-8')) + logger.debug(" + Build Platform: %s" % bpy.app.build_platform.decode('UTF-8')) + + # Start exporting the elements in the scene + scene.dot_scene(target_path, target_file_name_no_ext) + Report.show() + + # Flush and close all logging file handlers + if config.get('ENABLE_LOGGING') is True and file_handler is not None: + for logger_name in logging.Logger.manager.loggerDict.keys(): + logging.getLogger(logger_name).handlers.clear() + + file_handler.flush() + file_handler.close() + + return {'FINISHED'} + + filepath : StringProperty(name="File Path", + description="Filepath used for exporting Ogre .scene file", + maxlen=1024, + default="", + subtype='FILE_PATH') + + # Basic options + # NOTE config values are automatically propagated if you name it like: EX_ + # Properties can also be enabled for a specific converter by adding V1 or V2 in the name: + # EX_V1_ for OgreXMLConverter + # EX_V2_ for OgreMeshTool + # EX_Vx_ for OgreXMLConverter and OgreMeshTool (hide only when no converter found) + + # General + EX_SWAP_AXIS : EnumProperty( + items=config.AXIS_MODES, + name='Swap Axis', + description='Axis swapping mode', + default=config.get('SWAP_AXIS')) = {} + EX_V2_MESH_TOOL_VERSION : EnumProperty( + items=config.MESH_TOOL_VERSIONS, + name='Mesh Export Version', + description='Specify Ogre version format to write', + default=config.get('MESH_TOOL_VERSION')) = {} + EX_EXPORT_XML_DELETE : BoolProperty( + name="Clean up XML files", + description="""Remove the generated XML files after binary conversion. +(The removal will only happen if OgreXMLConverter/OgreMeshTool finishes successfully)""", + default=config.get('EXPORT_XML_DELETE')) = {} + + # Scene + EX_SCENE : BoolProperty( + name="Export Scene", + description="Export current scene (OgreDotScene XML file)", + default=config.get('SCENE')) = {} + EX_SELECTED_ONLY : BoolProperty( + name="Export Selected Only", + description="Export only selected objects.\nTurn on to avoid exporting non-selected stuff", + default=config.get('SELECTED_ONLY')) = {} + EX_EXPORT_HIDDEN : BoolProperty( + name="Export Hidden Also", + description="Export hidden meshes in addition to visible ones.\nTurn off to avoid exporting hidden stuff", + default=config.get('EXPORT_HIDDEN')) = {} + #EX_EXPORT_USER : BoolProperty( + # name="Export User Properties", + # description="Export user properties such as as physical properties.\nTurn off to avoid exporting the user data", + # default=config.get('EXPORT_USER')) = {} + EX_FORCE_CAMERA : BoolProperty( + name="Force Camera", + description="Export active camera, even if not selected", + default=config.get('FORCE_CAMERA')) = {} + EX_FORCE_LIGHTS : BoolProperty( + name="Force Lights", + description="Export all Lights, even if not selected", + default=config.get('FORCE_LIGHTS')) = {} + EX_NODE_ANIMATION : BoolProperty( + name="Export Node Animations", + description="Export Node Animations, these are animations of the objects properties like position, rotation and scale", + default=config.get('NODE_ANIMATION')) = {} +# EX_NODE_KEYFRAMES : BoolProperty( +# name="Only write Node Keyframes", +# description="""The default behaviour when exporting Node Animations is to write every keyframe. +#Select this option if you want to have more control of the Node Animation in your Ogre application +#Don't select this option if you have any fine tuning of the F-Curves in Blender, since they won't get exported. +#NOTE: Node Animations based on the 'Follow Path' constraint will most likely fail with this option set to True.""", +# default=config.get('NODE_KEYFRAMES')) = {} + EX_EXPORT_SKYBOX : BoolProperty( + name="Export SkyBox", + description="Export SkyBox when there is an environment texture plugged to the World background.\nThis is useful to convert from HDRi images to a CubeMap format.", + default=config.get('EXPORT_SKYBOX')) = {} + EX_SKYBOX_RESOLUTION : IntProperty( + name="SkyBox Resolution", + description="Resolution for the exported CubeMap images of the SkyBox", + min=512, max=16384, + default=config.get('SKYBOX_RESOLUTION')) = {} + + # Materials + EX_MATERIALS : BoolProperty( + name="Export Materials", + description="Exports .material scripts", + default=config.get('MATERIALS')) = {} + EX_SEPARATE_MATERIALS : BoolProperty( + name="Separate Materials", + description="Exports a .material file for each material\n(rather than putting all materials into a single .material file)", + default=config.get('SEPARATE_MATERIALS')) = {} + EX_COPY_SHADER_PROGRAMS : BoolProperty( + name="Copy Shader Programs", + description="When using script inheritance copy the source shader programs to the output path", + default=config.get('COPY_SHADER_PROGRAMS')) = {} + EX_USE_FFP_PARAMETERS : BoolProperty( + name="Fixed Function Parameters", + description="Convert material parameters to Blinn-Phong model", + default=config.get('USE_FFP_PARAMETERS')) = {} + + # Textures + EX_DDS_MIPS : IntProperty( + name="DDS Mips", + description="Number of Mip Maps (DDS)", + min=0, max=16, + default=config.get('DDS_MIPS')) = {} + EX_FORCE_IMAGE_FORMAT : EnumProperty( + items=material.IMAGE_FORMATS, + name="Convert Images", + description="Convert all textures to selected image format", + default=config.get('FORCE_IMAGE_FORMAT')) = {} + + # Armature + EX_ARMATURE_ANIMATION : BoolProperty( + name="Armature Animation", + description="Export armature animations (updates the .skeleton file)", + default=config.get('ARMATURE_ANIMATION')) = {} + EX_SHARED_ARMATURE : BoolProperty( + name="Shared Armature", + description="""Export a single .skeleton file for objects that have the same Armature parent +(useful for: shareSkeletonInstanceWith()) +NOTE: The name of the .skeleton file will be that of the Armature""", + default=config.get('SHARED_ARMATURE')) = {} + EX_ONLY_KEYFRAMES : BoolProperty( + name="Only Keyframes", + description="Only export Keyframes.\nNOTE: Exported animation won't be affected by Inverse Kinematics, Drivers and modified F-Curves", + default=config.get('ONLY_KEYFRAMES')) = {} + EX_ONLY_DEFORMABLE_BONES : BoolProperty( + name="Only Deformable Bones", + description="Only exports bones that are deformable. Useful for hiding IK-Bones used in Blender.\nNOTE: Any bone with deformable children/descendants will be output as well", + default=config.get('ONLY_DEFORMABLE_BONES')) = {} + EX_ONLY_KEYFRAMED_BONES : BoolProperty( + name="Only Keyframed Bones", + description="Only exports bones that have been keyframed for a given animation.\nUseful to limit the set of bones on a per-animation basis", + default=config.get('ONLY_KEYFRAMED_BONES')) = {} + EX_OGRE_INHERIT_SCALE : BoolProperty( + name="OGRE Inherit Scale", + description="Whether the OGRE bones have the 'inherit scale' flag on.\nIf the animation has scale in it, the exported animation needs to be\nadjusted to account for the state of the inherit-scale flag in OGRE", + default=config.get('OGRE_INHERIT_SCALE')) = {} + EX_TRIM_BONE_WEIGHTS : FloatProperty( + name="Trim Weights", + description="Ignore bone weights below this value (Ogre supports 4 bones per vertex)", + min=0.0, max=0.5, + default=config.get('TRIM_BONE_WEIGHTS')) = {} + + # Mesh Options + EX_MESH : BoolProperty( + name="Export Meshes", + description="Export meshes", + default=config.get('MESH')) = {} + EX_MESH_OVERWRITE : BoolProperty( + name="Export Meshes (overwrite)", + description="Export meshes (overwrite existing files)", + default=config.get('MESH_OVERWRITE')) = {} + EX_ARRAY : BoolProperty( + name="Optimise Arrays", + description="Optimise array modifiers as instances (constant offset only)", + default=config.get('ARRAY')) = {} + EX_V1_EXTREMITY_POINTS : IntProperty( + name="Extremity Points", + description="""Submeshes can have optional 'extremity points' stored with them to allow +submeshes to be sorted with respect to each other in the case of transparency. +For some meshes with transparent materials (partial transparency) this can be useful""", + min=0, max=65536, + default=config.get('EXTREMITY_POINTS')) = {} + EX_Vx_GENERATE_EDGE_LISTS : BoolProperty( + name="Generate Edge Lists", + description="Generate Edge Lists (for Stencil Shadows)", + default=config.get('GENERATE_EDGE_LISTS')) = {} + EX_GENERATE_TANGENTS : EnumProperty( + items=config.TANGENT_MODES, + name="Tangents", + description="Export tangents generated by Blender", + default=config.get('GENERATE_TANGENTS')) = {} + EX_Vx_PACK_INT_10_10_10_2 : BoolProperty( + name="Pack into 'INT_10_10_10_2' format", + description="""Ogre now supports normalized INT_10_10_10_2 as the normal format. +This packs 3 signed values with 10bit precision and a fourth 2bit value into 4 bytes; the size of a single float. +If you are using normal-maps, you will notice how this format is perfect to store a tangent with parity, while only requiring 25% of storage compared to 4 floats""", + default=config.get('PACK_INT_10_10_10_2')) = {} + EX_Vx_OPTIMISE_ANIMATIONS : BoolProperty( + name="Optimise Animations", + description="DON'T optimise out redundant tracks & keyframes", + default=config.get('OPTIMISE_ANIMATIONS')) = {} + EX_Vx_OPTIMISE_VERTEX_CACHE : BoolProperty( + name="Optimise Vertex Cache", + description="""This reorders the index buffer of the mesh such that triangles are rendered in order of proximity. +If enabled, the MeshUpgrader will print the change of the "average cache miss ratio (ACMR)" metric. +It measures the number of cache misses per triangle and thus ranges from 3.0 (all 3 vertices missed) to about 0.5 for an optimized mesh""", + default=config.get('OPTIMISE_VERTEX_CACHE')) = {} + EX_V2_OPTIMISE_VERTEX_BUFFERS : BoolProperty( + name="Optimise Vertex Buffers For Shaders", + description="Optimise vertex buffers for shaders.\nSee Vertex Buffers Options for more settings", + default=config.get('OPTIMISE_VERTEX_BUFFERS')) = {} + EX_V2_OPTIMISE_VERTEX_BUFFERS_OPTIONS : StringProperty( + name="Vertex Buffers Options", + description="""Used when optimizing vertex buffers for shaders. +Available flags are: +p - converts POSITION to 16-bit floats. +q - converts normal tangent and bitangent (28-36 bytes) to QTangents (8 bytes). +u - converts UVs to 16-bit floats. +s - make shadow mapping passes have their own optimised buffers. Overrides existing ones if any. +S - strips the buffers for shadow mapping (consumes less space and memory)""", + maxlen=5, + default=config.get('OPTIMISE_VERTEX_BUFFERS_OPTIONS')) = {} + + # LOD + EX_LOD_GENERATION : EnumProperty( + items=config.LOD_METHODS, + name='LOD Generation Method', + description='Method of generating LOD levels', + default=config.get('LOD_GENERATION')) = {} + EX_LOD_LEVELS : IntProperty( + name="LOD Levels", + description="Number of LOD levels", + min=0, max=32, + default=config.get('LOD_LEVELS')) = {} + EX_LOD_DISTANCE : IntProperty( + name="LOD Distance", + description="Distance increment to reduce LOD", + min=0, max=2000, + default=config.get('LOD_DISTANCE')) = {} + EX_LOD_PERCENT : IntProperty( + name="LOD Percentage", + description="LOD percentage reduction", + min=0, max=99, + default=config.get('LOD_PERCENT')) = {} + + # Pose Animation + EX_SHAPE_ANIMATIONS : BoolProperty( + name="Shape Animation", + description="Export shape animations (updates the .mesh file)", + default=config.get('SHAPE_ANIMATIONS')) = {} + EX_SHAPE_NORMALS : BoolProperty( + name="Shape Normals", + description="Export normals in shape animations (updates the .mesh file)", + default=config.get('SHAPE_NORMALS')) = {} + + # Logging + EX_Vx_ENABLE_LOGGING : BoolProperty( + name="Write Exporter Logs", + description="Write Log file to the output directory (blender2ogre.log)", + default=config.get('ENABLE_LOGGING')) = {} + + # It seems that it is not possible to exclude DEBUG when selecting a log level + EX_Vx_DEBUG_LOGGING : BoolProperty( + name="Debug Logging", + description="Whether to show DEBUG log messages", + default=config.get('DEBUG_LOGGING')) = {} + + # It was decided to make this an option that is not user-facing + #EX_Vx_SHOW_LOG_NAME : BoolProperty( + # name="Show Log name", + # description="Show .py file from where each log message originated", + # default=config.get('SHOW_LOG_NAME')) = {} + +class OP_ogre_export(bpy.types.Operator, _OgreCommonExport_): + '''Export Ogre Scene''' + bl_idname = "ogre.export" + bl_label = "Export Ogre" + bl_options = {'REGISTER'} + # export logic is contained in the subclass + + def __init__(self, *args, **kwargs): + bpy.types.Operator.__init__(self, *args, **kwargs) + _OgreCommonExport_.__init__(self) diff --git a/assets/blender/scripts/blender2ogre/io_ogre/ui/importer.py b/assets/blender/scripts/blender2ogre/io_ogre/ui/importer.py new file mode 100644 index 0000000..c1c48a1 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/ui/importer.py @@ -0,0 +1,354 @@ +# When bpy is already in local, we know this is not the initial import... +if "bpy" in locals(): + import importlib + #print("Reloading modules: ogre_import") + importlib.reload(ogre_import) + +import bpy, os, getpass, math, mathutils, logging, datetime + +from pprint import pprint +from bpy.props import EnumProperty, BoolProperty, FloatProperty, StringProperty, IntProperty +from .. import config +from ..report import Report +from ..util import * +from ..xml import * +from ..ogre import ogre_import + +logger = logging.getLogger('import') + +def auto_register(register): + yield OP_ogre_import + + if register: + if bpy.app.version >= (4, 1, 0): + bpy.utils.register_class(OGRE_FH_import) + bpy.types.TOPBAR_MT_file_import.append(menu_func) + else: + if bpy.app.version >= (4, 1, 0): + bpy.utils.unregister_class(OGRE_FH_import) + bpy.types.TOPBAR_MT_file_import.remove(menu_func) + +def menu_func(self, context): + """ invoked when import in drop down menu is clicked """ + op = self.layout.operator(OP_ogre_import.bl_idname, text="Ogre3D (.scene and .mesh)") + return op + +class _OgreCommonImport_(object): + + last_import_path = None + called_from_UI = False + + @classmethod + def poll(cls, context): + if context.mode != 'EDIT_MESH': + return True + + def __init__(self): + # Check that converter is setup + self.converter = detect_converter_type() + + def invoke(self, context, event): + """ + By default the file handler invokes the operator with the filepath property set. + In this example if this property is set the operator is executed, if not the + file select window is invoked. + This depends on setting ``options={'SKIP_SAVE'}`` to the property options to avoid + to reuse filepath data between operator calls. + """ + if self.filepath: + return self.execute(context) + + # Update the interface with the config values + for key, value in config.CONFIG.items(): + for prefix in ["IM_", "IM_Vx_", "IM_V1_", "IM_V2_"]: + attr_name = prefix + key + if getattr(self, attr_name, None) is not None: + setattr(self, attr_name, value) + + wm = context.window_manager + fs = wm.fileselect_add(self) + + return {'RUNNING_MODAL'} + + def draw(self, context): + layout = self.layout + self.called_from_UI = True + + if self.converter == "unknown": + layout.label(text="No converter found! Please check your preferences.", icon='ERROR') + else: + layout.label(text="Using '%s'" % self.converter, icon='INFO') + + # The Sections are listed in an array to have them in this particular order + sections = ["General", "Armature", "Mesh", "Shape Keys", "Logging"] + + # Icons to use for the sections + section_icons = { + "General" : "WORLD", "Armature" : "ARMATURE_DATA", "Mesh" : "MESH_DATA", "Shape Keys" : "ANIM_DATA", "Logging" : "TEXT" + } + + # Options associated with each section + section_options = { + "General" : ["IM_SWAP_AXIS", "IM_V2_MESH_TOOL_VERSION", "IM_IMPORT_XML_DELETE"], + "Armature" : ["IM_IMPORT_ANIMATIONS", "IM_ROUND_FRAMES", "IM_USE_SELECTED_SKELETON"], + "Mesh" : ["IM_IMPORT_NORMALS", "IM_MERGE_SUBMESHES"], + "Shape Keys" : ["IM_IMPORT_SHAPEKEYS"], + "Logging" : ["IM_Vx_ENABLE_LOGGING", "IM_Vx_DEBUG_LOGGING"] + } + + for section in sections: + row = layout.row() + box = row.box() + box.label(text=section, icon=section_icons[section]) + for prop in section_options[section]: + if prop.startswith('IM_V1_'): + if self.converter == "OgreXMLConverter": + box.prop(self, prop) + elif prop.startswith('IM_V2_'): + if self.converter == "OgreMeshTool": + box.prop(self, prop) + elif prop.startswith('IM_Vx_'): + if self.converter != "unknown": + box.prop(self, prop) + elif prop.startswith('IM_'): + box.prop(self, prop) + + def execute(self, context): + """ Calls to this Operator can set unfiltered filepaths, ensure the file extension is .mesh, .xml or .scene. """ + if not self.filepath or not (\ + self.filepath.endswith(".mesh") or \ + self.filepath.endswith(".xml") or \ + self.filepath.endswith(".scene")): + return {'CANCELLED'} + + # Add warning about missing XML converter + Report.reset() + if self.converter == "unknown": + Report.errors.append( + "Cannot find suitable OgreXMLConverter or OgreMeshTool executable." + + "Import XML mesh - does NOT automatically convert .mesh to .xml file. You MUST run converter on the mesh manually.") + + logger.debug("Context.blend_data: %s" % context.blend_data.filepath) + logger.debug("Context.scene.name: %s" % context.scene.name) + logger.debug("Self.filepath: %s" % self.filepath) + logger.debug("Self.last_import_path: %s" % self.last_import_path) + + # Load addonPreference in CONFIG + config.update_from_addon_preference(context) + + # Resolve path from opened .blend if available. + # Normally it's not if blender was opened with "Recover Last Session". + # After import is done once, remember that path when re-importing. + if not self.last_import_path: + # First import during this blender run + if context.blend_data.filepath != "": + path, name = os.path.split(context.blend_data.filepath) + self.last_import_path = os.path.join(path, name.split('.')[0]) + + if not self.last_import_path: + self.last_import_path = os.path.expanduser("~") + + if self.filepath == "" or not self.filepath: + self.filepath = "blender2ogre" + + logger.debug("Self.filepath: %s" % self.filepath) + + # Update saved defaults to new settings and also print import code + kw = {} + + print ("_" * 80,"\n") + + script_text = "# Blender Import Script:\n\n" + script_text += "import bpy\n" + script_text += "bpy.ops.ogre.import_mesh(\n" + script_text += " filepath='%s', \n" % os.path.abspath(self.filepath).replace('\\', '\\\\') + for name in dir(_OgreCommonImport_): + conf_name = "" + if name.startswith('IM_V1_') or \ + name.startswith('IM_V2_') or \ + name.startswith('IM_Vx_'): + conf_name = name[6:] + elif name.startswith('IM_'): + conf_name = name[3:] + if conf_name not in config.CONFIG.keys(): + continue + attribute = getattr(self, name) + kw[ conf_name ] = attribute + if config._CONFIG_DEFAULTS_ALL[ conf_name ] != attribute: + if type(attribute) == str: + script_text += " %s='%s', \n" % (name, attribute) + else: + script_text += " %s=%s, \n" % (name, attribute) + script_text += ")\n" + + print(script_text) + + print ("_" * 80,"\n") + + # Let's save the script in a text block if called from the UI + if self.called_from_UI: + text_block_name = "ogre_import-" + datetime.datetime.now().strftime("%Y%m%d%H%M") + logger.info("* Creating Text Block '%s' with import script" % text_block_name) + if text_block_name not in bpy.data.texts: + #text_block = bpy.data.texts[text_block_name] + text_block = bpy.data.texts.new(text_block_name) + text_block.from_string(script_text) + + config.update(**kw) + + target_path, target_file_name = os.path.split(os.path.abspath(self.filepath)) + target_file_name = clean_object_name(target_file_name) + target_file_name_no_ext = os.path.splitext(target_file_name)[0] + + file_handler = None + + # Add a file handler to all Logger instances + if config.get('ENABLE_LOGGING') is True: + log_file = os.path.join(target_path, "blender2ogre.log") + logger.info("Writing log file to: %s" % log_file) + + file_handler = logging.FileHandler(filename=log_file, mode='w', encoding='utf-8', delay=False) + + # Show the python file name from where each log message originated + SHOW_LOG_NAME = False + + if SHOW_LOG_NAME: + file_formatter = logging.Formatter(fmt='%(asctime)s %(name)9s.py [%(levelname)5s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + else: + file_formatter = logging.Formatter(fmt='%(asctime)s [%(levelname)5s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + + file_handler.setFormatter(file_formatter) + + for logger_name in logging.Logger.manager.loggerDict.keys(): + logging.getLogger(logger_name).addHandler(file_handler) + + logger.info("Target_path: %s" % target_path) + logger.info("Target_file_name: %s" % target_file_name) + + Report.importing = True + if target_file_name.lower().endswith(".scene"): + ogre_import.load_scene(os.path.join(target_path, target_file_name)) + else: + ogre_import.load_mesh(os.path.join(target_path, target_file_name)) + Report.show() + + # Flush and close all logging file handlers + if config.get('ENABLE_LOGGING') is True: + for logger_name in logging.Logger.manager.loggerDict.keys(): + logger_instance = logging.getLogger(logger_name) + + # Remove handlers + logger_instance.handlers.clear() + + file_handler.flush() + file_handler.close() + + return {'FINISHED'} + + filepath : StringProperty(name="File Path", + description="Filepath used for importing Ogre .mesh and .scene files", + maxlen=1024, + default="", + options={'SKIP_SAVE'}, + subtype='FILE_PATH') + + filter_glob : StringProperty( + default="*.mesh;*.xml;*.scene;", + options={'HIDDEN'}) + + # Basic options + # NOTE config values are automatically propagated if you name it like: IM_ + # Properties can also be enabled for a specific converter by adding V1 or V2 in the name: + # IM_V1_ for OgreXMLConverter + # IM_V2_ for OgreMeshTool + # IM_Vx_ for OgreXMLConverter and OgreMeshTool (hide only when no converter found) + + # General + IM_SWAP_AXIS : EnumProperty( + items=config.AXIS_MODES, + name='Swap Axis', + description='Axis swapping mode', + default=config.get('SWAP_AXIS')) = {} + + IM_V2_MESH_TOOL_VERSION : EnumProperty( + items=config.MESH_TOOL_VERSIONS, + name='Mesh Import Version', + description='Specify Ogre version format to read', + default=config.get('MESH_TOOL_VERSION')) = {} + + IM_IMPORT_XML_DELETE : BoolProperty( + name="Clean up XML files", + description="Remove the generated XML files after binary conversion. \n(The removal will only happen if OgreXMLConverter/OgreMeshTool finishes successfully)", + default=config.get('IMPORT_XML_DELETE')) = {} + + # Mesh + IM_IMPORT_NORMALS : BoolProperty( + name="Import Normals", + description="Import custom mesh normals", + default=config.get('IMPORT_NORMALS')) = {} + + IM_MERGE_SUBMESHES : BoolProperty( + name="Merge Submeshes", + description="Whether to merge submeshes to form a single mesh with different materials", + default=config.get('MERGE_SUBMESHES')) = {} + + # Armature + IM_IMPORT_ANIMATIONS : BoolProperty( + name="Import animation", + description="Import animations as actions", + default=config.get('IMPORT_ANIMATIONS')) = {} + + IM_ROUND_FRAMES : BoolProperty( + name="Adjust frame rate", + description="Adjust scene frame rate to match imported animation", + default=config.get('ROUND_FRAMES')) + + IM_USE_SELECTED_SKELETON : BoolProperty( + name='Use selected skeleton', + description='Link with selected armature object rather than importing a skeleton.\nUse this for importing skinned meshes that don\'t have their own skeleton.\nMake sure you have the correct skeleton selected or the weight maps may get mixed up.', + default=config.get('USE_SELECTED_SKELETON')) = {} + + # Shape Keys + IM_IMPORT_SHAPEKEYS : BoolProperty( + name="Import shape keys", + description="Import shape keys (morphs)", + default=config.get('IMPORT_SHAPEKEYS')) = {} + + # Logging + IM_Vx_ENABLE_LOGGING : BoolProperty( + name="Write Importer Logs", + description="Write Log file to the output directory (blender2ogre.log)", + default=config.get('ENABLE_LOGGING')) = {} + + # It seems that it is not possible to exclude DEBUG when selecting a log level + IM_Vx_DEBUG_LOGGING : BoolProperty( + name="Debug Logging", + description="Whether to show DEBUG log messages", + default=config.get('DEBUG_LOGGING')) = {} + + +# Support for Blender 4.1+ drag and drop +# (https://docs.blender.org/api/4.1/bpy.types.FileHandler.html) +if bpy.app.version >= (4, 1, 0): + class OGRE_FH_import(bpy.types.FileHandler): + bl_idname = "OGRE_FH_import" + bl_label = "Import Ogre drag and drop support" + bl_import_operator = "ogre.import_mesh" + bl_file_extensions = ".mesh;.xml;.scene;" + + @classmethod + def poll_drop(cls, context): + if context.mode != 'EDIT_MESH': + return True + + +class OP_ogre_import(bpy.types.Operator, _OgreCommonImport_): + '''Import Ogre Scene''' + bl_idname = "ogre.import_mesh" + bl_label = "Import Ogre" + bl_options = {'REGISTER'} + # import logic is contained in the subclass + + def __init__(self, *args, **kwargs): + bpy.types.Operator.__init__(self, *args, **kwargs) + _OgreCommonImport_.__init__(self) diff --git a/assets/blender/scripts/blender2ogre/io_ogre/util.py b/assets/blender/scripts/blender2ogre/io_ogre/util.py new file mode 100644 index 0000000..5a23969 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/util.py @@ -0,0 +1,879 @@ +from os.path import split, splitext +import bpy, logging, logging, mathutils, os, re, subprocess, sys, time +from . import config +from . report import Report + +logger = logging.getLogger('util') + +class ProgressBar: + def __init__(self, name, total): + self.name = name + self.progressScale = 1.0 / total + self.step = max(1, int(total / 100)) + + # Initialize progress through Blender cursor + bpy.context.window_manager.progress_begin(0, 100) + + def update(self, value): + if value % self.step != 0: + return + + # Update progress in console + percent = (value + 1) * self.progressScale + sys.stdout.write( "\r + "+self.name+" [" + '=' * int(percent * 50) + '>' + '.' * int(50 - percent * 50) + "] " + str(round(percent * 100)) + "% ") + sys.stdout.flush() + + # Update progress through Blender cursor + bpy.context.window_manager.progress_update(percent) + +def xml_converter_parameters(): + """ + Return the name of the ogre converter + """ + if sys.platform.startswith("win"): + # Don't display the Windows GPF dialog if the invoked program dies. + # See comp.os.ms-windows.programmer.win32 + # How to suppress crash notification dialog?, Jan 14,2004 - + # Raymond Chen's response [1] + + import ctypes + SEM_NOGPFAULTERRORBOX = 0x0002 # From MSDN + ctypes.windll.kernel32.SetErrorMode(SEM_NOGPFAULTERRORBOX); + + exe = config.get('OGRETOOLS_XML_CONVERTER') + proc = subprocess.Popen([exe,'-v'],stdout=subprocess.PIPE) + output, _ = proc.communicate() + + pattern = re.compile("OgreXMLConverter ([^ ]+) \((\d+)\.(\d+).(\d+)\) ([^ ]+)") + + match = pattern.match(output.decode('utf-8')) + + if match: + version = (int(match.group(2)),int(match.group(3)),int(match.group(4))) + return (match.group(1), version, match.group(5)) + + return ("unknown", (1,9,0),"unknown") # means pre 1.10 + +def xml_converter_version(): + return xml_converter_parameters()[1] + +def mesh_tool_parameters(): + """ + Extract OgreMeshTool version info and stuff + """ + exe = config.get('OGRETOOLS_XML_CONVERTER') + exe_path, name = os.path.split(exe) + proc = subprocess.Popen([exe], stdout=subprocess.PIPE, cwd=exe_path) + output, _ = proc.communicate() + + pattern = re.compile("OgreMeshTool ([^ ]+) \((\d+)\.(\d+).(\d+)\) ([^ ]+)") + match = pattern.match(output.decode('utf-8')) + + if match: + version = (int(match.group(2)), int(match.group(3)), int(match.group(4))) + return (match.group(1), version, match.group(5)) + + return ("unknown", (0,0,0), "unknown") # should not happen + +def mesh_tool_version(): + return mesh_tool_parameters()[1] + +# Calls OgreMeshUpgrader to perform: +# - Edge List / LOD generation +# - Optimize vertex buffers for shaders +def mesh_upgrade_tool(infile): + exe = config.get('OGRETOOLS_MESH_UPGRADER') + + # OgreMeshUpgrader only works in tandem with OgreXMLConverter, which are both Ogre v1.x tools. + # For Ogre v2.x we will use OgreMeshTool, which can perform the same operations + if detect_converter_type() != "OgreXMLConverter": + return + + output_path, filename = os.path.split(infile) + + if not os.path.exists(infile): + logger.warn("Cannot find file mesh file: %s, unable run OgreMeshUpgrader" % filename) + + if config.get('LOD_GENERATION') == '0': + Report.warnings.append("OgreMeshUpgrader failed, LODs will not be generated for this mesh: %s" % filename) + + if config.get('GENERATE_EDGE_LISTS') is True: + Report.warnings.append("OgreMeshUpgrader failed, Edge Lists will not be generated for this mesh: %s" % filename) + + if config.get('OPTIMISE_VERTEX_CACHE') is True: + Report.warnings.append("OgreMeshUpgrader failed, Vertex Cache will not be optimized for this mesh: %s" % filename) + + if config.get('PACK_INT_10_10_10_2') is True: + Report.warnings.append("OgreMeshUpgrader failed, Normals won't be packed for this mesh: %s" % filename) + + return + + # Extract converter type from its output + try: + exe_path, exe_name = os.path.split(exe) + proc = subprocess.Popen([exe], stdout=subprocess.PIPE, cwd=exe_path) + output, _ = proc.communicate() + output = output.decode('utf-8') + except: + output = "" + + # Check to see if the executable is actually OgreMeshUpgrader + if output.find("OgreMeshUpgrader") == -1: + logger.warn("Cannot find suitable OgreMeshUpgrader executable, unable to generate LODs / Edge Lists / Vertex buffer optimization") + return + + cmd = [exe] + + if config.get('LOD_LEVELS') > 0 and config.get('LOD_GENERATION') == '0': + cmd.append('-l') + cmd.append(str(config.get('LOD_LEVELS'))) + + cmd.append('-d') + cmd.append(str(config.get('LOD_DISTANCE'))) + + cmd.append('-p') + cmd.append(str(config.get('LOD_PERCENT'))) + + # Edge Lists: why is the logic so convoluted? + # OGRE < 14.0: the option is '-e' and the option is NOT to generate the edge lists (reverse logic which created the whole problem) + # OGRE >= 14.0: the option is now named '-el' and the option is to generate the edge lists + + # [OGRE >= 14.0] Generate Edge Lists (-el = generate edge lists (for stencil shadows)) + if config.get('GENERATE_EDGE_LISTS') is True: + # If OGRE version is >= 14.0 + if output.find("-el") != -1: + cmd.append('-el') + # [OGRE < 14.0] Don't generate Edge Lists (-e = DON'T generate edge lists (for stencil shadows)) + else: + # If OGRE version is < 14.0 + if output.find("-el") == -1: + cmd.append('-e') + + # Vertex Cache Optimization + # https://www.ogre3d.org/2024/02/26/ogre-14-2-released#vertex-cache-optimization-in-meshupgrader + if config.get('OPTIMISE_VERTEX_CACHE') is True: + if output.find("-optvtxcache") == -1: + logger.warn("Vertex Cache Optimization requested, but this version of OgreMeshUpgrader does not support it (OGRE >= 14.2)") + Report.warnings.append("Vertex Cache Optimization requested, but this version of OgreMeshUpgrader does not support it (OGRE >= 14.2)") + else: + cmd.append('-optvtxcache') + + # Normal Packing + # https://www.ogre3d.org/2022/06/07/ogre-13-4-released#vetint1010102norm-support-added + if config.get('PACK_INT_10_10_10_2') is True: + if output.find("-pack") == -1: + logger.warn("Normal Packing requested, but this version of OgreMeshUpgrader does not support it (OGRE >= 13.4)") + Report.warnings.append("Normal Packing requested, but this version of OgreMeshUpgrader does not support it (OGRE >= 13.4)") + else: + cmd.append('-pack') + + # Put logfile into output directory + use_logger = False + logfile = os.path.join(output_path, 'OgreMeshUpgrader.log') + + # Check to see if the -log option is available in this OgreMeshUpgrader version + if output.find("-log") != -1: + use_logger = True + cmd.append('-log') + cmd.append(logfile) + + # Finally, specify input file + cmd.append(infile) + + if config.get('LOD_LEVELS') > 0 and config.get('LOD_GENERATION') == '0': + logger.info("* Generating %s LOD levels for mesh: %s" % (config.get('LOD_LEVELS'), filename)) + + if config.get('GENERATE_EDGE_LISTS') is True and ('-e' not in cmd or '-el' in cmd): + logger.info("* Generating Edge Lists for mesh: %s" % filename) + + if config.get('OPTIMISE_VERTEX_CACHE') is True and '-optvtxcache' in cmd: + logger.info("* Optimizing Vertex Cache for mesh: %s" % filename) + + if config.get('PACK_INT_10_10_10_2') is True and '-pack' in cmd: + logger.info("* Packing Normals for mesh: %s" % filename) + + # First try to execute with the -log option + logger.debug("%s" % " ".join(cmd)) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) + output, error = proc.communicate() + + if use_logger == False: + # If this OgreMeshUpgrader does not have -log then use python to write the output of stdout to a log file + with open(logfile, 'w') as log: + log.write(output) + + if proc.returncode != 0: + logger.warn("OgreMeshUpgrader failed, LODs / Edge Lists / Vertex buffer optimizations will not be generated for this mesh: %s" % filename) + + if config.get('LOD_LEVELS') > 0 and config.get('LOD_GENERATION') == '0': + Report.warnings.append("OgreMeshUpgrader failed, LODs will not be generated for this mesh: %s" % filename) + + if config.get('GENERATE_EDGE_LISTS') is True: + Report.warnings.append("OgreMeshUpgrader failed, Edge Lists will not be generated for this mesh: %s" % filename) + + if config.get('OPTIMISE_VERTEX_CACHE') is True: + Report.warnings.append("OgreMeshUpgrader failed, Vertex Cache will not be optimized for this mesh: %s" % filename) + + if config.get('PACK_INT_10_10_10_2') is True: + Report.warnings.append("OgreMeshUpgrader failed, Normals won't be packed for this mesh: %s" % filename) + + if error != None: + logger.error(error) + logger.warn(output) + else: + if config.get('LOD_LEVELS') > 0 and config.get('LOD_GENERATION') == '0': + logger.info("- Generated %s LOD levels for mesh: %s" % (config.get('LOD_LEVELS'), filename)) + + if config.get('GENERATE_EDGE_LISTS') is True and ('-e' not in cmd or '-el' in cmd): + logger.info("- Generated Edge Lists for mesh: %s" % filename) + + if config.get('OPTIMISE_VERTEX_CACHE') is True and '-optvtxcache' in cmd: + logger.info("- Optimized Vertex Cache for mesh: %s" % filename) + + if config.get('PACK_INT_10_10_10_2') is True and '-pack' in cmd: + logger.info("- Packed Normals for mesh: %s" % filename) + +def detect_converter_type(): + # todo: executing the same exe twice might not be efficient but will do for now + # (twice because version will be extracted later in xml_converter_parameters) + exe = config.get('OGRETOOLS_XML_CONVERTER') + + # extract converter type from its output + try: + proc = subprocess.Popen([exe], stdout=subprocess.PIPE) + output, _ = proc.communicate() + output = output.decode('utf-8') + except Exception as e: + logger.warn(e) + output = "" + + if output.find("OgreXMLConverter") != -1: + return "OgreXMLConverter" + if output.find("OgreMeshTool") != -1: + return "OgreMeshTool" + return "unknown" + +def mesh_convert(infile): + # todo: Show a UI dialog to show this error. It's pretty fatal for normal usage. + # We should show how to configure the converter location in config panel or tell the default path. + exe = config.get('OGRETOOLS_XML_CONVERTER') + + converter_type = detect_converter_type() + if converter_type == "OgreXMLConverter": + version = xml_converter_version() + elif converter_type == "OgreMeshTool": + version = mesh_tool_version() + elif converter_type == "unknown": + logger.warn("Cannot find suitable OgreXMLConverter or OgreMeshTool executable") + Report.warnings.append("Cannot find suitable OgreXMLConverter or OgreMeshTool executable, binary mesh files won't be generated") + return False + + cmd = [exe] + + if converter_type == "OgreXMLConverter": + # Use quiet mode by default (comment this if you want more debug info out) + cmd.append('-q') + + # use ubyte4_norm colour type + if version >= (1, 12, 7): + cmd.append('-byte') + + # Put logfile into output directory + logfile_path, name = os.path.split(infile) + cmd.append('-log') + cmd.append(os.path.join(logfile_path, 'OgreXMLConverter.log')) + + # Finally, specify input file + cmd.append(infile) + + ret = subprocess.call(cmd) + + # Instead of asserting, report an error + if ret != 0: + logger.error("OgreXMLConverter returned with non-zero status, check OgreXMLConverter.log") + logger.info(" ".join(cmd)) + Report.errors.append("OgreXMLConverter finished with non-zero status converting mesh: (%s), unable to proceed" % name) + return False + else: + return True + + else: + # Convert to v2 format if required + cmd.append('-%s' % config.get('MESH_TOOL_VERSION')) + + # Ask MeshTool to "unoptimize" if necessary. Otherwise we can't read half and qtangents + cmd.append("-U") + # Finally, specify input file + cmd.append(infile) + # MeshTool needs us to specify output file + cmd.append(infile + ".xml") + + # OgreMeshTool must be run from its own directory (so setting cwd accordingly) + # otherwise it will complain about missing render system (missing plugins_tools.cfg) + exe_path, name = os.path.split(exe) + logger.debug("%s" % " ".join(cmd)) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, cwd=exe_path) + output, error = proc.communicate() + + # Open log file to replace old logging feature that the new tool dropped + # The log file will be created alongside the exported mesh + if config.get('ENABLE_LOGGING') is True: + logfile_path, name = os.path.split(infile) + logfile = os.path.join(logfile_path, 'OgreMeshTool.log') + + with open(logfile, 'w') as log: + log.write(output) + + # Check converter status + if proc.returncode != 0: + logger.error("OgreMeshTool finished with non-zero status, check OgreMeshTool.log") + logger.info(" ".join(cmd)) + Report.errors.append("OgreMeshTool finished with non-zero status converting mesh: (%s), unable to proceed" % name) + return False + else: + return True + + +def xml_convert(infile, has_uvs=False): + # todo: Show a UI dialog to show this error. It's pretty fatal for normal usage. + # We should show how to configure the converter location in config panel or tell the default path. + exe = config.get('OGRETOOLS_XML_CONVERTER') + + converter_type = detect_converter_type() + if converter_type == "OgreXMLConverter": + version = xml_converter_version() + elif converter_type == "OgreMeshTool": + version = mesh_tool_version() + elif converter_type == "unknown": + logger.warn("Cannot find suitable OgreXMLConverter or OgreMeshTool executable") + Report.warnings.append("Cannot find suitable OgreXMLConverter or OgreMeshTool executable, binary mesh files won't be generated") + return + + cmd = [exe] + + if config.get('EXTREMITY_POINTS') > 0 and converter_type == "OgreXMLConverter": + cmd.append('-x') + cmd.append(config.get('EXTREMITY_POINTS')) + + # OgreMeshTool (OGRE v2): -e = DON'T generate edge lists (for stencil shadows) + # OgreXMLConverter (OGRE < 1.10): -e = DON'T generate edge lists (for stencil shadows) + if config.get('GENERATE_EDGE_LISTS') is False and (version < (1,10,0) or converter_type == "OgreMeshTool"): + cmd.append('-e') + + if config.get('GENERATE_TANGENTS') != "0" and converter_type == "OgreMeshTool": + cmd.append('-t') + cmd.append('-ts') + cmd.append(str(config.get('GENERATE_TANGENTS'))) + + if config.get('OPTIMISE_VERTEX_BUFFERS') and converter_type == "OgreMeshTool": + cmd.append('-O') + cmd.append(config.get('OPTIMISE_VERTEX_BUFFERS_OPTIONS')) + + if config.get('OPTIMISE_ANIMATIONS') is not True: + cmd.append('-o') + + if converter_type == "OgreXMLConverter": + # Use quiet mode by default (comment this if you want more debug info out) + cmd.append('-q') + + # use ubyte4_norm colour type + if version >= (1, 12, 7): + cmd.append('-byte') + + # Put logfile into output directory + logfile_path, name = os.path.split(infile) + cmd.append('-log') + cmd.append(os.path.join(logfile_path, 'OgreXMLConverter.log')) + + # Finally, specify input file + cmd.append(infile) + + logger.debug("%s" % " ".join(cmd)) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) + output, error = proc.communicate() + + # Instead of asserting, report an error + if proc.returncode != 0: + logger.error("OgreXMLConverter returned with non-zero status, check OgreXMLConverter.log") + logger.info(" ".join(cmd)) + Report.errors.append("OgreXMLConverter finished with non-zero status converting mesh: (%s), it might not have been properly generated" % name) + + # Clean up .xml file after successful conversion + if (proc.returncode == 0) and (config.get('EXPORT_XML_DELETE') is True): + logger.info("Removing generated xml file after conversion: %s" % infile) + os.remove(infile) + + else: + # Convert to v2 format if required + cmd.append('-%s' % config.get('MESH_TOOL_VERSION')) + + # If requested by the user, generate LOD levels through OgreMeshUpgrader/OgreMeshTool + if config.get('LOD_LEVELS') > 0 and config.get('LOD_GENERATION') == '0': + cmd.append('-l') + cmd.append(str(config.get('LOD_LEVELS'))) + + cmd.append('-d') + cmd.append(str(config.get('LOD_DISTANCE'))) + + cmd.append('-p') + cmd.append(str(config.get('LOD_PERCENT'))) + + # Finally, specify input file + cmd.append(infile) + + # Log command to console + logger.info("Converting mesh from XML to binary: %s" % " ".join(cmd)) + + # OgreMeshTool must be run from its own directory (so setting cwd accordingly) + # otherwise it will complain about missing render system (missing plugins_tools.cfg) + exe_path, name = os.path.split(exe) + logger.debug("%s" % " ".join(cmd)) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, cwd=exe_path) + output, error = proc.communicate() + + # Open log file to replace old logging feature that the new tool dropped + # The log file will be created alongside the exported mesh + if config.get('ENABLE_LOGGING') is True: + logfile_path, name = os.path.split(infile) + logfile = os.path.join(logfile_path, 'OgreMeshTool.log') + + with open(logfile, 'w') as log: + log.write(output) + + # Check converter status + if proc.returncode != 0: + logger.error("OgreMeshTool finished with non-zero status, check OgreMeshTool.log") + logger.info(" ".join(cmd)) + Report.errors.append("OgreMeshTool finished with non-zero status converting mesh: (%s), it might not have been properly generated" % name) + + # Clean up .xml file after successful conversion + if (proc.returncode == 0) and (config.get('EXPORT_XML_DELETE') is True): + logger.info("Removing generated xml file after conversion: %s" % infile) + os.remove(infile) + +def image_magick( image, origin_filepath, target_filepath, separate_channel=None): + exe = config.get('IMAGE_MAGICK_CONVERT') + cmd = [ exe, origin_filepath ] + + x,y = image.size + + if separate_channel is not None: + cmd.append('-set') + cmd.append('colorspace') + cmd.append('RGB') + cmd.append('-channel') + cmd.append('{}'.format(separate_channel)) + cmd.append('-separate') + + if x > config.get('MAX_TEXTURE_SIZE') or y > config.get('MAX_TEXTURE_SIZE'): + cmd.append( '-resize' ) + cmd.append( str(config.get('MAX_TEXTURE_SIZE')) ) + + if target_filepath.endswith('.dds'): + cmd.append('-define') + cmd.append('dds:mipmaps={}'.format(config.get('DDS_MIPS'))) + + cmd.append(target_filepath) + logger.debug('image magick: "%s"', ' '.join(cmd)) + subprocess.call(cmd) + +def swap(vec): + if config.get('SWAP_AXIS') == 'xyz': return vec + elif config.get('SWAP_AXIS') == 'xzy': + if len(vec) == 3: return mathutils.Vector( [vec.x, vec.z, vec.y] ) + elif len(vec) == 4: return mathutils.Quaternion( [ vec.w, vec.x, vec.z, vec.y] ) + elif config.get('SWAP_AXIS') == '-xzy': + if len(vec) == 3: return mathutils.Vector( [-vec.x, vec.z, vec.y] ) + elif len(vec) == 4: return mathutils.Quaternion( [ vec.w, -vec.x, vec.z, vec.y] ) + elif config.get('SWAP_AXIS') == 'xz-y': + if len(vec) == 3: return mathutils.Vector( [vec.x, vec.z, -vec.y] ) + elif len(vec) == 4: return mathutils.Quaternion( [ vec.w, vec.x, vec.z, -vec.y] ) + else: + logging.warn( 'unknown swap axis mode %s', config.get('SWAP_AXIS') ) + assert 0 + +def uid(ob): + if ob.uid == 0: + high = 0 + multires = 0 + for o in bpy.data.objects: + if o.uid > high: high = o.uid + if o.use_multires_lod: multires += 1 + high += 1 + (multires*10) + if high < 100: high = 100 # start at 100 + ob.uid = high + return ob.uid + +def timer_diff_str(start): + return "%0.2f" % (time.time()-start) + +def find_bone_index( ob, arm, groupidx): # sometimes the groups are out of order, this finds the right index. + if groupidx < len(ob.vertex_groups): # reported by Slacker + vg = ob.vertex_groups[ groupidx ] + j = 0 + for i, bone in enumerate(arm.pose.bones): + if (config.get('ONLY_DEFORMABLE_BONES') is True) and (bone.bone.use_deform is False): + j = j + 1 # if we skip bones we need to adjust the id + if bone.name == vg.name: + return i-j + else: + logger.warn('<%s> Object vertex groups (%s) not in sync with armature: %s', ob.name, groupidx, arm.name) + +def mesh_is_smooth( mesh ): + for face in mesh.tessfaces: + if face.use_smooth: return True + +def find_uv_layer_index( uvname, material=None ): + # This breaks if users have uv layers with same name with different indices over different objects + idx = 0 + for mesh in bpy.data.meshes: + if material is None or material.name in mesh.materials: + if mesh.uv_textures: + names = [ uv.name for uv in mesh.uv_textures ] + if uvname in names: + idx = names.index( uvname ) + break # should we check all objects using material and enforce the same index? + return idx + +def has_custom_property( a, name ): + for prop in a.items(): + n,val = prop + if n == name: + return True + +def is_strictly_simple_terrain( ob ): + # A default plane, with simple-subsurf and displace modifier on Z + if len(ob.data.vertices) != 4 and len(ob.data.tessfaces) != 1: + return False + elif len(ob.modifiers) < 2: + return False + elif ob.modifiers[0].type != 'SUBSURF' or ob.modifiers[1].type != 'DISPLACE': + return False + elif ob.modifiers[0].subdivision_type != 'SIMPLE': + return False + elif ob.modifiers[1].direction != 'Z': + return False # disallow NORMAL and other modes + else: + return True + +def get_image_textures( mat ): + r = [] + for s in mat.texture_paint_images: + if s: + r.append( s ) + return r + +def texture_image_path(image): + if not image: + return None + + if image.library: # support library linked textures + libpath = split(bpy.path.abspath(image.library.filepath))[0] + return bpy.path.abspath(image.filepath, libpath) + else: + if image.packed_file: + return image.name + ".png" + + return bpy.path.abspath( image.filepath ) + +def objects_merge_materials(objs): + """ + return a list that contains unique material objects + """ + materials = set() + for obj in objs: + for mat in obj.data.materials: + # adapt to Blender API change: https://developer.blender.org/docs/release_notes/4.2/eevee/#shading-modes + mat['visible_shadow'] = obj.visible_shadow + materials.add(mat) + return materials + +def indent( level, *args ): + if not args: + return ' ' * level + else: + a = '' + for line in args: + a += ' ' * level + a += line + a += '\n' + return a + +def gather_instances(): + instances = {} + for ob in bpy.context.scene.objects: + if ob.data and ob.data.users > 1: + if ob.data not in instances: + instances[ ob.data ] = [] + instances[ ob.data ].append( ob ) + return instances + +def select_instances( context, name ): + for ob in bpy.context.scene.objects: + ob.select_set(False) + ob = bpy.context.scene.objects[ name ] + if ob.data: + inst = gather_instances() + for ob in inst[ ob.data ]: ob.select_set(True) + bpy.context.scene.objects.active = ob + +def select_group( context, name, options={} ): + for ob in bpy.context.scene.objects: + ob.select_set(False) + for grp in bpy.data.collections: + if grp.name == name: + # context.scene.objects.active = grp.objects + # Note that the context is read-only. These values cannot be modified directly, + # though they may be changed by running API functions or by using the data API. + # So bpy.context.object = obj will raise an error. But bpy.context.scene.objects.active = obj + # will work as expected. - http://wiki.blender.org/index.php?title=Dev:2.5/Py/API/Intro&useskin=monobook + bpy.context.scene.objects.active = grp.objects[0] + for ob in grp.objects: + ob.select_set(True) + else: + pass + +def get_objects_using_materials( mats ): + obs = [] + for ob in bpy.data.objects: + if ob.type == 'MESH': + for mat in ob.data.materials: + if mat in mats: + if ob not in obs: + obs.append( ob ) + break + return obs + +def get_materials_using_image( img ): + mats = [] + for mat in bpy.data.materials: + for slot in get_image_textures( mat ): + if slot.texture.image == img: + if mat not in mats: + mats.append( mat ) + return mats + +def get_parent_matrix( ob, objects ): + if not ob.parent: + return mathutils.Matrix(((1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1))) # Requiered for Blender SVN > 2.56 + else: + if ob.parent in objects: + return ob.parent.matrix_world.copy() + else: + return get_parent_matrix(ob.parent, objects) + +def merge_group( group ): + logger.info('+ Merge Group: %s' % group.name ) + copies = [] + copies_meshes = [] + for ob in group.objects: + if ob.type == 'MESH': + o2 = ob.copy() + copies.append( o2 ) + o2.data = bpy.data.meshes.new_from_object( o2 ) + copies_meshes.append( o2.data ) + while o2.modifiers: + o2.modifiers.remove( o2.modifiers[0] ) + bpy.context.scene.collection.objects.link( o2 ) #; o2.select = True + + name = group.name[len("merge."):] if group.name != "merge." else "mergeGroup" + + merged = merge( copies ) + merged.name = name + merged.data.name = name #2.8 not renaming, readonly? + + # Clean up orphan meshes + for copy_mesh in copies_meshes: + if copy_mesh.name != name: + logger.debug("Removing temporary mesh: %s" % copy_mesh.name) + bpy.data.meshes.remove(copy_mesh) + + return merged + +def merge_objects( objects, name='_temp_', transform=None ): + assert objects + copies = [] + for ob in objects: + ob.select_set(False) + if ob.type == 'MESH': + o2 = ob.copy(); copies.append( o2 ) + o2.data = o2.to_mesh() # collaspe modifiers + while o2.modifiers: + o2.modifiers.remove( o2.modifiers[0] ) + if transform: + o2.matrix_world = transform @ o2.matrix_local + bpy.context.scene.collection.objects.link( o2 ) #; o2.select_set(True) + merged = merge( copies ) + merged.name = name + merged.data.name = name #2.8 not renaming, readonly? + + return merged + +def merge( objects ): + for ob in bpy.context.selected_objects: + ob.select_set(False) + for ob in objects: + logger.info(' - %s' % ob.name) + ob.select_set(True) + assert not ob.library + #2.8update + bpy.context.view_layer.objects.active = ob + bpy.ops.object.join() + + return bpy.context.active_object + +def get_merge_group( ob, prefix='merge' ): + m = [] + for grp in ob.users_collection: + if grp.name.lower().startswith(prefix + "."): + m.append( grp ) + if len(m)==1: + #if ob.data.users != 1: + # logger.warn( 'An instance cannot be in a merge group' ) + # return + return m[0] + elif m: + logger.warn('Object %s in two %s groups at the same time' % (ob.name, prefix)) + return None + +def wordwrap( txt ): + r = [''] + for word in txt.split(' '): # do not split on tabs + word = word.replace('\t', ' '*3) + r[-1] += word + ' ' + if len(r[-1]) > 90: + r.append( '' ) + return r + +def get_lights_by_type( T ): + r = [] + for ob in bpy.context.scene.objects: + if ob.type == 'LIGHT': + if ob.data.type == T: + r.append( ob ) + return r + +invalid_chars_in_name = '"<>\:' # "<> is xml prohibited, : is Ogre prohibited, \ is standard escape char +invalid_chars_in_filename = '/|?*' + invalid_chars_in_name +invalid_chars_spaces = ' \t' + +def clean_object_name(value, invalid_chars = invalid_chars_in_filename, spaces = True): + if spaces: + invalid_chars += invalid_chars_spaces + + for invalid_char in invalid_chars: + value = value.replace(invalid_char, '_') + return value; + +def get_subcollision_meshes(): + ''' returns all collision meshes found in the scene ''' + r = [] + for ob in bpy.context.scene.objects: + if ob.type=='MESH' and ob.subcollision: r.append( ob ) + return r + +def get_objects_with_subcollision(): + ''' returns objects that have active sub-collisions ''' + r = [] + for ob in bpy.context.scene.objects: + if ob.type=='MESH' and ob.collision_mode not in ('NONE', 'PRIMITIVE'): + r.append( ob ) + return r + +def get_subcollisions(ob): + prefix = '%s.' %ob.collision_mode + r = [] + for child in ob.children: + if child.subcollision and child.name.startswith( prefix ): + r.append( child ) + return r + +class IndentedWriter(object): + """ + Can be used to write well formed documents. + + w = IndentedWriter() + with w.word("hello").embed(): + w.indent().word("world").string("!!!").nl() + with w.word("hello").embed(): + w.iline("schnaps") + + print(w.text) + > hello { + world "!!!" + hello { + schnaps + } + } + """ + + sym_stack = [] + text = "" + embed_syms = None + + def __init__(self, indent = 0): + for i in range(indent): + sym_stack.append(None) + + def __enter__(self, **kwargs): + begin_sym, end_sym, nl, space = self.embed_syms + if space: + self.write(" ") + self.write(begin_sym) + if nl: + self.nl() + self.sym_stack.append(end_sym) + + def __exit__(self, *kwargs): + sym = self.sym_stack.pop() + self.indent().write(sym).nl() + + def embed(self, begin_sym="{", end_sym="}", nl=True, space=True): + self.embed_syms = (begin_sym, end_sym, nl, space) + return self + + def string(self, text): + self.write("\"") + self.write(text) + self.write("\"") + return self + + def indent(self, plus=0): + return self.write(" " * (len(self.sym_stack) + plus)) + + def real(self, f): + return self.word(str(f)) + + def integer(self, i): + return self.word(str(i)) + + def round(self, f, p=6): + """ + Adds a rounded float + f: float value + p: precision + """ + return self.word(str(round(f, p))) + + def nl(self): + self.write("\n") + return self + + def write(self, text): + self.text += text + return self + + def word(self, text): + return self.write(" ").write(str(text)) + + def iwrite(self, text): + return self.indent().write(str(text)) + + def iword(self, text): + return self.indent().write(str(text)) + + def iline(self, text): + return self.indent().line(text) + + def line(self, text): + return self.write(text + "\n") diff --git a/assets/blender/scripts/blender2ogre/io_ogre/xml.py b/assets/blender/scripts/blender2ogre/io_ogre/xml.py new file mode 100644 index 0000000..7291013 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/io_ogre/xml.py @@ -0,0 +1,95 @@ +from xml.sax.saxutils import XMLGenerator, quoteattr + +class SimpleSaxWriter(): + def __init__(self, output, root_tag, root_attrs): + self.output = output + self.root_tag = root_tag + self.indent=0 + output.write("\n") + self.start_tag(root_tag, root_attrs) + + def _out_tag(self, name, attrs, isLeaf): + # sorted attributes -- don't want attributes output in random order, which is what the XMLGenerator class does + self.output.write(" " * self.indent) + self.output.write("<%s" % name) + sortedNames = sorted( attrs.keys() ) # sorted list of attribute names + for name in sortedNames: + value = attrs[ name ] + # if not of type string, + if not isinstance(value, str): + # turn it into a string + value = str(value) + self.output.write(" %s=%s" % (name, quoteattr(value))) + if isLeaf: + self.output.write("/") + else: + self.indent += 4 + self.output.write(">\n") + + def start_tag(self, name, attrs): + self._out_tag(name, attrs, False) + + def end_tag(self, name): + self.indent -= 4 + self.output.write(" " * self.indent) + self.output.write("\n" % name) + + def leaf_tag(self, name, attrs): + self._out_tag(name, attrs, True) + + def close(self): + self.end_tag( self.root_tag ) + +class RElement(object): + def appendChild( self, child ): + self.childNodes.append( child ) + + def setAttribute( self, name, value ): + self.attributes[name]=value + + def __init__(self, tag): + self.tagName = tag + self.childNodes = [] + self.attributes = {} + + def toprettyxml(self, lines, indent ): + s = '<%s ' % self.tagName + sortedNames = sorted( self.attributes.keys() ) + for name in sortedNames: + value = self.attributes[name] + if not isinstance(value, str): + value = str(value) + s += '%s=%s ' % (name, quoteattr(value)) + if not self.childNodes: + s += '/>'; lines.append( (' '*indent)+s ) + else: + s += '>'; lines.append( (' '*indent)+s ) + indent += 1 + for child in self.childNodes: + child.toprettyxml( lines, indent ) + indent -= 1 + lines.append((' '*indent) + '' % self.tagName ) + +class RDocument(object): + def __init__(self): + self.documentElement = None + self.comments = [] + + def appendChild(self, root): + self.documentElement = root + + def addComment(self, text): + self.comments.append("".format(text)) + + def createElement(self, tag): + e = RElement(tag) + e.document = self + return e + + def toprettyxml(self): + indent = 0 + lines = [''] + lines += self.comments + self.documentElement.toprettyxml(lines, indent) + return '\n'.join(lines) + diff --git a/assets/blender/scripts/blender2ogre/refactor.txt b/assets/blender/scripts/blender2ogre/refactor.txt new file mode 100644 index 0000000..dd9d0de --- /dev/null +++ b/assets/blender/scripts/blender2ogre/refactor.txt @@ -0,0 +1,64 @@ + +The new structure: + ++= io_ogre/ # python source code + +- __init__.py # changelog, blender registration and importing + += ogre/ # code specific to ogre, export formats, xml, materials, ... + +- converter.py # OgreXmlConverter wrapper + +- export.py # general exporting api + +- material.py + +- mesh.py + +- program.py + +- skeleton.py + += tundra/ # real extend plugin (not working yet) + +- config.py # configuration loading, saving + +- util.py # general purpose functions used in many places + +- report.py # the reporting as a module rather than inlined + +- xml.py # xml utility + +- properties.py # add custom types to objects, materials, ... + +- ... + += ui/ + +- __init__.py # general purpose ui elements + +- export.py # export panel to export scene/mesh + +- helper.py # help/api docu for the plugin + +- material.py # material specific additions + += launch/ # several launch scripts (windows bat, linux sh, ...) + + +A more clear separation between realXtend, Jmoneky, ... has been made. As I see this now this plugin +is mainly for exporting to ogre file format, thus all other code should be moved out... + +What I have done: + +* moved 'general purpuse' code into util.py +* CMesh is not used in the source code. moved to unused.py +* JMonkey moved to unused. There was a comment saying 'todo: remove...' +* The setup of _TXML_ -> _OgreCommonExport_ +-> OgrePlugin + | + +-> realXtend Plugin + +is a bit weired, I don't see why there should be a dependency on _TXML_, since it is only used +for realXtend... now it is the following way: + +_OgreCommonExport_ +-> OgrePlugin + | + +-> _TXML_ -> realXtend Plugin + +OgrePlugin does not redefine properties of _OgreCommonExport_ (EX_* = TypeProperty(...) ) +* different parts of ogre export (image,material,mesh,program,skeleton) got their own modules +* moved the rpython code to the rogre folder +* moved NVIDIA texture tool doc into unused.py +* usage of CONFIG is discouraged and you should use config.get('setting') and config.update(key=value, key1=value2, ...) + +This resulted in about ~2200 loc in blender2ogre/__init__.py (from 7730). +I even further reduced it to 2000 loc, and removed some duplicate code. +6.8.14: further stripped down to 1310 loc + + +Second refactoring round +=== + +* for all usable ui features the classes has been renamed. + OP_name - an operator + MT_name - a menu + HT_name - a header diff --git a/assets/blender/scripts/blender2ogre/test/run.py b/assets/blender/scripts/blender2ogre/test/run.py new file mode 100644 index 0000000..07aa3e6 --- /dev/null +++ b/assets/blender/scripts/blender2ogre/test/run.py @@ -0,0 +1,3 @@ +import bpy +bpy.ops.preferences.addon_enable(module='io_ogre') +bpy.ops.ogre.export(filepath="test.scene") \ No newline at end of file diff --git a/assets/blender/scripts/export_buildings.py b/assets/blender/scripts/export_buildings.py index e9efe97..a674647 100644 --- a/assets/blender/scripts/export_buildings.py +++ b/assets/blender/scripts/export_buildings.py @@ -12,6 +12,8 @@ argv = sys.argv argv = argv[argv.index("--") + 1:] sys.path.insert(0, os.getcwd() + "/assets/blender/scripts") +sys.path.insert(1, os.getcwd() + "/assets/blender/scripts/blender2ogre") +import io_ogre gltf_file = argv[0] print("Exporting to " + gltf_file) @@ -40,6 +42,30 @@ basepath = os.getcwd() # export_morph_tangent=False, export_morph_animation=True, export_morph_reset_sk_data=True, # export_lights=False, export_nla_strips=True, will_save_settings=False, filter_glob="*.glb") +for obj in bpy.data.objects: + if obj.name.endswith("-col"): + bpy.data.objects.remove(obj) + +scene_file = gltf_file.replace(".glb", "").replace(".gltf", "") + ".scene" +bpy.ops.ogre.export(filepath=scene_file, + EX_SWAP_AXIS='xz-y', + EX_V2_MESH_TOOL_VERSION='v2', + EX_EXPORT_XML_DELETE=True, + EX_SCENE=True, + EX_SELECTED_ONLY=False, + EX_EXPORT_HIDDEN=False, + EX_FORCE_CAMERA=False, + EX_FORCE_LIGHTS=False, + EX_NODE_ANIMATION=True, + EX_MATERIALS=True, + EX_SEPARATE_MATERIALS=False, + EX_COPY_SHADER_PROGRAMS=True, + EX_MESH=True, + EX_LOD_LEVELS=3, + EX_LOD_DISTANCE=100, + EX_LOD_PERCENT=40 +) + bpy.ops.export_scene.gltf(filepath=gltf_file, use_selection=False, check_existing=False, diff --git a/assets/blender/vehicles/boat.blend b/assets/blender/vehicles/boat.blend index f38451f..8206a1b 100644 --- a/assets/blender/vehicles/boat.blend +++ b/assets/blender/vehicles/boat.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eb5a1cdd5a23fbddb41cbcedc1490d19950232230e24afb2ee0e50f3c1be1c3 -size 337947 +oid sha256:8792f5e4530a5e830614a6d6cfff1471b04d9cbbda2cb96711252a392bb81597 +size 375867 diff --git a/lua-scripts/data.lua b/lua-scripts/data.lua index 9c57179..078d135 100644 --- a/lua-scripts/data.lua +++ b/lua-scripts/data.lua @@ -137,6 +137,7 @@ function Quest(name, book) return end if event == "narration_progress" then + print(" in progress...") this:_narration() elseif event == "narration_answered" then local answer = narration_get_answer() @@ -157,6 +158,7 @@ function Quest(name, book) local have_choice = false local have_paragraph = false if not this.active then + print("not active") return end if this.story:can_continue() then @@ -191,10 +193,6 @@ function Quest(name, book) else print("can NOT continue") end - if not have_choice and not have_paragraph then - this:complete() - this.active = false - end print(ret) if (#choices > 0) then print("choices!!!") @@ -202,6 +200,12 @@ function Quest(name, book) else narrate(ret) end + if not have_choice and not have_paragraph then + this:complete() + this.active = false + else + print("can continue") + end end, complete = function(this) print(this.name .. 'complete') @@ -224,6 +228,11 @@ function StartGameQuest() quest.activate = function(this) print('activate...') local mc_is_free = function() + this.boat_id = ecs_vehicle_set("boat", 0, 0, -20, 1.75) + this.trigger_id = ecs_child_character_trigger(this.boat_id, "entered_boat", 0, 0, 0, 3, 3) + this.boat = true + local npc = ecs_npc_set("normal-female.glb", 0, 2, -20, 1.75) + ecs_character_physics_control(npc, false) ecs_character_params_set("player", "gravity", true) ecs_character_params_set("player", "buoyancy", true) end @@ -234,15 +243,13 @@ function StartGameQuest() this.base.complete(this) this.active = false if not this.boat then - local boat = ecs_vehicle_set("boat", 0, 0, -20, 1.75) - local trigger = ecs_child_character_trigger(boat, "entered_boat", 0, 0, 0, 3, 3) - local npc = ecs_npc_set("normal-female.glb", 0, 2, -20, 1.75) - this.boat = true + ecs_save_object_debug(boat, 'boat.scene') end end return quest end quests = {} +-- ecs_set_debug_drawing(true) setup_handler(function(event) print(event) for k, v in pairs(quests) do @@ -253,12 +260,11 @@ setup_handler(function(event) if event == "startup" then main_menu() elseif event == "narration_progress" then - _narration() + print("narration progress!") elseif event == "narration_answered" then local answer = narration_get_answer() story:choose(answer) print("answered:", answer) - _narration() elseif event == "new_game" then ecs_character_params_set("player", "gravity", true) ecs_character_params_set("player", "buoyancy", false) diff --git a/src/gamedata/BoatModule.cpp b/src/gamedata/BoatModule.cpp index 6cc4a25..4765090 100644 --- a/src/gamedata/BoatModule.cpp +++ b/src/gamedata/BoatModule.cpp @@ -12,24 +12,80 @@ BoatModule::BoatModule(flecs::world &ecs) ecs.module(); ecs.component(); ecs.component(); - ecs.component().on_set([](flecs::entity e, BoatType &type) { - int i; - if (e.has() || e.has()) - return; - BoatBase &boat = e.ensure(); - boat.mEnt = ECS::get().mScnMgr->createEntity( - type.resourceName); - boat.mNode = ECS::get() - .mScnMgr->getRootSceneNode() - ->createChildSceneNode(type.position, - type.orientation); - boat.mNode->attachObject(boat.mEnt); + ecs.system("CreateBoat") + .kind(flecs::OnUpdate) + .without() + .without() + .each([](flecs::entity e, const EngineData &eng, + BoatType &type) { + BoatBase &boat = e.ensure(); + if (type.resourceName.find(".glb") != + std::string::npos) { + boat.mEnt = ECS::get() + .mScnMgr->createEntity( + type.resourceName); + boat.mNode = eng.mScnMgr->getRootSceneNode() + ->createChildSceneNode( + type.position, + type.orientation); + boat.mNode->attachObject(boat.mEnt); - BoatBody &body = e.ensure(); - body.body = ECS::get().mWorld->addRigidBody( - 0, boat.mEnt, Ogre::Bullet::CT_HULL, nullptr, 2, - 0x7fffffff); - e.modified(); - }); + BoatBody &body = e.ensure(); + body.body = + ECS::get() + .mWorld->addRigidBody( + 0, boat.mEnt, + Ogre::Bullet::CT_HULL, + nullptr, 2, 0x7fffffff); + } else if (type.resourceName.find(".scene") != + std::string::npos) { + boat.mNode = + ECS::get() + .mScnMgr->getRootSceneNode() + ->createChildSceneNode( + type.position, + type.orientation); + Ogre::SceneNode *attachment = + eng.mScnMgr->getRootSceneNode() + ->createChildSceneNode(); + attachment->loadChildren(type.resourceName); + std::vector v = + attachment->getChildren(); + int i; + OgreAssert(v.size() == 1, "Bad nodes count"); + Ogre::Any any = + static_cast(v[0]) + ->getUserObjectBindings() + .getUserAny("type"); + OgreAssert(any.has_value(), + "bas node type costom prop"); + Ogre::String obj_type = + Ogre::any_cast(any); + std::cout << "type: " << obj_type << std::endl; + OgreAssert(obj_type == "boat", "not a boat"); + boat.mNode = + static_cast(v[0]); + boat.mNode->_setDerivedPosition(type.position); + boat.mNode->_setDerivedOrientation( + type.orientation); + boat.mEnt = static_cast( + boat.mNode->getAttachedObject("boat")); + BoatBody &body = e.ensure(); + body.body = + ECS::get() + .mWorld->addRigidBody( + 0, boat.mEnt, + Ogre::Bullet::CT_HULL, + nullptr, 2, 0x7fffffff); +#if 0 + boat.mEnt = eng.mScnMgr->getEntity("boat"); + boat.mNode = boat.mEnt->get + /* no need to attach anything */ + BoatBody &body = + e.ensure(); +#endif + } + e.modified(); + }); } } \ No newline at end of file diff --git a/src/gamedata/CharacterModule.cpp b/src/gamedata/CharacterModule.cpp index 641aef8..5462c06 100644 --- a/src/gamedata/CharacterModule.cpp +++ b/src/gamedata/CharacterModule.cpp @@ -10,12 +10,15 @@ namespace ECS { CharacterModule::CharacterModule(flecs::world &ecs) { + struct TriggerPhysicsChange {}; ecs.module(); ecs.component(); ecs.component(); ecs.component(); ecs.component(); ecs.component(); + ecs.component(); + ecs.component(); ecs.system("UpdateTimer") .kind(flecs::OnUpdate) .each([this](EngineData &eng, CharacterBase &ch) { @@ -207,6 +210,7 @@ CharacterModule::CharacterModule(flecs::world &ecs) .with() .with() .with() + .without() .each([this](flecs::entity e, const EngineData &eng, const CharacterBase &ch, CharacterVelocity &gr) { Ogre::Vector3 gravity(0, -9.8f, 0); @@ -245,6 +249,7 @@ CharacterModule::CharacterModule(flecs::world &ecs) .with() .without() .with() + .without() .each([this](flecs::entity e, const EngineData &eng, const CharacterBase &ch, CharacterVelocity &gr) { Ogre::Vector3 gravity(0, -9.8f, 0); @@ -728,6 +733,21 @@ CharacterModule::CharacterModule(flecs::world &ecs) << "\n"; }); #endif + ecs.system("UpdatePhysics") + .kind(flecs::OnUpdate) + .with() + .write() + .each([](flecs::entity e, const EngineData &eng, + const CharacterBody &body) { + if (e.has()) { + eng.mWorld->getBtWorld()->removeAction( + body.mController); + } else { + eng.mWorld->getBtWorld()->addAction( + body.mController); + } + e.remove(); + }); } void CharacterModule::setAnimation(AnimationControl &anim) diff --git a/src/gamedata/CharacterModule.h b/src/gamedata/CharacterModule.h index ee0e4e6..6c3b57e 100644 --- a/src/gamedata/CharacterModule.h +++ b/src/gamedata/CharacterModule.h @@ -9,6 +9,8 @@ struct Character {}; /* tag */ struct Player {}; /* tag */ struct CharacterGravity {}; struct CharacterBuoyancy {}; +struct CharacterDisablePhysics {}; +struct CharacterUpdatePhysicsState {}; struct CharacterBase { Ogre::String type; float mTimer; diff --git a/src/gamedata/Components.h b/src/gamedata/Components.h index 62df8d9..ec10646 100644 --- a/src/gamedata/Components.h +++ b/src/gamedata/Components.h @@ -24,6 +24,7 @@ struct EngineData { float startupDelay; int width; int height; + bool enableDbgDraw; }; struct Vector3 { float x; @@ -68,6 +69,9 @@ struct App { OgreBites::InputListenerChain *mInput; std::vector listeners; }; +struct CollisionShape { + btCollisionShape *shape; +}; struct InWater {}; struct TerrainReady {}; struct WaterReady {}; diff --git a/src/gamedata/EventTriggerModule.cpp b/src/gamedata/EventTriggerModule.cpp index be929ff..b41435e 100644 --- a/src/gamedata/EventTriggerModule.cpp +++ b/src/gamedata/EventTriggerModule.cpp @@ -81,12 +81,14 @@ ECS::EventTriggerModule::EventTriggerModule(flecs::world &ecs) e.get().position, Ogre::Quaternion(0, 0, 0, 1)); } + /* Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createManual( "trigger", "General"); Ogre::Entity *ent = ECS::get().mScnMgr->createEntity(mesh); body.mSceneNode->attachObject(ent); +*/ body.mBody = new btPairCachingGhostObject(); body.mBody->getWorldTransform().setOrigin(Ogre::Bullet::convert( body.mSceneNode->_getDerivedPosition())); @@ -100,8 +102,42 @@ ECS::EventTriggerModule::EventTriggerModule(flecs::world &ecs) if (kinematic) flags |= btCollisionObject::CF_STATIC_OBJECT; body.mBody->setCollisionFlags(flags); + /* ECS::get().mWorld->attachCollisionObject( body.mBody, ent, 16, 0x1); +*/ + ECS::get().mWorld->getBtWorld()->addCollisionObject( + body.mBody, 16, 0x1); + struct EntityCollisionListener { + const Ogre::MovableObject *entity; + Ogre::Bullet::CollisionListener *listener; + }; + body.mBody->setUserPointer( + new EntityCollisionListener{ nullptr, nullptr }); + class CollisionObject { + protected: + btCollisionObject *mBtBody; + btCollisionWorld *mBtWorld; + + public: + CollisionObject(btCollisionObject *btBody, + btCollisionWorld *btWorld) + : mBtBody(btBody) + , mBtWorld(btWorld) + { + } + virtual ~CollisionObject() + { + mBtWorld->removeCollisionObject(mBtBody); + delete mBtBody->getCollisionShape(); + delete mBtBody; + } + }; + auto objWrapper = std::make_shared( + body.mBody, + ECS::get().mWorld->getBtWorld()); + body.mSceneNode->getUserObjectBindings().setUserAny( + "BtCollisionObject", objWrapper); }); ecs.component().on_set( [](flecs::entity e, EventTrigger &ev) { diff --git a/src/gamedata/GameData.cpp b/src/gamedata/GameData.cpp index 961e52f..95a07ce 100644 --- a/src/gamedata/GameData.cpp +++ b/src/gamedata/GameData.cpp @@ -79,8 +79,8 @@ void setup(Ogre::SceneManager *scnMgr, Ogre::Bullet::DynamicsWorld *world, #endif }); ecs.set({ scnMgr, world, 0.0f, 5.0f, - (int)window->getWidth(), - (int)window->getHeight() }); + (int)window->getWidth(), (int)window->getHeight(), + false }); ecs.set({ cameraNode, camera, false }); ecs.add(); ecs.add(); diff --git a/src/gamedata/LuaData.cpp b/src/gamedata/LuaData.cpp index 03892d4..3ccf4f7 100644 --- a/src/gamedata/LuaData.cpp +++ b/src/gamedata/LuaData.cpp @@ -109,6 +109,14 @@ int luaLibraryLoader(lua_State *L) return 1; } +struct LuaChildEventTrigger { + flecs::entity parent_e; + Ogre::Vector3 position; + float halfheight; + float radius; + Ogre::String event; +}; + static void installLibraryLoader(lua_State *L) { // Insert the c++ func 'luaLibraryLoader' into package.loaders. @@ -262,8 +270,11 @@ LuaData::LuaData() Ogre::Quaternion orientation(Ogre::Radian(yaw), Ogre::Vector3(0, 1, 0)); Ogre::Vector3 position(x, y, z); - e.set({ "boat.glb", position, orientation }); - lua_pushinteger(L, idmap.add_entity(e)); + e.set( + { "boat.scene", position, orientation }); + int ret = idmap.add_entity(e); + lua_pushinteger(L, ret); + std::cout << "boat created: " << ret << std::endl; return 1; } lua_pushinteger(L, -1); @@ -301,23 +312,18 @@ LuaData::LuaData() luaL_checktype(L, 6, LUA_TNUMBER); // halfh luaL_checktype(L, 7, LUA_TNUMBER); // radius int parent = lua_tointeger(L, 1); + std::cout << "parent: " << parent << std::endl; flecs::entity parent_e = idmap.get_entity(parent); - Ogre::SceneNode *parentNode = nullptr; - if (parent_e.has()) - parentNode = parent_e.get().mBodyNode; - - else if (parent_e.has()) - parentNode = parent_e.get().mNode; - flecs::entity e = ECS::get().entity().child_of(parent_e); Ogre::String event = lua_tostring(L, 2); float x = lua_tonumber(L, 3); float y = lua_tonumber(L, 4); float z = lua_tonumber(L, 5); float h = lua_tonumber(L, 6); float r = lua_tonumber(L, 7); - OgreAssert(parentNode, "bad parent"); Ogre::Vector3 position(x, y, z); - e.set({ parentNode, position, h, r, event }); + flecs::entity e = ECS::get().entity().child_of(parent_e); + e.set( + { parent_e, position, h, r, event }); lua_pushinteger(L, idmap.add_entity(e)); return 1; }); @@ -345,6 +351,54 @@ LuaData::LuaData() return 1; }); lua_setglobal(L, "ecs_npc_set"); + lua_pushcfunction(L, [](lua_State *L) -> int { + OgreAssert(lua_gettop(L) == 1, "Bad parameters"); + luaL_checktype(L, 1, LUA_TSTRING); // type + const char *fileName = lua_tostring(L, 1); + ECS::get().mScnMgr->getRootSceneNode()->saveChildren( + fileName); + return 0; + }); + lua_setglobal(L, "ecs_save_scene_debug"); + lua_pushcfunction(L, [](lua_State *L) -> int { + OgreAssert(lua_gettop(L) == 2, "Bad parameters"); + luaL_checktype(L, 1, LUA_TNUMBER); // object + luaL_checktype(L, 2, LUA_TSTRING); // name + int object = lua_tointeger(L, 1); + flecs::entity object_e = idmap.get_entity(object); + const char *fileName = lua_tostring(L, 2); + Ogre::SceneNode *node = nullptr; + if (object_e.has()) + node = object_e.get().mBodyNode; + else if (object_e.has()) + node = object_e.get().mNode; + if (node) + node->saveChildren(fileName); + return 0; + }); + lua_setglobal(L, "ecs_save_object_debug"); + lua_pushcfunction(L, [](lua_State *L) -> int { + luaL_checktype(L, 1, LUA_TBOOLEAN); // object + ECS::get_mut().enableDbgDraw = lua_toboolean(L, 1); + ECS::modified(); + return 0; + }); + lua_setglobal(L, "ecs_set_debug_drawing"); + lua_pushcfunction(L, [](lua_State *L) -> int { + OgreAssert(lua_gettop(L) == 2, "Bad parameters"); + luaL_checktype(L, 1, LUA_TNUMBER); // object + luaL_checktype(L, 2, LUA_TBOOLEAN); + int object = lua_tointeger(L, 1); + flecs::entity object_e = idmap.get_entity(object); + bool enable = lua_toboolean(L, 2); + if (enable) + object_e.remove(); + else + object_e.add(); + object_e.add(); + return 0; + }); + lua_setglobal(L, "ecs_character_physics_control"); } LuaData::~LuaData() @@ -391,6 +445,7 @@ void LuaData::lateSetup() LuaModule::LuaModule(flecs::world &ecs) { + ecs.component(); ecs.component() .on_add([](LuaBase &lua) { lua.mLua = new LuaData; @@ -399,6 +454,7 @@ LuaModule::LuaModule(flecs::world &ecs) }) .add(flecs::Singleton); ecs.add(); + ecs.system("LuaUpdate") .kind(flecs::OnUpdate) .each([](const EngineData &eng, LuaBase &lua) { @@ -414,5 +470,36 @@ LuaModule::LuaModule(flecs::world &ecs) } } }); + ecs.system( + "CreateChildTrigger") + .kind(flecs::OnUpdate) + .without() + .write() + .each([](flecs::entity e, const EngineData &env, + const LuaChildEventTrigger &lct) { + Ogre::SceneNode *parentNode = nullptr; + flecs::entity parent_e = lct.parent_e; + if (parent_e.has()) { + parentNode = + parent_e.get().mBodyNode; + OgreAssert( + parent_e.get().mBodyNode, + "bad node"); + + } else if (parent_e.has()) { + parentNode = parent_e.get().mNode; + OgreAssert(parent_e.get().mNode, + "bad node"); + } else + return; + EventTrigger &trigger = e.ensure(); + OgreAssert(parentNode, "bad parent"); + trigger.position = lct.position; + trigger.halfheight = lct.halfheight; + trigger.radius = lct.radius; + trigger.event = lct.event; + trigger.parent = parentNode; + e.modified(); + }); } } \ No newline at end of file