From e7787b6aad105328852fa8c8d2485d44d96d8be2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 30 Apr 2026 14:22:08 +1000 Subject: [PATCH] ifcviewer: hemisphere ambient + fill light + cavity hint Replaces the flat 0.25 ambient + single-Lambert key with three cheap shape-readability tricks, all in the fragment shader: - Hemisphere ambient (sky/ground tint mixed by n.z) so floors, ceilings, and walls get visibly different ambient colour even when shadowed. +Z is world-up. - Secondary fill light at 35% intensity from roughly the opposite horizontal direction so backs of objects are not pitch black. - Cavity hint: clamp(length(fwidth(n)) * 1.5, 0, 0.35) darkens fragments where adjacent normals diverge sharply. Catches wall-floor seams, column-slab joints, and stair edges as faint dark lines without any post-process. Total cost: ~8 extra ALU ops per fragment, no extra passes, no extra buffers. No change to cull/HiZ/MDI. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer/ViewportWindow.cpp | 45 +++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 9a124cf9b3..c393f8c994 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -155,7 +155,10 @@ in vec3 v_world_pos; flat in uint v_object_id; flat in uint v_selected; -uniform vec3 u_light_dir; +uniform vec3 u_light_dir; // primary key direction (world-space) +uniform vec3 u_fill_dir; // secondary fill direction +uniform vec3 u_sky_color; // hemisphere top tint +uniform vec3 u_ground_color; // hemisphere bottom tint // Section planes — clip the half-space dot(n,p)+d > 0. AND-combined. const int MAX_CLIP_PLANES = 8; @@ -177,10 +180,28 @@ void main() { // true and has no effect. vec3 n = normalize(v_normal); if (!gl_FrontFacing) n = -n; - float ndotl = max(dot(n, u_light_dir), 0.0); - float ambient = 0.25; - float diffuse = 0.75 * ndotl; - vec3 color = v_color.rgb * (ambient + diffuse); + + // Hemisphere ambient: faces pointing up read sky, down read ground. + // World-up is +Z so n.z drives the mix. Floors/ceilings/walls get + // visibly distinct ambient even when shadowed. + float hemi_t = 0.5 + 0.5 * n.z; + vec3 ambient = mix(u_ground_color, u_sky_color, hemi_t); + + // Key + fill direct light. Fill is a softer secondary so backs of + // objects don't go pitch black — keeps shape readable from any angle. + float key = max(dot(n, u_light_dir), 0.0); + float fill = max(dot(n, u_fill_dir), 0.0) * 0.35; + + vec3 color = v_color.rgb * (ambient + (key + fill) * 0.7); + + // Cavity shading: where adjacent fragments have a sharp normal change + // (concave creases, edges where two faces meet), darken slightly. + // length(fwidth(n)) spikes at those boundaries; the clamp caps how + // dark a single edge can get so the effect is a hint, not a heavy + // outline. + float cavity = clamp(length(fwidth(n)) * 1.5, 0.0, 0.35); + color *= (1.0 - cavity); + if (v_selected == 1u) color = mix(color, vec3(0.2, 0.6, 1.0), 0.5); frag_color = vec4(color, v_color.a); } @@ -2402,9 +2423,21 @@ void ViewportWindow::render() { gl_->glUseProgram(main_program_); GLint u_vp = gl_->glGetUniformLocation(main_program_, "u_view_projection"); GLint u_light = gl_->glGetUniformLocation(main_program_, "u_light_dir"); + GLint u_fill = gl_->glGetUniformLocation(main_program_, "u_fill_dir"); + GLint u_sky = gl_->glGetUniformLocation(main_program_, "u_sky_color"); + GLint u_ground = gl_->glGetUniformLocation(main_program_, "u_ground_color"); GLint u_sel = gl_->glGetUniformLocation(main_program_, "u_selected_id"); gl_->glUniformMatrix4fv(u_vp, 1, GL_FALSE, vp.constData()); - gl_->glUniform3f(u_light, 0.3f, 0.5f, 0.8f); + // Key light: high noon-ish from off-camera; fill ~120° away so back-of- + // object surfaces still get some direct contribution. Sky/ground tints + // are a neutral cool-on-warm pairing — readable on white walls and grey + // slabs without colouring shaded faces noticeably. + // Both directions are roughly unit-length (~0.99) — matches how + // u_light_dir was already supplied and keeps the brightness math sane. + gl_->glUniform3f(u_light, 0.3f, 0.5f, 0.8f); + gl_->glUniform3f(u_fill, -0.3f, -0.5f, 0.8f); + gl_->glUniform3f(u_sky, 0.55f, 0.60f, 0.70f); + gl_->glUniform3f(u_ground, 0.35f, 0.32f, 0.28f); gl_->glUniform1ui(u_sel, selected_object_id_); uploadClipPlaneUniforms(main_program_);