From d679bfdb2da445da129f65b5f53e108b3ebe6e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 7 Dec 2023 14:57:53 +0100 Subject: [PATCH] python: expose the whole MaterialTools library. --- doc/python/conf.py | 4 +- doc/python/magnum.materialtools.rst | 42 +++++ doc/python/pages/changelog.rst | 3 +- package/ci/appveyor-desktop-gles.bat | 2 +- package/ci/appveyor-desktop.bat | 2 +- package/ci/unix-desktop-gles.sh | 2 +- package/ci/unix-desktop.sh | 2 +- src/python/CMakeLists.txt | 1 + src/python/magnum/CMakeLists.txt | 20 ++ src/python/magnum/__init__.py | 2 +- src/python/magnum/bootstrap.h | 1 + src/python/magnum/magnum.cpp | 6 + src/python/magnum/materialtools.cpp | 135 +++++++++++++ src/python/magnum/staticconfigure.h.cmake | 1 + src/python/magnum/test/material.gltf | 9 + src/python/magnum/test/test_materialtools.py | 188 +++++++++++++++++++ src/python/magnum/test/test_trade.py | 12 +- src/python/magnum/trade.cpp | 4 +- src/python/setup.py.cmake | 1 + 19 files changed, 423 insertions(+), 14 deletions(-) create mode 100644 doc/python/magnum.materialtools.rst create mode 100644 src/python/magnum/materialtools.cpp create mode 100644 src/python/magnum/test/test_materialtools.py diff --git a/doc/python/conf.py b/doc/python/conf.py index 6289f51..2c7a475 100644 --- a/doc/python/conf.py +++ b/doc/python/conf.py @@ -15,6 +15,7 @@ import corrade.utility import magnum import magnum.gl +import magnum.materialtools import magnum.meshtools import magnum.platform import magnum.platform.egl @@ -31,7 +32,7 @@ import magnum.trade # So the doc see everything # TODO: use just +=, m.css should reorder this on its own corrade.__all__ = ['containers', 'pluginmanager', 'utility', 'BUILD_DEPRECATED', 'BUILD_STATIC', 'BUILD_MULTITHREADED', 'TARGET_UNIX', 'TARGET_APPLE', 'TARGET_IOS', 'TARGET_IOS_SIMULATOR', 'TARGET_WINDOWS', 'TARGET_WINDOWS_RT', 'TARGET_EMSCRIPTEN', 'TARGET_ANDROID'] -magnum.__all__ = ['math', 'gl', 'meshtools', 'platform', 'primitives', 'shaders', 'scenegraph', 'scenetools', 'text', 'trade', 'BUILD_DEPRECATED', 'BUILD_STATIC', 'TARGET_GL', 'TARGET_GLES', 'TARGET_GLES2', 'TARGET_WEBGL', 'TARGET_EGL', 'TARGET_VK'] + magnum.__all__ +magnum.__all__ = ['math', 'gl', 'materialtools', 'meshtools', 'platform', 'primitives', 'shaders', 'scenegraph', 'scenetools', 'text', 'trade', 'BUILD_DEPRECATED', 'BUILD_STATIC', 'TARGET_GL', 'TARGET_GLES', 'TARGET_GLES2', 'TARGET_WEBGL', 'TARGET_EGL', 'TARGET_VK'] + magnum.__all__ # hide values of the preprocessor defines to avoid confusion by assigning a # class without __repr__ to them @@ -214,6 +215,7 @@ INPUT_DOCS = [ 'magnum.rst', 'magnum.gl.rst', 'magnum.math.rst', + 'magnum.materialtools.rst', 'magnum.meshtools.rst', 'magnum.platform.rst', 'magnum.scenegraph.rst', diff --git a/doc/python/magnum.materialtools.rst b/doc/python/magnum.materialtools.rst new file mode 100644 index 0000000..037dc7c --- /dev/null +++ b/doc/python/magnum.materialtools.rst @@ -0,0 +1,42 @@ +.. + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +.. + +.. py:function:: magnum.materialtools.filter_attributes + :raise AssertionError: If size of :p:`attributes_to_keep` is different than + :ref:`trade.MaterialData.attribute_data_offset()` for + :ref:`trade.MaterialData.layer_count` +.. py:function:: magnum.materialtools.filter_layers + :raise AssertionError: If size of :p:`layers_to_keep` is different than + :ref:`trade.MaterialData.layer_count` +.. py:function:: magnum.materialtools.filter_attributes_layers + :raise AssertionError: If size of :p:`attributes_to_keep` is different than + :ref:`trade.MaterialData.attribute_data_offset()` for + :ref:`trade.MaterialData.layer_count` + :raise AssertionError: If size of :p:`layers_to_keep` is different than + :ref:`trade.MaterialData.layer_count` +.. py:function:: magnum.materialtools.merge + :raise RuntimeError: If merge failed due to a conflict +.. py:function:: magnum.materialtools.phong_to_pbr_metallic_roughness + :raise RuntimeError: If conversion failed diff --git a/doc/python/pages/changelog.rst b/doc/python/pages/changelog.rst index 4702136..4c610e1 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -158,7 +158,8 @@ Changelog :ref:`trade.AbstractImporter.scene()` and related importer APIs - Exposed :ref:`Color3.red()` and other convenience constructors (see :gh:`mosra/magnum-bindings#12`) -- Exposed the :ref:`scenetools` and :ref:`text` libraries +- Exposed the :ref:`materialtools`, :ref:`scenetools` and :ref:`text` + libraries - Exposed :ref:`utility.copy()` for convenient, fast and safe copying of multi-dimensional strided arrays - Exposed the minimal interface of :ref:`utility.ConfigurationGroup` and diff --git a/package/ci/appveyor-desktop-gles.bat b/package/ci/appveyor-desktop-gles.bat index 8de9a13..e0729c7 100644 --- a/package/ci/appveyor-desktop-gles.bat +++ b/package/ci/appveyor-desktop-gles.bat @@ -46,7 +46,7 @@ cmake .. ^ -DMAGNUM_TARGET_EGL=OFF ^ -DMAGNUM_WITH_AUDIO=OFF ^ -DMAGNUM_WITH_DEBUGTOOLS=OFF ^ - -DMAGNUM_WITH_MATERIALTOOLS=OFF ^ + -DMAGNUM_WITH_MATERIALTOOLS=ON ^ -DMAGNUM_WITH_GL=ON ^ -DMAGNUM_WITH_MESHTOOLS=ON ^ -DMAGNUM_WITH_PRIMITIVES=ON ^ diff --git a/package/ci/appveyor-desktop.bat b/package/ci/appveyor-desktop.bat index 66a77c3..1b54039 100644 --- a/package/ci/appveyor-desktop.bat +++ b/package/ci/appveyor-desktop.bat @@ -57,7 +57,7 @@ cmake .. ^ -DMAGNUM_WITH_AUDIO=OFF ^ -DMAGNUM_WITH_DEBUGTOOLS=OFF ^ -DMAGNUM_WITH_GL=ON ^ - -DMAGNUM_WITH_MATERIALTOOLS=OFF ^ + -DMAGNUM_WITH_MATERIALTOOLS=ON ^ -DMAGNUM_WITH_MESHTOOLS=ON ^ -DMAGNUM_WITH_PRIMITIVES=ON ^ -DMAGNUM_WITH_SCENEGRAPH=ON ^ diff --git a/package/ci/unix-desktop-gles.sh b/package/ci/unix-desktop-gles.sh index bb3b036..356bfce 100755 --- a/package/ci/unix-desktop-gles.sh +++ b/package/ci/unix-desktop-gles.sh @@ -32,7 +32,7 @@ cmake .. \ -DMAGNUM_WITH_AUDIO=OFF \ -DMAGNUM_WITH_DEBUGTOOLS=OFF \ -DMAGNUM_WITH_GL=ON \ - -DMAGNUM_WITH_MATERIALTOOLS=OFF \ + -DMAGNUM_WITH_MATERIALTOOLS=ON \ -DMAGNUM_WITH_MESHTOOLS=ON \ -DMAGNUM_WITH_PRIMITIVES=ON \ -DMAGNUM_WITH_SCENEGRAPH=ON \ diff --git a/package/ci/unix-desktop.sh b/package/ci/unix-desktop.sh index c717cea..c01bc9b 100755 --- a/package/ci/unix-desktop.sh +++ b/package/ci/unix-desktop.sh @@ -31,7 +31,7 @@ cmake .. \ -DMAGNUM_WITH_AUDIO=OFF \ -DMAGNUM_WITH_DEBUGTOOLS=OFF \ -DMAGNUM_WITH_GL=ON \ - -DMAGNUM_WITH_MATERIALTOOLS=OFF \ + -DMAGNUM_WITH_MATERIALTOOLS=ON \ -DMAGNUM_WITH_MESHTOOLS=ON \ -DMAGNUM_WITH_PRIMITIVES=ON \ -DMAGNUM_WITH_SCENEGRAPH=ON \ diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 947c4b1..0c79f81 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -64,6 +64,7 @@ foreach(target corrade_pluginmanager corrade_utility magnum_gl + magnum_materialtools magnum_meshtools magnum_primitives magnum_scenegraph diff --git a/src/python/magnum/CMakeLists.txt b/src/python/magnum/CMakeLists.txt index 6d15812..41458e2 100644 --- a/src/python/magnum/CMakeLists.txt +++ b/src/python/magnum/CMakeLists.txt @@ -30,6 +30,7 @@ set(CMAKE_FOLDER "Magnum/Python") # *Not* REQUIRED find_package(Magnum COMPONENTS GL + MaterialTools MeshTools Primitives SceneGraph @@ -71,6 +72,9 @@ set(magnum_LIBS ) set(magnum_gl_SRCS gl.cpp) +set(magnum_materialtools_SRCS + materialtools.cpp) + set(magnum_meshtools_SRCS meshtools.cpp) @@ -108,6 +112,17 @@ if(NOT MAGNUM_BUILD_STATIC) LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum) endif() + if(Magnum_MaterialTools_FOUND) + pybind11_add_module(magnum_materialtools ${pybind11_add_module_SYSTEM} ${magnum_materialtools_SRCS}) + target_include_directories(magnum_materialtools PRIVATE + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/src/python) + target_link_libraries(magnum_materialtools PRIVATE Magnum::MaterialTools) + set_target_properties(magnum_materialtools PROPERTIES + OUTPUT_NAME "materialtools" + LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum) + endif() + if(Magnum_MeshTools_FOUND) pybind11_add_module(magnum_meshtools ${pybind11_add_module_SYSTEM} ${magnum_meshtools_SRCS}) target_include_directories(magnum_meshtools PRIVATE @@ -195,6 +210,11 @@ else() list(APPEND magnum_LIBS Magnum::GL) endif() + if(Magnum_MaterialTools_FOUND) + list(APPEND magnum_SRCS ${magnum_materialtools_SRCS}) + list(APPEND magnum_LIBS Magnum::MaterialTools) + endif() + if(Magnum_MeshTools_FOUND) list(APPEND magnum_SRCS ${magnum_meshtools_SRCS}) list(APPEND magnum_LIBS Magnum::MeshTools) diff --git a/src/python/magnum/__init__.py b/src/python/magnum/__init__.py index 7de9d02..9eb1135 100644 --- a/src/python/magnum/__init__.py +++ b/src/python/magnum/__init__.py @@ -39,7 +39,7 @@ sys.modules['magnum.math'] = math # In case Magnum is built statically, the whole core project is put into # _magnum. Then we need to do the same as above but for all modules. -for i in ['gl', 'meshtools', 'platform', 'primitives', 'scenegraph', 'scenetools', 'shaders', 'text', 'trade']: +for i in ['gl', 'materialtools', 'meshtools', 'platform', 'primitives', 'scenegraph', 'scenetools', 'shaders', 'text', 'trade']: if i in globals(): sys.modules['magnum.' + i] = globals()[i] # Platform has subpackages diff --git a/src/python/magnum/bootstrap.h b/src/python/magnum/bootstrap.h index 27b4c1f..e7deb06 100644 --- a/src/python/magnum/bootstrap.h +++ b/src/python/magnum/bootstrap.h @@ -70,6 +70,7 @@ void mathMatrixDouble(py::module_& root, PyTypeObject* metaclass); void mathRange(py::module_& root, py::module_& m); void gl(py::module_& m); +void materialtools(py::module_& m); void meshtools(py::module_& m); void primitives(py::module_& m); void scenegraph(py::module_& m); diff --git a/src/python/magnum/magnum.cpp b/src/python/magnum/magnum.cpp index 5963626..ab145e0 100644 --- a/src/python/magnum/magnum.cpp +++ b/src/python/magnum/magnum.cpp @@ -821,6 +821,12 @@ PYBIND11_MODULE(_magnum, m) { magnum::trade(trade); #endif + #ifdef Magnum_MaterialTools_FOUND + /* Depends on trade */ + py::module_ materialtools = m.def_submodule("materialtools"); + magnum::materialtools(materialtools); + #endif + #ifdef Magnum_MeshTools_FOUND /* Depends on trade and gl */ py::module_ meshtools = m.def_submodule("meshtools"); diff --git a/src/python/magnum/materialtools.cpp b/src/python/magnum/materialtools.cpp new file mode 100644 index 0000000..f0e1743 --- /dev/null +++ b/src/python/magnum/materialtools.cpp @@ -0,0 +1,135 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include /* for std::vector */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Magnum/Trade/PythonBindings.h" +#include "corrade/EnumOperators.h" +#include "magnum/bootstrap.h" + +namespace magnum { + +void materialtools(py::module_& m) { + m.doc() = "Material tools"; + + #ifndef MAGNUM_BUILD_STATIC + /* These are a part of the same module in the static build, no need to + import (also can't import because there it's _magnum.*) */ + py::module_::import("magnum.trade"); + #endif + + py::enum_{m, "MergeConflicts", "Material merge conflict resolution"} + .value("FAIL", MaterialTools::MergeConflicts::Fail) + .value("KEEP_FIRST_IF_SAME_TYPE", MaterialTools::MergeConflicts::KeepFirstIfSameType) + .value("KEEP_FIRST_IGNORE_TYPE", MaterialTools::MergeConflicts::KeepFirstIgnoreType); + + py::enum_ phongToPbrMetallicRoughnessFlag{m, "PhongToPbrMetallicRoughnessFlags", "Phong to PBR conversion flags"}; + phongToPbrMetallicRoughnessFlag + .value("KEEP_ORIGINAL_ATTRIBUTES", MaterialTools::PhongToPbrMetallicRoughnessFlag::KeepOriginalAttributes) + .value("DROP_UNCOVERTIBLE_ATTRIBUTES", MaterialTools::PhongToPbrMetallicRoughnessFlag::DropUnconvertibleAttributes) + .value("FAIL_ON_UNCONVERTIBLE_ATTRIBUTES", MaterialTools::PhongToPbrMetallicRoughnessFlag::FailOnUnconvertibleAttributes) + .value("NONE", MaterialTools::PhongToPbrMetallicRoughnessFlag{}) + .value("ALL", MaterialTools::PhongToPbrMetallicRoughnessFlag(Containers::enumCastUnderlyingType(~MaterialTools::PhongToPbrMetallicRoughnessFlags{}))); + corrade::enumOperators(phongToPbrMetallicRoughnessFlag); + + m + .def("copy", static_cast(MaterialTools::copy), "Make an owned copy of the material", py::arg("material")) + .def("filter_attributes", [](const Trade::MaterialData& material, const Containers::BitArrayView attributesToKeep, Trade::MaterialType typesToKeep) { + if(attributesToKeep.size() != material.attributeData().size()) { + PyErr_Format(PyExc_AssertionError, "expected %u bits but got %zu", material.attributeData().size(), attributesToKeep.size()); + throw py::error_already_set{}; + } + + return MaterialTools::filterAttributes(material, attributesToKeep, typesToKeep); + }, "Filter material attributes", py::arg("material"), py::arg("attributes_to_keep"), py::arg("types_to_keep") = Trade::MaterialType(Containers::enumCastUnderlyingType(~Trade::MaterialTypes{}))) + .def("filter_layers", [](const Trade::MaterialData& material, const Containers::BitArrayView layersToKeep, Trade::MaterialType typesToKeep) { + if(layersToKeep.size() != material.layerCount()) { + PyErr_Format(PyExc_AssertionError, "expected %u bits but got %zu", material.layerCount(), layersToKeep.size()); + throw py::error_already_set{}; + } + + return MaterialTools::filterLayers(material, layersToKeep, typesToKeep); + }, "Filter material layers", py::arg("material"), py::arg("layers_to_keep"), py::arg("types_to_keep") = Trade::MaterialType(Containers::enumCastUnderlyingType(~Trade::MaterialTypes{}))) + .def("filter_attributes_layers", [](const Trade::MaterialData& material, const Containers::BitArrayView attributesToKeep, const Containers::BitArrayView layersToKeep, Trade::MaterialType typesToKeep) { + if(attributesToKeep.size() != material.attributeData().size()) { + PyErr_Format(PyExc_AssertionError, "expected %u attribute bits but got %zu", material.attributeData().size(), attributesToKeep.size()); + throw py::error_already_set{}; + } + if(layersToKeep.size() != material.layerCount()) { + PyErr_Format(PyExc_AssertionError, "expected %u layer bits but got %zu", material.layerCount(), layersToKeep.size()); + throw py::error_already_set{}; + } + + return MaterialTools::filterAttributesLayers(material, attributesToKeep, layersToKeep, typesToKeep); + }, "Filter material attributes and layers", py::arg("material"), py::arg("attributes_to_keep"), py::arg("layers_to_keep"), py::arg("types_to_keep") = Trade::MaterialType(Containers::enumCastUnderlyingType(~Trade::MaterialTypes{}))) + .def("merge", [](const Trade::MaterialData& first, const Trade::MaterialData& second, MaterialTools::MergeConflicts conflicts) { + Containers::Optional out = MaterialTools::merge(first, second, conflicts); + if(!out) { + PyErr_SetString(PyExc_RuntimeError, "material merge failed"); + throw py::error_already_set{}; + } + + return *Utility::move(out); + }, "Merge two materials", py::arg("first"), py::arg("second"), py::arg("conflicts") = MaterialTools::MergeConflicts::Fail) + .def("phong_to_pbr_metallic_roughness", [](const Trade::MaterialData& material, MaterialTools::PhongToPbrMetallicRoughnessFlag flags) { + Containers::Optional out = MaterialTools::phongToPbrMetallicRoughness(material, flags); + if(!out) { + PyErr_SetString(PyExc_RuntimeError, "material conversion failed"); + throw py::error_already_set{}; + } + + return *Utility::move(out); + }, "Convert a Phong material to PBR metallic/roughness", py::arg("material"), py::arg("flags") = MaterialTools::PhongToPbrMetallicRoughnessFlag{}) + .def("remove_duplicates", [](std::vector> materials) { + std::vector indices; + indices.resize(materials.size()); + const std::size_t count = MaterialTools::removeDuplicatesInto(Containers::Iterable{materials.data(), materials.size(), sizeof(std::reference_wrapper), [](const void* data) -> const Trade::MaterialData& { + return static_cast*>(data)->get(); + }}, Containers::arrayView(indices)); + + return std::make_pair(std::move(indices), count); + }, "Remove duplicate materials from a list", py::arg("materials")); +} + +} + +#ifndef MAGNUM_BUILD_STATIC +/* TODO: remove declaration when https://github.com/pybind/pybind11/pull/1863 + is released */ +extern "C" PYBIND11_EXPORT PyObject* PyInit_materialtools(); +PYBIND11_MODULE(materialtools, m) { + magnum::materialtools(m); +} +#endif diff --git a/src/python/magnum/staticconfigure.h.cmake b/src/python/magnum/staticconfigure.h.cmake index 8f0208f..4651c60 100644 --- a/src/python/magnum/staticconfigure.h.cmake +++ b/src/python/magnum/staticconfigure.h.cmake @@ -27,6 +27,7 @@ on case-insensitive filesystems */ #cmakedefine Magnum_GL_FOUND +#cmakedefine Magnum_MaterialTools_FOUND #cmakedefine Magnum_MeshTools_FOUND #cmakedefine Magnum_Primitives_FOUND #cmakedefine Magnum_SceneGraph_FOUND diff --git a/src/python/magnum/test/material.gltf b/src/python/magnum/test/material.gltf index 69725b0..6abfffb 100644 --- a/src/python/magnum/test/material.gltf +++ b/src/python/magnum/test/material.gltf @@ -39,6 +39,7 @@ }, { "name": "Material with an empty layer", + "emissiveFactor": [0.5, 0.4, 0.3], "extensions": { "KHR_materials_clearcoat": {} } @@ -46,6 +47,14 @@ { "name": "A broken material", "alphaMode": false + }, + { + "name": "Specular/glossiness", + "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "specularFactor": [0.1, 0.2, 0.6] + } + } } ], "samplers": [ diff --git a/src/python/magnum/test/test_materialtools.py b/src/python/magnum/test/test_materialtools.py new file mode 100644 index 0000000..e10ae12 --- /dev/null +++ b/src/python/magnum/test/test_materialtools.py @@ -0,0 +1,188 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021, 2022, 2023 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import os +import unittest + +from corrade import containers +from magnum import * +from magnum import materialtools, trade +import magnum + +class Copy(unittest.TestCase): + def test(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "material.gltf")) + + material = importer.material(0) + self.assertEqual(material.attribute_data_flags, trade.DataFlags.OWNED|trade.DataFlags.MUTABLE) + + copy = materialtools.copy(material) + self.assertEqual(copy.attribute_data_flags, trade.DataFlags.OWNED|trade.DataFlags.MUTABLE) + +class Filter(unittest.TestCase): + def test(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + # This adds extra Diffuse attributes for BaseColor and a Phong type, + # don't want + importer.configuration['phongMaterialFallback'] = False + importer.open_file(os.path.join(os.path.dirname(__file__), "material.gltf")) + + material = importer.material(0) + self.assertEqual(material.types, trade.MaterialTypes.PBR_METALLIC_ROUGHNESS|trade.MaterialTypes.PBR_CLEAR_COAT) + self.assertEqual(material.layer_count, 2) + self.assertEqual(material.attribute_count(0), 3) + self.assertEqual(material.attribute_count(1), 8) + + # Unlike MeshData or SceneData the tools always produce a new copy of + # the attribute list, so we don't need to worry about data ownership + # and such + + attributes_to_keep = containers.BitArray.direct_init(material.attribute_data_offset(material.layer_count), True) + attributes_to_keep[material.attribute_id(trade.MaterialAttribute.DOUBLE_SIDED)] = False + attributes_to_keep[material.attribute_data_offset(1) + material.attribute_id(1, trade.MaterialAttribute.LAYER_FACTOR_TEXTURE_MATRIX)] = False + filtered_attributes = materialtools.filter_attributes(material, attributes_to_keep, ~trade.MaterialTypes.PBR_CLEAR_COAT) + self.assertEqual(filtered_attributes.types, trade.MaterialTypes.PBR_METALLIC_ROUGHNESS) + self.assertEqual(filtered_attributes.layer_count, 2) + self.assertEqual(filtered_attributes.attribute_count(0), 2) + self.assertEqual(filtered_attributes.attribute_count(1), 7) + self.assertFalse(filtered_attributes.has_attribute(trade.MaterialAttribute.DOUBLE_SIDED)) + self.assertFalse(filtered_attributes.has_attribute(1, trade.MaterialAttribute.LAYER_FACTOR_TEXTURE_MATRIX)) + + layers_to_keep = containers.BitArray.direct_init(material.layer_count, True) + layers_to_keep[1] = False + filtered_layers = materialtools.filter_layers(material, layers_to_keep, ~trade.MaterialTypes.PBR_CLEAR_COAT) + self.assertEqual(filtered_layers.types, trade.MaterialTypes.PBR_METALLIC_ROUGHNESS) + self.assertEqual(filtered_layers.layer_count, 1) + self.assertEqual(filtered_layers.attribute_count(), 3) + + filtered_attributes_layers = materialtools.filter_attributes_layers(material, attributes_to_keep, layers_to_keep, ~trade.MaterialTypes.PBR_CLEAR_COAT) + self.assertEqual(filtered_attributes_layers.types, trade.MaterialTypes.PBR_METALLIC_ROUGHNESS) + self.assertEqual(filtered_attributes_layers.layer_count, 1) + self.assertEqual(filtered_attributes_layers.attribute_count(), 2) + self.assertFalse(filtered_attributes_layers.has_attribute(trade.MaterialAttribute.DOUBLE_SIDED)) + + def test_invalid_size(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + # This adds extra Diffuse attributes for BaseColor and a Phong type, + # don't want + importer.configuration['phongMaterialFallback'] = False + importer.open_file(os.path.join(os.path.dirname(__file__), "material.gltf")) + + material = importer.material(0) + self.assertEqual(material.types, trade.MaterialTypes.PBR_METALLIC_ROUGHNESS|trade.MaterialTypes.PBR_CLEAR_COAT) + self.assertEqual(material.layer_count, 2) + self.assertEqual(material.attribute_count(0), 3) + self.assertEqual(material.attribute_count(1), 8) + + with self.assertRaisesRegex(AssertionError, "expected 11 bits but got 12"): + materialtools.filter_attributes(material, containers.BitArray.value_init(12)) + with self.assertRaisesRegex(AssertionError, "expected 2 bits but got 3"): + materialtools.filter_layers(material, containers.BitArray.value_init(3)) + with self.assertRaisesRegex(AssertionError, "expected 11 attribute bits but got 12"): + materialtools.filter_attributes_layers(material, containers.BitArray.value_init(12), containers.BitArray.value_init(2)) + with self.assertRaisesRegex(AssertionError, "expected 2 layer bits but got 3"): + materialtools.filter_attributes_layers(material, containers.BitArray.value_init(11), containers.BitArray.value_init(3)) + +class Merge(unittest.TestCase): + def test(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + # This adds extra Diffuse attributes for BaseColor and a Phong type, + # don't want + importer.configuration['phongMaterialFallback'] = False + importer.open_file(os.path.join(os.path.dirname(__file__), "material.gltf")) + + a = importer.material('A material with a layer') + self.assertEqual(a.types, trade.MaterialTypes.PBR_METALLIC_ROUGHNESS|trade.MaterialTypes.PBR_CLEAR_COAT) + self.assertEqual(a.layer_count, 2) + self.assertEqual(a.attribute_count(0), 3) + self.assertEqual(a.attribute_count(1), 8) + + b = importer.material('Material with an empty layer') + self.assertEqual(b.types, trade.MaterialTypes.PBR_CLEAR_COAT) + self.assertEqual(b.layer_count, 2) + self.assertEqual(b.attribute_count(0), 1) + self.assertEqual(b.attribute_count(1), 3) + + merged = materialtools.merge(a, b, materialtools.MergeConflicts.KEEP_FIRST_IF_SAME_TYPE) + self.assertEqual(merged.types, trade.MaterialTypes.PBR_METALLIC_ROUGHNESS|trade.MaterialTypes.PBR_CLEAR_COAT) + self.assertEqual(merged.layer_count, 2) + self.assertEqual(merged.attribute_count(0), 4) + self.assertEqual(merged.attribute_count(1), 8) + + def test_failed(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "material.gltf")) + + a = importer.material('A material with a layer') + b = importer.material('Material with an empty layer') + + with self.assertRaisesRegex(RuntimeError, "material merge failed"): + materialtools.merge(a, b) + +class PhongToPbrMetallicRoughness(unittest.TestCase): + def test(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + # We actually *do* want the Phong stuff + importer.configuration['phongMaterialFallback'] = True + importer.open_file(os.path.join(os.path.dirname(__file__), "material.gltf")) + material = importer.material(0) + + # Keep just the diffuse color, drop everything else + attributes_to_keep = containers.BitArray.value_init(material.attribute_data_offset(material.layer_count)) + attributes_to_keep[material.attribute_id(trade.MaterialAttribute.DIFFUSE_COLOR)] = True + layers_to_keep = containers.BitArray.value_init(material.layer_count) + layers_to_keep[0] = True + filtered = materialtools.filter_attributes_layers(material, attributes_to_keep, layers_to_keep, ~(trade.MaterialTypes.PBR_METALLIC_ROUGHNESS|trade.MaterialTypes.PBR_CLEAR_COAT)) + self.assertEqual(filtered.types, trade.MaterialTypes.PHONG) + self.assertEqual(filtered.layer_count, 1) + self.assertEqual(filtered.attribute_count(), 1) + self.assertTrue(filtered.has_attribute(trade.MaterialAttribute.DIFFUSE_COLOR)) + + pbr = materialtools.phong_to_pbr_metallic_roughness(filtered) + self.assertEqual(pbr.types, trade.MaterialTypes.PBR_METALLIC_ROUGHNESS) + self.assertEqual(pbr.layer_count, 1) + self.assertEqual(pbr.attribute_count(), 1) + self.assertTrue(pbr.has_attribute(trade.MaterialAttribute.BASE_COLOR)) + + def test_failed(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "material.gltf")) + material = importer.material('Specular/glossiness') + + with self.assertRaisesRegex(RuntimeError, "material conversion failed"): + materialtools.phong_to_pbr_metallic_roughness(material, materialtools.PhongToPbrMetallicRoughnessFlags.FAIL_ON_UNCONVERTIBLE_ATTRIBUTES) + +class RemoveDuplicates(unittest.TestCase): + def test(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "material.gltf")) + a = importer.material(0) + b = importer.material(1) + c = importer.material(2) + + indices, count = materialtools.remove_duplicates([a, b, a, c, c, b]) + self.assertEqual(indices, [0, 1, 0, 3, 3, 1]) + self.assertEqual(count, 3) diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index 3afada0..2228036 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -1842,10 +1842,10 @@ class Importer(unittest.TestCase): with self.assertRaisesRegex(IndexError, "index 5 out of range for 5 entries"): mesh_importer.mesh(5) - with self.assertRaisesRegex(IndexError, "index 4 out of range for 4 entries"): - material_importer.material_name(4) - with self.assertRaisesRegex(IndexError, "index 4 out of range for 4 entries"): - material_importer.material(4) + with self.assertRaisesRegex(IndexError, "index 5 out of range for 5 entries"): + material_importer.material_name(5) + with self.assertRaisesRegex(IndexError, "index 5 out of range for 5 entries"): + material_importer.material(5) with self.assertRaisesRegex(IndexError, "index 3 out of range for 3 entries"): texture_importer.texture_name(3) @@ -2013,7 +2013,7 @@ class Importer(unittest.TestCase): importer = trade.ImporterManager().load_and_instantiate('GltfImporter') importer.open_file(os.path.join(os.path.dirname(__file__), 'material.gltf')) - self.assertEqual(importer.material_count, 4) + self.assertEqual(importer.material_count, 5) self.assertEqual(importer.material_name(2), 'Material with an empty layer') self.assertEqual(importer.material_for_name('Material with an empty layer'), 2) @@ -2031,7 +2031,7 @@ class Importer(unittest.TestCase): importer = trade.ImporterManager().load_and_instantiate('GltfImporter') importer.open_file(os.path.join(os.path.dirname(__file__), 'material.gltf')) - with self.assertRaisesRegex(KeyError, "name Nonexistent not found among 4 entries"): + with self.assertRaisesRegex(KeyError, "name Nonexistent not found among 5 entries"): importer.material('Nonexistent') def test_material_failed(self): diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index 6f6c973..3764ef2 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -1382,7 +1382,9 @@ void trade(py::module_& m) { .value("PHONG", Trade::MaterialType::Phong) .value("PBR_METALLIC_ROUGHNESS", Trade::MaterialType::PbrMetallicRoughness) .value("PBR_SPECULAR_GLOSSINESS", Trade::MaterialType::PbrSpecularGlossiness) - .value("PBR_CLEAR_COAT", Trade::MaterialType::PbrClearCoat); + .value("PBR_CLEAR_COAT", Trade::MaterialType::PbrClearCoat) + .value("NONE", Trade::MaterialType{}) + .value("ALL", Trade::MaterialType(Containers::enumCastUnderlyingType(~Trade::MaterialType{}))); corrade::enumOperators(materialType); py::enum_{m, "MaterialAlphaMode", "Material alpha mode"} diff --git a/src/python/setup.py.cmake b/src/python/setup.py.cmake index 80c960c..0d96692 100644 --- a/src/python/setup.py.cmake +++ b/src/python/setup.py.cmake @@ -39,6 +39,7 @@ extension_paths = { 'corrade.utility': '${corrade_utility_file}', '_magnum': '$', 'magnum.gl': '${magnum_gl_file}', + 'magnum.materialtools': '${magnum_materialtools_file}', 'magnum.meshtools': '${magnum_meshtools_file}', 'magnum.primitives': '${magnum_primitives_file}', 'magnum.scenegraph': '${magnum_scenegraph_file}',