From 8ecb7fabd0ac8517858e5d6714d843fc1f3eea73 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Mon, 8 Jun 2026 19:05:38 +0100 Subject: [PATCH 1/5] Add C API for f3d::mesh_view + optional in-memory base-color texture Expose the zero-copy f3d::mesh_view path through the C API so bindings can show in-memory meshes (geometry, point/cell scalars, animation) without copying arrays into libf3d: - c/types_c_api.h: enum f3d_mesh_data_type_t; structs f3d_data_array_t, f3d_cell_array_t, f3d_memory_view_t. - c/scene_c_api.{h,cxx}: f3d_scene_add_mesh_view() with to_cpp_* converters and a concrete c_mesh_view keeping the caller's data pointers. Also add an optional in-memory base-color (and optional emissive) texture carried on the mesh_view itself, so a textured/draped mesh needs no temp image file: - library/public/mesh_view.h: texture_t {width,height,components,data, emissive} + texture_t baseColorTexture in memory_view_t. - library/src/scene_impl.cxx: build a vtkImageData+vtkTexture (sRGB) from baseColorTexture and hand it to the importer. - vtkext vtkF3DGenericImporter: SetBaseColorTexture(vtkTexture*, bool emissive), applied per block in CreateActorForBlock. - vtkext vtkF3DRenderer: the rendered coloring actor is a clone of the importer's OriginalActor; propagate albedoTex/emissiveTex from the source actor to the clone when no global texture override is set (RGBA forces translucent). Without this an actor-level texture renders blank. All additive (336 insertions, no deletions). --- c/scene_c_api.cxx | 125 ++++++++++++++++++ c/scene_c_api.h | 23 ++++ c/types_c_api.h | 86 ++++++++++++ library/public/mesh_view.h | 20 +++ library/src/scene_impl.cxx | 31 +++++ .../private/module/vtkF3DGenericImporter.cxx | 24 ++++ vtkext/private/module/vtkF3DGenericImporter.h | 8 ++ vtkext/private/module/vtkF3DRenderer.cxx | 19 +++ 8 files changed, 336 insertions(+) diff --git a/c/scene_c_api.cxx b/c/scene_c_api.cxx index 3cf07ad5fd..81c4ec5efe 100644 --- a/c/scene_c_api.cxx +++ b/c/scene_c_api.cxx @@ -1,9 +1,13 @@ #include "scene_c_api.h" +#include "mesh_view.h" #include "scene.h" #include "types.h" +#include #include #include +#include +#include #include namespace @@ -116,6 +120,102 @@ f3d::mesh_t to_cpp_mesh(const f3d_mesh_t* c_mesh) return cpp_mesh; } + +//---------------------------------------------------------------------------- +f3d::mesh_view::data_array_t to_cpp_data_array(const f3d_data_array_t& c) +{ + f3d::mesh_view::data_array_t a; + if (c.name) + { + a.name = c.name; + } + a.type = static_cast(c.type); + a.data = c.data; + a.components = c.components ? c.components : 1; + a.stride = c.stride ? c.stride : 1; + a.timeDependent = c.time_dependent != 0; + return a; +} + +//---------------------------------------------------------------------------- +f3d::mesh_view::cell_array_t to_cpp_cell_array(const f3d_cell_array_t& c) +{ + f3d::mesh_view::cell_array_t a; + // 0 from a zero-initialized C struct means "no cell": map it to the C++ default of 1. + a.offsetCount = c.offset_count ? c.offset_count : 1; + a.offsets = to_cpp_data_array(c.offsets); + a.indexCount = c.index_count; + a.indices = to_cpp_data_array(c.indices); + return a; +} + +//---------------------------------------------------------------------------- +f3d::mesh_view::memory_view_t to_cpp_memory_view(const f3d_memory_view_t* c) +{ + f3d::mesh_view::memory_view_t v; + v.pointCount = c->point_count; + v.points = to_cpp_data_array(c->points); + v.normals = to_cpp_data_array(c->normals); + v.textureCoordinates = to_cpp_data_array(c->texture_coordinates); + v.vertices = to_cpp_cell_array(c->vertices); + v.lines = to_cpp_cell_array(c->lines); + v.polygons = to_cpp_cell_array(c->polygons); + + v.pointScalars.reserve(c->point_scalars_count); + for (size_t i = 0; i < c->point_scalars_count; ++i) + { + v.pointScalars.push_back(to_cpp_data_array(c->point_scalars[i])); + } + + v.cellScalars.reserve(c->cell_scalars_count); + for (size_t i = 0; i < c->cell_scalars_count; ++i) + { + v.cellScalars.push_back(to_cpp_data_array(c->cell_scalars[i])); + } + + v.baseColorTexture.data = c->base_color_texture; + v.baseColorTexture.width = c->base_color_texture_width; + v.baseColorTexture.height = c->base_color_texture_height; + v.baseColorTexture.components = + c->base_color_texture_components ? c->base_color_texture_components : 3; + v.baseColorTexture.emissive = c->base_color_texture_emissive != 0; + return v; +} + +//---------------------------------------------------------------------------- +// Concrete mesh_view holding a zero-copy snapshot of the caller's arrays. The data +// pointers inside View reference caller-owned memory; only the small metadata (names, +// layout, vectors of scalar descriptors) is owned here. +class c_mesh_view : public f3d::mesh_view +{ +public: + c_mesh_view(std::string name, std::array range, f3d::mesh_view::memory_view_t view) + : Name(std::move(name)) + , Range(range) + , View(std::move(view)) + { + } + + std::string getName() const override + { + return this->Name; + } + + std::array getTimeRange() const override + { + return this->Range; + } + + f3d::mesh_view::memory_view_t getMemoryView(double) const override + { + return this->View; + } + +private: + std::string Name; + std::array Range; + f3d::mesh_view::memory_view_t View; +}; } //---------------------------------------------------------------------------- @@ -221,6 +321,31 @@ int f3d_scene_add_mesh(f3d_scene_t* scene, const f3d_mesh_t* mesh) return 1; } +//---------------------------------------------------------------------------- +int f3d_scene_add_mesh_view( + f3d_scene_t* scene, const f3d_memory_view_t* view, const char* name, double t_min, double t_max) +{ + if (!scene || !view) + { + return 0; + } + + f3d::scene* cpp_scene = reinterpret_cast(scene); + + try + { + cpp_scene->add(std::make_shared(name ? std::string(name) : std::string(), + std::array{ t_min, t_max }, to_cpp_memory_view(view))); + } + catch (const f3d::scene::load_failure_exception& e) + { + f3d::log::error("Failed to add mesh view to scene: {}", e.what()); + return 0; + } + + return 1; +} + //---------------------------------------------------------------------------- int f3d_scene_add_buffer(f3d_scene_t* scene, void* buffer, size_t size) { diff --git a/c/scene_c_api.h b/c/scene_c_api.h index 1a09027b92..12cf9698ba 100644 --- a/c/scene_c_api.h +++ b/c/scene_c_api.h @@ -44,6 +44,29 @@ extern "C" */ F3D_EXPORT int f3d_scene_add_mesh(f3d_scene_t* scene, const f3d_mesh_t* mesh); + /** + * @brief Add a zero-copy in-memory mesh view into the scene. + * + * Unlike f3d_scene_add_mesh (which copies all arrays into F3D), this keeps references + * to the caller-owned arrays described by @p view: no data is copied. All pointers in + * @p view must therefore remain valid and allocated until the scene is cleared. The + * array metadata (names, layout) is copied internally, so @p view itself may be a + * transient/stack value. + * + * Animation: provide a non-degenerate [t_min, t_max] range and mutate the referenced + * buffers in place between renders (the pointers stay the same). Use t_min == t_max for + * a static mesh. + * + * @param scene Scene handle. + * @param view Mesh memory view describing caller-owned arrays. + * @param name Optional mesh name (may be NULL). + * @param t_min Animation time range minimum. + * @param t_max Animation time range maximum. + * @return 1 on success, 0 on failure. + */ + F3D_EXPORT int f3d_scene_add_mesh_view( + f3d_scene_t* scene, const f3d_memory_view_t* view, const char* name, double t_min, double t_max); + /** * @brief Add and load a memory buffer into the scene. * diff --git a/c/types_c_api.h b/c/types_c_api.h index 1ea4d12b10..e81b93664d 100644 --- a/c/types_c_api.h +++ b/c/types_c_api.h @@ -160,6 +160,92 @@ extern "C" */ F3D_EXPORT int f3d_mesh_is_valid(const f3d_mesh_t* mesh, char** error_message); + /** + * @brief Scalar data types supported by a zero-copy mesh view. + * + * Values mirror f3d::mesh_view::data_type and MUST stay in the same order. + */ + typedef enum f3d_mesh_data_type_t + { + F3D_MESH_DATA_U8 = 0, + F3D_MESH_DATA_I8, + F3D_MESH_DATA_U16, + F3D_MESH_DATA_I16, + F3D_MESH_DATA_U32, + F3D_MESH_DATA_I32, + F3D_MESH_DATA_U64, + F3D_MESH_DATA_I64, + F3D_MESH_DATA_F32, + F3D_MESH_DATA_F64 + } f3d_mesh_data_type_t; + + /** + * @brief Zero-copy view of an existing data array (mirrors f3d::mesh_view::data_array_t). + * + * `data` is NOT copied: it must stay valid and allocated for as long as the mesh view + * stays in the scene. `name` may be NULL. `components` and `stride` default to 1 when + * left at 0. `stride` is counted in elements (not bytes). Set `time_dependent` to 0 for + * arrays whose contents never change to help performance. + */ + typedef struct f3d_data_array_t + { + const char* name; + f3d_mesh_data_type_t type; + const void* data; + size_t components; + size_t stride; + int time_dependent; + } f3d_data_array_t; + + /** + * @brief Zero-copy view of a cell array (mirrors f3d::mesh_view::cell_array_t). + * + * `offset_count` must equal the number of cells + 1 (1 means no cell). The last offset + * value must equal `index_count`. `offsets`/`indices` must use an integer type + * (I32/U32/I64/U64) and share the same type. + */ + typedef struct f3d_cell_array_t + { + size_t offset_count; + f3d_data_array_t offsets; + size_t index_count; + f3d_data_array_t indices; + } f3d_cell_array_t; + + /** + * @brief Zero-copy view of a mesh in memory (mirrors f3d::mesh_view::memory_view_t). + * + * Every pointer referenced here must stay valid while the mesh view is in the scene. + * `points` must have 3 components and type F32 or F64. `normals` (3 comps) and + * `texture_coordinates` (2 comps) are optional (leave `.data` NULL to skip). The + * `*_scalars` arrays may be NULL when their count is 0. + */ + typedef struct f3d_memory_view_t + { + size_t point_count; + f3d_data_array_t points; + f3d_data_array_t normals; + f3d_data_array_t texture_coordinates; + + f3d_cell_array_t vertices; + f3d_cell_array_t lines; + f3d_cell_array_t polygons; + + const f3d_data_array_t* point_scalars; + size_t point_scalars_count; + const f3d_data_array_t* cell_scalars; + size_t cell_scalars_count; + + /* Optional in-memory base-color texture (sampled via texture_coordinates). Leave + base_color_texture NULL for none. Pixels are row-major uint8, components 3 (RGB) or + 4 (RGBA). Set base_color_texture_emissive != 0 to also use it as the emissive map. */ + const void* base_color_texture; + size_t base_color_texture_width; + size_t base_color_texture_height; + size_t base_color_texture_components; + int base_color_texture_emissive; + } f3d_memory_view_t; + /** * @brief Enumeration of light types. */ diff --git a/library/public/mesh_view.h b/library/public/mesh_view.h index 38c5853432..e27bd80099 100644 --- a/library/public/mesh_view.h +++ b/library/public/mesh_view.h @@ -131,6 +131,23 @@ class F3D_EXPORT mesh_view data_array_t indices; }; + /** + * Structure representing an in-memory base-color (albedo) texture, sampled through the + * mesh `textureCoordinates`. Avoids writing a temporary image file to disk. `data` is a + * row-major uint8 buffer of `width * height * components` bytes, with `components` equal + * to 3 (RGB) or 4 (RGBA). Leave `data` null (the default) for no texture. The pointer + * must remain valid until the mesh is removed from the scene. When `emissive` is true the + * same image is additionally installed as the emissive texture (for unlit/flat display). + */ + struct texture_t + { + size_t width = 0; + size_t height = 0; + size_t components = 3; + const void* data = nullptr; + bool emissive = false; + }; + /** * Structure representing a view of the mesh in memory at a given time. * The pointers provided in this structure must remain valid once the mesh is added to the scene. @@ -155,6 +172,9 @@ class F3D_EXPORT mesh_view // scalars std::vector pointScalars; std::vector cellScalars; + + // optional in-memory base-color texture (sampled via textureCoordinates) + texture_t baseColorTexture; }; /** diff --git a/library/src/scene_impl.cxx b/library/src/scene_impl.cxx index 859d52d64f..507187a969 100644 --- a/library/src/scene_impl.cxx +++ b/library/src/scene_impl.cxx @@ -24,12 +24,16 @@ #include #include #include +#include #include #include +#include #include #include #include +#include + // requires https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12411 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251110) #include @@ -731,6 +735,33 @@ scene& scene_impl::add([[maybe_unused]] std::shared_ptr mesh) vtkNew importer; importer->SetInternalReader(vtkSource); + // Optional in-memory base-color texture carried by the mesh_view (gap #1 fold): build a + // vtkTexture once and hand it to the importer, which applies it to the imported actor. + { + const auto textureView = mesh->getMemoryView(timeRange[0]); + const auto& bct = textureView.baseColorTexture; + if (bct.data != nullptr && bct.width > 0 && bct.height > 0) + { + if (bct.components != 3 && bct.components != 4) + { + throw scene::load_failure_exception( + "Mesh view base color texture must have 3 or 4 components"); + } + vtkNew img; + img->SetDimensions(static_cast(bct.width), static_cast(bct.height), 1); + img->AllocateScalars(VTK_UNSIGNED_CHAR, static_cast(bct.components)); + std::memcpy(img->GetScalarPointer(), bct.data, + bct.width * bct.height * bct.components * sizeof(unsigned char)); + + vtkNew texture; + texture->SetInputData(img); + texture->InterpolateOn(); + texture->UseSRGBColorSpaceOn(); // base color is authored in sRGB + texture->Update(); + importer->SetBaseColorTexture(texture, bct.emissive); + } + } + std::string name = mesh->getName(); log::debug("Loading 3D scene from memory"); diff --git a/vtkext/private/module/vtkF3DGenericImporter.cxx b/vtkext/private/module/vtkF3DGenericImporter.cxx index d9d91c6ead..db196d3dc0 100644 --- a/vtkext/private/module/vtkF3DGenericImporter.cxx +++ b/vtkext/private/module/vtkF3DGenericImporter.cxx @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -42,6 +43,10 @@ struct vtkF3DGenericImporter::Internals std::vector Blocks; std::string OutputDescription; + // Optional in-memory base-color texture (from mesh_view), applied to every imported actor. + vtkSmartPointer BaseColorTexture = nullptr; + bool BaseColorTextureEmissive = false; + bool HasAnimation = false; bool AnimationEnabled = false; std::array TimeRange; @@ -187,6 +192,18 @@ void vtkF3DGenericImporter::CreateActorForBlock( bd.Actor->GetProperty()->SetBaseIOR(1.5); bd.Actor->GetProperty()->SetInterpolationToPBR(); + // In-memory base-color texture carried by mesh_view: applied here on the actor so it + // survives without any global renderer texture override (gap #1 fold). The renderer's + // coloring pass only overrides the base-color texture when a global one is set. + if (this->Pimpl->BaseColorTexture) + { + bd.Actor->GetProperty()->SetBaseColorTexture(this->Pimpl->BaseColorTexture); + if (this->Pimpl->BaseColorTextureEmissive) + { + bd.Actor->GetProperty()->SetEmissiveTexture(this->Pimpl->BaseColorTexture); + } + } + ren->AddActor(bd.Actor); this->ActorCollection->AddItem(bd.Actor); @@ -287,6 +304,13 @@ void vtkF3DGenericImporter::SetInternalReader(vtkAlgorithm* reader) } } +//---------------------------------------------------------------------------- +void vtkF3DGenericImporter::SetBaseColorTexture(vtkTexture* texture, bool emissive) +{ + this->Pimpl->BaseColorTexture = texture; + this->Pimpl->BaseColorTextureEmissive = emissive; +} + //---------------------------------------------------------------------------- std::string vtkF3DGenericImporter::GetOutputsDescription() { diff --git a/vtkext/private/module/vtkF3DGenericImporter.h b/vtkext/private/module/vtkF3DGenericImporter.h index fe8c19220e..051782e87a 100644 --- a/vtkext/private/module/vtkF3DGenericImporter.h +++ b/vtkext/private/module/vtkF3DGenericImporter.h @@ -17,6 +17,7 @@ class vtkMultiBlockDataSet; class vtkPartitionedDataSet; class vtkPartitionedDataSetCollection; class vtkPolyData; +class vtkTexture; class vtkF3DGenericImporter : public vtkF3DImporter { public: @@ -29,6 +30,13 @@ class vtkF3DGenericImporter : public vtkF3DImporter */ void SetInternalReader(vtkAlgorithm* reader); + /** + * Set an in-memory base-color (albedo) texture to apply to the imported actor(s), as + * carried by f3d::mesh_view::memory_view_t::baseColorTexture. When @p emissive is true the + * same texture is also installed as the emissive texture. Pass nullptr to clear. + */ + void SetBaseColorTexture(vtkTexture* texture, bool emissive = false); + /** * Get a string describing the outputs */ diff --git a/vtkext/private/module/vtkF3DRenderer.cxx b/vtkext/private/module/vtkF3DRenderer.cxx index 2670b0ffd7..1553ff1578 100644 --- a/vtkext/private/module/vtkF3DRenderer.cxx +++ b/vtkext/private/module/vtkF3DRenderer.cxx @@ -2599,6 +2599,19 @@ void vtkF3DRenderer::ConfigureActorsProperties() coloring.OriginalActor->ForceTranslucentOn(); } } + else if (vtkTexture* origAlbedo = coloring.OriginalActor->GetProperty()->GetTexture("albedoTex")) + { + // No global override: honour a base-color texture carried by the source actor (e.g. + // f3d::mesh_view::memory_view_t::baseColorTexture), propagating it to the rendered + // coloring actor (which is a separate actor from OriginalActor). + coloring.Actor->GetProperty()->SetBaseColorTexture(origAlbedo); + if (origAlbedo->GetImageDataInput(0) && + origAlbedo->GetImageDataInput(0)->GetNumberOfScalarComponents() == 4) + { + coloring.Actor->ForceTranslucentOn(); + coloring.OriginalActor->ForceTranslucentOn(); + } + } if (this->TextureMaterial.has_value()) { @@ -2613,6 +2626,12 @@ void vtkF3DRenderer::ConfigureActorsProperties() coloring.Actor->GetProperty()->SetEmissiveTexture(emissTex); coloring.OriginalActor->GetProperty()->SetEmissiveTexture(emissTex); } + else if (vtkTexture* origEmis = coloring.OriginalActor->GetProperty()->GetTexture("emissiveTex")) + { + // No global override: honour an emissive texture carried by the source actor (e.g. + // f3d::mesh_view::memory_view_t::baseColorTexture with emissive=true). + coloring.Actor->GetProperty()->SetEmissiveTexture(origEmis); + } if (emissiveFactor) { From 32a39332365e38c3706417892e35c202406fb012 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Tue, 9 Jun 2026 21:09:53 +0100 Subject: [PATCH 2/5] Add C API test for f3d_scene_add_mesh_view Smoke test for the zero-copy mesh_view C binding: builds a small in-memory mesh (a triangle with a per-point scalar) via f3d_memory_view_t and adds it to the scene with f3d_scene_add_mesh_view. Gated on the same VTK version as the other zero-copy tests (stride arrays). --- c/testing/CMakeLists.txt | 8 ++++ c/testing/test_scene_mesh_view.c | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 c/testing/test_scene_mesh_view.c diff --git a/c/testing/CMakeLists.txt b/c/testing/CMakeLists.txt index d7287a6f2a..89a75d2fc6 100644 --- a/c/testing/CMakeLists.txt +++ b/c/testing/CMakeLists.txt @@ -18,6 +18,14 @@ if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250501) ) endif() +# Zero-copy mesh_view C API (f3d_scene_add_mesh_view). Needs stride arrays +# https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12411 +if(VTK_VERSION VERSION_GREATER_EQUAL 9.6.20251110) + list(APPEND f3d_c_api_tests_list + test_scene_mesh_view.c + ) +endif() + set(CMAKE_TESTDRIVER_EXTRA_INCLUDES "") set(CMAKE_TESTDRIVER_ARGVC_FUNCTION "") set(CMAKE_TESTDRIVER_BEFORE_TESTMAIN "") diff --git a/c/testing/test_scene_mesh_view.c b/c/testing/test_scene_mesh_view.c new file mode 100644 index 0000000000..00dd7d5b83 --- /dev/null +++ b/c/testing/test_scene_mesh_view.c @@ -0,0 +1,65 @@ +#include +#include +#include + +#include +#include + +int test_scene_mesh_view() +{ + f3d_engine_t* engine = f3d_engine_create(1); // offscreen + if (!engine) + { + puts("[ERROR] Failed to create engine"); + return 1; + } + + f3d_scene_t* scene = f3d_engine_get_scene(engine); + if (!scene) + { + puts("[ERROR] Failed to get scene"); + f3d_engine_delete(engine); + return 1; + } + + // A single triangle with a per-point scalar. All arrays are caller-owned and stay alive + // for the whole function: f3d_scene_add_mesh_view keeps references, it does not copy them. + float points[] = { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.0f }; + unsigned int offsets[] = { 0, 3 }; // one polygon of 3 corners (offset_count = cells + 1) + unsigned int indices[] = { 0, 1, 2 }; + float height[] = { 0.0f, 0.5f, 1.0f }; + + f3d_data_array_t point_scalar; + memset(&point_scalar, 0, sizeof(point_scalar)); + point_scalar.name = "height"; + point_scalar.type = F3D_MESH_DATA_F32; + point_scalar.data = height; + point_scalar.components = 1; + + f3d_memory_view_t view; + memset(&view, 0, sizeof(view)); // zero => no normals/texcoords/texture, identity transform + view.point_count = 3; + view.points.type = F3D_MESH_DATA_F32; + view.points.data = points; + view.points.components = 3; + view.polygons.offset_count = 2; + view.polygons.offsets.type = F3D_MESH_DATA_U32; + view.polygons.offsets.data = offsets; + view.polygons.index_count = 3; + view.polygons.indices.type = F3D_MESH_DATA_U32; + view.polygons.indices.data = indices; + view.point_scalars = &point_scalar; + view.point_scalars_count = 1; + + // Static mesh (t_min == t_max). + if (f3d_scene_add_mesh_view(scene, &view, "triangle", 0.0, 0.0) != 1) + { + puts("[ERROR] f3d_scene_add_mesh_view failed"); + f3d_engine_delete(engine); + return 1; + } + + f3d_scene_clear(scene); + f3d_engine_delete(engine); + return 0; +} From 9e71d9895a8f119917a45e487339e8043df41044 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Thu, 11 Jun 2026 11:37:43 +0100 Subject: [PATCH 3/5] Update c/testing/CMakeLists.txt Co-authored-by: Mathieu Westphal --- c/testing/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c/testing/CMakeLists.txt b/c/testing/CMakeLists.txt index 89a75d2fc6..0ba4a91e9b 100644 --- a/c/testing/CMakeLists.txt +++ b/c/testing/CMakeLists.txt @@ -18,7 +18,7 @@ if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250501) ) endif() -# Zero-copy mesh_view C API (f3d_scene_add_mesh_view). Needs stride arrays +# Zero-copy mesh_view C API (f3d_scene_add_mesh_view). Needs strided arrays # https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12411 if(VTK_VERSION VERSION_GREATER_EQUAL 9.6.20251110) list(APPEND f3d_c_api_tests_list From 3391c89b31249f8ace1117d17d8d8592b3d0e54c Mon Sep 17 00:00:00 2001 From: Joaquim Date: Thu, 11 Jun 2026 12:38:20 +0100 Subject: [PATCH 4/5] Address review: use f3d::image for the in-memory base-color texture --- c/scene_c_api.cxx | 18 ++++++++++++------ library/public/mesh_view.h | 27 ++++++++------------------- library/src/scene_impl.cxx | 19 +++++++++++-------- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/c/scene_c_api.cxx b/c/scene_c_api.cxx index 81c4ec5efe..f2c673123f 100644 --- a/c/scene_c_api.cxx +++ b/c/scene_c_api.cxx @@ -173,12 +173,18 @@ f3d::mesh_view::memory_view_t to_cpp_memory_view(const f3d_memory_view_t* c) v.cellScalars.push_back(to_cpp_data_array(c->cell_scalars[i])); } - v.baseColorTexture.data = c->base_color_texture; - v.baseColorTexture.width = c->base_color_texture_width; - v.baseColorTexture.height = c->base_color_texture_height; - v.baseColorTexture.components = - c->base_color_texture_components ? c->base_color_texture_components : 3; - v.baseColorTexture.emissive = c->base_color_texture_emissive != 0; + if (c->base_color_texture != nullptr && c->base_color_texture_width > 0 && + c->base_color_texture_height > 0) + { + const unsigned int comps = static_cast( + c->base_color_texture_components ? c->base_color_texture_components : 3); + f3d::image img(static_cast(c->base_color_texture_width), + static_cast(c->base_color_texture_height), comps, f3d::image::ChannelType::BYTE); + // setContent deep-copies, so the caller's buffer need not outlive this call + img.setContent(const_cast(c->base_color_texture)); + v.baseColorTexture = std::move(img); + v.baseColorTextureEmissive = c->base_color_texture_emissive != 0; + } return v; } diff --git a/library/public/mesh_view.h b/library/public/mesh_view.h index e27bd80099..0318a4777b 100644 --- a/library/public/mesh_view.h +++ b/library/public/mesh_view.h @@ -3,6 +3,7 @@ #include "exception.h" #include "export.h" +#include "image.h" /// @cond #include @@ -131,23 +132,6 @@ class F3D_EXPORT mesh_view data_array_t indices; }; - /** - * Structure representing an in-memory base-color (albedo) texture, sampled through the - * mesh `textureCoordinates`. Avoids writing a temporary image file to disk. `data` is a - * row-major uint8 buffer of `width * height * components` bytes, with `components` equal - * to 3 (RGB) or 4 (RGBA). Leave `data` null (the default) for no texture. The pointer - * must remain valid until the mesh is removed from the scene. When `emissive` is true the - * same image is additionally installed as the emissive texture (for unlit/flat display). - */ - struct texture_t - { - size_t width = 0; - size_t height = 0; - size_t components = 3; - const void* data = nullptr; - bool emissive = false; - }; - /** * Structure representing a view of the mesh in memory at a given time. * The pointers provided in this structure must remain valid once the mesh is added to the scene. @@ -173,8 +157,13 @@ class F3D_EXPORT mesh_view std::vector pointScalars; std::vector cellScalars; - // optional in-memory base-color texture (sampled via textureCoordinates) - texture_t baseColorTexture; + // Optional in-memory base-color (albedo) texture, sampled through the mesh + // `textureCoordinates`. Avoids writing a temporary image file to disk. An empty image + // (the default) means no texture; otherwise it must be a BYTE image with 3 (RGB) or + // 4 (RGBA) channels. When `baseColorTextureEmissive` is true the same image is + // additionally installed as the emissive texture (for unlit/flat display). + image baseColorTexture; + bool baseColorTextureEmissive = false; }; /** diff --git a/library/src/scene_impl.cxx b/library/src/scene_impl.cxx index 507187a969..624c3413c6 100644 --- a/library/src/scene_impl.cxx +++ b/library/src/scene_impl.cxx @@ -739,26 +739,29 @@ scene& scene_impl::add([[maybe_unused]] std::shared_ptr mesh) // vtkTexture once and hand it to the importer, which applies it to the imported actor. { const auto textureView = mesh->getMemoryView(timeRange[0]); - const auto& bct = textureView.baseColorTexture; - if (bct.data != nullptr && bct.width > 0 && bct.height > 0) + const f3d::image& bct = textureView.baseColorTexture; + const unsigned int tw = bct.getWidth(); + const unsigned int th = bct.getHeight(); + const unsigned int tc = bct.getChannelCount(); + if (tw > 0 && th > 0 && bct.getContent() != nullptr) { - if (bct.components != 3 && bct.components != 4) + if (tc != 3 && tc != 4) { throw scene::load_failure_exception( "Mesh view base color texture must have 3 or 4 components"); } vtkNew img; - img->SetDimensions(static_cast(bct.width), static_cast(bct.height), 1); - img->AllocateScalars(VTK_UNSIGNED_CHAR, static_cast(bct.components)); - std::memcpy(img->GetScalarPointer(), bct.data, - bct.width * bct.height * bct.components * sizeof(unsigned char)); + img->SetDimensions(static_cast(tw), static_cast(th), 1); + img->AllocateScalars(VTK_UNSIGNED_CHAR, static_cast(tc)); + std::memcpy(img->GetScalarPointer(), bct.getContent(), + static_cast(tw) * th * tc * sizeof(unsigned char)); vtkNew texture; texture->SetInputData(img); texture->InterpolateOn(); texture->UseSRGBColorSpaceOn(); // base color is authored in sRGB texture->Update(); - importer->SetBaseColorTexture(texture, bct.emissive); + importer->SetBaseColorTexture(texture, textureView.baseColorTextureEmissive); } } From e815de67eec5135a857f01e99f879f97f9f0b92c Mon Sep 17 00:00:00 2001 From: Joaquim Date: Thu, 11 Jun 2026 13:42:34 +0100 Subject: [PATCH 5/5] mesh_view C API: handle exceptions, expose texture to Python, add texture tests --- c/scene_c_api.cxx | 12 ++++ c/testing/test_scene_mesh_view.c | 49 +++++++++++++++ python/F3DPythonBindings.cxx | 9 ++- python/testing/CMakeLists.txt | 1 + .../testing/test_scene_mesh_view_texture.py | 62 +++++++++++++++++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 python/testing/test_scene_mesh_view_texture.py diff --git a/c/scene_c_api.cxx b/c/scene_c_api.cxx index f2c673123f..4b0a6b1660 100644 --- a/c/scene_c_api.cxx +++ b/c/scene_c_api.cxx @@ -348,6 +348,18 @@ int f3d_scene_add_mesh_view( f3d::log::error("Failed to add mesh view to scene: {}", e.what()); return 0; } + catch (const std::exception& e) + { + // to_cpp_memory_view builds an f3d::image for the base-color texture, which can throw + // outside load_failure_exception; never let a C++ exception cross the C ABI boundary. + f3d::log::error("Failed to add mesh view to scene: {}", e.what()); + return 0; + } + catch (...) + { + f3d::log::error("Failed to add mesh view to scene: unknown error"); + return 0; + } return 1; } diff --git a/c/testing/test_scene_mesh_view.c b/c/testing/test_scene_mesh_view.c index 00dd7d5b83..19e17b726d 100644 --- a/c/testing/test_scene_mesh_view.c +++ b/c/testing/test_scene_mesh_view.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -59,6 +60,54 @@ int test_scene_mesh_view() return 1; } + f3d_scene_clear(scene); + + // A textured quad: exercises the optional in-memory base-color texture path + // (raw bytes -> f3d::image via setContent -> vtkTexture on the imported actor). + float qpoints[] = { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }; + float qtexcoords[] = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f }; + unsigned int qoffsets[] = { 0, 4 }; // one quad polygon + unsigned int qindices[] = { 0, 1, 2, 3 }; + // 2x2 RGB checker (row-major): red, green / blue, yellow. + unsigned char texels[] = { 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0 }; + + f3d_memory_view_t qview; + memset(&qview, 0, sizeof(qview)); + qview.point_count = 4; + qview.points.type = F3D_MESH_DATA_F32; + qview.points.data = qpoints; + qview.points.components = 3; + qview.texture_coordinates.type = F3D_MESH_DATA_F32; + qview.texture_coordinates.data = qtexcoords; + qview.texture_coordinates.components = 2; + qview.polygons.offset_count = 2; + qview.polygons.offsets.type = F3D_MESH_DATA_U32; + qview.polygons.offsets.data = qoffsets; + qview.polygons.index_count = 4; + qview.polygons.indices.type = F3D_MESH_DATA_U32; + qview.polygons.indices.data = qindices; + qview.base_color_texture = texels; + qview.base_color_texture_width = 2; + qview.base_color_texture_height = 2; + qview.base_color_texture_components = 3; + qview.base_color_texture_emissive = 1; // also use as emissive (flat full-strength) + + if (f3d_scene_add_mesh_view(scene, &qview, "textured_quad", 0.0, 0.0) != 1) + { + puts("[ERROR] f3d_scene_add_mesh_view (textured) failed"); + f3d_engine_delete(engine); + return 1; + } + + // Render once so the base-color texture is actually built and applied. + f3d_window_t* window = f3d_engine_get_window(engine); + if (!window || f3d_window_render(window) != 1) + { + puts("[ERROR] render of textured mesh_view failed"); + f3d_engine_delete(engine); + return 1; + } + f3d_scene_clear(scene); f3d_engine_delete(engine); return 0; diff --git a/python/F3DPythonBindings.cxx b/python/F3DPythonBindings.cxx index cace9ed5ce..b264993a9c 100644 --- a/python/F3DPythonBindings.cxx +++ b/python/F3DPythonBindings.cxx @@ -630,7 +630,14 @@ PYBIND11_MODULE(pyf3d, module) throw std::runtime_error("No cell scalar with name " + name); } it->timeDependent = timeDependent; - }); + }) + // Optional in-memory base-color (albedo) texture, sampled via texture_coordinates. Assign an + // f3d.Image (a BYTE image with 3 or 4 channels); leave default (empty) for no texture. When + // base_color_texture_emissive is true the same image is also installed as the emissive map + // (flat / full-strength display). + .def_readwrite("base_color_texture", &f3d::mesh_view::memory_view_t::baseColorTexture) + .def_readwrite("base_color_texture_emissive", + &f3d::mesh_view::memory_view_t::baseColorTextureEmissive); class PyMesh : public f3d::mesh_view diff --git a/python/testing/CMakeLists.txt b/python/testing/CMakeLists.txt index c97d1faa58..ab97998392 100644 --- a/python/testing/CMakeLists.txt +++ b/python/testing/CMakeLists.txt @@ -30,6 +30,7 @@ if(F3D_MODULE_UI) if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251110) list(APPEND pyf3dTests_list test_scene_zero_copy.py + test_scene_mesh_view_texture.py ) endif() endif() diff --git a/python/testing/test_scene_mesh_view_texture.py b/python/testing/test_scene_mesh_view_texture.py new file mode 100644 index 0000000000..7e12940798 --- /dev/null +++ b/python/testing/test_scene_mesh_view_texture.py @@ -0,0 +1,62 @@ +import math +import numpy as np + +import f3d + + +def test_scene_mesh_view_texture(): + """The in-memory base-color texture of a mesh_view (memory_view.base_color_texture) + is exposed to Python and actually applied at render time. A solid-red BYTE image is + used as an emissive base-color texture on a quad; the rendered frame must therefore + contain strongly-red pixels (no baseline needed).""" + + engine = f3d.Engine.create(True) # offscreen + engine.window.size = 300, 300 + + # A unit quad in the z = 0 plane with full-range texture coordinates. + points = np.array( + [[-1.0, -1.0, 0.0], [1.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, 1.0, 0.0]], + dtype=np.float32, + ) + texcoords = np.array( + [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], dtype=np.float32 + ) + face_offsets = np.array([0, 4], dtype=np.int32) + face_indices = np.array([0, 1, 2, 3], dtype=np.int32) + + # A 2x2 solid-red RGB texture (BYTE). Solid colour keeps the check framing-robust. + tex = f3d.Image(2, 2, 3, f3d.Image.ChannelType.BYTE) + tex.content = bytes([255, 0, 0] * 4) + + memory_view = f3d.MeshMemoryView() + memory_view.points = points + memory_view.texture_coordinates = texcoords + memory_view.polygons_offsets = face_offsets + memory_view.polygons_indices = face_indices + memory_view.base_color_texture = tex + memory_view.base_color_texture_emissive = True # show flat/full-strength + + class TexturedQuad(f3d.MeshView): + def get_time_range(self): + return 0.0, 0.0 + + def get_name(self): + return "TexturedQuad" + + def get_memory_view(self, time): + return memory_view + + engine.scene.add(TexturedQuad()) + + img = engine.window.render_to_image() + assert img.width == 300 and img.height == 300 + + rgb = np.frombuffer(img.content, dtype=np.uint8).reshape(img.height, img.width, -1) + r, g, b = rgb[..., 0].astype(int), rgb[..., 1].astype(int), rgb[..., 2].astype(int) + strongly_red = (r > 200) & (g < 60) & (b < 60) + + # The red emissive texture must be visible: a meaningful fraction of red pixels. + assert strongly_red.sum() > 1000, ( + f"expected the red base-color texture to render, " + f"got {int(strongly_red.sum())} strongly-red pixels" + )