From d508646ed94fd27fa10a90efb2101e19132f4c5c Mon Sep 17 00:00:00 2001 From: Matteo De Carlo Date: Wed, 3 Jan 2024 16:47:13 +0100 Subject: [PATCH] Blender exporter plugin - first draft pybind11 required Signed-off-by: Matteo De Carlo --- .gitignore | 4 + BlenderExporter/CMakeLists.txt | 10 + BlenderExporter/python_bindings.cpp | 311 ++++++++++++++++++ BlenderExporter/test.py | 23 ++ .../wicked_blender_exporter/__init__.py | 224 +++++++++++++ CMakeLists.txt | 8 + 6 files changed, 580 insertions(+) create mode 100644 BlenderExporter/CMakeLists.txt create mode 100644 BlenderExporter/python_bindings.cpp create mode 100755 BlenderExporter/test.py create mode 100644 BlenderExporter/wicked_blender_exporter/__init__.py diff --git a/.gitignore b/.gitignore index ccbe8a3949..9a97f70ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ bld/ [Bb]in/ [Oo]bj/ +# Python build results +*.pyc +*.cpython*.so + # Roslyn cache directories *.ide/ diff --git a/BlenderExporter/CMakeLists.txt b/BlenderExporter/CMakeLists.txt new file mode 100644 index 0000000000..f478ba5a58 --- /dev/null +++ b/BlenderExporter/CMakeLists.txt @@ -0,0 +1,10 @@ +set (SOURCE_FILES + python_bindings.cpp +) + +find_package(pybind11 CONFIG REQUIRED) +pybind11_add_module(pywickedengine MODULE ${SOURCE_FILES}) +target_compile_definitions(pywickedengine PUBLIC PYTHON_BINDINGS) +target_link_libraries(pywickedengine PUBLIC WickedEngine) +target_compile_definitions(pywickedengine PUBLIC "WICKED_ROOT_DIR=\"${WICKED_ROOT_DIR}\"") +install(TARGETS pywickedengine DESTINATION "~/.config/blender/4.0/scripts/addons/pywickedengine/") diff --git a/BlenderExporter/python_bindings.cpp b/BlenderExporter/python_bindings.cpp new file mode 100644 index 0000000000..a8ca44a147 --- /dev/null +++ b/BlenderExporter/python_bindings.cpp @@ -0,0 +1,311 @@ +#include "wiArchive.h" +#include "wiResourceManager.h" +#include "wiScene.h" +#include "wiInitializer.h" +#include "wiApplication.h" +#include "wiECS.h" +#include "wiRenderer.h" + +#include +#include +#include +#include + +namespace py = pybind11; + +PYBIND11_MAKE_OPAQUE(wi::vector); +PYBIND11_MAKE_OPAQUE(wi::vector); +PYBIND11_MAKE_OPAQUE(wi::vector); +PYBIND11_MAKE_OPAQUE(wi::vector); +PYBIND11_MAKE_OPAQUE(wi::vector); +PYBIND11_MAKE_OPAQUE(wi::vector); +PYBIND11_MAKE_OPAQUE(wi::vector); +PYBIND11_MAKE_OPAQUE(wi::scene::NameComponent); +PYBIND11_MAKE_OPAQUE(wi::scene::MeshComponent); +PYBIND11_MAKE_OPAQUE(wi::scene::MaterialComponent); +PYBIND11_MAKE_OPAQUE(wi::scene::TransformComponent); +PYBIND11_MAKE_OPAQUE(wi::scene::ObjectComponent); + +static wi::Application app; + +void init_wicked(py::module_& mod) +{ + //TODO we can probably initialize less stuff + + sdl2::sdlsystem_ptr_t system = sdl2::make_sdlsystem(SDL_INIT_VIDEO); + if (!system) { + throw sdl2::SDLError("Error creating SDL2 system"); + } + + sdl2::window_ptr_t window = sdl2::make_window("WickedBlenderExporter", + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + 50, 50, + SDL_WINDOW_HIDDEN | SDL_WINDOW_VULKAN); + if (!window) { + throw sdl2::SDLError("Error creating window"); + } + + app.SetWindow(window.get()); + wi::renderer::SetShaderSourcePath(WICKED_ROOT_DIR"/WickedEngine/shaders/"); + wi::renderer::SetShaderPath(WICKED_ROOT_DIR"/build/BlenderExporter/shaders/"); + + wi::initializer::InitializeComponentsImmediate(); + app.Initialize(); + app.Update(0.33); +} + +void export_primitives(py::module_& mod) +{ + py::class_(mod, "XMFLOAT2") + .def(py::init<>()) + .def(py::init()) + ; + py::class_(mod, "XMFLOAT3") + .def(py::init<>()) + .def(py::init()) + ; + py::class_(mod, "XMFLOAT4") + .def(py::init<>()) + .def(py::init()) + ; + py::class_(mod, "XMUINT4") + .def(py::init<>()) + .def(py::init()) + ; + + py::bind_vector>(mod, "VectorXMFLOAT2"); + py::bind_vector>(mod, "VectorXMFLOAT3"); + py::bind_vector>(mod, "VectorXMFLOAT4"); + py::bind_vector>(mod, "VectorXMUINT4"); + py::bind_vector>(mod, "VectorU8"); + py::bind_vector>(mod, "VectorU32"); + py::bind_vector>(mod, "VectorMeshSubset"); + +} + +void export_resourcemanager(py::module_& mod) +{ + py::enum_(mod, "ResourceManagerMode") + .value("DISCARD_FILEDATA_AFTER_LOAD", wi::resourcemanager::Mode::DISCARD_FILEDATA_AFTER_LOAD) + .value("ALLOW_RETAIN_FILEDATA", wi::resourcemanager::Mode::ALLOW_RETAIN_FILEDATA) + .value("ALLOW_RETAIN_FILEDATA_BUT_DISABLE_EMBEDDING", wi::resourcemanager::Mode::ALLOW_RETAIN_FILEDATA_BUT_DISABLE_EMBEDDING) + .export_values(); + + mod.def("resourcemanager_SetMode", &wi::resourcemanager::SetMode); +} + +void export_Archive(py::module_& mod) +{ + py::class_(mod, "Archive") + .def(py::init<>()) + .def(py::init()) + .def("IsOpen", &wi::Archive::IsOpen) + .def("Close", &wi::Archive::Close) + .def("SaveFile", &wi::Archive::SaveFile) + .def("SaveHeaderFile", &wi::Archive::SaveHeaderFile) + ; +} + +void export_Scene(py::module_& mod) +{ + py::class_(mod, "Scene") + .def(py::init<>()) + //.def_readwrite("componentLibrary", &wi::scene::Scene::componentLibrary) + .def("names", [](const wi::scene::Scene &_this){ return &_this.names; }) + .def("meshes", [](const wi::scene::Scene &_this){ return &_this.meshes; }) + .def("transforms", [](const wi::scene::Scene &_this){ return &_this.transforms; }) + .def("materials", [](const wi::scene::Scene &_this){ return &_this.materials; }) + .def("objects", [](const wi::scene::Scene &_this){ return &_this.objects; }) + .def("Serialize", &wi::scene::Scene::Serialize) + .def("Entity_CreateTransform", &wi::scene::Scene::Entity_CreateTransform) + .def("Entity_CreateMaterial", &wi::scene::Scene::Entity_CreateMaterial) + .def("Entity_CreateObject", &wi::scene::Scene::Entity_CreateObject) + .def("Entity_CreateMesh", &wi::scene::Scene::Entity_CreateMesh) + .def("Entity_CreateLight", &wi::scene::Scene::Entity_CreateLight) + .def("Entity_CreateForce", &wi::scene::Scene::Entity_CreateForce) + .def("Entity_CreateEnvironmentProbe", &wi::scene::Scene::Entity_CreateEnvironmentProbe) + .def("Entity_CreateDecal", &wi::scene::Scene::Entity_CreateDecal) + .def("Entity_CreateCamera", &wi::scene::Scene::Entity_CreateCamera) + .def("Entity_CreateEmitter", &wi::scene::Scene::Entity_CreateEmitter) + .def("Entity_CreateHair", &wi::scene::Scene::Entity_CreateHair) + .def("Entity_CreateSound", &wi::scene::Scene::Entity_CreateSound) + .def("Entity_CreateVideo", &wi::scene::Scene::Entity_CreateVideo) + .def("Entity_CreateCube", &wi::scene::Scene::Entity_CreateCube) + .def("Entity_CreatePlane", &wi::scene::Scene::Entity_CreatePlane) + .def("Entity_CreateSphere", &wi::scene::Scene::Entity_CreateSphere) + .def("Component_Attach", &wi::scene::Scene::Component_Attach) + .def("Component_Attach", [](wi::scene::Scene& _this, wi::ecs::Entity entity, wi::ecs::Entity parent) { return _this.Component_Attach(entity, parent); }) + .def("Component_Detach", &wi::scene::Scene::Component_Detach) + .def("Component_DetachChildren", &wi::scene::Scene::Component_DetachChildren) + .def("RunAnimationUpdateSystem", &wi::scene::Scene::RunAnimationUpdateSystem) + .def("RunTransformUpdateSystem", &wi::scene::Scene::RunTransformUpdateSystem) + .def("RunHierarchyUpdateSystem", &wi::scene::Scene::RunHierarchyUpdateSystem) + .def("RunExpressionUpdateSystem", &wi::scene::Scene::RunExpressionUpdateSystem) + .def("RunProceduralAnimationUpdateSystem", &wi::scene::Scene::RunProceduralAnimationUpdateSystem) + .def("RunArmatureUpdateSystem", &wi::scene::Scene::RunArmatureUpdateSystem) + .def("RunMeshUpdateSystem", &wi::scene::Scene::RunMeshUpdateSystem) + .def("RunMaterialUpdateSystem", &wi::scene::Scene::RunMaterialUpdateSystem) + .def("RunImpostorUpdateSystem", &wi::scene::Scene::RunImpostorUpdateSystem) + .def("RunObjectUpdateSystem", &wi::scene::Scene::RunObjectUpdateSystem) + .def("RunCameraUpdateSystem", &wi::scene::Scene::RunCameraUpdateSystem) + .def("RunDecalUpdateSystem", &wi::scene::Scene::RunDecalUpdateSystem) + .def("RunProbeUpdateSystem", &wi::scene::Scene::RunProbeUpdateSystem) + .def("RunForceUpdateSystem", &wi::scene::Scene::RunForceUpdateSystem) + .def("RunLightUpdateSystem", &wi::scene::Scene::RunLightUpdateSystem) + .def("RunParticleUpdateSystem", &wi::scene::Scene::RunParticleUpdateSystem) + .def("RunWeatherUpdateSystem", &wi::scene::Scene::RunWeatherUpdateSystem) + .def("RunSoundUpdateSystem", &wi::scene::Scene::RunSoundUpdateSystem) + .def("RunVideoUpdateSystem", &wi::scene::Scene::RunVideoUpdateSystem) + .def("RunScriptUpdateSystem", &wi::scene::Scene::RunScriptUpdateSystem) + .def("RunSpriteUpdateSystem", &wi::scene::Scene::RunSpriteUpdateSystem) + .def("RunFontUpdateSystem", &wi::scene::Scene::RunFontUpdateSystem) + ; +} + +template +void register_component_manager(py::module_& mod, const char* name) +{ + typedef wi::ecs::ComponentManager Manager; + py::class_>(mod, name) + .def("Clear", &Manager::Clear) + // .def("Copy", &Manager::Copy) + // .def("Merge", &Manager::Merge) + .def("Create", &Manager::Create) + .def("Remove", &Manager::Remove) + .def("MoveItem", &Manager::MoveItem) + .def("Contains", &Manager::Contains) + .def("GetComponent", [](Manager* _this, wi::ecs::Entity entity) { return static_cast(_this->GetComponent(entity)); }) + .def("GetIndex", &Manager::GetIndex) + .def("GetCount", [](const Manager* _this) { return _this->GetCount(); }) + .def("GetEntity", &Manager::GetEntity) + .def("GetEntityArray", &Manager::GetEntityArray) + .def("GetComponentArray", &Manager::GetComponentArray) + ; +} + +void export_ECS(py::module_& mod) +{ + using namespace wi::ecs; + using namespace wi::scene; + // py::class_(mod, "ComponentLibrary"); + + // py::class_(mod, "Entity"); + mod.def("CreateEntity", &CreateEntity); + + //maybe Components need to not handle memory by python + // `std::unique_ptr` + py::class_(mod, "MeshComponent_MeshSubset") + .def(py::init<>()) + .def_readwrite("materialID", &MeshComponent::MeshSubset::materialID) + .def_readwrite("indexOffset", &MeshComponent::MeshSubset::indexOffset) + .def_readwrite("indexCount", &MeshComponent::MeshSubset::indexCount) + .def_readwrite("materialIndex", &MeshComponent::MeshSubset::materialIndex) + ; + + py::class_>(mod, "NameComponent") + .def_readwrite("name", &NameComponent::name) + ; + + py::class_>(mod, "MeshComponent") + .def_readwrite("vertex_positions", &MeshComponent::vertex_positions) + .def_readwrite("vertex_normals", &MeshComponent::vertex_normals) + .def_readwrite("vertex_tangents", &MeshComponent::vertex_tangents) + .def_readwrite("vertex_uvset_0", &MeshComponent::vertex_uvset_0) + .def_readwrite("vertex_uvset_1", &MeshComponent::vertex_uvset_1) + .def_readwrite("vertex_boneindices", &MeshComponent::vertex_boneindices) + .def_readwrite("vertex_boneweights", &MeshComponent::vertex_boneweights) + .def_readwrite("vertex_atlas", &MeshComponent::vertex_atlas) + .def_readwrite("vertex_colors", &MeshComponent::vertex_colors) + .def_readwrite("vertex_windweights", &MeshComponent::vertex_windweights) + .def_readwrite("indices", &MeshComponent::indices) + .def_readwrite("subsets", &MeshComponent::subsets) + .def_readwrite("tessellationFactor", &MeshComponent::tessellationFactor) + .def_readwrite("armatureID", &MeshComponent::armatureID) + .def("ComputeNormals", &MeshComponent::ComputeNormals) + .def("ComputeNormals", &MeshComponent::CreateRenderData) + ; + + py::enum_(mod, "MeshComponentComputeNormals") + .value("HARD", MeshComponent::COMPUTE_NORMALS::COMPUTE_NORMALS_HARD) + .value("SMOOTH", MeshComponent::COMPUTE_NORMALS::COMPUTE_NORMALS_SMOOTH) + .value("SMOOTH_FAST", MeshComponent::COMPUTE_NORMALS::COMPUTE_NORMALS_SMOOTH_FAST) + .export_values() + ; + + py::class_(mod, "TransformComponent") + .def(py::init<>()) + .def_readwrite("scale_local", &TransformComponent::scale_local) + .def_readwrite("rotation_local", &TransformComponent::rotation_local) + .def_readwrite("translation_local", &TransformComponent::translation_local) + .def_readwrite("world", &TransformComponent::world) + .def("GetPosition", &TransformComponent::GetPosition) + .def("GetRotation", &TransformComponent::GetRotation) + .def("GetScale", &TransformComponent::GetScale) + .def("GetPositionV", &TransformComponent::GetPositionV) + .def("GetRotationV", &TransformComponent::GetRotationV) + .def("GetScaleV", &TransformComponent::GetScaleV) + .def("GetLocalMatrix", &TransformComponent::GetLocalMatrix) + .def("UpdateTransform", &TransformComponent::UpdateTransform) + .def("UpdateTransform_Parented", &TransformComponent::UpdateTransform_Parented) + .def("ApplyTransform", &TransformComponent::ApplyTransform) + .def("ClearTransform", &TransformComponent::ClearTransform) + // .def("Translate", &TransformComponent::Translate) + // .def("Translate", &TransformComponent::Translate) + .def("RotateRollPitchYaw", &TransformComponent::RotateRollPitchYaw) + // .def("Rotate", &TransformComponent::Rotate) + // .def("Rotate", &TransformComponent::Rotate) + // .def("Scale", &TransformComponent::Scale) + // .def("Scale", &TransformComponent::Scale) + // .def("MatrixTransform", &TransformComponent::MatrixTransform) + // .def("MatrixTransform", &TransformComponent::MatrixTransform) + .def("Lerp", &TransformComponent::Lerp) + .def("CatmullRom", &TransformComponent::CatmullRom) + ; + + + py::class_>(mod, "MaterialComponent") + .def_readwrite("baseColor", &MaterialComponent::baseColor) + ; + + py::class_>(mod, "ObjectComponent") + .def_readwrite("meshID", &ObjectComponent::meshID) + .def_readwrite("cascadeMask", &ObjectComponent::cascadeMask) + .def_readwrite("filterMask", &ObjectComponent::filterMask) + .def_readwrite("color", &ObjectComponent::color) + .def_readwrite("emissiveColor", &ObjectComponent::emissiveColor) + .def_readwrite("userStencilRef", &ObjectComponent::userStencilRef) + .def_readwrite("lod_distance_multiplier", &ObjectComponent::lod_distance_multiplier) + .def_readwrite("draw_distance", &ObjectComponent::draw_distance) + .def_readwrite("lightmapWidth", &ObjectComponent::lightmapWidth) + .def_readwrite("lightmapHeight", &ObjectComponent::lightmapHeight) + .def_readwrite("lightmapTextureData", &ObjectComponent::lightmapTextureData) + .def_readwrite("sort_priority", &ObjectComponent::sort_priority) + .def_readwrite("filterMaskDynamic", &ObjectComponent::filterMaskDynamic) + .def_readwrite("lightmap", &ObjectComponent::lightmap) + .def_readwrite("lightmapIterationCount", &ObjectComponent::lightmapIterationCount) + .def_readwrite("center", &ObjectComponent::center) + .def_readwrite("radius", &ObjectComponent::radius) + .def_readwrite("fadeDistance", &ObjectComponent::fadeDistance) + .def_readwrite("lod", &ObjectComponent::lod) + .def_readwrite("mesh_index", &ObjectComponent::mesh_index) + .def_readwrite("sort_bits", &ObjectComponent::sort_bits) + ; + + // MANAGERS ------------------------------------------- + register_component_manager(mod, "NameComponentManager"); + register_component_manager(mod, "TransformComponentManager"); + register_component_manager(mod, "MeshComponentManager"); + register_component_manager(mod, "MaterialComponentManager"); + register_component_manager(mod, "ObjectComponentManager"); +} + +PYBIND11_MODULE(pywickedengine, mod) +{ + init_wicked(mod); + export_primitives(mod); + export_resourcemanager(mod); + export_ECS(mod); + export_Archive(mod); + export_Scene(mod); +} diff --git a/BlenderExporter/test.py b/BlenderExporter/test.py new file mode 100755 index 0000000000..1e9ebf2068 --- /dev/null +++ b/BlenderExporter/test.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import pywickedengine + +dump_to_header = False + +def main(): + filename = "/tmp/test.wiscene" + ar = pywickedengine.Archive() if dump_to_header else pywickedengine.Archive(filename, False) + if not ar.IsOpen(): + print("ERROR, ARCHIVE DID NOT OPEN") + return + + scene = pywickedengine.Scene() + scene.Serialize(ar) + + if dump_to_header: + ar.SaveHeaderFile("/tmp/test.h","test")# wi::helper::RemoveExtension(wi::helper::GetFileNameFromPath(filename))); + + ar.Close() + +if __name__ == "__main__": + main() diff --git a/BlenderExporter/wicked_blender_exporter/__init__.py b/BlenderExporter/wicked_blender_exporter/__init__.py new file mode 100644 index 0000000000..d3012ce2fa --- /dev/null +++ b/BlenderExporter/wicked_blender_exporter/__init__.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 + +import bpy +import os +import ctypes +from mathutils import Vector + +import pywickedengine as wi + +bl_info = { + "name": "Wicked wiscene Extension", + "category": "Generic", + "version": (1, 0, 0), + "blender": (4, 0, 0), + 'location': 'File > Export > wiscene', + 'description': 'Addon to add wiscene export to file functionality.', + 'tracker_url': "https://github.com/turanszkij/WickedEngine/issues/", + 'isDraft': True, + 'developer': "Matteo De Carlo", # Replace this + 'url': 'https://github.com/turanszkij/WickedEngine/', +} + + +def WIVector3(blvector3: Vector): + import pywickedengine as wi + return wi.XMFLOAT3(blvector3[0], blvector3[2], blvector3[1]) + +def wicked_add_mesh(item, scene, root): + print(f"Adding {item.name} mesh ...") + import pywickedengine as wi + entity = scene.Entity_CreateObject(item.name) + object = scene.objects().GetComponent(entity) + scene.meshes().Create(entity) + #entity = scene.Entity_CreateMesh(item.name) + #scene.Component_Attach(entity, root) #TODO this creates a self referencing entity, getting the loading screen stuck in an infintie while loop + mesh = scene.meshes().GetComponent(entity) + transform = scene.transforms().GetComponent(entity) + transform.translation_local = WIVector3(item.location) + transform.UpdateTransform() + object.meshID = entity + + if scene.materials().GetCount() == 0: + scene.materials().Create(wi.CreateEntity()) + + #vertices[i].co # position data + #vertices[i].undeformed_co # position data - no deforming modifiers applied + + # create only one submesh for now + #TODO later create a submesh for each material (or similar) + subset = wi.MeshComponent_MeshSubset() + material = scene.materials().GetComponent(subset.materialID) + vertex_offset = len(mesh.vertex_positions) + + #index_remap = [0,1,-1] + index_remap = [0,0,0] + + # append vertices + print(f"Adding vertex data (len={len(item.data.vertices)})") + for vertex in item.data.vertices: + mesh.vertex_positions.append(WIVector3(vertex.co)) + mesh.vertex_normals.append(WIVector3(vertex.normal)) + mesh.vertex_uvset_0.append(wi.XMFLOAT2(0,0)) + + print("Adding index data") + for face in item.data.loop_triangles: + #print("Polygon index: %d, length: %d" % (face.index, 3)) + offset=0 + for i in face.loops: + ix = item.data.loops[i + index_remap[offset]].vertex_index + mesh.indices.append(ix) + #print(" Vertex: %d %r" % (item.data.loops[i].vertex_index, item.data.vertices[ix].co)) + #print(" UV: %r" % item.data.uv_layers.active.data[i].uv) + offset +=1 + #verts_in_face = face.vertices[:] + #print("face index ", face.index) + #print("normal ", face.normal) + #for vert in verts_in_face: + # print("vertex coords ", item.data.vertices[vert].co) + + if len(mesh.vertex_normals) == 0 and len(mesh.vertex_positions) > 0: + for _ in range(len(mesh.vertex_positions)): + mesh.vertex_normals.append(wi.XMFLOAT3(0,0,0)) + mesh.ComputeNormals(wi.MeshComponentComputeNormals.SMOOTH_FAST); + + subset.materialID = scene.materials().GetEntity(0)#max(0, face.material)) + subset.indexOffset = 0 + subset.indexCount = len(mesh.indices) + mesh.subsets.append(subset) + + print(f"Finished adding {item.name} mesh") + + +def create_wiscene(scene_name): + import pywickedengine as wi + ### Load Scene + scene = wi.Scene() + rootEntity = wi.CreateEntity() + scene.transforms().Create(rootEntity) + scene.names().Create(rootEntity) + scene.names().GetComponent(rootEntity).name = scene_name + + #TODO materials + + # Meshes + print("Exporting meshes") + for item in bpy.data.objects: + print(f"Element in scene {item.name}") + match item.type: + case 'MESH': + wicked_add_mesh(item, scene, rootEntity) + print("Finished exporting meshes") + + #TODO armatures + + #TODO transform hierarchy + + #TODO armature-bone mappings + + #TODO animations + + #TODO lights + + #TODO cameras + + ## Correct orientation after importing + #scene.Update(0) + #TODO FlipZAxis + ## Update hte scene to have up to date values immediately after loading: + ## e.g. snap to camera functionality relies on this + #scene.Update(0) + + print("FINISH") + return scene + + +def write_wiscene(context, filepath, dump_to_header: bool): + print("running write_wiscene...") + + filename = os.path.splitext(filepath)[0] + scene_name = os.path.basename(filename) + + ar = wi.Archive() if dump_to_header else wi.Archive(filepath, False) + if not ar.IsOpen(): + print("ERROR, ARCHIVE DID NOT OPEN") + return {'ERROR'} + + scene = create_wiscene(scene_name) + + ### Serialize Scene + scene.Serialize(ar) + + if dump_to_header: + ar.SaveHeaderFile(filename+".h", scene_name) + + ar.Close() + print("finished write_wiscene") + return {'FINISHED'} + + +# ExportHelper is a helper class, defines filename and +# invoke() function which calls the file selector. +from bpy_extras.io_utils import ExportHelper +from bpy.props import StringProperty, BoolProperty, EnumProperty +from bpy.types import Operator + + +class ExportWiScene(Operator, ExportHelper): + """This appears in the tooltip of the operator and in the generated docs""" + bl_idname = "export_wicked.wiscene" # important since its how bpy.ops.export_wicked.wiscene is constructed + bl_label = "Export Wicked Scene" + + # ExportHelper mix-in class uses this. + filename_ext = ".wiscene" + + filter_glob: StringProperty( + default="*.wiscene", + options={'HIDDEN'}, + maxlen=1023, # Max internal buffer length, longer would be clamped. + ) + + # List of operator properties, the attributes will be assigned + # to the class instance from the operator settings before calling. + dump_to_header: BoolProperty( + name="Dump to Header", + description="Creates a C/C++ header with a byte array containing the serialized scene", + default=False, + ) + + type: EnumProperty( + name="Example Enum", + description="Choose between two items", + items=( + ('OPT_A', "First Option", "Description one"), + ('OPT_B', "Second Option", "Description two"), + ), + default='OPT_A', + ) + + def execute(self, context): + return write_wiscene(context, self.filepath, self.dump_to_header) + + +# Only needed if you want to add into a dynamic menu +def menu_func_export(self, context): + self.layout.operator(ExportWiScene.bl_idname, text="Wicked Scene (.wiscene)") + + +# Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access). +def register(): + bpy.utils.register_class(ExportWiScene) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + + +def unregister(): + bpy.utils.unregister_class(ExportWiScene) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + + +if __name__ == "__main__": + register() + + # test call + bpy.ops.export_wicked.wiscene('INVOKE_DEFAULT') + diff --git a/CMakeLists.txt b/CMakeLists.txt index 397039fd2a..46682e002f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ option(WICKED_EDITOR "Build WickedEngine editor" ON) option(WICKED_TESTS "Build WickedEngine tests" ON) option(WICKED_IMGUI_EXAMPLE "Build WickedEngine imgui example" ON) option(WICKED_LINUX_TEMPLATE "Build WickedEngine Linux template" ON) +option(BLENDER_EXPORTER "Build blender wiscene exporter plugin" OFF) # Configure CMake global variables @@ -63,3 +64,10 @@ endif() if (WICKED_LINUX_TEMPLATE) add_subdirectory(Template_Linux) endif() + +if (BLENDER_EXPORTER) + if (NOT ${WICKED_DYNAMIC_LIBRARY}) + message(FATAL_ERROR "WICKED_DYNAMIC_LIBRARY required to build blender exporter") + endif() + add_subdirectory(BlenderExporter) +endif()