diff --git a/src/bonsaiviewer/MainWindow.cpp b/src/bonsaiviewer/MainWindow.cpp index 452274b827..e7d00480b9 100644 --- a/src/bonsaiviewer/MainWindow.cpp +++ b/src/bonsaiviewer/MainWindow.cpp @@ -510,6 +510,15 @@ void MainWindow::setupStatus() { vp->applyNavPreset(AppSettings::navPresetName(preset)); }); + // Backface culling: apply the persisted choice and re-apply live on change. + if (auto* vp = viewport_widget_->viewport()) + vp->setBackfaceCulling(AppSettings::instance().backfaceCulling()); + connect(&AppSettings::instance(), &AppSettings::backfaceCullingChanged, this, + [this](bool enabled) { + if (auto* vp = viewport_widget_->viewport()) + vp->setBackfaceCulling(enabled); + }); + connect(session_state_, &bonsaiviewer::SessionState::statusMessageChanged, this, [this](const QString& mode, const QString& detail) { status_mode_label_->setText(mode); diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index 778a724cc0..1010753feb 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -479,6 +479,12 @@ void ViewportCore::setNavPreset(const char* name) { nav_bindings_ = { B::Middle, M::Plain, B::Middle, M::Shift, B::Left, M::Plain }; } +void ViewportCore::setBackfaceCulling(bool enabled) { + if (backface_culling_ == enabled) return; + backface_culling_ = enabled; + host_->requestFrame(); +} + bool ViewportCore::frameSelection() { if (selection_.count() == 0) return false; float lo[3] = { std::numeric_limits::infinity(), @@ -1150,6 +1156,19 @@ bool ViewportCore::buildPipelines() { return false; } + // ---- Backface-culling-off variant of the opaque pipeline ----------- + // The "Backface Culling" setting picks between this and main_pipeline_ at + // draw time (opaque pass). Identical but cullMode None, so single-sided + // IFC meshes show their back faces. + WGPURenderPipelineDescriptor rp_desc_nc = rp_desc; + rp_desc_nc.label = svFromCStr("ifcviewer-wgpu.main_pipeline_no_cull"); + rp_desc_nc.primitive.cullMode = WGPUCullMode_None; + main_pipeline_no_cull_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc_nc); + if (!main_pipeline_no_cull_) { + Log::warn() << "wgpu main no-cull render pipeline creation failed"; + return false; + } + // ---- Transparent variant of the main pipeline ---------------------- // Same shader, same layout, same vertex pulling, same depth test — // differs only in: @@ -1796,6 +1815,7 @@ void ViewportCore::shutdown() { if (selection_flags_buffer_) { wgpuBufferRelease(selection_flags_buffer_); selection_flags_buffer_ = nullptr; } selection_flags_capacity_ = 0; if (main_pipeline_) { wgpuRenderPipelineRelease(main_pipeline_); main_pipeline_ = nullptr; } + if (main_pipeline_no_cull_) { wgpuRenderPipelineRelease(main_pipeline_no_cull_); main_pipeline_no_cull_ = nullptr; } if (main_pipeline_transparent_) { wgpuRenderPipelineRelease(main_pipeline_transparent_); main_pipeline_transparent_ = nullptr; } section_gizmo_.destroy(); if (main_shader_module_) { wgpuShaderModuleRelease(main_shader_module_); main_shader_module_ = nullptr; } @@ -6400,9 +6420,10 @@ void ViewportCore::render() { WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc); // Two-pass main render: opaque first, then transparent. - if (main_pipeline_ && main_pipeline_transparent_ + if (main_pipeline_ && main_pipeline_no_cull_ && main_pipeline_transparent_ && frame_bind_group_ && !models_gpu_.empty()) { - wgpuRenderPassEncoderSetPipeline(pass, main_pipeline_); + wgpuRenderPassEncoderSetPipeline(pass, + backface_culling_ ? main_pipeline_ : main_pipeline_no_cull_); wgpuRenderPassEncoderSetBindGroup(pass, 0, frame_bind_group_, 0, nullptr); for (const auto& [session_model_id, m] : models_gpu_) { diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index e03d60d3fe..797793214c 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -223,6 +223,12 @@ public: void setNavPreset(const char* name); const NavBindings& navBindings() const { return nav_bindings_; } + // Toggle backface culling of opaque geometry. Off draws back faces too + // (useful for single-sided IFC meshes). Switches the opaque pipeline at + // draw time — no rebuild. + void setBackfaceCulling(bool enabled); + bool backfaceCulling() const { return backface_culling_; } + // Frame the current selection: union the selected objects' world AABBs and // fit the camera to them (same 1.30 padding as the desktop "F" hotkey). // No-op with an empty selection or no resolvable AABBs; returns whether it @@ -909,7 +915,9 @@ private: WGPUBindGroupLayout model_bgl_ = nullptr; // group 1 WGPUPipelineLayout pipeline_layout_ = nullptr; WGPURenderPipeline main_pipeline_ = nullptr; + WGPURenderPipeline main_pipeline_no_cull_ = nullptr; // backface culling off WGPURenderPipeline main_pipeline_transparent_ = nullptr; + bool backface_culling_ = true; // Section-plane gizmo, shared by desktop + web (both render via render()). // Lifted out of the Qt-coupled OverlayRenderer so one identical gizmo draws // everywhere; the desktop's OverlayRenderer no longer draws it. diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 7c3fc394c9..c1acab8bbc 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -1499,6 +1499,10 @@ void ViewportWindow::applyNavPreset(const char* name) { select_button_ = toQtBtn(b.select); select_mods_ = toQtMod(b.select_mod); } +void ViewportWindow::setBackfaceCulling(bool enabled) { + core_.setBackfaceCulling(enabled); +} + // ----------------------------------------------------------------------------- // One-shot framebuffer capture → PNG // ----------------------------------------------------------------------------- diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 82a77cc892..746811a2cf 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -247,6 +247,7 @@ public: // Sources the shared binding table from ViewportCore; called from init // (env / persisted setting) and live from the Settings dialog. void applyNavPreset(const char* name); + void setBackfaceCulling(bool enabled); // Queue a one-shot framebuffer capture: the next rendered frame is