Skip to content

Commit ab2a180

Browse files
committed
3dgs-examples-tests-docs: examples, unit tests, pybind, tutorial
Examples: - examples/cpp/GaussianSplat.cpp: interactive viewer with red sphere for depth compositing validation; command-line options for sh_degree, antialias, RenderConfig tuning - examples/python/visualization/draw_from_csv.py: load and visualize 3DGS scenes from CSV files with draw() Unit tests: - cpp/tests/visualization/rendering/GaussianSplatRender.cpp: RenderToImage golden PNG test (36x20 viewport, AllClose atol=5); OPEN3D_TEST_GENERATE_REFERENCE=1 regenerates reference image - cpp/tests/io/FileFormatIO.cpp: .ply and .splat round-trip IO tests for GS attributes (positions, rotations, scales, opacity, f_dc, f_rest) - cpp/tests/t/geometry/PointCloud.cpp: Rotate/Scale/Translate/Transform correctness tests for GS PointClouds including SH rotation validation Python bindings: - cpp/pybind/t/io/class_io.cpp: expose GS IO options (sh_degree etc.) - cpp/pybind/visualization/rendering/rendering.cpp: expose RenderConfig, GS texture query, RenderToDepthImage for 3DGS - cpp/pybind/io/class_io.cpp: .splat file extension registration Tutorial: - docs/tutorial/visualization/gaussian_splatting.ipynb: end-to-end notebook demonstrating load, visualize, RenderToImage, RenderToDepthImage - docs/tutorial/visualization/index.rst: add gaussian_splatting to toctree Minor cpp example cleanup (missing include fix in OffscreenRendering, OnlineSLAM*, TICP examples)
1 parent cfaa971 commit ab2a180

20 files changed

Lines changed: 1185 additions & 15 deletions

