mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
GSplat: Implement SPZ format importer
The SPZ format is more space efficient storing gaussian splats, and is quite popular on the Internet. This commit implements it naively without using the SPZ library. Motivation: - Simplifies platform maintenance. - Allows to have writes of different versions. For example, glTF currently mentions SPZ v2 in the extension draft while the current SPZ library is v4 (it is a bit unclear whether glTF is talking about library or format version tho). - Lowers peak memory usage by avoiding multiple copies of the same data in memory. Currently does not implement extensions reading, and does not handle the anti-alias flag. Expects the content to follow the convention of Right-Up-Back. An observation was made that not all editors and tools create SPZ files with this convention. Even the ply_to_spz command line tool from the SPZ repository does not transform the typical PLY convention assumed by the library to the SPZ convention. In practice it means that a manual re-orientation of the import might be required. The commit is limited to import via the File menu and dropping the file onto the viewport. Support of importing SPZ via geometry nodes is coming soon. Ref #159470
This commit is contained in:
parent
9f130c92dc
commit
330ef38ef3
23 changed files with 1478 additions and 0 deletions
|
|
@ -475,6 +475,7 @@ option(WITH_IO_PLY "Enable PLY 3D file format support (*.ply)" ON)
|
|||
option(WITH_IO_STL "Enable STL 3D file format support (*.stl)" ON)
|
||||
option(WITH_IO_FBX "Enable FBX 3D file format support (*.fbx)" ON)
|
||||
option(WITH_IO_GREASE_PENCIL "Enable Grease Pencil file format IO (*.svg, *.pdf)" ON)
|
||||
option(WITH_IO_SPZ "Enable SPZ compressed 3D gaussian splats file format support (*.spz)" ON)
|
||||
|
||||
# Sound output
|
||||
option(WITH_SDL_AUDIO "Enable SDL audio support" OFF)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ set(WITH_IO_FBX ON CACHE BOOL "" FORCE)
|
|||
set(WITH_IO_GREASE_PENCIL ON CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_PLY ON CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_STL ON CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_SPZ ON CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_WAVEFRONT_OBJ ON CACHE BOOL "" FORCE)
|
||||
set(WITH_LIBMV ON CACHE BOOL "" FORCE)
|
||||
set(WITH_LIBMV_SCHUR_SPECIALIZATIONS ON CACHE BOOL "" FORCE)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ set(WITH_INPUT_NDOF OFF CACHE BOOL "" FORCE)
|
|||
set(WITH_INTERNATIONAL OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_PLY OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_STL OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_SPZ OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_WAVEFRONT_OBJ OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_GREASE_PENCIL OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_FBX OFF CACHE BOOL "" FORCE)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ set(WITH_IO_FBX ON CACHE BOOL "" FORCE)
|
|||
set(WITH_IO_GREASE_PENCIL ON CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_PLY ON CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_STL ON CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_SPZ ON CACHE BOOL "" FORCE)
|
||||
set(WITH_IO_WAVEFRONT_OBJ ON CACHE BOOL "" FORCE)
|
||||
set(WITH_LIBMV ON CACHE BOOL "" FORCE)
|
||||
set(WITH_LIBMV_SCHUR_SPECIALIZATIONS ON CACHE BOOL "" FORCE)
|
||||
|
|
|
|||
|
|
@ -402,6 +402,8 @@ class TOPBAR_MT_file_import(Menu):
|
|||
if bpy.app.build_options.io_ply:
|
||||
self.layout.operator(
|
||||
"wm.ply_import", text=FileHandler.label_with_extensions("IO_FH_ply"))
|
||||
if bpy.app.build_options.io_spz:
|
||||
self.layout.operator("wm.spz_import", text=FileHandler.label_with_extensions("IO_FH_spz"))
|
||||
if bpy.app.build_options.io_stl:
|
||||
self.layout.operator(
|
||||
"wm.stl_import", text=FileHandler.label_with_extensions("IO_FH_stl"))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ set(INC
|
|||
../../io/fbx
|
||||
../../io/grease_pencil
|
||||
../../io/ply
|
||||
../../io/spz
|
||||
../../io/stl
|
||||
../../io/usd
|
||||
../../io/wavefront_obj
|
||||
|
|
@ -28,6 +29,7 @@ set(SRC
|
|||
io_obj.cc
|
||||
io_ops.cc
|
||||
io_ply_ops.cc
|
||||
io_spz_ops.cc
|
||||
io_stl_ops.cc
|
||||
io_usd.cc
|
||||
io_utils.cc
|
||||
|
|
@ -40,6 +42,7 @@ set(SRC
|
|||
io_obj.hh
|
||||
io_ops.hh
|
||||
io_ply_ops.hh
|
||||
io_spz_ops.hh
|
||||
io_stl_ops.hh
|
||||
io_usd.hh
|
||||
io_utils.hh
|
||||
|
|
@ -71,6 +74,13 @@ if(WITH_IO_PLY)
|
|||
add_definitions(-DWITH_IO_PLY)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_SPZ)
|
||||
list(APPEND LIB
|
||||
bf_io_spz
|
||||
)
|
||||
add_definitions(-DWITH_IO_SPZ)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_STL)
|
||||
list(APPEND LIB
|
||||
bf_io_stl
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include "io_grease_pencil.hh"
|
||||
#include "io_obj.hh"
|
||||
#include "io_ply_ops.hh"
|
||||
#include "io_spz_ops.hh"
|
||||
#include "io_stl_ops.hh"
|
||||
|
||||
namespace blender {
|
||||
|
|
@ -79,6 +80,11 @@ void ED_operatortypes_io()
|
|||
ed::io::stl_file_handler_add();
|
||||
#endif
|
||||
|
||||
#ifdef WITH_IO_SPZ
|
||||
WM_operatortype_append(WM_OT_spz_import);
|
||||
ed::io::spz_file_handler_add();
|
||||
#endif
|
||||
|
||||
#ifdef WITH_IO_FBX
|
||||
WM_operatortype_append(WM_OT_fbx_import);
|
||||
ed::io::fbx_file_handler_add();
|
||||
|
|
|
|||
105
source/blender/editors/io/io_spz_ops.cc
Normal file
105
source/blender/editors/io/io_spz_ops.cc
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup editor/io
|
||||
*/
|
||||
|
||||
#ifdef WITH_IO_SPZ
|
||||
|
||||
# include "io_spz_ops.hh"
|
||||
|
||||
# include <memory>
|
||||
|
||||
# include "BLI_string_utf8.hh"
|
||||
|
||||
# include "BKE_context.hh"
|
||||
# include "BKE_file_handler.hh"
|
||||
|
||||
# include "RNA_access.hh"
|
||||
# include "RNA_define.hh"
|
||||
|
||||
# include "WM_api.hh"
|
||||
# include "WM_types.hh"
|
||||
|
||||
# include "ED_outliner.hh"
|
||||
|
||||
# include "IO_spz.hh"
|
||||
# include "io_utils.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
static wmOperatorStatus wm_spz_import_exec(bContext *C, wmOperator *op)
|
||||
{
|
||||
SPZImportParams params{
|
||||
.reports = op->reports,
|
||||
};
|
||||
|
||||
const Vector<std::string> paths = ed::io::paths_from_operator_properties(op->ptr);
|
||||
|
||||
if (paths.is_empty()) {
|
||||
BKE_report(op->reports, RPT_ERROR, "No filepath given");
|
||||
return OPERATOR_CANCELLED;
|
||||
}
|
||||
|
||||
for (const std::string &path : paths) {
|
||||
params.filepath = path;
|
||||
SPZ_import(C, ¶ms);
|
||||
}
|
||||
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene);
|
||||
WM_event_add_notifier(C, NC_SCENE | ND_OB_ACTIVE, scene);
|
||||
WM_event_add_notifier(C, NC_SCENE | ND_LAYER_CONTENT, scene);
|
||||
ED_outliner_select_sync_from_object_tag(C);
|
||||
|
||||
return OPERATOR_FINISHED;
|
||||
}
|
||||
|
||||
static void wm_spz_import_draw(bContext * /*C*/, wmOperator * /*op*/) {}
|
||||
|
||||
void WM_OT_spz_import(wmOperatorType *ot)
|
||||
{
|
||||
PropertyRNA *prop;
|
||||
|
||||
ot->name = "Import SPZ";
|
||||
ot->description = "Import an SPZ file as a gaussian splat point cloud object";
|
||||
ot->idname = "WM_OT_spz_import";
|
||||
|
||||
ot->invoke = ed::io::filesel_drop_import_invoke;
|
||||
ot->exec = wm_spz_import_exec;
|
||||
ot->ui = wm_spz_import_draw;
|
||||
ot->poll = WM_operator_winactive;
|
||||
ot->flag = OPTYPE_UNDO;
|
||||
|
||||
WM_operator_properties_filesel(ot,
|
||||
FILE_TYPE_FOLDER,
|
||||
FILE_BLENDER,
|
||||
FILE_OPENFILE,
|
||||
WM_FILESEL_FILEPATH | WM_FILESEL_FILES | WM_FILESEL_DIRECTORY,
|
||||
FILE_DEFAULTDISPLAY,
|
||||
FILE_SORT_DEFAULT);
|
||||
|
||||
/* Only show `.spz` files by default. */
|
||||
prop = RNA_def_string(ot->srna, "filter_glob", "*.spz", 0, "Extension Filter", "");
|
||||
RNA_def_property_flag(prop, PROP_HIDDEN);
|
||||
}
|
||||
|
||||
namespace ed::io {
|
||||
|
||||
void spz_file_handler_add()
|
||||
{
|
||||
auto fh = std::make_unique<bke::FileHandlerType>();
|
||||
STRNCPY_UTF8(fh->idname, "IO_FH_spz");
|
||||
STRNCPY_UTF8(fh->import_operator, "WM_OT_spz_import");
|
||||
STRNCPY_UTF8(fh->label, "SPZ");
|
||||
STRNCPY_UTF8(fh->file_extensions_str, ".spz");
|
||||
fh->poll_drop = poll_file_object_drop;
|
||||
bke::file_handler_add(std::move(fh));
|
||||
}
|
||||
|
||||
} // namespace ed::io
|
||||
} // namespace blender
|
||||
|
||||
#endif /* WITH_IO_SPZ */
|
||||
21
source/blender/editors/io/io_spz_ops.hh
Normal file
21
source/blender/editors/io/io_spz_ops.hh
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup editor/io
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct wmOperatorType;
|
||||
|
||||
void WM_OT_spz_import(wmOperatorType *ot);
|
||||
|
||||
namespace ed::io {
|
||||
void spz_file_handler_add();
|
||||
}
|
||||
|
||||
} // namespace blender
|
||||
|
|
@ -14,6 +14,10 @@ if(WITH_IO_PLY)
|
|||
add_subdirectory(ply)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_SPZ)
|
||||
add_subdirectory(spz)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_STL)
|
||||
add_subdirectory(stl)
|
||||
endif()
|
||||
|
|
|
|||
42
source/blender/io/spz/CMakeLists.txt
Normal file
42
source/blender/io/spz/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(INC
|
||||
.
|
||||
intern
|
||||
../common
|
||||
../../makesrna
|
||||
)
|
||||
|
||||
set(INC_SYS
|
||||
)
|
||||
|
||||
set(SRC
|
||||
IO_spz.cc
|
||||
importer/spz_import.cc
|
||||
|
||||
IO_spz.hh
|
||||
importer/spz_import.hh
|
||||
|
||||
intern/spz_read.cc
|
||||
intern/spz_read_gzip.cc
|
||||
intern/spz_read_ngsp.cc
|
||||
intern/spz_read.hh
|
||||
intern/spz_read_common.cc
|
||||
intern/spz_read_common.hh
|
||||
)
|
||||
|
||||
set(LIB
|
||||
PRIVATE bf::blenlib
|
||||
PRIVATE bf::blenkernel
|
||||
PRIVATE bf::dna
|
||||
PRIVATE bf::depsgraph
|
||||
PRIVATE bf_io_common
|
||||
PRIVATE bf::intern::guardedalloc # Indirect dependnecy of the bf::blenkernel.
|
||||
PRIVATE bf::intern::clog
|
||||
PRIVATE bf::dependencies::zlib
|
||||
PRIVATE bf::dependencies::zstd
|
||||
)
|
||||
|
||||
blender_add_lib(bf_io_spz "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
25
source/blender/io/spz/IO_spz.cc
Normal file
25
source/blender/io/spz/IO_spz.cc
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#include "IO_spz.hh"
|
||||
|
||||
#include "importer/spz_import.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
void SPZ_import(bContext *C, const SPZImportParams *import_params)
|
||||
{
|
||||
io::spz::importer_main(C, *import_params);
|
||||
}
|
||||
|
||||
PointCloud *SPZ_import_point_cloud(const SPZImportParams *import_params)
|
||||
{
|
||||
return io::spz::import_point_cloud(*import_params);
|
||||
}
|
||||
|
||||
} // namespace blender
|
||||
29
source/blender/io/spz/IO_spz.hh
Normal file
29
source/blender/io/spz/IO_spz.hh
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct bContext;
|
||||
struct PointCloud;
|
||||
struct ReportList;
|
||||
|
||||
struct SPZImportParams {
|
||||
/* Full path to the source SPZ file to import. */
|
||||
std::string filepath;
|
||||
|
||||
ReportList *reports = nullptr;
|
||||
};
|
||||
|
||||
void SPZ_import(bContext *C, const SPZImportParams *import_params);
|
||||
PointCloud *SPZ_import_point_cloud(const SPZImportParams *import_params);
|
||||
|
||||
} // namespace blender
|
||||
87
source/blender/io/spz/importer/spz_import.cc
Normal file
87
source/blender/io/spz/importer/spz_import.cc
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#include "spz_import.hh"
|
||||
|
||||
#include "DNA_collection_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
#include "DNA_pointcloud_types.h"
|
||||
#include "DNA_scene_types.h"
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.hh"
|
||||
|
||||
#include "BKE_context.hh"
|
||||
#include "BKE_layer.hh"
|
||||
#include "BKE_library.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_pointcloud.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
#include "DEG_depsgraph_build.hh"
|
||||
|
||||
#include "IO_spz.hh"
|
||||
|
||||
#include "spz_read.hh"
|
||||
|
||||
namespace blender::io::spz {
|
||||
|
||||
void importer_main(const bContext *C, const SPZImportParams &import_params)
|
||||
{
|
||||
PointCloud *point_cloud = read_spz_file(import_params.filepath, import_params.reports);
|
||||
if (!point_cloud) {
|
||||
return;
|
||||
}
|
||||
|
||||
Main *bmain = CTX_data_main(C);
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
ViewLayer *view_layer = CTX_data_view_layer(C);
|
||||
|
||||
/* Name used for both object and its data. */
|
||||
char object_name[FILE_MAX];
|
||||
STRNCPY(object_name, BLI_path_basename(import_params.filepath.c_str()));
|
||||
BLI_path_extension_strip(object_name);
|
||||
|
||||
PointCloud *point_cloud_in_main = BKE_pointcloud_add(bmain, object_name);
|
||||
BKE_pointcloud_nomain_to_pointcloud(point_cloud, point_cloud_in_main);
|
||||
|
||||
BKE_view_layer_base_deselect_all(*bmain, scene, view_layer);
|
||||
|
||||
LayerCollection *layer_collection = BKE_layer_collection_get_active_editable(view_layer);
|
||||
if (!ID_IS_EDITABLE(layer_collection->collection)) {
|
||||
BKE_report(import_params.reports,
|
||||
RPT_WARNING,
|
||||
"Could not find an editable collection in current scene, imported data will not be "
|
||||
"instantiated");
|
||||
}
|
||||
|
||||
Object *object = BKE_object_add_only_object(bmain, OB_POINTCLOUD, object_name);
|
||||
object->data = id_cast<ID *>(point_cloud_in_main);
|
||||
BKE_collection_object_add(bmain, layer_collection->collection, object);
|
||||
BKE_view_layer_synced_ensure(*bmain, scene, view_layer);
|
||||
if (Base *base = BKE_view_layer_base_find(view_layer, object)) {
|
||||
/* `base` will be nullptr if the Object could not be instantiated in the current view layer. */
|
||||
BKE_view_layer_base_select_and_set_active(view_layer, base);
|
||||
}
|
||||
|
||||
DEG_id_tag_update(&layer_collection->collection->id, ID_RECALC_SYNC_TO_EVAL);
|
||||
DEG_id_tag_update_ex(bmain,
|
||||
&object->id,
|
||||
ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
|
||||
ID_RECALC_BASE_FLAGS);
|
||||
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
|
||||
DEG_relations_tag_update(bmain);
|
||||
}
|
||||
|
||||
PointCloud *import_point_cloud(const SPZImportParams &import_params)
|
||||
{
|
||||
return read_spz_file(import_params.filepath, import_params.reports);
|
||||
}
|
||||
|
||||
} // namespace blender::io::spz
|
||||
25
source/blender/io/spz/importer/spz_import.hh
Normal file
25
source/blender/io/spz/importer/spz_import.hh
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct bContext;
|
||||
struct PointCloud;
|
||||
struct SPZImportParams;
|
||||
|
||||
namespace io::spz {
|
||||
|
||||
/* Main import function used from within Blender. */
|
||||
void importer_main(const bContext *C, const SPZImportParams &import_params);
|
||||
|
||||
PointCloud *import_point_cloud(const SPZImportParams &import_params);
|
||||
|
||||
} // namespace io::spz
|
||||
} // namespace blender
|
||||
80
source/blender/io/spz/intern/spz_read.cc
Normal file
80
source/blender/io/spz/intern/spz_read.cc
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#include "spz_read.hh"
|
||||
|
||||
#include <bit>
|
||||
|
||||
#include "BLI_fileops.hh"
|
||||
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "spz_read_common.hh"
|
||||
|
||||
namespace blender::io::spz {
|
||||
|
||||
static CLG_LogRef LOG = {"io.spz"};
|
||||
|
||||
template<class T> static bool read_spz_scalar(FILE *file, T &data)
|
||||
{
|
||||
if (fread(&data, sizeof(T), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
PointCloud *read_spz_file(const StringRefNull filepath, ReportList *reports)
|
||||
{
|
||||
if constexpr (std::endian::native != std::endian::little) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Big endian machines are not supported");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FILE *file = BLI_fopen(filepath.c_str(), "rb");
|
||||
if (!file) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error opening file for read");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t magic = 0;
|
||||
if (!read_spz_scalar(file, magic)) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading magic value from file");
|
||||
fclose(file);
|
||||
return nullptr;
|
||||
}
|
||||
CLOG_DEBUG(&LOG, "Got file header magic: 0x%X", magic);
|
||||
|
||||
/* Rewind the file stream so that specialized version reading can start from its beginning. */
|
||||
if (fseek(file, 0, SEEK_SET) == -1) {
|
||||
fclose(file);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PointCloud *point_cloud = nullptr;
|
||||
|
||||
if (magic == SPZ_HEADER_MAGIC) {
|
||||
CLOG_DEBUG(&LOG, "Detected uncompressed NGSP magic");
|
||||
point_cloud = read_spz_ngsp_file(file, reports);
|
||||
}
|
||||
else if ((magic & 0xffff) == 0x8b1f) {
|
||||
/* Format prior to v4: single GZip stream. */
|
||||
CLOG_DEBUG(&LOG, "Detected Gzip compressed stream");
|
||||
point_cloud = read_spz_gzip_compressed_file(file, reports);
|
||||
}
|
||||
else {
|
||||
BKE_reportf(reports, RPT_ERROR, "SPZ Read: Unrecognized file format");
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
|
||||
return point_cloud;
|
||||
}
|
||||
|
||||
} // namespace blender::io::spz
|
||||
35
source/blender/io/spz/intern/spz_read.hh
Normal file
35
source/blender/io/spz/intern/spz_read.hh
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct PointCloud;
|
||||
struct ReportList;
|
||||
|
||||
namespace io::spz {
|
||||
|
||||
/* Read the SPZ file pointed by the filepath.
|
||||
* Supports multiple SPZ file formats. */
|
||||
PointCloud *read_spz_file(StringRefNull filepath, ReportList *reports);
|
||||
|
||||
/* Read SZP file that uses a single GZip stream to compress the whole file.
|
||||
* This applies to SPZ format versions 2 and 3. */
|
||||
PointCloud *read_spz_gzip_compressed_file(FILE *file, ReportList *reports);
|
||||
|
||||
/* Read SZP file that uses NGSP format (introduced in version 4, uncompressed header, multiple
|
||||
* zstd compressed streams). */
|
||||
PointCloud *read_spz_ngsp_file(FILE *file, ReportList *reports);
|
||||
|
||||
} // namespace io::spz
|
||||
} // namespace blender
|
||||
149
source/blender/io/spz/intern/spz_read_common.cc
Normal file
149
source/blender/io/spz/intern/spz_read_common.cc
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "spz_read_common.hh"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "BLI_array.hh"
|
||||
#include "BLI_assert.hh"
|
||||
#include "BLI_enumerable_thread_specific.hh"
|
||||
#include "BLI_math_rotation.hh"
|
||||
|
||||
#include "IO_gsplat.hh"
|
||||
|
||||
namespace blender::io::spz {
|
||||
|
||||
namespace {
|
||||
|
||||
/* Spherical harmonics rotation matrices for R_x(+pi/2).
|
||||
* Each entry operates on the 2l+1 coefficients of band l (l = 1..4).
|
||||
*
|
||||
* The table is adopted from the SPZ Library project:
|
||||
*
|
||||
* MIT License
|
||||
* Copyright (c) 2024 Niantic Labs
|
||||
*
|
||||
* There are modifications in the code to use Blender utilities, and also to operate on float3
|
||||
* elements rather than on scalars.
|
||||
*
|
||||
* The matrices structure seems to match description given in the
|
||||
*
|
||||
* Didier Pinchon1 and Philip E. Hoggan
|
||||
* "Rotation matrices for real spherical harmonics: general rotations of atomic orbitals in
|
||||
* space-fixed axes" (2007)
|
||||
* Journal of Physics A: Mathematical and Theoretical.
|
||||
* DOI:10.1088/1751-8113/40/7/011
|
||||
*
|
||||
* This could also be confirmed by checking against a naive implementation of the Wigner D-matrix
|
||||
* [WignerDMatrix]. When doing so a basis conversion needs to be applied to convert complex
|
||||
* spherical harmonics to real [SphericalRealForm].
|
||||
*
|
||||
* References:
|
||||
* [WignerDMatrix] https://en.wikipedia.org/wiki/Wigner_D-matrix
|
||||
* [SphericalRealForm] https://en.wikipedia.org/wiki/Spherical_harmonics#Real_form */
|
||||
inline constexpr std::array<void (*)(MutableSpan<float3>), 4>
|
||||
ANALYTIC_ROTATE_PLUS_PI_HALF_ABOUT_X_TABLE = {
|
||||
[](const MutableSpan<float3> p) {
|
||||
BLI_assert(p.size() == 3);
|
||||
const float3 t0 = p[0], t1 = p[1], t2 = p[2];
|
||||
p[0] = t1;
|
||||
p[1] = -t0;
|
||||
p[2] = t2;
|
||||
},
|
||||
[](const MutableSpan<float3> p) {
|
||||
BLI_assert(p.size() == 5);
|
||||
std::array<float3, 5> s;
|
||||
MutableSpan(s).copy_from(p);
|
||||
const float s3 = std::sqrt(3.0f);
|
||||
p[0] = s[3];
|
||||
p[1] = -s[1];
|
||||
p[2] = -0.5f * s[2] - (s3 / 2.0f) * s[4];
|
||||
p[3] = -s[0];
|
||||
p[4] = -(s3 / 2.0f) * s[2] + 0.5f * s[4];
|
||||
},
|
||||
[](const MutableSpan<float3> p) {
|
||||
BLI_assert(p.size() == 7);
|
||||
std::array<float3, 7> s{};
|
||||
MutableSpan(s).copy_from(p);
|
||||
const float s15 = std::sqrt(15.0f);
|
||||
p[0] = -std::sqrt(5.0f / 8.0f) * s[3] + std::sqrt(3.0f / 8.0f) * s[5];
|
||||
p[1] = -s[1];
|
||||
p[2] = -std::sqrt(3.0f / 8.0f) * s[3] - std::sqrt(5.0f / 8.0f) * s[5];
|
||||
p[3] = std::sqrt(5.0f / 8.0f) * s[0] + std::sqrt(3.0f / 8.0f) * s[2];
|
||||
p[4] = -0.25f * s[4] - (s15 / 4.0f) * s[6];
|
||||
p[5] = -std::sqrt(3.0f / 8.0f) * s[0] + std::sqrt(5.0f / 8.0f) * s[2];
|
||||
p[6] = -(s15 / 4.0f) * s[4] + 0.25f * s[6];
|
||||
},
|
||||
[](const MutableSpan<float3> p) {
|
||||
BLI_assert(p.size() == 9);
|
||||
std::array<float3, 9> s{};
|
||||
MutableSpan(s).copy_from(p);
|
||||
const float s2 = std::sqrt(2.0f);
|
||||
const float s5 = std::sqrt(5.0f);
|
||||
const float s7 = std::sqrt(7.0f);
|
||||
const float s14 = std::sqrt(14.0f);
|
||||
const float s35 = std::sqrt(35.0f);
|
||||
p[0] = -(s14 / 4.0f) * s[5] + (s2 / 4.0f) * s[7];
|
||||
p[1] = -0.75f * s[1] + (s7 / 4.0f) * s[3];
|
||||
p[2] = -(s2 / 4.0f) * s[5] - (s14 / 4.0f) * s[7];
|
||||
p[3] = (s7 / 4.0f) * s[1] + 0.75f * s[3];
|
||||
p[4] = (3.0f / 8.0f) * s[4] + (s5 / 4.0f) * s[6] + (s35 / 8.0f) * s[8];
|
||||
p[5] = (s14 / 4.0f) * s[0] + (s2 / 4.0f) * s[2];
|
||||
p[6] = (s5 / 4.0f) * s[4] + 0.5f * s[6] - (s7 / 4.0f) * s[8];
|
||||
p[7] = -(s2 / 4.0f) * s[0] + (s14 / 4.0f) * s[2];
|
||||
p[8] = (s35 / 8.0f) * s[4] - (s7 / 4.0f) * s[6] + 0.125f * s[8];
|
||||
},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void convert_axis_to_blender(const MutableSpan<float3> positions,
|
||||
const MutableSpan<math::Quaternion> rotations,
|
||||
const Span<MutableSpan<float3>> sh_attrs)
|
||||
{
|
||||
/* By default, SPZ stores data in an RUB (Right, Up, Back) coordinate system following the OpenGL
|
||||
* and three.js convention. Blender is RFU (Right, Forward, Up). */
|
||||
|
||||
/* TODO(sergey): Handle extensions that define coordinate system. */
|
||||
/* Example: SPZ_ADOBE_coordinate_system. */
|
||||
|
||||
const int sh_degrees = gsplat::degree_for_dimension(sh_attrs.size());
|
||||
|
||||
struct LocalData {
|
||||
Array<float3> sh_data;
|
||||
};
|
||||
threading::EnumerableThreadSpecific<LocalData> all_tls;
|
||||
threading::parallel_for(IndexRange(positions.size()), 32, [&](const IndexRange range) {
|
||||
LocalData &tls = all_tls.local();
|
||||
for (const int i : range) {
|
||||
positions[i] = {positions[i].x, -positions[i].z, positions[i].y};
|
||||
|
||||
/* Simplified expression for the following:
|
||||
* math::to_quaternion(math::AxisAngle(math::AxisSigned::X_POS, M_PI_2)) * rotations[i]. */
|
||||
rotations[i] = math::Quaternion(M_SQRT1_2 * (rotations[i].w - rotations[i].x),
|
||||
M_SQRT1_2 * (rotations[i].x + rotations[i].w),
|
||||
M_SQRT1_2 * (rotations[i].y - rotations[i].z),
|
||||
M_SQRT1_2 * (rotations[i].z + rotations[i].y));
|
||||
|
||||
if (sh_degrees == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tls.sh_data.is_empty()) {
|
||||
tls.sh_data.reinitialize(sh_attrs.size());
|
||||
}
|
||||
|
||||
MutableSpan<float3> sh_data = tls.sh_data;
|
||||
gsplat::get_spherical_harmonics(sh_attrs, i, sh_data);
|
||||
for (int band = 0; band < sh_degrees; ++band) {
|
||||
ANALYTIC_ROTATE_PLUS_PI_HALF_ABOUT_X_TABLE[band](
|
||||
sh_data.slice(band * (band + 2), 2 * (band + 1) + 1));
|
||||
}
|
||||
gsplat::set_spherical_harmonics(sh_attrs, i, sh_data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace blender::io::spz
|
||||
254
source/blender/io/spz/intern/spz_read_common.hh
Normal file
254
source/blender/io/spz/intern/spz_read_common.hh
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "BLI_array.hh"
|
||||
#include "BLI_math_constants.hh"
|
||||
#include "BLI_math_quaternion_types.hh"
|
||||
#include "BLI_math_vector.hh"
|
||||
#include "BLI_math_vector_types.hh"
|
||||
#include "BLI_span.hh"
|
||||
#include "BLI_unroll.hh"
|
||||
|
||||
#include "IO_gsplat.hh"
|
||||
|
||||
namespace blender::io::spz {
|
||||
|
||||
/* Magic number in the file header for the tile identification. Common for all format versions.
|
||||
* It is bytes N, G, S, P in the file order. */
|
||||
constexpr uint32_t SPZ_HEADER_MAGIC = 0x5053474e;
|
||||
|
||||
enum HeaderFlag {
|
||||
SPZ_HEADER_ANTIALIASED = 0x01,
|
||||
SPZ_HEADER_HAS_EXTENSIONS = 0x02,
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
|
||||
template<class ReaderType>
|
||||
[[nodiscard]] inline bool read_single_position_as_fixed(ReaderType &reader, int3 &fixed)
|
||||
{
|
||||
using PackedFloat = std::array<uint8_t, 3>;
|
||||
using PackedFloat3 = std::array<PackedFloat, 3>;
|
||||
PackedFloat3 packed;
|
||||
if (!reader.read(packed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unroll<3>([&](auto i) {
|
||||
fixed[i] = packed[i][0];
|
||||
fixed[i] |= packed[i][1] << 8;
|
||||
fixed[i] |= packed[i][2] << 16;
|
||||
fixed[i] |= (fixed[i] & 0x800000) ? 0xff000000 : 0;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline float inv_sigmoid(const float x)
|
||||
{
|
||||
return std::log(x / (1.0f - x));
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
template<class ReaderType>
|
||||
[[nodiscard]] inline bool read_positions(ReaderType &reader,
|
||||
const int fractional_bits,
|
||||
const MutableSpan<float3> positions)
|
||||
{
|
||||
/* From documentation:
|
||||
*
|
||||
* Positions are represented as (x, y, z) coordinates, each as a 24-bit fixed point signed
|
||||
* integer. The number of fractional bits is determined by the fractionalBits field in the
|
||||
* header. */
|
||||
BLI_assert(fractional_bits >= 0 && fractional_bits < 24);
|
||||
const float scale = 1.0f / (1 << fractional_bits);
|
||||
for (float3 &position : positions) {
|
||||
int3 fixed;
|
||||
if (!internal::read_single_position_as_fixed(reader, fixed)) {
|
||||
return false;
|
||||
}
|
||||
position = float3(fixed) * scale;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class ReaderType>
|
||||
[[nodiscard]] inline bool read_alphas(ReaderType &reader, const MutableSpan<float4> radiance_base)
|
||||
{
|
||||
for (float4 &base : radiance_base) {
|
||||
uint8_t alpha;
|
||||
if (!reader.read(alpha)) {
|
||||
return false;
|
||||
}
|
||||
if (false) {
|
||||
/* The naive implementation.
|
||||
* The inv_sigmoid() and decode_opacity() are actually canceling each other.
|
||||
* Code kept for the reference and possible situation when other activation functions are
|
||||
* used in the future. */
|
||||
base.w = internal::inv_sigmoid(float(alpha) / 255.0f);
|
||||
base.w = gsplat::OriginalActivationFunctions::decode_opacity(base.w);
|
||||
}
|
||||
else {
|
||||
base.w = float(alpha) / 255.0f;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class ReaderType>
|
||||
[[nodiscard]] inline bool read_colors(ReaderType &reader, const MutableSpan<float4> radiance_base)
|
||||
{
|
||||
/* From SPZ:
|
||||
* Scale factor for DC color components. To convert to RGB, we should multiply by 0.282, but it
|
||||
* can be useful to represent base colors that are out of range if the higher spherical
|
||||
* harmonics bands bring them back into range so we multiply by a smaller value. */
|
||||
constexpr float COLOR_SCALE = 0.15f;
|
||||
const float INV_COLOR_SCALE = 1.0f / COLOR_SCALE;
|
||||
|
||||
for (float4 &base : radiance_base) {
|
||||
std::array<uint8_t, 3> color_buffer;
|
||||
if (!reader.read_buffer(color_buffer)) {
|
||||
return false;
|
||||
}
|
||||
base.x = ((color_buffer[0] / 255.0f) - 0.5f) * INV_COLOR_SCALE;
|
||||
base.y = ((color_buffer[1] / 255.0f) - 0.5f) * INV_COLOR_SCALE;
|
||||
base.z = ((color_buffer[2] / 255.0f) - 0.5f) * INV_COLOR_SCALE;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class ReaderType>
|
||||
[[nodiscard]] inline bool read_scales(ReaderType &reader, const MutableSpan<float3> scales)
|
||||
{
|
||||
/* From documentation:
|
||||
*
|
||||
* Scales are represented as (x, y, z) components, each represented as an 8-bit log-encoded
|
||||
* integer. */
|
||||
for (float3 &scale : scales) {
|
||||
std::array<uint8_t, 3> encoded;
|
||||
if (!reader.read_buffer(encoded)) {
|
||||
return false;
|
||||
}
|
||||
scale = float3(encoded[0], encoded[1], encoded[2]) / 16.0f - 10.0f;
|
||||
scale = gsplat::OriginalActivationFunctions::decode_scale(scale);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class ReaderType>
|
||||
[[nodiscard]] inline bool read_rotations(ReaderType &reader,
|
||||
const int version,
|
||||
const MutableSpan<math::Quaternion> rotations)
|
||||
{
|
||||
if (version == 2) {
|
||||
/* From documentation:
|
||||
*
|
||||
* In version 2, rotations are represented as the (x, y, z) components of the normalized
|
||||
* rotation quaternion. The w component can be derived from the others and is not stored.
|
||||
* Each component is encoded as an 8-bit signed integer. */
|
||||
for (math::Quaternion &rotation : rotations) {
|
||||
std::array<uint8_t, 3> packed;
|
||||
if (!reader.read_buffer(packed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float3 first3 = float3(packed[0], packed[1], packed[2]) / 127.5f - 1.0f;
|
||||
rotation.w = std::sqrt(std::max(0.0f, 1.0f - math::dot(first3, first3)));
|
||||
rotation.x = first3.x;
|
||||
rotation.y = first3.y;
|
||||
rotation.z = first3.z;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (version >= 3) {
|
||||
/* From documentation:
|
||||
*
|
||||
* In version 3, rotations are represented as the smallest three components of the normalized
|
||||
* rotation quaternion, for optimal rotation accuracy. The largest component can be derived
|
||||
* from the others and is not stored. Its index is stored on 2 bits and each of the smallest
|
||||
* three components is encoded as a 10-bit signed integer. */
|
||||
constexpr uint32_t COMPONENT_MASK = (1 << 9) - 1;
|
||||
for (math::Quaternion &rotation : rotations) {
|
||||
/* Read as individual bytes and re-pack to integer to ensure endianess. */
|
||||
std::array<uint8_t, 4> packed_bytes;
|
||||
if (!reader.read_buffer(packed_bytes)) {
|
||||
return false;
|
||||
}
|
||||
float spz_rotation[4];
|
||||
uint32_t packed = packed_bytes[0] + (packed_bytes[1] << 8) + (packed_bytes[2] << 16) +
|
||||
(packed_bytes[3] << 24);
|
||||
const int largest_index = packed >> 30;
|
||||
float sum_sq = 0.0f;
|
||||
for (int i = 3; i >= 0; --i) {
|
||||
if (i == largest_index) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t mag = packed & COMPONENT_MASK;
|
||||
const float sign = ((packed >> 9) & 0x1) == 1 ? -1.0f : 1.0f;
|
||||
spz_rotation[i] = M_SQRT1_2 * ((float)mag) / float(COMPONENT_MASK) * sign;
|
||||
sum_sq += spz_rotation[i] * spz_rotation[i];
|
||||
packed = packed >> 10;
|
||||
}
|
||||
spz_rotation[largest_index] = sqrt(math::max(1.0f - sum_sq, 0.0f));
|
||||
/* Convert to Blender's (w, x, y, z). */
|
||||
rotation = math::Quaternion(
|
||||
spz_rotation[3], spz_rotation[0], spz_rotation[1], spz_rotation[2]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
template<class ReaderType>
|
||||
[[nodiscard]] inline bool read_sh(ReaderType &reader, const Span<MutableSpan<float3>> sh)
|
||||
{
|
||||
/* From documentation:
|
||||
*
|
||||
* The coefficients for a gaussian are organized such that the color channel is the inner
|
||||
* (faster varying) axis, and the coefficient is the outer (slower varying) axis
|
||||
* Each coefficient is represented as an 8-bit signed integer. Additional quantization can be
|
||||
* performed to attain a higher compression ratio */
|
||||
|
||||
if (sh.is_empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const int64_t num_points = sh[0].size();
|
||||
const int num_coefficients = sh.size();
|
||||
|
||||
Array<uint8_t> packed_coefficients(3 * num_coefficients);
|
||||
|
||||
for (int64_t i = 0; i < num_points; ++i) {
|
||||
if (!reader.read_buffer(packed_coefficients)) {
|
||||
return false;
|
||||
}
|
||||
for (int j = 0; j < num_coefficients; ++j) {
|
||||
const float3 coefficients(packed_coefficients[3 * j + 0],
|
||||
packed_coefficients[3 * j + 1],
|
||||
packed_coefficients[3 * j + 2]);
|
||||
sh[j][i] = (coefficients - 128.0f) / 128.0f;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void convert_axis_to_blender(MutableSpan<float3> positions,
|
||||
MutableSpan<math::Quaternion> rotations,
|
||||
Span<MutableSpan<float3>> sh_attrs);
|
||||
|
||||
} // namespace blender::io::spz
|
||||
210
source/blender/io/spz/intern/spz_read_gzip.cc
Normal file
210
source/blender/io/spz/intern/spz_read_gzip.cc
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#include "spz_read.hh"
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
#include "BLI_array.hh"
|
||||
#include "BLI_assert.hh"
|
||||
#include "BLI_span.hh"
|
||||
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_pointcloud.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "IO_gsplat.hh"
|
||||
#include "IO_validate.hh"
|
||||
|
||||
#include "spz_read_common.hh"
|
||||
|
||||
namespace blender::io::spz {
|
||||
|
||||
static CLG_LogRef LOG = {"io.spz"};
|
||||
|
||||
namespace {
|
||||
|
||||
struct PackedGaussiansHeader {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t num_points;
|
||||
uint8_t sh_degree;
|
||||
uint8_t fractional_bits;
|
||||
uint8_t flags;
|
||||
uint8_t reserved;
|
||||
};
|
||||
static_assert(sizeof(PackedGaussiansHeader) == 16);
|
||||
|
||||
/* Helper class that takes care of reading data from FILE in chunks. */
|
||||
class BufferedFileReader {
|
||||
static constexpr int BUFFER_SIZE = 65536;
|
||||
|
||||
FILE *file_ = nullptr;
|
||||
Array<uint8_t> buffer_;
|
||||
|
||||
public:
|
||||
explicit BufferedFileReader(FILE *file) : file_(file), buffer_(BUFFER_SIZE) {}
|
||||
|
||||
std::optional<Span<uint8_t>> read()
|
||||
{
|
||||
const size_t num_read_bytes = fread(buffer_.data(), 1, buffer_.size(), file_);
|
||||
if (ferror(file_)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return buffer_.as_span().slice(0, num_read_bytes);
|
||||
}
|
||||
};
|
||||
|
||||
/* Helper class that allows reading a Gzip-compressed file from disk with the minimum amount of
|
||||
* extra memory usage, but allowing to easily access data of specific size. */
|
||||
class StreamedGzipReader {
|
||||
BufferedFileReader buffered_reader_;
|
||||
z_stream stream_{};
|
||||
|
||||
public:
|
||||
explicit StreamedGzipReader(FILE *file) : buffered_reader_(file) {}
|
||||
|
||||
~StreamedGzipReader()
|
||||
{
|
||||
inflateEnd(&stream_);
|
||||
}
|
||||
|
||||
bool initialize()
|
||||
{
|
||||
/* The window size matches the SPZ library: here 16 means enable automatic gzip header
|
||||
* detection; consider switching this to 32 to enable both automated gzip and zlib header
|
||||
* detection. */
|
||||
return inflateInit2(&stream_, 16 | MAX_WBITS) == Z_OK;
|
||||
}
|
||||
|
||||
bool read_buffer(const MutableSpan<uint8_t> buffer)
|
||||
{
|
||||
stream_.avail_out = buffer.size();
|
||||
stream_.next_out = buffer.data();
|
||||
|
||||
do {
|
||||
/* There might be left-over data in the input since previous read.
|
||||
* If there is, process that data first. Otherwise, load the next chunk from the input file.
|
||||
*/
|
||||
if (stream_.avail_in == 0) {
|
||||
std::optional<Span<uint8_t>> read_result = buffered_reader_.read();
|
||||
if (!read_result.has_value() || read_result->size() == 0) {
|
||||
return false;
|
||||
}
|
||||
stream_.avail_in = read_result->size();
|
||||
stream_.next_in = const_cast<Bytef *>(read_result->data());
|
||||
}
|
||||
|
||||
const int ret = inflate(&stream_, Z_NO_FLUSH);
|
||||
BLI_assert(ret != Z_STREAM_ERROR);
|
||||
if (ret == Z_STREAM_END) {
|
||||
if (stream_.avail_out != 0) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (ret != Z_OK) {
|
||||
/* TODO(sergey): Report error. */
|
||||
return false;
|
||||
}
|
||||
} while (stream_.avail_out != 0);
|
||||
|
||||
/* TODO(sergey): Investigate whether decompressing more data ahead of time helps performance.
|
||||
*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class T> bool read(T &data)
|
||||
{
|
||||
return read_buffer(MutableSpan<uint8_t>(reinterpret_cast<uint8_t *>(&data), sizeof(T)));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
PointCloud *read_spz_gzip_compressed_file(FILE *file, ReportList *reports)
|
||||
{
|
||||
StreamedGzipReader reader(file);
|
||||
|
||||
if (!reader.initialize()) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error initializing Zlib decompression");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PackedGaussiansHeader header;
|
||||
if (!reader.read(header)) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading header from GZip stream");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CLOG_DEBUG(&LOG, "SPZ header magic: 0x%X", header.magic);
|
||||
CLOG_DEBUG(&LOG, "SPZ header version: %u", header.version);
|
||||
|
||||
if (header.magic != SPZ_HEADER_MAGIC) {
|
||||
BKE_reportf(reports, RPT_ERROR, "SPZ Read: Unexpected SPZ header magic 0x%X", header.magic);
|
||||
return nullptr;
|
||||
}
|
||||
if (header.version != 2 && header.version != 3) {
|
||||
BKE_reportf(reports, RPT_ERROR, "SPZ Read: Unsupported SPZ version %u", header.version);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CLOG_DEBUG(&LOG, "SPZ header num_points: %u", header.num_points);
|
||||
CLOG_DEBUG(&LOG, "SPZ header sh_degree: %d", int(header.sh_degree));
|
||||
CLOG_DEBUG(&LOG, "SPZ header fractional_bits: %d", int(header.fractional_bits));
|
||||
CLOG_DEBUG(&LOG, "SPZ header flags: %d", header.flags);
|
||||
|
||||
if (header.flags & SPZ_HEADER_ANTIALIASED) {
|
||||
/* TODO(sergey): Support antialiased data. */
|
||||
CLOG_WARN(&LOG, "SPZ data was trained with antialiasing which is not fully supported");
|
||||
}
|
||||
if (header.flags & SPZ_HEADER_HAS_EXTENSIONS) {
|
||||
/* TODO(sergey): Support extensions. */
|
||||
CLOG_WARN(&LOG, "SPZ file contains extensions that are not yet supported");
|
||||
}
|
||||
|
||||
if (!validate::size_fits_in_int(header.num_points)) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Too many points");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PointCloud *point_cloud = BKE_pointcloud_new_nomain(PointCloudType::GSplat, header.num_points);
|
||||
gsplat::GsplatMutableAttributeAccessor accessor(*point_cloud, header.sh_degree);
|
||||
|
||||
const MutableSpan<float3> positions = accessor.positions_for_write();
|
||||
const MutableSpan<math::Quaternion> rotations = accessor.rotations_for_write();
|
||||
const Span<MutableSpan<float3>> sh_attrs = accessor.sh_for_write();
|
||||
|
||||
if (!read_positions(reader, header.fractional_bits, positions) ||
|
||||
!read_alphas(reader, accessor.radiance_base_for_write()) ||
|
||||
!read_colors(reader, accessor.radiance_base_for_write()) ||
|
||||
!read_scales(reader, accessor.scales_for_write()) ||
|
||||
!read_rotations(reader, header.version, rotations) || !read_sh(reader, sh_attrs))
|
||||
{
|
||||
accessor.finish();
|
||||
BKE_id_free(nullptr, &point_cloud->id);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* TODO(sergey): Handle extensions. */
|
||||
|
||||
convert_axis_to_blender(positions, rotations, sh_attrs);
|
||||
|
||||
accessor.finish();
|
||||
|
||||
return point_cloud;
|
||||
}
|
||||
|
||||
} // namespace blender::io::spz
|
||||
379
source/blender/io/spz/intern/spz_read_ngsp.cc
Normal file
379
source/blender/io/spz/intern/spz_read_ngsp.cc
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup spz
|
||||
*/
|
||||
|
||||
#include "spz_read.hh"
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
#include "BLI_array.hh"
|
||||
#include "BLI_fileops.hh"
|
||||
#include "BLI_math_base.hh"
|
||||
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_pointcloud.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "IO_gsplat.hh"
|
||||
#include "IO_validate.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "spz_read_common.hh"
|
||||
|
||||
namespace blender::io::spz {
|
||||
|
||||
static CLG_LogRef LOG = {"io.spz"};
|
||||
|
||||
namespace {
|
||||
|
||||
struct NgspFileHeader {
|
||||
uint32_t magic; /* 0x5053474e ("NGSP") */
|
||||
uint32_t version; /* 4 */
|
||||
uint32_t num_points;
|
||||
uint8_t sh_degree;
|
||||
uint8_t fractional_bits;
|
||||
uint8_t flags;
|
||||
uint8_t num_streams; /* The number of ZSTD-compressed attribute streams (typically 6). */
|
||||
uint32_t toc_byte_offset; /* Byte offset from file start to the TOC */
|
||||
uint8_t reserved[12]; /* zero, reserved for future use. */
|
||||
};
|
||||
static_assert(sizeof(NgspFileHeader) == 32);
|
||||
|
||||
struct StreamInfo {
|
||||
/* Offset of the stream from the beginning of the file. */
|
||||
uint64_t compressed_offset;
|
||||
|
||||
uint64_t compressed_size;
|
||||
uint64_t uncompressed_size;
|
||||
};
|
||||
|
||||
/* Helper class that takes care of reading data from SPZ stream from file in chunks. */
|
||||
class BufferedStreamReader {
|
||||
static constexpr int BUFFER_SIZE = 65536;
|
||||
|
||||
FILE *file_ = nullptr;
|
||||
StreamInfo stream_info_;
|
||||
Array<uint8_t> buffer_;
|
||||
|
||||
uint64_t num_read_bytes_ = 0;
|
||||
|
||||
public:
|
||||
BufferedStreamReader(FILE *file, const StreamInfo &stream_info)
|
||||
: file_(file), stream_info_(stream_info), buffer_(BUFFER_SIZE)
|
||||
{
|
||||
}
|
||||
|
||||
bool initialize()
|
||||
{
|
||||
return BLI_fseek(file_, int64_t(stream_info_.compressed_offset), SEEK_SET) != -1;
|
||||
}
|
||||
|
||||
std::optional<Span<uint8_t>> read()
|
||||
{
|
||||
const uint64_t num_bytes_to_read = math::min(stream_info_.compressed_size - num_read_bytes_,
|
||||
uint64_t(buffer_.size()));
|
||||
const size_t num_read_bytes = fread(buffer_.data(), 1, num_bytes_to_read, file_);
|
||||
if (ferror(file_) || num_bytes_to_read != num_read_bytes) {
|
||||
return std::nullopt;
|
||||
}
|
||||
num_read_bytes_ += num_bytes_to_read;
|
||||
return buffer_.as_span().slice(0, num_read_bytes);
|
||||
}
|
||||
};
|
||||
|
||||
/* Helper class that allows reading a Gzip-compressed file from disk with the minimum amount of
|
||||
* extra memory usage, but allowing to easily access data of specific size. */
|
||||
class StreamedZstdReader {
|
||||
BufferedStreamReader buffered_reader_;
|
||||
ZSTD_DCtx *dctx_ = nullptr;
|
||||
ZSTD_inBuffer input_buffer_;
|
||||
|
||||
public:
|
||||
StreamedZstdReader(FILE *file, const StreamInfo &stream_info)
|
||||
: buffered_reader_(file, stream_info)
|
||||
{
|
||||
}
|
||||
|
||||
~StreamedZstdReader()
|
||||
{
|
||||
if (dctx_) {
|
||||
ZSTD_freeDCtx(dctx_);
|
||||
}
|
||||
}
|
||||
|
||||
bool initialize()
|
||||
{
|
||||
if (!buffered_reader_.initialize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dctx_ = ZSTD_createDCtx();
|
||||
|
||||
input_buffer_.src = nullptr;
|
||||
input_buffer_.size = 0;
|
||||
input_buffer_.pos = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_buffer(const MutableSpan<uint8_t> buffer)
|
||||
{
|
||||
ZSTD_outBuffer output_buffer = {.dst = buffer.data(), .size = size_t(buffer.size()), .pos = 0};
|
||||
|
||||
do {
|
||||
/* There might be left-over data in the input since previous read.
|
||||
* If there is, process that data first. Otherwise, load the next chunk from the input file.
|
||||
*/
|
||||
if (input_buffer_.pos == input_buffer_.size) {
|
||||
std::optional<Span<uint8_t>> read_result = buffered_reader_.read();
|
||||
if (!read_result.has_value() || read_result->size() == 0) {
|
||||
return false;
|
||||
}
|
||||
input_buffer_.src = read_result->data();
|
||||
input_buffer_.size = read_result->size();
|
||||
input_buffer_.pos = 0;
|
||||
}
|
||||
|
||||
const size_t ret = ZSTD_decompressStream(dctx_, &output_buffer, &input_buffer_);
|
||||
if (ZSTD_isError(ret)) {
|
||||
// TODO(sergey): Report error using ZSTD_getErrorName(lastResult) ?
|
||||
return false;
|
||||
}
|
||||
} while (output_buffer.pos < output_buffer.size);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class T> bool read(T &data)
|
||||
{
|
||||
return read_buffer(MutableSpan<uint8_t>(reinterpret_cast<uint8_t *>(&data), sizeof(T)));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
template<class T> static bool read_spz_struct(FILE *file, T &data)
|
||||
{
|
||||
if (fread(&data, sizeof(T), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_toc(FILE *file, const NgspFileHeader &header, Array<StreamInfo> &stream_infos)
|
||||
{
|
||||
struct PackedStreamInfo {
|
||||
uint64_t compressed_size;
|
||||
uint64_t uncompressed_size;
|
||||
};
|
||||
static_assert(sizeof(PackedStreamInfo) == 16);
|
||||
|
||||
if (header.num_streams == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (BLI_fseek(file, header.toc_byte_offset, SEEK_SET) == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<PackedStreamInfo> packed_stream_infos(header.num_streams);
|
||||
if (fread(packed_stream_infos.data(), sizeof(PackedStreamInfo), header.num_streams, file) !=
|
||||
header.num_streams)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stream_infos.reinitialize(header.num_streams);
|
||||
stream_infos[0].compressed_offset = header.toc_byte_offset +
|
||||
packed_stream_infos.size() * sizeof(PackedStreamInfo);
|
||||
stream_infos[0].compressed_size = packed_stream_infos[0].compressed_size;
|
||||
stream_infos[0].uncompressed_size = packed_stream_infos[0].uncompressed_size;
|
||||
|
||||
for (int i = 1; i < header.num_streams; ++i) {
|
||||
stream_infos[i].compressed_offset = stream_infos[i - 1].compressed_offset +
|
||||
stream_infos[i - 1].compressed_size;
|
||||
stream_infos[i].compressed_size = packed_stream_infos[i].compressed_size;
|
||||
stream_infos[i].uncompressed_size = packed_stream_infos[i].uncompressed_size;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_spz(FILE *file,
|
||||
const NgspFileHeader &header,
|
||||
const Span<StreamInfo> stream_infos,
|
||||
ReportList *reports,
|
||||
gsplat::GsplatMutableAttributeAccessor &accessor)
|
||||
{
|
||||
if (stream_infos.size() < 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const MutableSpan<float3> positions = accessor.positions_for_write();
|
||||
const MutableSpan<math::Quaternion> rotations = accessor.rotations_for_write();
|
||||
const Span<MutableSpan<float3>> sh_attrs = accessor.sh_for_write();
|
||||
|
||||
/* Positions. */
|
||||
{
|
||||
StreamedZstdReader reader(file, stream_infos[0]);
|
||||
if (!reader.initialize()) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error initializing positions reader");
|
||||
return false;
|
||||
}
|
||||
if (!read_positions(reader, header.fractional_bits, positions)) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading positions");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Alphas and colors. */
|
||||
{
|
||||
StreamedZstdReader reader(file, stream_infos[1]);
|
||||
if (!reader.initialize()) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error initializing alphas reader");
|
||||
return false;
|
||||
}
|
||||
if (!read_alphas(reader, accessor.radiance_base_for_write())) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading alphas");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
{
|
||||
StreamedZstdReader reader(file, stream_infos[2]);
|
||||
if (!reader.initialize()) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error initializing colors reader");
|
||||
return false;
|
||||
}
|
||||
if (!read_colors(reader, accessor.radiance_base_for_write())) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading colors");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scales. */
|
||||
{
|
||||
StreamedZstdReader reader(file, stream_infos[3]);
|
||||
if (!reader.initialize()) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error initializing scales reader");
|
||||
return false;
|
||||
}
|
||||
if (!read_scales(reader, accessor.scales_for_write())) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading scales");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Rotations. */
|
||||
{
|
||||
StreamedZstdReader reader(file, stream_infos[4]);
|
||||
if (!reader.initialize()) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error initializing rotations reader");
|
||||
return false;
|
||||
}
|
||||
if (!read_rotations(reader, header.version, rotations)) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading rotations");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* SH. */
|
||||
{
|
||||
StreamedZstdReader reader(file, stream_infos[5]);
|
||||
if (!reader.initialize()) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error initializing SH reader");
|
||||
return false;
|
||||
}
|
||||
if (!read_sh(reader, sh_attrs)) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading spherical harmonics");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO(sergey): Handle extensions. */
|
||||
|
||||
convert_axis_to_blender(positions, rotations, sh_attrs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PointCloud *read_spz_ngsp_file(FILE *file, ReportList *reports)
|
||||
{
|
||||
NgspFileHeader header;
|
||||
if (!read_spz_struct(file, header)) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Error reading NGSP header");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CLOG_DEBUG(&LOG, "SPZ header magic: 0x%X", header.magic);
|
||||
CLOG_DEBUG(&LOG, "SPZ header version: %u", header.version);
|
||||
|
||||
if (header.magic != SPZ_HEADER_MAGIC) {
|
||||
BKE_reportf(reports, RPT_ERROR, "SPZ Read: Unexpected SPZ header magic 0x%X", header.magic);
|
||||
return nullptr;
|
||||
}
|
||||
if (header.version != 4) {
|
||||
BKE_reportf(reports, RPT_ERROR, "SPZ Read: Unsupported SPZ version %u", header.version);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CLOG_DEBUG(&LOG, "SPZ header num_points: %u", header.num_points);
|
||||
CLOG_DEBUG(&LOG, "SPZ header sh_degree: %d", int(header.sh_degree));
|
||||
CLOG_DEBUG(&LOG, "SPZ header fractional_bits: %d", int(header.fractional_bits));
|
||||
CLOG_DEBUG(&LOG, "SPZ header flags: %d", int(header.flags));
|
||||
CLOG_DEBUG(&LOG, "SPZ header num_streams: %d", int(header.num_streams));
|
||||
CLOG_DEBUG(&LOG, "SPZ header toc_byte_offset: %u", header.toc_byte_offset);
|
||||
|
||||
if (header.flags & SPZ_HEADER_ANTIALIASED) {
|
||||
/* TODO(sergey): Support antialiased data. */
|
||||
CLOG_WARN(&LOG, "SPZ data was trained with antialiasing which is not fully supported");
|
||||
}
|
||||
if (header.flags & SPZ_HEADER_HAS_EXTENSIONS) {
|
||||
/* TODO(sergey): Support extensions. */
|
||||
CLOG_WARN(&LOG, "SPZ file contains extensions that are not yet supported");
|
||||
}
|
||||
|
||||
if (header.toc_byte_offset < sizeof(NgspFileHeader)) {
|
||||
BKE_report(
|
||||
reports, RPT_ERROR, "SPZ Read: TOC byte offset is less than the size of the header");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Array<StreamInfo> stream_infos;
|
||||
if (!read_toc(file, header, stream_infos)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* There is expected to be 6 streams: positions, alphas, colors, scales, rotations, sh. */
|
||||
if (stream_infos.size() < 6) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Unexpected number of Zstd streams");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!validate::size_fits_in_int(header.num_points)) {
|
||||
BKE_report(reports, RPT_ERROR, "SPZ Read: Too many points");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PointCloud *pointcloud = BKE_pointcloud_new_nomain(PointCloudType::GSplat, header.num_points);
|
||||
gsplat::GsplatMutableAttributeAccessor accessor(*pointcloud, header.sh_degree);
|
||||
|
||||
if (!read_spz(file, header, stream_infos, reports, accessor)) {
|
||||
accessor.finish();
|
||||
BKE_id_free(nullptr, &pointcloud->id);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
accessor.finish();
|
||||
|
||||
return pointcloud;
|
||||
}
|
||||
|
||||
} // namespace blender::io::spz
|
||||
|
|
@ -298,6 +298,10 @@ if(WITH_IO_PLY)
|
|||
add_definitions(-DWITH_IO_PLY)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_SPZ)
|
||||
add_definitions(-DWITH_IO_SPZ)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_STL)
|
||||
add_definitions(-DWITH_IO_STL)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ static PyStructSequence_Field app_builtopts_info_fields[] = {
|
|||
{"mod_remesh", nullptr},
|
||||
{"io_wavefront_obj", nullptr},
|
||||
{"io_ply", nullptr},
|
||||
{"io_spz", nullptr},
|
||||
{"io_stl", nullptr},
|
||||
{"io_fbx", nullptr},
|
||||
{"io_gpencil", nullptr},
|
||||
|
|
@ -250,6 +251,12 @@ static PyObject *make_builtopts_info()
|
|||
SetObjIncref(Py_False);
|
||||
#endif
|
||||
|
||||
#ifdef WITH_IO_SPZ
|
||||
SetObjIncref(Py_True);
|
||||
#else
|
||||
SetObjIncref(Py_False);
|
||||
#endif
|
||||
|
||||
#ifdef WITH_IO_STL
|
||||
SetObjIncref(Py_True);
|
||||
#else
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue