Geometry Nodes: Implement Import SPZ node

A straightforward node that accepts file path to an SPZ file and
outputs point cloud of type 3D Gaussian Splats.

The node performs conversion from the SPZ coordinate convention
to Blender coordinate convention. Note that this convention is not
always followed by all assets on the Internet. The unfortunate part
is that it is not trivial to convert the coordinate systems as the
spherical harmonics attribute requires special handling.

For the efficiency extra parameters might need to be added to the
node. Or, alternatively, spherical harmonics need to become a
native attribute type, so that we can have transform nodes applied
on them.

Ref #159470
This commit is contained in:
Sergey Sharybin 2026-09-08 12:32:30 +02:00 • committed by Sergey Sharybin
parent 330ef38ef3
commit 1be8f41103
6 changed files with 117 additions and 1 deletions

View file

@ -595,6 +595,7 @@ class NODE_MT_gn_input_import_base(node_add_menu.NodeMenu):
self.node_operator(layout, "GeometryNodeImportCSV", label="CSV (.csv)")
self.node_operator(layout, "GeometryNodeImportOBJ", label="Wavefront (.obj)")
self.node_operator(layout, "GeometryNodeImportPLY", label="Stanford PLY (.ply)")
self.node_operator(layout, "GeometryNodeImportSPZ", label="SPZ (.spz)")
self.node_operator(layout, "GeometryNodeImportSTL", label="STL (.stl)")
self.node_operator(layout, "GeometryNodeImportText", label="Text (.txt)")
self.node_operator(layout, "GeometryNodeImportVDB", label="OpenVDB (.vdb)")

View file

@ -1252,6 +1252,9 @@ static wmOperatorStatus node_add_import_node_exec(bContext *C, wmOperator *op)
else if (is_geometry_tree && path.endswith(".ply")) {
node = add_node(*C, "GeometryNodeImportPLY"_ustr, snode->runtime->cursor);
}
else if (is_geometry_tree && path.endswith(".spz")) {
node = add_node(*C, "GeometryNodeImportSPZ"_ustr, snode->runtime->cursor);
}
else if (is_geometry_tree && path.endswith(".stl")) {
node = add_node(*C, "GeometryNodeImportSTL"_ustr, snode->runtime->cursor);
}

View file

@ -1117,7 +1117,7 @@ static bool node_import_file_drop_poll(bContext *C, wmDrag *drag, const wmEvent
}
if (is_geometry_tree &&
(path.endswith(".csv") || path.endswith(".obj") || path.endswith(".ply") ||
path.endswith(".stl") || path.endswith(".vdb")))
path.endswith(".stl") || path.endswith(".vdb") || path.endswith(".spz")))
{
return true;
}

View file

@ -11151,6 +11151,7 @@ static void rna_def_nodes(BlenderRNA *brna)
define("GeometryNode", "GeometryNodeImportCSV");
define("GeometryNode", "GeometryNodeImportOBJ");
define("GeometryNode", "GeometryNodeImportPLY");
define("GeometryNode", "GeometryNodeImportSPZ");
define("GeometryNode", "GeometryNodeImportSTL");
define("GeometryNode", "GeometryNodeImportText");
define("GeometryNode", "GeometryNodeImportVDB");

View file

@ -18,6 +18,7 @@ set(INC
../../io/common
../../io/csv
../../io/ply
../../io/spz
../../io/stl
../../io/wavefront_obj
# RNA_prototypes.hh
@ -138,6 +139,7 @@ set(SRC
nodes/node_geo_image_texture.cc
nodes/node_geo_import_csv.cc
nodes/node_geo_import_obj.cc
nodes/node_geo_import_spz.cc
nodes/node_geo_import_ply.cc
nodes/node_geo_import_stl.cc
nodes/node_geo_import_text.cc
@ -360,6 +362,14 @@ set(LIB
PRIVATE bf::dependencies::optional::bullet
)
if(WITH_IO_SPZ)
list(APPEND LIB
PRIVATE bf_io_common
PRIVATE bf_io_spz
)
add_definitions(-DWITH_IO_SPZ)
endif()
if(WITH_IO_STL)
list(APPEND LIB
PRIVATE bf_io_common

View file

@ -0,0 +1,101 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "node_geometry_util.hh"
#include "BLI_generic_key_string.hh"
#include "BLI_listbase.hh"
#include "BLI_memory_cache_file_load.hh"
#include "BLI_string.hh"
#include "BKE_report.hh"
#include "IO_spz.hh"
namespace blender::nodes::node_geo_import_spz {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::String>("Path"_ustr)
.subtype(PROP_FILEPATH)
.path_filter("*.spz")
.optional_label()
.description("Path to a SPZ file");
b.add_output<decl::Geometry>("Points"_ustr);
}
class LoadSpzCache : public memory_cache::CachedValue {
public:
GeometrySet geometry;
Vector<NodeWarning> warnings;
void count_memory(MemoryCounter &counter) const override
{
this->geometry.count_memory(counter);
}
};
static void node_geo_exec(GeoNodeExecParams params)
{
#ifdef WITH_IO_SPZ
const std::optional<std::string> path = params.ensure_absolute_path(
params.extract_input<std::string>("Path"_ustr));
if (!path) {
params.set_default_remaining_outputs();
return;
}
std::shared_ptr<const LoadSpzCache> cached_value = memory_cache::get_loaded<LoadSpzCache>(
GenericStringKey{"import_spz_node"}, {StringRefNull(*path)}, [&]() {
SPZImportParams import_params;
import_params.filepath = *path;
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); })
import_params.reports = &reports;
PointCloud *point_cloud = SPZ_import_point_cloud(&import_params);
auto cached_value = std::make_unique<LoadSpzCache>();
cached_value->geometry = GeometrySet::from_pointcloud(point_cloud);
for (Report &report : (import_params.reports)->list) {
cached_value->warnings.append_as(report);
}
return cached_value;
});
for (const NodeWarning &warning : cached_value->warnings) {
params.error_message_add(warning.type, warning.message);
}
params.set_output("Points"_ustr, cached_value->geometry);
#else
params.error_message_add(NodeWarningType::Error,
TIP_("Disabled, Blender was compiled without SPZ I/O"));
params.set_default_remaining_outputs();
#endif
}
static void node_register()
{
static bke::bNodeType ntype;
geo_node_type_base(&ntype, "GeometryNodeImportSPZ"_ustr);
ntype.ui_name = "Import SPZ";
ntype.ui_description =
"Import a point cloud object that is rendered as gaussian splat from an SPZ file";
ntype.nclass = NODE_CLASS_INPUT;
ntype.geometry_node_execute = node_geo_exec;
ntype.declare = node_declare;
bke::node_register_type(ntype);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_geo_import_spz