|
32 | 32 | #pragma warning(pop) |
33 | 33 | #endif // _MSC_VER |
34 | 34 |
|
| 35 | +#include <algorithm> |
| 36 | +#include <cstdint> |
| 37 | +#include <vector> |
| 38 | + |
35 | 39 | #include "open3d/utility/Logging.h" |
36 | 40 | #include "open3d/visualization/rendering/filament/FilamentEngine.h" |
37 | 41 | #include "open3d/visualization/rendering/filament/FilamentRenderer.h" |
| 42 | +#include "open3d/visualization/rendering/filament/FilamentResourceManager.h" |
38 | 43 | #include "open3d/visualization/rendering/filament/FilamentScene.h" |
39 | 44 | #include "open3d/visualization/rendering/filament/FilamentView.h" |
| 45 | +#include "open3d/visualization/rendering/gaussian_splat/GaussianSplatRenderer.h" |
40 | 46 |
|
41 | 47 | namespace open3d { |
42 | 48 | namespace visualization { |
43 | 49 | namespace rendering { |
44 | 50 |
|
| 51 | +namespace { |
| 52 | + |
| 53 | +/// Composite shader stores premultiplied RGB in \p gs_rgba; blend like ImGui |
| 54 | +/// \c One / \c OneMinusSrcAlpha over an opaque Filament base. |
| 55 | +void BlendPremultipliedSplatOverRgb8( |
| 56 | + uint8_t* base_rgb, int n_channels, const float* gs_rgba, int w, int h) { |
| 57 | + const int n = w * h; |
| 58 | + for (int i = 0; i < n; ++i) { |
| 59 | + const float fr = gs_rgba[i * 4 + 0]; |
| 60 | + const float fg = gs_rgba[i * 4 + 1]; |
| 61 | + const float fb = gs_rgba[i * 4 + 2]; |
| 62 | + const float fa = gs_rgba[i * 4 + 3]; |
| 63 | + const float br = static_cast<float>(base_rgb[i * n_channels + 0]) * |
| 64 | + (1.f / 255.f); |
| 65 | + const float bg = static_cast<float>(base_rgb[i * n_channels + 1]) * |
| 66 | + (1.f / 255.f); |
| 67 | + const float bb = static_cast<float>(base_rgb[i * n_channels + 2]) * |
| 68 | + (1.f / 255.f); |
| 69 | + const float r = fr + br * (1.f - fa); |
| 70 | + const float g = fg + bg * (1.f - fa); |
| 71 | + const float b = fb + bb * (1.f - fa); |
| 72 | + base_rgb[i * n_channels + 0] = static_cast<uint8_t>( |
| 73 | + std::min(std::max(r, 0.f), 1.f) * 255.f + 0.5f); |
| 74 | + base_rgb[i * n_channels + 1] = static_cast<uint8_t>( |
| 75 | + std::min(std::max(g, 0.f), 1.f) * 255.f + 0.5f); |
| 76 | + base_rgb[i * n_channels + 2] = static_cast<uint8_t>( |
| 77 | + std::min(std::max(b, 0.f), 1.f) * 255.f + 0.5f); |
| 78 | + if (n_channels == 4) { |
| 79 | + base_rgb[i * n_channels + 3] = 255; |
| 80 | + } |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +} // namespace |
| 85 | + |
45 | 86 | FilamentRenderToBuffer::FilamentRenderToBuffer(filament::Engine& engine) |
46 | 87 | : engine_(engine) { |
47 | 88 | renderer_ = engine_.createRenderer(); |
@@ -121,6 +162,12 @@ void FilamentRenderToBuffer::SetDimensions(const std::uint32_t width, |
121 | 162 | width_ = width; |
122 | 163 | height_ = height; |
123 | 164 |
|
| 165 | + // Allocate cached Filament color/depth attachments for Gaussian splat |
| 166 | + // zero-copy and for readPixels of the Filament base pass. |
| 167 | + if (scene_ && scene_->HasGaussianSplatGeometry()) { |
| 168 | + view_->EnableViewCaching(true); |
| 169 | + } |
| 170 | + |
124 | 171 | if (depth_image_) { |
125 | 172 | buffer_size_ = width * height * sizeof(std::float_t); |
126 | 173 | } else { |
@@ -173,31 +220,221 @@ void FilamentRenderToBuffer::ReadPixelsCallback(void*, size_t, void* user) { |
173 | 220 | delete params; |
174 | 221 | } |
175 | 222 |
|
| 223 | +// Ordering mirrors FilamentRenderer::{BeginFrame,Draw,EndFrame}. |
| 224 | +// Stage 1 (Geometry) runs before Filament's beginFrame. |
| 225 | +// Stage 2 (Composite) runs after render() on non-Apple, after endFrame() on |
| 226 | +// Apple. |
176 | 227 | void FilamentRenderToBuffer::Render() { |
177 | 228 | frame_done_ = false; |
178 | 229 | scene_->HideRefractedMaterials(); |
| 230 | + |
| 231 | + const bool has_gaussian = |
| 232 | + gaussian_splat_renderer_ && scene_->HasGaussianSplatGeometry(); |
| 233 | + const bool run_gs_pipeline = has_gaussian; |
| 234 | + |
| 235 | + if (run_gs_pipeline) { |
| 236 | + gaussian_splat_renderer_->RequestRedrawForView(*view_); |
| 237 | + if (depth_image_) { |
| 238 | + // Signal that a depth readback is needed so the composite pass |
| 239 | + // allocates and populates the merged_depth_u16_tex scratch texture. |
| 240 | + gaussian_splat_renderer_->RequestDepthReadbackForView(*view_, true); |
| 241 | + } |
| 242 | + gaussian_splat_renderer_->BeginFrame(); |
| 243 | +#if !defined(__APPLE__) |
| 244 | + // Drain Filament work before Gaussian compute dispatches (shared |
| 245 | + // GL/Vulkan queue on non-Apple backends). |
| 246 | + engine_.flushAndWait(); |
| 247 | +#endif |
| 248 | + gaussian_splat_renderer_->RenderGeometryStage(*view_, *scene_); |
| 249 | + } |
| 250 | + |
179 | 251 | if (renderer_->beginFrame(swapchain_)) { |
180 | 252 | renderer_->render(view_->GetNativeView()); |
181 | 253 |
|
| 254 | +#if !defined(__APPLE__) |
| 255 | + if (run_gs_pipeline) { |
| 256 | + engine_.flushAndWait(); |
| 257 | + gaussian_splat_renderer_->RenderCompositeStage(*view_); |
| 258 | + } |
| 259 | +#endif |
| 260 | + |
182 | 261 | using namespace filament; |
183 | 262 | using namespace backend; |
184 | 263 |
|
185 | | - auto format = (n_channels_ == 3 ? PixelDataFormat::RGB |
186 | | - : PixelDataFormat::RGBA); |
187 | | - auto type = PixelDataType::UBYTE; |
188 | | - if (depth_image_) { |
189 | | - format = PixelDataFormat::DEPTH_COMPONENT; |
190 | | - type = PixelDataType::FLOAT; |
191 | | - } |
192 | | - auto user_param = new PBDParams(this, callback_); |
193 | | - PixelBufferDescriptor pd(buffer_, buffer_size_, format, type, |
194 | | - ReadPixelsCallback, user_param); |
195 | 264 | auto vp = view_->GetNativeView()->getViewport(); |
196 | 265 |
|
197 | | - renderer_->readPixels(vp.left, vp.bottom, vp.width, vp.height, |
198 | | - std::move(pd)); |
199 | | - |
200 | 266 | renderer_->endFrame(); |
| 267 | + |
| 268 | +#if defined(__APPLE__) |
| 269 | + if (run_gs_pipeline) { |
| 270 | + gaussian_splat_renderer_->RenderCompositeStage(*view_); |
| 271 | + } |
| 272 | +#endif |
| 273 | + |
| 274 | + engine_.flushAndWait(); |
| 275 | + |
| 276 | + auto* resource_mgr = &EngineInstance::GetResourceManager(); |
| 277 | + |
| 278 | + RenderTargetHandle view_rt_h = view_->GetRenderTargetHandle(); |
| 279 | + filament::RenderTarget* native_view_rt = nullptr; |
| 280 | + if (view_rt_h) { |
| 281 | + auto weak_vrt = resource_mgr->GetRenderTarget(view_rt_h); |
| 282 | + if (auto vrt = weak_vrt.lock()) { |
| 283 | + native_view_rt = vrt.get(); |
| 284 | + } |
| 285 | + } |
| 286 | + |
| 287 | + RenderTargetHandle gs_rt = |
| 288 | + run_gs_pipeline |
| 289 | + ? gaussian_splat_renderer_->GetColorReadbackRT(*view_) |
| 290 | + : RenderTargetHandle(); |
| 291 | + filament::RenderTarget* native_gs_rt = nullptr; |
| 292 | + if (gs_rt) { |
| 293 | + auto weak_rt = resource_mgr->GetRenderTarget(gs_rt); |
| 294 | + if (auto rt_sptr = weak_rt.lock()) { |
| 295 | + native_gs_rt = rt_sptr.get(); |
| 296 | + } |
| 297 | + } |
| 298 | + |
| 299 | + if (!depth_image_ && run_gs_pipeline && native_view_rt) { |
| 300 | + // Issue both readPixels (base + GS overlay) together, then do one |
| 301 | + // more flushAndWait to collect both callbacks synchronously. |
| 302 | + // |
| 303 | + // Metal readPixels from a render target only supports RGBA+UBYTE, |
| 304 | + // not RGB+UBYTE (Metal has no native RGB texture format). Always |
| 305 | + // read RGBA8 for the base and strip alpha when n_channels_==3. |
| 306 | + // On GL, RGBA also works fine — use one path for both backends. |
| 307 | + |
| 308 | + const size_t n_pixels = static_cast<size_t>(width_) * height_; |
| 309 | + |
| 310 | + // Scratch buffers for the two parallel readPixels callbacks. |
| 311 | + std::vector<uint8_t> base_rgba(n_pixels * 4); |
| 312 | + std::vector<float> gs_f32; |
| 313 | + |
| 314 | + PixelBufferDescriptor base_pd( |
| 315 | + base_rgba.data(), base_rgba.size(), PixelDataFormat::RGBA, |
| 316 | + PixelDataType::UBYTE, [](void*, size_t, void*) {}, nullptr); |
| 317 | + renderer_->readPixels(native_view_rt, vp.left, vp.bottom, vp.width, |
| 318 | + vp.height, std::move(base_pd)); |
| 319 | + |
| 320 | + if (native_gs_rt) { |
| 321 | + gs_f32.resize(n_pixels * 4); |
| 322 | + PixelBufferDescriptor gs_pd( |
| 323 | + gs_f32.data(), gs_f32.size() * sizeof(float), |
| 324 | + PixelDataFormat::RGBA, PixelDataType::FLOAT, |
| 325 | + [](void*, size_t, void*) {}, nullptr); |
| 326 | + renderer_->readPixels(native_gs_rt, vp.left, vp.bottom, |
| 327 | + vp.width, vp.height, std::move(gs_pd)); |
| 328 | + } |
| 329 | + |
| 330 | + // One more flush ensures both callbacks complete before we proceed. |
| 331 | + engine_.flushAndWait(); |
| 332 | + |
| 333 | + // Unpack RGBA8 base → output buffer (strip alpha for RGB). |
| 334 | + const uint8_t* src = base_rgba.data(); |
| 335 | + uint8_t* dst = buffer_; |
| 336 | + const int nc = static_cast<int>(n_channels_); |
| 337 | + const int np = static_cast<int>(n_pixels); |
| 338 | + for (int i = 0; i < np; ++i) { |
| 339 | + dst[i * nc + 0] = src[i * 4 + 0]; |
| 340 | + dst[i * nc + 1] = src[i * 4 + 1]; |
| 341 | + dst[i * nc + 2] = src[i * 4 + 2]; |
| 342 | + if (nc == 4) dst[i * nc + 3] = src[i * 4 + 3]; |
| 343 | + } |
| 344 | + if (native_gs_rt && !gs_f32.empty()) { |
| 345 | + BlendPremultipliedSplatOverRgb8(buffer_, nc, gs_f32.data(), |
| 346 | + int(width_), int(height_)); |
| 347 | + } |
| 348 | + |
| 349 | + // Deliver result now; the BeginFrame flushAndWait is a no-op since |
| 350 | + // all GPU work has already been collected above. |
| 351 | + if (callback_) { |
| 352 | + callback_({static_cast<std::size_t>(width_), |
| 353 | + static_cast<std::size_t>(height_), |
| 354 | + static_cast<std::size_t>(n_channels_), buffer_, |
| 355 | + buffer_size_}); |
| 356 | + callback_ = nullptr; |
| 357 | + } |
| 358 | + frame_done_ = true; |
| 359 | + } else if (depth_image_ && run_gs_pipeline && |
| 360 | + gaussian_splat_renderer_) { |
| 361 | + // GPU-merged depth path: the composite pass has already merged |
| 362 | + // GS and Filament depth into a normalised R16UI texture. |
| 363 | + // Read it back directly — no CPU merge required. |
| 364 | + std::vector<std::uint16_t> merged_u16; |
| 365 | + const bool got_merged = |
| 366 | + gaussian_splat_renderer_->ReadMergedDepthToUint16Cpu( |
| 367 | + *view_, merged_u16, |
| 368 | + static_cast<std::uint32_t>(width_), |
| 369 | + static_cast<std::uint32_t>(height_)) && |
| 370 | + merged_u16.size() == width_ * height_; |
| 371 | + if (got_merged) { |
| 372 | + // Convert normalised uint16 [0,65535] -> Filament inverse |
| 373 | + // depth [0,1]. Renderer::RenderToDepthImage applies the final |
| 374 | + // user-facing conversion for z_in_view_space/normalized modes. |
| 375 | + float* dst = reinterpret_cast<float*>(buffer_); |
| 376 | + for (size_t i = 0; i < merged_u16.size(); ++i) { |
| 377 | + dst[i] = merged_u16[i] / 65535.f; |
| 378 | + } |
| 379 | + if (callback_) { |
| 380 | + callback_({static_cast<std::size_t>(width_), |
| 381 | + static_cast<std::size_t>(height_), 1u, buffer_, |
| 382 | + buffer_size_}); |
| 383 | + callback_ = nullptr; |
| 384 | + } |
| 385 | + frame_done_ = true; |
| 386 | + } else { |
| 387 | + // Try GS-only composite depth (R32F) when no scene depth |
| 388 | + // was available for merging. |
| 389 | + std::vector<float> gs_depth; |
| 390 | + const bool got_gs_depth = |
| 391 | + gaussian_splat_renderer_->ReadCompositeDepthToFloatCpu( |
| 392 | + *view_, gs_depth, |
| 393 | + static_cast<std::uint32_t>(width_), |
| 394 | + static_cast<std::uint32_t>(height_)) && |
| 395 | + gs_depth.size() == width_ * height_; |
| 396 | + if (got_gs_depth) { |
| 397 | + float* dst = reinterpret_cast<float*>(buffer_); |
| 398 | + std::copy(gs_depth.begin(), gs_depth.end(), dst); |
| 399 | + if (callback_) { |
| 400 | + callback_({static_cast<std::size_t>(width_), |
| 401 | + static_cast<std::size_t>(height_), 1u, |
| 402 | + buffer_, buffer_size_}); |
| 403 | + callback_ = nullptr; |
| 404 | + } |
| 405 | + frame_done_ = true; |
| 406 | + } else { |
| 407 | + // Final fallback: Filament depth only via readPixels |
| 408 | + // (backend unsupported or no GS depth available). |
| 409 | + auto* user_param = new PBDParams(this, callback_); |
| 410 | + PixelBufferDescriptor pd(buffer_, buffer_size_, |
| 411 | + PixelDataFormat::DEPTH_COMPONENT, |
| 412 | + PixelDataType::FLOAT, |
| 413 | + ReadPixelsCallback, user_param); |
| 414 | + renderer_->readPixels(vp.left, vp.bottom, vp.width, |
| 415 | + vp.height, std::move(pd)); |
| 416 | + } |
| 417 | + } |
| 418 | + } else { |
| 419 | + if (!depth_image_ && run_gs_pipeline && !native_view_rt) { |
| 420 | + utility::LogWarning( |
| 421 | + "Gaussian splat offscreen: FilamentView has no render " |
| 422 | + "target; expected EnableViewCaching. Reading the " |
| 423 | + "swapchain — splat composite may be missing."); |
| 424 | + } |
| 425 | + auto format = (n_channels_ == 3 ? PixelDataFormat::RGB |
| 426 | + : PixelDataFormat::RGBA); |
| 427 | + auto type = PixelDataType::UBYTE; |
| 428 | + if (depth_image_) { |
| 429 | + format = PixelDataFormat::DEPTH_COMPONENT; |
| 430 | + type = PixelDataType::FLOAT; |
| 431 | + } |
| 432 | + auto* user_param = new PBDParams(this, callback_); |
| 433 | + PixelBufferDescriptor pd(buffer_, buffer_size_, format, type, |
| 434 | + ReadPixelsCallback, user_param); |
| 435 | + renderer_->readPixels(vp.left, vp.bottom, vp.width, vp.height, |
| 436 | + std::move(pd)); |
| 437 | + } |
201 | 438 | } |
202 | 439 | scene_->HideRefractedMaterials(false); |
203 | 440 |
|
|
0 commit comments