From 8eb32bb2cad8e76082d8a78544fe47853ad330c9 Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sun, 11 Jul 2021 00:07:22 +0000 Subject: [PATCH] blender: simple export addon This adds a Blender Addon for Abrasion which allows right-clicking on a collection to export it to a q3dm file. --- blender/BUILD.bazel | 24 ++++ blender/README.md | 16 +++ blender/addon.py | 55 ++++++++ blender/build/BUILD.bazel | 1 + blender/build/__inittmpl__.py | 41 ++++++ blender/build/blender.bzl | 113 ++++++++++++++++ blender/export.py | 117 ++++++++++++++++ third_party/licenses/APACHE-2.0.txt | 202 ++++++++++++++++++++++++++++ 8 files changed, 569 insertions(+) create mode 100644 blender/BUILD.bazel create mode 100644 blender/README.md create mode 100644 blender/addon.py create mode 100644 blender/build/BUILD.bazel create mode 100644 blender/build/__inittmpl__.py create mode 100644 blender/build/blender.bzl create mode 100644 blender/export.py create mode 100644 third_party/licenses/APACHE-2.0.txt diff --git a/blender/BUILD.bazel b/blender/BUILD.bazel new file mode 100644 index 0000000..2dfba23 --- /dev/null +++ b/blender/BUILD.bazel @@ -0,0 +1,24 @@ +load("@rules_python//python:defs.bzl", "py_binary") +load("@pydeps//:requirements.bzl", "requirement") +load("//blender/build:blender.bzl", "blender_addon") + +py_library( + name = "addon_py_lib", + srcs = [ + "addon.py", + "export.py", + ], + deps = [ + "@com_github_q3k_q3d//:q3d_py", + requirement("flatbuffers"), + ], +) + +blender_addon( + name = "addon", + deps = [ + ":addon_py_lib", + ], + module = 'abrasion.blender.addon', + addon_name = 'Abrasion', +) diff --git a/blender/README.md b/blender/README.md new file mode 100644 index 0000000..fcf6957 --- /dev/null +++ b/blender/README.md @@ -0,0 +1,16 @@ +Blender/Abrasion Addon +=== + +Building / Installing +--- + +To build: + + bazel buld //blender:addon + +Then symlink/copy `bazel-bin/blender/addon` into Blender's `scripts/addons/abrasion-addon`. **DO NOT INSTALL THE ADDON AS `abrasion`, THIS BREAKS IMPORT PATHS**/. + +Usage +--- + +Right click on a collection in the ourliner and select 'Export Abrasion Q3DM' to recursively export this collection as a q3dm file. It will be saved next to your .blend file, with a '.(collectionName).q3dm' suffix. diff --git a/blender/addon.py b/blender/addon.py new file mode 100644 index 0000000..4684653 --- /dev/null +++ b/blender/addon.py @@ -0,0 +1,55 @@ +import importlib + +import bpy + +from abrasion.blender.export import export + + +def console_print(context, *args, **kwargs): + for a in context.screen.areas: + if a.type != 'CONSOLE': + continue + + c = {} + c['area'] = a + c['space_data'] = a.spaces.active + c['region'] = a.regions[-1] + c['window'] = context.window + c['screen'] = context.screen + s = " ".join([str(arg) for arg in args]) + for line in s.split("\n"): + line = '[abrasion] ' + line + bpy.ops.console.scrollback_append(c, text=line) + + +class OUTLINER_OT_collection_abrasion(bpy.types.Operator): + """Foo.""" + bl_idname = "outliner.collection_abrasion" + bl_label = "Export Abrasion Q3M" + + @classmethod + def poll(cls, context): + return context.collection is not None + + def execute(self, context): + export( + context, + context.collection.all_objects.values(), + context.collection.name, + ) + return {'FINISHED'} + +def menu_func(self, context): + layout = self.layout + layout.separator() + layout.operator(OUTLINER_OT_collection_abrasion.bl_idname) + +def register(): + bpy.utils.register_class(OUTLINER_OT_collection_abrasion) + bpy.types.OUTLINER_MT_collection.append(menu_func) + print("Abrasion: Registered.") + +def unregister(): + bpy.utils.unregister_class(OUTLINER_OT_collection_abrasion) + bpy.types.OUTLINER_MT_collection.remove(menu_func) + print("Abrasion: Unregistered.") diff --git a/blender/build/BUILD.bazel b/blender/build/BUILD.bazel new file mode 100644 index 0000000..fff4d93 --- /dev/null +++ b/blender/build/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["__inittmpl__.py"]) diff --git a/blender/build/__inittmpl__.py b/blender/build/__inittmpl__.py new file mode 100644 index 0000000..0df2a13 --- /dev/null +++ b/blender/build/__inittmpl__.py @@ -0,0 +1,41 @@ +import json + +bl_info = { + 'author': 'Abrasion Authors', + 'version': (1, 0, 0), + 'blender': (2, 80, 0), + 'name': "%name%", + 'location': "%location%", + 'category': "%category%", +} + +import importlib +import os +import pathlib +import sys + + +_importpaths = json.loads("""%importpaths%""") +_this_path = str(pathlib.Path(__file__).parent.resolve()) + + +def register(): + if _this_path not in sys.path: + sys.path.append(_this_path) + for p in _importpaths: + fp = os.path.join(_this_path, p) + if fp not in sys.path: + sys.path.append(fp) + + import %module% + %module% = importlib.reload(%module%) + %module%.register() + + +def unregister(): + import %module% + %module%.unregister() + + +if __name__ == '__main__': + register() diff --git a/blender/build/blender.bzl b/blender/build/blender.bzl new file mode 100644 index 0000000..c0e70b2 --- /dev/null +++ b/blender/build/blender.bzl @@ -0,0 +1,113 @@ +# _prepend_workspace from github.com/google/subpar. +# Copyright 2016 Google Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0, see +# //third_party/licenses/APACHE-2.0.txt. +def _prepend_workspace(path, ctx): + # It feels like there should be an easier, less fragile way. + if path.startswith("../"): + # External workspace, for example + # '../protobuf/python/google/protobuf/any_pb2.py' + stored_path = path[len("../"):] + elif path.startswith("external/"): + # External workspace, for example + # 'external/protobuf/python/__init__.py' + stored_path = path[len("external/"):] + else: + # Main workspace, for example 'mypackage/main.py' + stored_path = ctx.workspace_name + "/" + path + return stored_path + +def _generate_subdirs(directory): + parts = directory.split('/') + cur = parts[0] + res = [] + for p in parts[1:]: + res.append(cur) + cur += '/' + p + res.append(cur) + return res + +def _blender_addon_impl(ctx): + root = ctx.attr.name + "/" + + out = [] + importpaths = [] + directories = {} + initpys = {} + for dep in ctx.attr.deps: + for s in dep[PyInfo].transitive_sources.to_list(): + p = _prepend_workspace(s.short_path, ctx) + f = ctx.actions.declare_file(root + p) + ctx.actions.symlink(output=f, target_file=s) + out.append(f) + + directory = '/'.join(p.split('/')[:-1]) + directories[directory] = True + + if p.endswith('/__init__.py'): + initpys[p] = True + + importpaths.append(dep[PyInfo].imports) + importpaths = depset([], transitive=importpaths).to_list() + + need_initpys = {} + for d in directories.keys(): + for subdir in _generate_subdirs(d)[::-1]: + if subdir in importpaths: + break + ipy = subdir + '/__init__.py' + if initpys.get(ipy): + continue + need_initpys[ipy] = True + + for ipy in need_initpys.keys(): + f = ctx.actions.declare_file(root + ipy) + ctx.actions.write(f, "") + out.append(f) + + addon_name = ctx.attr.addon_name or ctx.attr.name + + initfile = ctx.actions.declare_file(root + "__init__.py") + ctx.actions.expand_template( + template = ctx.file._initfile_template, + output = initfile, + substitutions = { + '%importpaths%': json.encode(importpaths), + '%module%': ctx.attr.module, + '%name%': addon_name, + '%location%': ctx.attr.addon_location, + '%category%': ctx.attr.addon_category, + }, + ) + out.append(initfile) + + return [ + DefaultInfo( + files = depset(out), + runfiles = ctx.runfiles(out), + ) + ] + +blender_addon = rule( + implementation = _blender_addon_impl, + attrs = { + 'deps': attr.label_list( + providers = [PyInfo], + ), + 'module': attr.string( + mandatory = True, + ), + 'addon_name': attr.string(), + 'addon_location': attr.string( + default = '', + ), + 'addon_category': attr.string( + default = 'Import-Export', + ), + + '_initfile_template': attr.label( + default = Label('//blender/build:__inittmpl__.py'), + allow_single_file = True, + ), + }, +) diff --git a/blender/export.py b/blender/export.py new file mode 100644 index 0000000..5cd2b15 --- /dev/null +++ b/blender/export.py @@ -0,0 +1,117 @@ +import flatbuffers +import Q3DModel.Model +import Q3DObject.Mesh + +import bpy + + +def export(context, objects, name='all'): + from abrasion.blender.addon import console_print + + console_print(context, "Starting export...") + path = bpy.data.filepath + if path == "": + raise Exception("File must be saved") + path += f".{name}.q3dm" + + builder = flatbuffers.Builder(1024) + depsgraph = context.evaluated_depsgraph_get() + + nodes = [] + + for o in objects: + console_print(context, f"{o.type}: {o.name}") + if o.type != 'MESH': + console_print(context, " ... skipped.") + continue + mesh = o.evaluated_get(depsgraph).data + mesh.calc_loop_triangles() + + fverts = [] + for vertex in mesh.vertices: + Q3DObject.Vertex.VertexStart(builder) + position = Q3DObject.Vector3.CreateVector3( + builder, + x = vertex.co.x, + y = vertex.co.y, + z = vertex.co.z, + ) + Q3DObject.Vertex.VertexAddPosition(builder, position) + normal = Q3DObject.Vector3.CreateVector3( + builder, + x = vertex.normal.x, + y = vertex.normal.y, + z = vertex.normal.z, + ) + Q3DObject.Vertex.VertexAddNormal(builder, normal) + fverts.append(Q3DObject.Vertex.VertexEnd(builder)) + nverts = len(fverts) + + Q3DObject.Mesh.MeshStartVerticesVector( + builder, len(fverts)) + for ivert in reversed(range(len(fverts))): + builder.PrependUOffsetTRelative(fverts[ivert]) + fverts = builder.EndVector() + + ntris = len(mesh.loop_triangles) + Q3DObject.Mesh.MeshStartTrianglesVector( + builder, ntris) + for triangle in mesh.loop_triangles: + Q3DObject.ITriangle.CreateITriangle( + builder, + triangle.vertices[0], + triangle.vertices[1], + triangle.vertices[2], + ) + ftris = builder.EndVector() + console_print(context, f" ... {nverts} vertices, {ntris} triangles") + + Q3DObject.Mesh.MeshStart(builder) + Q3DObject.Mesh.MeshAddItriangles(builder, ftris) + Q3DObject.Mesh.MeshAddVertices(builder, fverts) + fmesh = Q3DObject.Mesh.MeshEnd(builder) + + + Q3DModel.Node.NodeStart(builder) + Q3DModel.Node.NodeAddMesh(builder, fmesh) + c0 = o.matrix_world.col[0] + c1 = o.matrix_world.col[1] + c2 = o.matrix_world.col[2] + c3 = o.matrix_world.col[3] + Q3DModel.Node.NodeAddTransform(builder, Q3DModel.Matrix4.CreateMatrix4( + builder, + c0.x, c0.y, c0.z, c0.w, + c1.x, c1.y, c1.z, c1.w, + c2.x, c2.y, c2.z, c2.w, + c3.x, c3.y, c3.z, c3.w, + )) + nodes.append(Q3DModel.Node.NodeEnd(builder)) + + + Q3DModel.Node.NodeStartChildrenVector(builder, len(nodes)) + for node in nodes: + builder.PrependUOffsetTRelative(node) + nodes = builder.EndVector() + + Q3DModel.Node.NodeStart(builder) + Q3DModel.Node.NodeAddTransform(builder, Q3DModel.Matrix4.CreateMatrix4( + builder, + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + )) + Q3DModel.Node.NodeAddChildren(builder, nodes) + root = Q3DModel.Node.NodeEnd(builder) + + Q3DModel.Model.ModelStart(builder) + Q3DModel.Model.ModelAddRoot(builder, root) + model = Q3DModel.Model.ModelEnd(builder) + + builder.Finish(model, file_identifier=b'Q3DM') + buf = builder.Output() + with open(path, 'wb') as f: + f.write(buf) + console_print(context, f"Saved to {path}") + + diff --git a/third_party/licenses/APACHE-2.0.txt b/third_party/licenses/APACHE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/third_party/licenses/APACHE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.