From d106fc09d7e70faea9a7251c436d24bd1c4161ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 8 Feb 2023 00:15:45 +0100 Subject: [PATCH] python: direct access to trade.MeshData indices and attributes. In concrete types. Yes. Well, some of them for now, I still need to sleep on normalized and matrix types, how it makes the most sense to expose those. Oh, and array attributes as well. --- doc/python/magnum.trade.rst | 44 ++++++ src/python/magnum/test/test_trade.py | 182 +++++++++++++++++++++++++ src/python/magnum/trade.cpp | 195 ++++++++++++++++++++++++++- 3 files changed, 420 insertions(+), 1 deletion(-) diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst index 0d19de7..425fccb 100644 --- a/doc/python/magnum.trade.rst +++ b/doc/python/magnum.trade.rst @@ -72,6 +72,35 @@ :dox:`Trade::MeshData::findAttributeId()`, the desired workflow is instead calling :ref:`attribute_id()` and catching an exception if not found. + `Index and attribute data access`_ + ================================== + + The class makes use of Python's dynamic nature and provides direct access + to index and attribute data in their concrete types via :ref:`indices` and + :ref:`attribute()`. The returned views point to the underlying mesh data, + element access coverts to a type corresponding to a particular + :ref:`VertexFormat` and for performance-oriented access the view implements + a buffer protocol with a corresponding type annotation: + + .. + >>> from magnum import primitives, trade + >>> import numpy as np + + .. code:: pycon + + >>> mesh = primitives.cube_solid() + >>> list(mesh.indices)[:10] + [0, 1, 2, 0, 2, 3, 4, 5, 6, 4] + >>> list(mesh.attribute(trade.MeshAttribute.POSITION))[:3] + [Vector(-1, -1, 1), Vector(1, -1, 1), Vector(1, 1, 1)] + >>> np.array(mesh.attribute(trade.MeshAttribute.NORMAL), copy=False)[2] + (0., 0., 1.) + + Depending on the value of :ref:`index_data_flags` / :ref:`vertex_data_flags` + it's also possible to access the data in a mutable way via + :ref:`mutable_indices` and :ref:`mutable_attribute()`, for example to + perform a static transformation of the mesh before passing it to OpenGL. + .. py:property:: magnum.trade.MeshData.mutable_index_data :raise AttributeError: If :ref:`index_data_flags` doesn't contain :ref:`DataFlag.MUTABLE` @@ -86,6 +115,9 @@ :raise AttributeError: If :ref:`is_indexed` is :py:`False` .. py:property:: magnum.trade.MeshData.index_stride :raise AttributeError: If :ref:`is_indexed` is :py:`False` +.. py:property:: magnum.trade.MeshData.mutable_indices + :raise AttributeError: If :ref:`index_data_flags` doesn't contain + :ref:`DataFlag.MUTABLE` .. py:function:: magnum.trade.MeshData.attribute_name :raise IndexError: If :p:`id` is negative or not less than :ref:`attribute_count()` @@ -114,6 +146,18 @@ :ref:`attribute_count()` :raise KeyError: If :p:`id` is negative or not less than :ref:`attribute_count()` for :p:`name` +.. py:function:: magnum.trade.MeshData.attribute + :raise IndexError: If :p:`id` is negative or not less than + :ref:`attribute_count()` + :raise KeyError: If :p:`id` is negative or not less than + :ref:`attribute_count()` for :p:`name` +.. py:function:: magnum.trade.MeshData.mutable_attribute + :raise IndexError: If :p:`id` is negative or not less than + :ref:`attribute_count()` + :raise KeyError: If :p:`id` is negative or not less than + :ref:`attribute_count()` for :p:`name` + :raise AttributeError: If :ref:`vertex_data_flags` doesn't contain + :ref:`DataFlag.MUTABLE` .. py:class:: magnum.trade.ImporterManager :summary: Manager for :ref:`AbstractImporter` plugin instances diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index 1dc0921..33bf387 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -202,6 +202,95 @@ class MeshData(unittest.TestCase): del mutable_vertex_data self.assertEqual(sys.getrefcount(mesh), mesh_refcount) + def test_indices_access(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + + mesh = importer.mesh(0) + mesh_refcount = sys.getrefcount(mesh) + + indices = mesh.indices + self.assertEqual(indices.size, (3, )) + self.assertEqual(indices.stride, (2, )) + self.assertEqual(indices.format, 'H') + self.assertEqual(list(indices), [0, 2, 1]) + self.assertIs(indices.owner, mesh) + self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1) + + del indices + self.assertEqual(sys.getrefcount(mesh), mesh_refcount) + + mutable_indices = mesh.mutable_indices + self.assertEqual(mutable_indices.size, (3, )) + self.assertEqual(mutable_indices.stride, (2, )) + self.assertEqual(mutable_indices.format, 'H') + self.assertEqual(list(mutable_indices), [0, 2, 1]) + self.assertIs(mutable_indices.owner, mesh) + self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1) + + del mutable_indices + self.assertEqual(sys.getrefcount(mesh), mesh_refcount) + + def test_attribute_access(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + + mesh = importer.mesh(0) + mesh_refcount = sys.getrefcount(mesh) + self.assertEqual(mesh.attribute_id(trade.MeshAttribute.POSITION), 3) + + positions = mesh.attribute(3) + self.assertEqual(positions.size, (3, )) + self.assertEqual(positions.stride, (28, )) + self.assertEqual(positions.format, 'fff') + self.assertEqual(list(positions), [ + Vector3(-1, -1, 0.25), + Vector3(0, 1, 0.5), + Vector3(1, -1, 0.25) + ]) + self.assertIs(positions.owner, mesh) + self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1) + + del positions + self.assertEqual(sys.getrefcount(mesh), mesh_refcount) + + object_ids = mesh.attribute(trade.MeshAttribute.OBJECT_ID) + self.assertEqual(object_ids.size, (3, )) + self.assertEqual(object_ids.stride, (28, )) + self.assertEqual(object_ids.format, 'I') + self.assertEqual(list(object_ids), [216, 16777235, 2872872013]) + self.assertIs(object_ids.owner, mesh) + self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1) + + del object_ids + self.assertEqual(sys.getrefcount(mesh), mesh_refcount) + + mutable_positions = mesh.mutable_attribute(3) + self.assertEqual(mutable_positions.size, (3, )) + self.assertEqual(mutable_positions.stride, (28, )) + self.assertEqual(mutable_positions.format, 'fff') + self.assertEqual(list(mutable_positions), [ + Vector3(-1, -1, 0.25), + Vector3(0, 1, 0.5), + Vector3(1, -1, 0.25) + ]) + self.assertIs(mutable_positions.owner, mesh) + self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1) + + del mutable_positions + self.assertEqual(sys.getrefcount(mesh), mesh_refcount) + + mutable_object_ids = mesh.mutable_attribute(trade.MeshAttribute.OBJECT_ID) + self.assertEqual(mutable_object_ids.size, (3, )) + self.assertEqual(mutable_object_ids.stride, (28, )) + self.assertEqual(mutable_object_ids.format, 'I') + self.assertEqual(list(mutable_object_ids), [216, 16777235, 2872872013]) + self.assertIs(mutable_object_ids.owner, mesh) + self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1) + + del mutable_object_ids + self.assertEqual(sys.getrefcount(mesh), mesh_refcount) + def test_mutable_index_data_access(self): importer = trade.ImporterManager().load_and_instantiate('GltfImporter') importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) @@ -236,6 +325,45 @@ class MeshData(unittest.TestCase): mutable_vertex_data[21] = chr(76) self.assertEqual(vertex_data[21], chr(76)) + def test_mutable_indices_access(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + + mesh = importer.mesh(0) + self.assertEqual(mesh.index_data_flags, trade.DataFlag.OWNED|trade.DataFlag.MUTABLE) + + indices = mesh.indices + mutable_indices = mesh.mutable_indices + self.assertEqual(indices[1], 2) + self.assertEqual(mutable_indices[1], 2) + + mutable_indices[1] = 76 + self.assertEqual(indices[1], 76) + + def test_mutable_attributes_access(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + + mesh = importer.mesh(0) + self.assertEqual(mesh.index_data_flags, trade.DataFlag.OWNED|trade.DataFlag.MUTABLE) + self.assertEqual(mesh.attribute_id(trade.MeshAttribute.POSITION), 3) + + positions = mesh.attribute(3) + mutable_positions = mesh.mutable_attribute(3) + self.assertEqual(positions[1], Vector3(0, 1, 0.5)) + self.assertEqual(mutable_positions[1], Vector3(0, 1, 0.5)) + + mutable_positions[1] *= 2 + self.assertEqual(positions[1], Vector3(0, 2, 1)) + + object_ids = mesh.attribute(trade.MeshAttribute.OBJECT_ID) + mutable_object_ids = mesh.mutable_attribute(trade.MeshAttribute.OBJECT_ID) + self.assertEqual(object_ids[1], 16777235) + self.assertEqual(mutable_object_ids[1], 16777235) + + mutable_object_ids[1] //= 1000 + self.assertEqual(object_ids[1], 16777) + def test_data_access_not_mutable(self): mesh = primitives.cube_solid() # TODO split this once there's a mesh where only one or the other would @@ -245,8 +373,14 @@ class MeshData(unittest.TestCase): with self.assertRaisesRegex(AttributeError, "mesh index data is not mutable"): mesh.mutable_index_data + with self.assertRaisesRegex(AttributeError, "mesh index data is not mutable"): + mesh.mutable_indices with self.assertRaisesRegex(AttributeError, "mesh vertex data is not mutable"): mesh.mutable_vertex_data + with self.assertRaisesRegex(AttributeError, "mesh vertex data is not mutable"): + mesh.mutable_attribute(0) + with self.assertRaisesRegex(AttributeError, "mesh vertex data is not mutable"): + mesh.mutable_attribute(trade.MeshAttribute.POSITION) def test_nonindexed(self): importer = trade.ImporterManager().load_and_instantiate('GltfImporter') @@ -267,6 +401,10 @@ class MeshData(unittest.TestCase): mesh.index_offset with self.assertRaisesRegex(AttributeError, "mesh is not indexed"): mesh.index_stride + with self.assertRaisesRegex(AttributeError, "mesh is not indexed"): + mesh.indices + with self.assertRaisesRegex(AttributeError, "mesh is not indexed"): + mesh.mutable_indices def test_attribute_oob(self): importer = trade.ImporterManager().load_and_instantiate('GltfImporter') @@ -287,6 +425,10 @@ class MeshData(unittest.TestCase): mesh.attribute_stride(mesh.attribute_count()) with self.assertRaises(IndexError): mesh.attribute_array_size(mesh.attribute_count()) + with self.assertRaises(IndexError): + mesh.attribute(mesh.attribute_count()) + with self.assertRaises(IndexError): + mesh.mutable_attribute(mesh.attribute_count()) # Access by nonexistent name with self.assertRaises(KeyError): @@ -299,6 +441,10 @@ class MeshData(unittest.TestCase): mesh.attribute_stride(trade.MeshAttribute.TANGENT) with self.assertRaises(KeyError): mesh.attribute_array_size(trade.MeshAttribute.TANGENT) + with self.assertRaises(KeyError): + mesh.attribute(trade.MeshAttribute.TANGENT) + with self.assertRaises(KeyError): + mesh.mutable_attribute(trade.MeshAttribute.TANGENT) # Access by existing name + OOB ID with self.assertRaises(KeyError): @@ -311,6 +457,42 @@ class MeshData(unittest.TestCase): mesh.attribute_stride(trade.MeshAttribute.TEXTURE_COORDINATES, 2) with self.assertRaises(KeyError): mesh.attribute_array_size(trade.MeshAttribute.TEXTURE_COORDINATES, 2) + with self.assertRaises(KeyError): + mesh.attribute(trade.MeshAttribute.TEXTURE_COORDINATES, 2) + with self.assertRaises(KeyError): + mesh.mutable_attribute(trade.MeshAttribute.TEXTURE_COORDINATES, 2) + + def test_attribute_access_array(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + + mesh = importer.mesh(0) + self.assertEqual(mesh.attribute_id(trade.MeshAttribute.JOINT_IDS), 1) + + with self.assertRaisesRegex(NotImplementedError, "array attributes not implemented yet, sorry"): + mesh.attribute(1) + with self.assertRaisesRegex(NotImplementedError, "array attributes not implemented yet, sorry"): + mesh.mutable_attribute(1) + with self.assertRaisesRegex(NotImplementedError, "array attributes not implemented yet, sorry"): + mesh.attribute(trade.MeshAttribute.JOINT_IDS) + with self.assertRaisesRegex(NotImplementedError, "array attributes not implemented yet, sorry"): + mesh.mutable_attribute(trade.MeshAttribute.JOINT_IDS) + + def test_attribute_access_unsupported_format(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + + mesh = importer.mesh(0) + self.assertEqual(mesh.attribute_id(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE")), 8) + + with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"): + mesh.attribute(8) + with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"): + mesh.mutable_attribute(8) + with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"): + mesh.attribute(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE")) + with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"): + mesh.mutable_attribute(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE")) class Importer(unittest.TestCase): def test(self): diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index 8d8bf2c..380b4fb 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -27,7 +27,10 @@ #include #include #include /** @todo drop once we have our string casters */ +#include #include +#include +#include #include #include #include @@ -299,6 +302,115 @@ template accessorsForMeshIndexType(const MeshIndexType type) { + switch(type) { + #define _c(type, string) \ + case MeshIndexType::type: return { \ + string, \ + [](const char* item) { \ + return py::cast(*reinterpret_cast(item)); \ + }, \ + [](char* item, py::handle object) { \ + *reinterpret_cast(item) = py::cast(object); \ + }}; + _c(UnsignedByte, "B") + _c(UnsignedShort, "H") + _c(UnsignedInt, "I") + #undef _c + } + + return {}; +} + +Containers::Triple accessorsForVertexFormat(const VertexFormat format) { + switch(format) { + #define _c(format, string) \ + case VertexFormat::format: return { \ + string, \ + [](const char* item) { \ + return py::cast(*reinterpret_cast(item)); \ + }, \ + [](char* item, py::handle object) { \ + *reinterpret_cast(item) = py::cast(object); \ + }}; + /* Types (such as half-floats) that need to be cast before passed + from/to pybind that doesn't understand the type directly */ + #define _cc(format, castType, string) \ + case VertexFormat::format: return { \ + string, \ + [](const char* item) { \ + return py::cast(format(*reinterpret_cast(item))); \ + }, \ + [](char* item, py::handle object) { \ + *reinterpret_cast(item) = format(py::cast(object)); \ + }}; + _c(Float, "f") + _c(Double, "d") + _cc(UnsignedByte, UnsignedInt, "B") + _cc(Byte, Int, "b") + _cc(UnsignedShort, UnsignedInt, "H") + _cc(Short, Int, "h") + _c(UnsignedInt, "I") + _c(Int, "i") + + _c(Vector2, "ff") + _c(Vector2d, "dd") + _cc(Vector2ub, Vector2ui, "BB") + _cc(Vector2b, Vector2i, "bb") + _cc(Vector2us, Vector2ui, "HH") + _cc(Vector2s, Vector2i, "hh") + _c(Vector2ui, "II") + _c(Vector2i, "ii") + + _c(Vector3, "fff") + _c(Vector3d, "ddd") + _cc(Vector3ub, Vector3ui, "BBB") + _cc(Vector3b, Vector3i, "bbb") + _cc(Vector3us, Vector3ui, "HHH") + _cc(Vector3s, Vector3i, "hhh") + _c(Vector3ui, "III") + _c(Vector3i, "iii") + + _c(Vector4, "ffff") + _c(Vector4d, "dddd") + _cc(Vector4ub, Vector4ui, "BBBB") + _cc(Vector4b, Vector4i, "bbbb") + _cc(Vector4us, Vector4ui, "HHHH") + _cc(Vector4s, Vector4i, "hhhh") + _c(Vector4ui, "IIII") + _c(Vector4i, "iiii") + #undef _c + #undef _cc + + /** @todo handle half, normalized and matrix types */ + default: + return {}; + } + + return {}; +} + +template Containers::PyArrayViewHolder> meshIndicesView(Trade::MeshData& mesh, const Containers::StridedArrayView2D& data) { + const MeshIndexType type = mesh.indexType(); + const std::size_t itemsize = meshIndexTypeSize(type); + const Containers::Triple formatStringGetitemSetitem = accessorsForMeshIndexType(type); + /** @todo update this once there are plugins that can give back custom + index types */ + CORRADE_INTERNAL_ASSERT(formatStringGetitemSetitem.first()); + return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<1, T>{data.template transposed<0, 1>()[0], formatStringGetitemSetitem.first(), itemsize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, py::cast(mesh)); +} + +template Containers::PyArrayViewHolder> meshAttributeView(Trade::MeshData& mesh, const UnsignedInt id, const Containers::StridedArrayView2D& data) { + const VertexFormat format = mesh.attributeFormat(id); + const std::size_t itemsize = vertexFormatSize(format); + const Containers::Triple formatStringGetitemSetitem = accessorsForVertexFormat(format); + if(!formatStringGetitemSetitem.first()) { + PyErr_SetString(PyExc_NotImplementedError, "access to this vertex format is not implemented yet, sorry"); + throw py::error_already_set{}; + } + return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<1, T>{data.template transposed<0, 1>()[0], formatStringGetitemSetitem.first(), itemsize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, py::cast(mesh)); +} + } void trade(py::module_& m) { @@ -384,6 +496,24 @@ void trade(py::module_& m) { } return self.indexStride(); }, "Index stride") + .def_property_readonly("indices", [](Trade::MeshData& self) { + if(!self.isIndexed()) { + PyErr_SetString(PyExc_AttributeError, "mesh is not indexed"); + throw py::error_already_set{}; + } + return meshIndicesView(self, self.indices()); + }, "Indices") + .def_property_readonly("mutable_indices", [](Trade::MeshData& self) { + if(!self.isIndexed()) { + PyErr_SetString(PyExc_AttributeError, "mesh is not indexed"); + throw py::error_already_set{}; + } + if(!(self.indexDataFlags() & Trade::DataFlag::Mutable)) { + PyErr_SetString(PyExc_AttributeError, "mesh index data is not mutable"); + throw py::error_already_set{}; + } + return meshIndicesView(self, self.mutableIndices()); + }, "Mutable indices") .def_property_readonly("vertex_count", &Trade::MeshData::vertexCount, "Vertex count") /* Has to be a function instead of a property because there's an overload taking a name */ @@ -463,7 +593,70 @@ void trade(py::module_& m) { return self.attributeArraySize(*found); PyErr_SetString(PyExc_KeyError, ""); throw py::error_already_set{}; - }, "Array size of a named attribute", py::arg("name"), py::arg("id") = 0); + }, "Array size of a named attribute", py::arg("name"), py::arg("id") = 0) + .def("attribute", [](Trade::MeshData& self, UnsignedInt id) { + if(id >= self.attributeCount()) { + PyErr_SetString(PyExc_IndexError, ""); + throw py::error_already_set{}; + } + /** @todo handle arrays (return a 2D view, and especially annotate + the return type properly in the docs) */ + if(self.attributeArraySize(id) != 0) { + PyErr_SetString(PyExc_NotImplementedError, "array attributes not implemented yet, sorry"); + throw py::error_already_set{}; + } + return meshAttributeView(self, id, self.attribute(id)); + }, "Data for given attribute", py::arg("id")) + .def("mutable_attribute", [](Trade::MeshData& self, UnsignedInt id) { + if(id >= self.attributeCount()) { + PyErr_SetString(PyExc_IndexError, ""); + throw py::error_already_set{}; + } + if(!(self.vertexDataFlags() & Trade::DataFlag::Mutable)) { + PyErr_SetString(PyExc_AttributeError, "mesh vertex data is not mutable"); + throw py::error_already_set{}; + } + /** @todo handle arrays (return a 2D view, and especially annotate + the return type properly in the docs) */ + if(self.attributeArraySize(id) != 0) { + PyErr_SetString(PyExc_NotImplementedError, "array attributes not implemented yet, sorry"); + throw py::error_already_set{}; + } + return meshAttributeView(self, id, self.mutableAttribute(id)); + }, "Mutable data for given attribute", py::arg("id")) + .def("attribute", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) { + const Containers::Optional found = self.findAttributeId(name, id); + if(!found) { + PyErr_SetString(PyExc_KeyError, ""); + throw py::error_already_set{}; + } + /** @todo handle arrays (return a 2D view, and especially annotate + the return type properly in the docs) */ + if(self.attributeArraySize(*found) != 0) { + PyErr_SetString(PyExc_NotImplementedError, "array attributes not implemented yet, sorry"); + throw py::error_already_set{}; + } + return meshAttributeView(self, *found, self.attribute(*found)); + }, "Data for given named attribute", py::arg("name"), py::arg("id") = 0) + .def("mutable_attribute", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) { + const Containers::Optional found = self.findAttributeId(name, id); + if(!found) { + PyErr_SetString(PyExc_KeyError, ""); + throw py::error_already_set{}; + } + if(!(self.vertexDataFlags() & Trade::DataFlag::Mutable)) { + PyErr_SetString(PyExc_AttributeError, "mesh vertex data is not mutable"); + throw py::error_already_set{}; + } + /** @todo handle arrays (return a 2D view, and especially annotate + the return type properly in the docs) */ + if(self.attributeArraySize(*found) != 0) { + PyErr_SetString(PyExc_NotImplementedError, "array attributes not implemented yet, sorry"); + throw py::error_already_set{}; + } + return meshAttributeView(self, *found, self.mutableAttribute(*found)); + }, "Data for given named attribute", py::arg("name"), py::arg("id") = 0) + ; py::class_ imageData1D{m, "ImageData1D", "One-dimensional image data"}; py::class_ imageData2D{m, "ImageData2D", "Two-dimensional image data"};