@@ -39,6 +39,7 @@ use lambda::{
3939 } ,
4040 render_pass:: RenderPassBuilder ,
4141 scene_math:: {
42+ compute_model_matrix,
4243 compute_perspective_projection,
4344 compute_view_matrix,
4445 SimpleCamera ,
@@ -85,8 +86,10 @@ layout ( push_constant ) uniform Push {
8586
8687void main() {
8788 gl_Position = pc.mvp * vec4(vertex_position, 1.0);
88- // Rotate normals into world space using the model matrix (no scale/shear needed for this demo).
89- v_world_normal = mat3(pc.model) * vertex_normal;
89+ // Transform normals into world space using the model matrix.
90+ // Note: This demo uses only rigid transforms and a Y-mirror; `mat3(model)`
91+ // remains adequate and avoids unsupported `inverse` on some backends (MSL).
92+ v_world_normal = normalize(mat3(pc.model) * vertex_normal);
9093}
9194"# ;
9295
@@ -109,12 +112,18 @@ void main() {
109112const FRAGMENT_FLOOR_TINT_SOURCE : & str = r#"
110113#version 450
111114
115+ layout (location = 0) in vec3 v_world_normal;
112116layout (location = 0) out vec4 fragment_color;
113117
114118void main() {
115- // Slightly tint with alpha so the reflection appears through the floor.
116- // Brightened for visibility against a black clear.
117- fragment_color = vec4(0.2, 0.2, 0.23, 0.6);
119+ // Lit floor with partial transparency so the reflection shows through.
120+ vec3 N = normalize(v_world_normal);
121+ vec3 L = normalize(vec3(0.4, 0.7, 1.0));
122+ float diff = max(dot(N, L), 0.0);
123+ // Subtle base tint to suggest a surface, keep alpha low so reflection reads.
124+ vec3 base = vec3(0.10, 0.10, 0.11);
125+ vec3 color = base * (0.35 + 0.65 * diff);
126+ fragment_color = vec4(color, 0.15);
118127}
119128"# ;
120129
@@ -150,6 +159,7 @@ pub struct ReflectiveRoomExample {
150159 pass_id_color : Option < ResourceId > ,
151160 pipe_floor_mask : Option < ResourceId > ,
152161 pipe_reflected : Option < ResourceId > ,
162+ pipe_reflected_unmasked : Option < ResourceId > ,
153163 pipe_floor_visual : Option < ResourceId > ,
154164 pipe_normal : Option < ResourceId > ,
155165 width : u32 ,
@@ -160,6 +170,15 @@ pub struct ReflectiveRoomExample {
160170 stencil_enabled : bool ,
161171 depth_test_enabled : bool ,
162172 needs_rebuild : bool ,
173+ // Visual tuning
174+ floor_tilt_turns : f32 ,
175+ camera_distance : f32 ,
176+ camera_height : f32 ,
177+ camera_pitch_turns : f32 ,
178+ // When true, do not draw the floor surface; leaves a clean mirror.
179+ mirror_mode : bool ,
180+ // Debug: draw reflection even without stencil to verify visibility path.
181+ force_unmasked_reflection : bool ,
163182}
164183
165184impl Component < ComponentResult , String > for ReflectiveRoomExample {
@@ -226,6 +245,38 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
226245 self . depth_test_enabled
227246 ) ;
228247 }
248+ Some ( lambda:: events:: VirtualKey :: KeyF ) => {
249+ self . mirror_mode = !self . mirror_mode ;
250+ logging:: info!(
251+ "Toggled Mirror Mode (hide floor overlay) → {} (key: F)" ,
252+ self . mirror_mode
253+ ) ;
254+ }
255+ Some ( lambda:: events:: VirtualKey :: KeyR ) => {
256+ self . force_unmasked_reflection = !self . force_unmasked_reflection ;
257+ logging:: info!(
258+ "Toggled Force Unmasked Reflection → {} (key: R)" ,
259+ self . force_unmasked_reflection
260+ ) ;
261+ }
262+ Some ( lambda:: events:: VirtualKey :: KeyI ) => {
263+ // Pitch camera up (reduce downward angle)
264+ self . camera_pitch_turns =
265+ ( self . camera_pitch_turns - 0.01 ) . clamp ( 0.0 , 0.25 ) ;
266+ logging:: info!(
267+ "Camera pitch (turns) → {:.3}" ,
268+ self . camera_pitch_turns
269+ ) ;
270+ }
271+ Some ( lambda:: events:: VirtualKey :: KeyK ) => {
272+ // Pitch camera down (increase downward angle)
273+ self . camera_pitch_turns =
274+ ( self . camera_pitch_turns + 0.01 ) . clamp ( 0.0 , 0.25 ) ;
275+ logging:: info!(
276+ "Camera pitch (turns) → {:.3}" ,
277+ self . camera_pitch_turns
278+ ) ;
279+ }
229280 _ => { }
230281 } ,
231282 _ => { }
@@ -255,26 +306,30 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
255306 }
256307 // Camera
257308 let camera = SimpleCamera {
258- position : [ 0.0 , 1.2 , 3.5 ] ,
309+ position : [ 0.0 , self . camera_height , self . camera_distance ] ,
259310 field_of_view_in_turns : 0.24 ,
260311 near_clipping_plane : 0.1 ,
261312 far_clipping_plane : 100.0 ,
262313 } ;
263314
264315 // Cube animation
265316 let angle_y_turns = 0.12 * self . elapsed ;
266- let mut model: [ [ f32 ; 4 ] ; 4 ] = lambda:: math:: matrix:: identity_matrix ( 4 , 4 ) ;
267- model = lambda:: math:: matrix:: rotate_matrix (
268- model,
317+ // Build model with canonical order using the scene helpers:
318+ // world = T(0, +0.5, 0) * R_y(angle) * S(1)
319+ let model: [ [ f32 ; 4 ] ; 4 ] = compute_model_matrix (
320+ [ 0.0 , 0.5 , 0.0 ] ,
269321 [ 0.0 , 1.0 , 0.0 ] ,
270322 angle_y_turns,
323+ 1.0 ,
271324 ) ;
272- // Translate cube upward by 0.5 on Y
273- let t_up: [ [ f32 ; 4 ] ; 4 ] =
274- lambda:: math:: matrix:: translation_matrix ( [ 0.0 , 0.5 , 0.0 ] ) ;
275- model = model. multiply ( & t_up) ;
276325
277- let view = compute_view_matrix ( camera. position ) ;
326+ // View: pitch downward, then translate by camera position (R * T)
327+ let rot_x: [ [ f32 ; 4 ] ; 4 ] = lambda:: math:: matrix:: rotate_matrix (
328+ lambda:: math:: matrix:: identity_matrix ( 4 , 4 ) ,
329+ [ 1.0 , 0.0 , 0.0 ] ,
330+ -self . camera_pitch_turns ,
331+ ) ;
332+ let view = rot_x. multiply ( & compute_view_matrix ( camera. position ) ) ;
278333 let projection = compute_perspective_projection (
279334 camera. field_of_view_in_turns ,
280335 self . width . max ( 1 ) ,
@@ -286,29 +341,37 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
286341
287342 // Compute reflected transform only if stencil/reflection is enabled.
288343 let ( model_reflect, mvp_reflect) = if self . stencil_enabled {
289- let mut mr: [ [ f32 ; 4 ] ; 4 ] = lambda:: math:: matrix:: identity_matrix ( 4 , 4 ) ;
344+ // Reflection across the (possibly tilted) floor plane that passes
345+ // through the origin. Build the plane normal by rotating +Y by the
346+ // configured floor tilt around X.
347+ let angle = self . floor_tilt_turns * std:: f32:: consts:: PI * 2.0 ;
348+ let nx = 0.0f32 ;
349+ let ny = angle. cos ( ) ;
350+ let nz = -angle. sin ( ) ;
351+ // Reflection matrix R = I - 2*n*n^T for a plane through the origin.
352+ let ( nx2, ny2, nz2) = ( nx * nx, ny * ny, nz * nz) ;
290353 let s_mirror: [ [ f32 ; 4 ] ; 4 ] = [
291- [ 1.0 , 0.0 , 0.0 , 0.0 ] ,
292- [ 0.0 , - 1.0 , 0.0 , 0.0 ] ,
293- [ 0.0 , 0.0 , 1.0 , 0.0 ] ,
354+ [ 1.0 - 2.0 * nx2 , - 2.0 * nx * ny , - 2.0 * nx * nz , 0.0 ] ,
355+ [ - 2.0 * ny * nx , 1.0 - 2.0 * ny2 , - 2.0 * ny * nz , 0.0 ] ,
356+ [ - 2.0 * nz * nx , - 2.0 * nz * ny , 1.0 - 2.0 * nz2 , 0.0 ] ,
294357 [ 0.0 , 0.0 , 0.0 , 1.0 ] ,
295358 ] ;
296- mr = mr. multiply ( & s_mirror) ;
297- mr =
298- lambda:: math:: matrix:: rotate_matrix ( mr, [ 0.0 , 1.0 , 0.0 ] , angle_y_turns) ;
299- let t_down: [ [ f32 ; 4 ] ; 4 ] =
300- lambda:: math:: matrix:: translation_matrix ( [ 0.0 , -0.5 , 0.0 ] ) ;
301- mr = mr. multiply ( & t_down) ;
359+ let mr = s_mirror. multiply ( & model) ;
302360 let mvp_r = projection. multiply ( & view) . multiply ( & mr) ;
303361 ( mr, mvp_r)
304362 } else {
305363 // Unused in subsequent commands when stencil is disabled.
306364 ( lambda:: math:: matrix:: identity_matrix ( 4 , 4 ) , mvp)
307365 } ;
308366
309- // Floor model: at y = 0 plane
367+ // Floor model: plane through origin, tilted slightly around X for clarity
310368 let mut model_floor: [ [ f32 ; 4 ] ; 4 ] =
311369 lambda:: math:: matrix:: identity_matrix ( 4 , 4 ) ;
370+ model_floor = lambda:: math:: matrix:: rotate_matrix (
371+ model_floor,
372+ [ 1.0 , 0.0 , 0.0 ] ,
373+ self . floor_tilt_turns ,
374+ ) ;
312375 let mvp_floor = projection. multiply ( & view) . multiply ( & model_floor) ;
313376
314377 let viewport = ViewportBuilder :: new ( ) . build ( self . width , self . height ) ;
@@ -398,30 +461,54 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
398461 vertices : 0 ..cube_vertex_count,
399462 } ) ;
400463 }
464+ } else if self . force_unmasked_reflection {
465+ if let Some ( pipe_reflected_unmasked) = self . pipe_reflected_unmasked {
466+ cmds. push ( RenderCommand :: SetPipeline {
467+ pipeline : pipe_reflected_unmasked,
468+ } ) ;
469+ cmds. push ( RenderCommand :: BindVertexBuffer {
470+ pipeline : pipe_reflected_unmasked,
471+ buffer : 0 ,
472+ } ) ;
473+ cmds. push ( RenderCommand :: PushConstants {
474+ pipeline : pipe_reflected_unmasked,
475+ stage : PipelineStage :: VERTEX ,
476+ offset : 0 ,
477+ bytes : Vec :: from ( push_constants_to_words ( & PushConstant {
478+ mvp : mvp_reflect. transpose ( ) ,
479+ model : model_reflect. transpose ( ) ,
480+ } ) ) ,
481+ } ) ;
482+ cmds. push ( RenderCommand :: Draw {
483+ vertices : 0 ..cube_vertex_count,
484+ } ) ;
485+ }
401486 }
402487
403488 // Floor surface (tinted)
404- let pipe_floor_visual =
405- self . pipe_floor_visual . expect ( "floor visual pipeline" ) ;
406- cmds. push ( RenderCommand :: SetPipeline {
407- pipeline : pipe_floor_visual,
408- } ) ;
409- cmds. push ( RenderCommand :: BindVertexBuffer {
410- pipeline : pipe_floor_visual,
411- buffer : 0 ,
412- } ) ;
413- cmds. push ( RenderCommand :: PushConstants {
414- pipeline : pipe_floor_visual,
415- stage : PipelineStage :: VERTEX ,
416- offset : 0 ,
417- bytes : Vec :: from ( push_constants_to_words ( & PushConstant {
418- mvp : mvp_floor. transpose ( ) ,
419- model : model_floor. transpose ( ) ,
420- } ) ) ,
421- } ) ;
422- cmds. push ( RenderCommand :: Draw {
423- vertices : 0 ..floor_vertex_count,
424- } ) ;
489+ if !self . mirror_mode {
490+ let pipe_floor_visual =
491+ self . pipe_floor_visual . expect ( "floor visual pipeline" ) ;
492+ cmds. push ( RenderCommand :: SetPipeline {
493+ pipeline : pipe_floor_visual,
494+ } ) ;
495+ cmds. push ( RenderCommand :: BindVertexBuffer {
496+ pipeline : pipe_floor_visual,
497+ buffer : 0 ,
498+ } ) ;
499+ cmds. push ( RenderCommand :: PushConstants {
500+ pipeline : pipe_floor_visual,
501+ stage : PipelineStage :: VERTEX ,
502+ offset : 0 ,
503+ bytes : Vec :: from ( push_constants_to_words ( & PushConstant {
504+ mvp : mvp_floor. transpose ( ) ,
505+ model : model_floor. transpose ( ) ,
506+ } ) ) ,
507+ } ) ;
508+ cmds. push ( RenderCommand :: Draw {
509+ vertices : 0 ..floor_vertex_count,
510+ } ) ;
511+ }
425512
426513 // Normal cube
427514 let pipe_normal = self . pipe_normal . expect ( "normal pipeline" ) ;
@@ -482,6 +569,7 @@ impl Default for ReflectiveRoomExample {
482569 pass_id_color : None ,
483570 pipe_floor_mask : None ,
484571 pipe_reflected : None ,
572+ pipe_reflected_unmasked : None ,
485573 pipe_floor_visual : None ,
486574 pipe_normal : None ,
487575 width : 800 ,
@@ -491,6 +579,12 @@ impl Default for ReflectiveRoomExample {
491579 stencil_enabled : true ,
492580 depth_test_enabled : true ,
493581 needs_rebuild : false ,
582+ floor_tilt_turns : 0.0 , // Keep plane flat; angle comes from camera
583+ camera_distance : 4.0 ,
584+ camera_height : 3.0 ,
585+ camera_pitch_turns : 0.10 , // ~36 degrees downward
586+ mirror_mode : false ,
587+ force_unmasked_reflection : false ,
494588 } ;
495589 }
496590}
@@ -547,7 +641,8 @@ impl ReflectiveRoomExample {
547641 self . pipe_floor_mask = if self . stencil_enabled {
548642 let p = RenderPipelineBuilder :: new ( )
549643 . with_label ( "floor-mask" )
550- . with_culling ( CullingMode :: Back )
644+ // Disable culling to guarantee stencil writes regardless of winding.
645+ . with_culling ( CullingMode :: None )
551646 . with_depth_format ( DepthFormat :: Depth24PlusStencil8 )
552647 . with_depth_write ( false )
553648 . with_depth_compare ( CompareFunction :: Always )
@@ -598,7 +693,8 @@ impl ReflectiveRoomExample {
598693 self . pipe_reflected = if self . stencil_enabled {
599694 let mut builder = RenderPipelineBuilder :: new ( )
600695 . with_label ( "reflected-cube" )
601- . with_culling ( CullingMode :: None )
696+ // Mirrored transform reverses winding; cull front to keep visible faces.
697+ . with_culling ( CullingMode :: Front )
602698 . with_depth_format ( DepthFormat :: Depth24PlusStencil8 )
603699 . with_push_constant ( PipelineStage :: VERTEX , push_constants_size)
604700 . with_buffer (
@@ -629,16 +725,11 @@ impl ReflectiveRoomExample {
629725 read_mask : 0xFF ,
630726 write_mask : 0x00 ,
631727 } )
632- . with_multi_sample ( self . msaa_samples ) ;
633- if self . depth_test_enabled {
634- builder = builder
635- . with_depth_write ( true )
636- . with_depth_compare ( CompareFunction :: LessEqual ) ;
637- } else {
638- builder = builder
639- . with_depth_write ( false )
640- . with_depth_compare ( CompareFunction :: Always ) ;
641- }
728+ . with_multi_sample ( self . msaa_samples )
729+ // Render reflection regardless of depth to ensure visibility;
730+ // the floor overlay and stencil confine and visually place it.
731+ . with_depth_write ( false )
732+ . with_depth_compare ( CompareFunction :: Always ) ;
642733 let p = builder. build (
643734 render_context,
644735 & rp_color_desc,
@@ -650,6 +741,36 @@ impl ReflectiveRoomExample {
650741 None
651742 } ;
652743
744+ // Reflected cube pipeline without stencil (debug/fallback)
745+ let mut builder_unmasked = RenderPipelineBuilder :: new ( )
746+ . with_label ( "reflected-cube-unmasked" )
747+ . with_culling ( CullingMode :: Front )
748+ . with_depth_format ( DepthFormat :: Depth24PlusStencil8 )
749+ . with_push_constant ( PipelineStage :: VERTEX , push_constants_size)
750+ . with_buffer (
751+ BufferBuilder :: new ( )
752+ . with_length (
753+ cube_mesh. vertices ( ) . len ( ) * std:: mem:: size_of :: < Vertex > ( ) ,
754+ )
755+ . with_usage ( Usage :: VERTEX )
756+ . with_properties ( Properties :: DEVICE_LOCAL )
757+ . with_buffer_type ( BufferType :: Vertex )
758+ . build ( render_context, cube_mesh. vertices ( ) . to_vec ( ) )
759+ . map_err ( |e| format ! ( "Failed to create cube buffer: {}" , e) ) ?,
760+ cube_mesh. attributes ( ) . to_vec ( ) ,
761+ )
762+ . with_multi_sample ( self . msaa_samples )
763+ . with_depth_write ( false )
764+ . with_depth_compare ( CompareFunction :: Always ) ;
765+ let p_unmasked = builder_unmasked. build (
766+ render_context,
767+ & rp_color_desc,
768+ & self . shader_vs ,
769+ Some ( & self . shader_fs_lit ) ,
770+ ) ;
771+ self . pipe_reflected_unmasked =
772+ Some ( render_context. attach_pipeline ( p_unmasked) ) ;
773+
653774 // Floor visual pipeline
654775 let mut floor_builder = RenderPipelineBuilder :: new ( )
655776 . with_label ( "floor-visual" )
0 commit comments