cpp/pybind/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ if (WIN32)
4848
target_link_options(pybind PUBLIC "/force:multiple")
4949
# TODO(Sameer): Only export PyInit_pybind, open3d_core_cuda_device_count
5050
elseif (APPLE)
51+
# Symbol names in Apple's -exported_symbols_list must carry the leading
52+
# underscore that the C ABI prepends to all extern "C" symbols on macOS.
53+
# ld-prime (Xcode 16+) is strict about this; older ld-classic was lenient.
5154
file(GENERATE OUTPUT pybind.map CONTENT
5255
[=[_PyInit_pybind
53-
open3d_core_cuda_device_count
56+
_open3d_core_cuda_device_count
5457
]=])
5558
target_link_options(pybind PRIVATE $<$<CONFIG:Release>:
5659
-Wl,-exported_symbols_list

cpp/pybind/io/class_io.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ void pybind_class_io_declarations(py::module &m_io) {
9292
.value("CONTAINS_POINTS", FileGeometry::CONTAINS_POINTS)
9393
.value("CONTAINS_LINES", FileGeometry::CONTAINS_LINES)
9494
.value("CONTAINS_TRIANGLES", FileGeometry::CONTAINS_TRIANGLES)
95+
.value("CONTAINS_GAUSSIAN_SPLATS",
96+
FileGeometry::CONTAINS_GAUSSIAN_SPLATS)
9597
.export_values()
9698
.finalize();
9799
}

cpp/pybind/t/io/class_io.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,18 @@ void pybind_class_io_definitions(py::module &m_io) {
136136
remove_infinite_points, print_progress});
137137
return pcd;
138138
},
139-
"Function to read PointCloud with tensor attributes from file.",
139+
R"(Read a tensor :class:`open3d.t.geometry.PointCloud` from file.
140+
141+
For 3D Gaussian splat data (``.ply`` / ``.splat`` with the usual attributes):
142+
143+
- **In-memory ``scale``** on the returned point cloud is always **linear** (axis
144+
lengths), which matches the Filament / rendering path.
145+
- **PLY** files follow the common convention of storing **log-scale** per axis;
146+
the reader applies ``exp`` so the tensor attribute ``scale`` is linear.
147+
- **SPLAT** files store **linear** scales already; no conversion is applied.
148+
149+
When writing PLY from a Gaussian splat cloud, ``write_point_cloud`` converts
150+
``scale`` back to log-space for the same file convention.)",
140151
"filename"_a, "format"_a = "auto", "remove_nan_points"_a = false,
141152
"remove_infinite_points"_a = false, "print_progress"_a = false);
142153
docstring::FunctionDocInject(m_io, "read_point_cloud",
@@ -152,7 +163,12 @@ void pybind_class_io_definitions(py::module &m_io) {
152163
filename.string(), pointcloud,
153164
{write_ascii, compressed, print_progress});
154165
},
155-
"Function to write PointCloud with tensor attributes to file.",
166+
R"(Write a tensor :class:`open3d.t.geometry.PointCloud` to file.
167+
168+
For Gaussian splat clouds written as **PLY**, per-point ``scale`` is converted
169+
from the in-memory **linear** representation to **log-scale** in the file, matching
170+
common 3DGS PLY conventions (see :meth:`read_point_cloud`). SPLAT output writes
171+
linear scales directly.)",
156172
"filename"_a, "pointcloud"_a, "write_ascii"_a = false,
157173
"compressed"_a = false, "print_progress"_a = false);
158174
docstring::FunctionDocInject(m_io, "write_point_cloud",

cpp/pybind/visualization/rendering/rendering.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,35 @@ void pybind_rendering_definitions(py::module &m) {
449449
.def_readwrite("aspect_ratio", &MaterialRecord::aspect_ratio)
450450
.def_readwrite("ground_plane_axis",
451451
&MaterialRecord::ground_plane_axis)
452+
.def_readwrite("gaussian_splat_sh_degree",
453+
&MaterialRecord::gaussian_splat_sh_degree,
454+
"Max SH degree for Gaussian splat rendering (0-2).")
455+
.def_readwrite(
456+
"gaussian_splat_min_alpha",
457+
&MaterialRecord::gaussian_splat_min_alpha,
458+
"Minimum opacity threshold for Gaussian splat rendering "
459+
"(0.0-1.0). Splats with alpha below this are discarded.")
460+
.def_readwrite(
461+
"gaussian_splat_antialias",
462+
&MaterialRecord::gaussian_splat_antialias,
463+
"Enable density-compensation anti-aliasing for Gaussian "
464+
"splats. Multiplies each splat's opacity by "
465+
"sqrt(det(Sigma_orig)/det(Sigma_blurred)) to correct the "
466+
"over-brightening introduced by the subpixel blur kernel. "
467+
"Use this only if it was used in creating the Gaussian "
468+
"splats.")
469+
.def_readwrite(
470+
"gaussian_splat_max_tiles_per_splat",
471+
&MaterialRecord::gaussian_splat_max_tiles_per_splat,
472+
"Maximum number of screen tiles a single splat may cover. "
473+
"Increase for very large / close-up splats at the cost of "
474+
"higher GPU memory use. Default: 32.")
475+
.def_readwrite(
476+
"gaussian_splat_max_tile_entries_total",
477+
&MaterialRecord::gaussian_splat_max_tile_entries_total,
478+
"Total tile-coverage entry budget for the whole scene. "
479+
"Increase for dense or high-resolution scenes at the cost "
480+
"of GPU memory. Default: 33554432 (32 * 1024 * 1024).")
452481
.def_readwrite("shader", &MaterialRecord::shader);
453482

454483
// ---- TriangleMeshModel ----

cpp/tests/io/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
target_sources(tests PRIVATE
2+
FileFormatIO.cpp
23
FeatureIO.cpp
34
IJsonConvertibleIO.cpp
45
ImageIO.cpp

cpp/tests/io/FileFormatIO.cpp

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// ----------------------------------------------------------------------------
2+
// - Open3D: www.open3d.org -
3+
// ----------------------------------------------------------------------------
4+
// Copyright (c) 2018-2024 www.open3d.org
5+
// SPDX-License-Identifier: MIT
6+
// ----------------------------------------------------------------------------
7+
8+
#include "open3d/io/FileFormatIO.h"
9+
10+
#include <fstream>
11+
12+
#include "open3d/utility/FileSystem.h"
13+
#include "tests/Tests.h"
14+
15+
namespace open3d {
16+
namespace tests {
17+
namespace {
18+
19+
bool HasGeometry(io::FileGeometry geometry, io::FileGeometry flag) {
20+
return (int(geometry) & int(flag)) != 0;
21+
}
22+
23+
const char kPointPly[] = R"(ply
24+
format ascii 1.0
25+
element vertex 1
26+
property float x
27+
property float y
28+
property float z
29+
end_header
30+
0 0 0
31+
)";
32+
33+
const char kGaussianSplatPly[] = R"(ply
34+
format ascii 1.0
35+
element vertex 1
36+
property float x
37+
property float y
38+
property float z
39+
property float opacity
40+
property float scale_0
41+
property float scale_1
42+
property float scale_2
43+
property float rot_0
44+
property float rot_1
45+
property float rot_2
46+
property float rot_3
47+
property float f_dc_0
48+
property float f_dc_1
49+
property float f_dc_2
50+
end_header
51+
0 0 0 1 1 1 1 1 0 0 0 0.1 0.2 0.3
52+
)";
53+
54+
const char kMissingOpacityGaussianSplatPly[] = R"(ply
55+
format ascii 1.0
56+
element vertex 1
57+
property float x
58+
property float y
59+
property float z
60+
property float scale_0
61+
property float scale_1
62+
property float scale_2
63+
property float rot_0
64+
property float rot_1
65+
property float rot_2
66+
property float rot_3
67+
property float f_dc_0
68+
property float f_dc_1
69+
property float f_dc_2
70+
end_header
71+
0 0 0 1 1 1 1 0 0 0 0.1 0.2 0.3
72+
)";
73+
74+
} // namespace
75+
76+
TEST(FileFormatIO, ReadFileGeometryTypePLYPointCloud) {
77+
const std::string path = utility::filesystem::GetTempDirectoryPath() +
78+
"/file_type_points.ply";
79+
std::ofstream output(path, std::ios::binary);
80+
ASSERT_TRUE(output.is_open());
81+
output << kPointPly;
82+
output.close();
83+
84+
const auto geometry = io::ReadFileGeometryType(path);
85+
EXPECT_TRUE(HasGeometry(geometry, io::CONTAINS_POINTS));
86+
EXPECT_FALSE(HasGeometry(geometry, io::CONTAINS_GAUSSIAN_SPLATS));
87+
}
88+
89+
TEST(FileFormatIO, ReadFileGeometryTypePLYGaussianSplat) {
90+
const std::string path = utility::filesystem::GetTempDirectoryPath() +
91+
"/file_type_gaussian_splat.ply";
92+
std::ofstream output(path, std::ios::binary);
93+
ASSERT_TRUE(output.is_open());
94+
output << kGaussianSplatPly;
95+
output.close();
96+
97+
const auto geometry = io::ReadFileGeometryType(path);
98+
EXPECT_TRUE(HasGeometry(geometry, io::CONTAINS_POINTS));
99+
EXPECT_TRUE(HasGeometry(geometry, io::CONTAINS_GAUSSIAN_SPLATS));
100+
}
101+
102+
TEST(FileFormatIO, ReadFileGeometryTypePLYRequiresFullGaussianSplatCore) {
103+
const std::string path = utility::filesystem::GetTempDirectoryPath() +
104+
"/file_type_incomplete_gaussian_splat.ply";
105+
std::ofstream output(path, std::ios::binary);
106+
ASSERT_TRUE(output.is_open());
107+
output << kMissingOpacityGaussianSplatPly;
108+
output.close();
109+
110+
const auto geometry = io::ReadFileGeometryType(path);
111+
EXPECT_TRUE(HasGeometry(geometry, io::CONTAINS_POINTS));
112+
EXPECT_FALSE(HasGeometry(geometry, io::CONTAINS_GAUSSIAN_SPLATS));
113+
}
114+
115+
TEST(FileFormatIO, ReadFileGeometryTypeSPLAT) {
116+
const std::string path =
117+
utility::filesystem::GetTempDirectoryPath() + "/file_type.splat";
118+
std::ofstream output(path, std::ios::binary);
119+
ASSERT_TRUE(output.is_open());
120+
output.write("", 0);
121+
output.close();
122+
123+
const auto geometry = io::ReadFileGeometryType(path);
124+
EXPECT_TRUE(HasGeometry(geometry, io::CONTAINS_POINTS));
125+
EXPECT_TRUE(HasGeometry(geometry, io::CONTAINS_GAUSSIAN_SPLATS));
126+
}
127+
128+
} // namespace tests
129+
} // namespace open3d

0 commit comments

Comments
 (0)