Skip to content

Commit deb8aff

Browse files
committed
[remove] unmasked reflection toggle and update tutorials.
1 parent bf0e90a commit deb8aff

File tree

2 files changed

+52
-93
lines changed

2 files changed

+52
-93
lines changed

crates/lambda-rs/examples/reflective_room.rs

Lines changed: 2 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ pub struct ReflectiveRoomExample {
159159
pass_id_color: Option<ResourceId>,
160160
pipe_floor_mask: Option<ResourceId>,
161161
pipe_reflected: Option<ResourceId>,
162-
pipe_reflected_unmasked: Option<ResourceId>,
163162
pipe_floor_visual: Option<ResourceId>,
164163
pipe_normal: Option<ResourceId>,
165164
width: u32,
@@ -177,8 +176,6 @@ pub struct ReflectiveRoomExample {
177176
camera_pitch_turns: f32,
178177
// When true, do not draw the floor surface; leaves a clean mirror.
179178
mirror_mode: bool,
180-
// Debug: draw reflection even without stencil to verify visibility path.
181-
force_unmasked_reflection: bool,
182179
}
183180

184181
impl Component<ComponentResult, String> for ReflectiveRoomExample {
@@ -252,13 +249,7 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
252249
self.mirror_mode
253250
);
254251
}
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-
}
252+
// 'R' previously forced an unmasked reflection; now disabled.
262253
Some(lambda::events::VirtualKey::KeyI) => {
263254
// Pitch camera up (reduce downward angle)
264255
self.camera_pitch_turns =
@@ -461,28 +452,6 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
461452
vertices: 0..cube_vertex_count,
462453
});
463454
}
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-
}
486455
}
487456

488457
// Floor surface (tinted)
@@ -569,7 +538,6 @@ impl Default for ReflectiveRoomExample {
569538
pass_id_color: None,
570539
pipe_floor_mask: None,
571540
pipe_reflected: None,
572-
pipe_reflected_unmasked: None,
573541
pipe_floor_visual: None,
574542
pipe_normal: None,
575543
width: 800,
@@ -584,7 +552,6 @@ impl Default for ReflectiveRoomExample {
584552
camera_height: 3.0,
585553
camera_pitch_turns: 0.10, // ~36 degrees downward
586554
mirror_mode: false,
587-
force_unmasked_reflection: false,
588555
};
589556
}
590557
}
@@ -741,35 +708,7 @@ impl ReflectiveRoomExample {
741708
None
742709
};
743710

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));
711+
// No unmasked reflection pipeline in production example.
773712

774713
// Floor visual pipeline
775714
let mut floor_builder = RenderPipelineBuilder::new()

docs/tutorials/reflective-room.md

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
---
2-
title: "Reflective Room: Stencil Masked Reflections with MSAA"
2+
title: "Reflective Floor: StencilMasked Planar Reflections"
33
document_id: "reflective-room-tutorial-2025-11-17"
44
status: "draft"
55
created: "2025-11-17T00:00:00Z"
6-
last_updated: "2025-11-17T00:00:00Z"
7-
version: "0.1.0"
6+
last_updated: "2025-11-19T00:00:01Z"
7+
version: "0.2.1"
88
engine_workspace_version: "2023.1.30"
99
wgpu_version: "26.0.1"
1010
shader_backend_default: "naga"
1111
winit_version: "0.29.10"
12-
repo_commit: "ceaf345777d871912b2f92ae629a34b8e6f8654a"
12+
repo_commit: "bf0e90ae9ce653e1da2e1e594b22038094bada07"
1313
owners: ["lambda-sh"]
1414
reviewers: ["engine", "rendering"]
1515
tags: ["tutorial", "graphics", "stencil", "depth", "msaa", "mirror", "3d", "push-constants", "wgpu", "rust"]
1616
---
1717

1818
## Overview <a name="overview"></a>
19-
This tutorial builds a reflective floor using the stencil buffer with an optional depth test and 4× multi‑sample anti‑aliasing (MSAA). The scene renders in four phases: a floor mask into stencil, a mirrored cube clipped by the mask, a tinted floor surface, and a normal cube above the plane.
19+
This tutorial builds a reflective floor using the stencil buffer with an optional depth test and 4× multi‑sample anti‑aliasing (MSAA). The scene renders in four phases: a floor mask into stencil, a mirrored cube clipped by the mask, a translucent lit floor surface, and a normal cube above the plane. The camera looks down at a moderate angle so the reflection is clearly visible.
2020

2121
Reference implementation: `crates/lambda-rs/examples/reflective_room.rs`.
2222

@@ -50,7 +50,7 @@ Reference implementation: `crates/lambda-rs/examples/reflective_room.rs`.
5050
- Use the stencil buffer to restrict rendering to the floor area and show a mirrored reflection of a cube.
5151
- Support depth testing and 4× MSAA to improve geometric correctness and edge quality.
5252
- Drive transforms via push constants for model‑view‑projection (MVP) and model matrices.
53-
- Provide runtime toggles for MSAA, stencil, and depth testing.
53+
- Provide runtime toggles for MSAA, stencil, and depth testing, plus camera pitch and visibility helpers.
5454

5555
## Prerequisites <a name="prerequisites"></a>
5656
- Build the workspace: `cargo build --workspace`.
@@ -60,7 +60,7 @@ Reference implementation: `crates/lambda-rs/examples/reflective_room.rs`.
6060
- A pipeline that uses stencil state MUST render into a pass with a depth‑stencil attachment. Use `DepthFormat::Depth24PlusStencil8`.
6161
- The mask pass MUST disable depth writes and write stencil with `Replace` so the floor area becomes `1`.
6262
- The reflected cube pipeline MUST test stencil `Equal` against reference `1` and SHOULD set stencil write mask to `0x00`.
63-
- Mirroring across the floor plane flips face winding. Culling MUST be disabled for the reflected draw or the front‑face definition MUST be adjusted. This example disables culling.
63+
- Mirroring across the floor plane flips face winding. Culling MUST be disabled for the reflected draw or the front‑face definition MUST be adjusted. This example culls front faces for the reflected cube.
6464
- Push constant size and stage visibility MUST match the shader declaration. Two `mat4` values are sent to the vertex stage only (128 bytes total).
6565
- Matrix order MUST match the shader’s expectation. The example transposes matrices before upload to match GLSL column‑major multiplication.
6666
- The render pass and pipelines MUST use the same sample count when MSAA is enabled.
@@ -78,7 +78,7 @@ Pass 1: Depth/Stencil‑only (no color) — write stencil where floor covers
7878
│ stencil = 1 inside floor, 0 elsewhere; depth write off
7979
8080
Pass 2: Color (with depth/stencil) — draw reflected cube with stencil test == 1
81-
│ culling off; depth compare configurable
81+
│ culling front faces; depth compare configurable
8282
8383
Pass 3: Color — draw tinted floor (alpha) to show reflection
8484
@@ -124,7 +124,7 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample { /* lifecycle
124124
Narrative: The component stores GPU handles and toggles. When settings change, mark `needs_rebuild = true` and rebuild pipelines/passes on the next frame.
125125

126126
### Step 2 — Shaders and Push Constants <a name="step-2"></a>
127-
Use one vertex shader and two fragment shaders. The vertex shader expects push constants with two `mat4` values: the MVP and the model matrix, used to transform positions and rotate normals to world space.
127+
Use one vertex shader and two fragment shaders. The vertex shader expects push constants with two `mat4` values: the MVP and the model matrix, used to transform positions and rotate normals to world space. The floor fragment shader is lit and translucent so the reflection reads beneath it.
128128

129129
```glsl
130130
// Vertex (GLSL 450)
@@ -136,6 +136,7 @@ layout ( push_constant ) uniform Push { mat4 mvp; mat4 model; } pc;
136136
137137
void main() {
138138
gl_Position = pc.mvp * vec4(vertex_position, 1.0);
139+
// Transform normals by the model matrix; sufficient for rigid + mirror.
139140
v_world_normal = mat3(pc.model) * vertex_normal;
140141
}
141142
```
@@ -154,9 +155,17 @@ void main() {
154155
```
155156

156157
```glsl
157-
// Fragment (floor tint)
158+
// Fragment (floor: lit + translucent)
159+
layout (location = 0) in vec3 v_world_normal;
158160
layout (location = 0) out vec4 fragment_color;
159-
void main() { fragment_color = vec4(0.1, 0.1, 0.12, 0.5); }
161+
void main() {
162+
vec3 N = normalize(v_world_normal);
163+
vec3 L = normalize(vec3(0.4, 0.7, 1.0));
164+
float diff = max(dot(N, L), 0.0);
165+
vec3 base = vec3(0.10, 0.10, 0.11);
166+
vec3 color = base * (0.35 + 0.65 * diff);
167+
fragment_color = vec4(color, 0.15);
168+
}
160169
```
161170

162171
In Rust, pack push constants as 32‑bit words and transpose matrices before upload.
@@ -226,12 +235,12 @@ let pipe_floor_mask = RenderPipelineBuilder::new()
226235
```
227236

228237
### Step 6 — Pipeline: Reflected Cube (Stencil Test) <a name="step-6"></a>
229-
Render the mirrored cube only where the floor mask is present. Disable culling to avoid flipped winding after mirroring. Preserve depth testing configuration via toggles.
238+
Render the mirrored cube only where the floor mask is present. Mirroring flips the winding, so cull front faces for the reflected draw. Use `depth_compare = Always` and disable depth writes so the reflection remains visible; the stencil confines it to the floor.
230239

231240
```rust
232241
let mut builder = RenderPipelineBuilder::new()
233242
.with_label("reflected-cube")
234-
.with_culling(lambda::render::pipeline::CullingMode::None)
243+
.with_culling(lambda::render::pipeline::CullingMode::Front)
235244
.with_depth_format(lambda::render::texture::DepthFormat::Depth24PlusStencil8)
236245
.with_push_constant(PipelineStage::VERTEX, std::mem::size_of::<PushConstant>() as u32)
237246
.with_buffer(cube_vertex_buffer, cube_attributes)
@@ -240,13 +249,9 @@ let mut builder = RenderPipelineBuilder::new()
240249
back: StencilFaceState { compare: CompareFunction::Equal, fail_op: StencilOperation::Keep, depth_fail_op: StencilOperation::Keep, pass_op: StencilOperation::Keep },
241250
read_mask: 0xFF, write_mask: 0x00,
242251
})
243-
.with_multi_sample(msaa_samples);
244-
245-
builder = if depth_test_enabled {
246-
builder.with_depth_write(true).with_depth_compare(CompareFunction::LessEqual)
247-
} else {
248-
builder.with_depth_write(false).with_depth_compare(CompareFunction::Always)
249-
};
252+
.with_multi_sample(msaa_samples)
253+
.with_depth_write(false)
254+
.with_depth_compare(CompareFunction::Always);
250255

251256
let pipe_reflected = builder.build(ctx, &pass_color, &shader_vs, Some(&shader_fs_lit));
252257
```
@@ -292,13 +297,16 @@ let pipe_normal = normal.build(ctx, &pass_color, &shader_vs, Some(&shader_fs_lit
292297
```
293298

294299
### Step 9 — Per‑Frame Transforms and Reflection <a name="step-9"></a>
295-
Compute camera, model rotation, and the mirror transform across the floor plane. The reflection scales Y by −1 and translates downward to align with the mirrored cube below the floor.
300+
Compute camera, model rotation, and the mirror transform across the floor plane. The camera pitches downward and translates to a higher vantage point. Build the mirror using the plane‑reflection matrix `R = I − 2 n n^T` for a plane through the origin with unit normal `n` (for a flat floor, `n = (0,1,0)`).
296301

297302
```rust
298303
use lambda::render::scene_math::{compute_perspective_projection, compute_view_matrix, SimpleCamera};
299304

300-
let camera = SimpleCamera { position: [0.0, 1.2, 3.5], field_of_view_in_turns: 0.24, near_clipping_plane: 0.1, far_clipping_plane: 100.0 };
301-
let view = compute_view_matrix(camera.position);
305+
let camera = SimpleCamera { position: [0.0, 3.0, 4.0], field_of_view_in_turns: 0.24, near_clipping_plane: 0.1, far_clipping_plane: 100.0 };
306+
// View = R_x(-pitch) * T(-position)
307+
let pitch_turns = 0.10; // ~36 degrees downward
308+
let rot_x = lambda::math::matrix::rotate_matrix(lambda::math::matrix::identity_matrix(4,4), [1.0,0.0,0.0], -pitch_turns);
309+
let view = rot_x.multiply(&compute_view_matrix(camera.position));
302310
let projection = compute_perspective_projection(camera.field_of_view_in_turns, width.max(1), height.max(1), camera.near_clipping_plane, camera.far_clipping_plane);
303311

304312
let angle_y = 0.12 * elapsed;
@@ -307,10 +315,15 @@ model = lambda::math::matrix::rotate_matrix(model, [0.0, 1.0, 0.0], angle_y);
307315
model = model.multiply(&lambda::math::matrix::translation_matrix([0.0, 0.5, 0.0]));
308316
let mvp = projection.multiply(&view).multiply(&model);
309317

310-
let mirror = [ [1.0, 0.0, 0.0, 0.0], [0.0, -1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0] ];
311-
let mut model_reflect = lambda::math::matrix::identity_matrix(4, 4).multiply(&mirror);
312-
model_reflect = lambda::math::matrix::rotate_matrix(model_reflect, [0.0, 1.0, 0.0], angle_y);
313-
model_reflect = model_reflect.multiply(&lambda::math::matrix::translation_matrix([0.0, -0.5, 0.0]));
318+
let n = [0.0f32, 1.0, 0.0];
319+
let (nx, ny, nz) = (n[0], n[1], n[2]);
320+
let mirror = [
321+
[1.0 - 2.0*nx*nx, -2.0*nx*ny, -2.0*nx*nz, 0.0],
322+
[-2.0*ny*nx, 1.0 - 2.0*ny*ny, -2.0*ny*nz, 0.0],
323+
[-2.0*nz*nx, -2.0*nz*ny, 1.0 - 2.0*nz*nz, 0.0],
324+
[0.0, 0.0, 0.0, 1.0],
325+
];
326+
let model_reflect = mirror.multiply(&model);
314327
let mvp_reflect = projection.multiply(&view).multiply(&model_reflect);
315328
```
316329

@@ -358,8 +371,10 @@ cmds.push(RenderCommand::EndRenderPass);
358371
Support runtime toggles to observe the impact of each setting:
359372

360373
- `M` toggles MSAA between `` and ``. Rebuild passes and pipelines when it changes.
361-
- `S` toggles the stencil reflection. When disabled, skip the mask and reflected draw.
374+
- `S` toggles the stencil reflection. When disabled, the example skips the mask and reflected draw.
362375
- `D` toggles depth testing. When disabled, set depth compare to `Always` and disable depth writes on pipelines.
376+
- `F` toggles the floor overlay (mirror mode). When enabled, the reflection shows without the translucent floor surface.
377+
- `I` and `K` adjust the camera pitch up/down in small steps.
363378
- On window resize, update stored `width` and `height` and use them when computing the viewport and projection matrix.
364379

365380
Reference: `crates/lambda-rs/examples/reflective_room.rs:164`.
@@ -370,18 +385,22 @@ Reference: `crates/lambda-rs/examples/reflective_room.rs:164`.
370385
- Expected behavior:
371386
- A cube rotates above a reflective floor. The reflection appears only inside the floor area and shows correct mirroring.
372387
- Press `S` to toggle the reflection (stencil). The reflected cube disappears when stencil is off.
373-
- Press `D` to toggle depth testing. With depth off, the reflection still clips to the floor via stencil, but depth ordering may show artifacts if geometry overlaps.
374-
- Press `M` to toggle MSAA. With `` MSAA, edges appear smoother; with ``, edges appear more aliased.
388+
- Press `F` to hide/show the floor overlay to see a clean mirror.
389+
- Press `I`/`K` to adjust camera pitch; ensure the reflection remains visible at moderate angles.
390+
- Press `D` to toggle depth testing. With depth off, the reflection still clips to the floor via stencil.
391+
- Press `M` to toggle MSAA. With `` MSAA, edges appear smoother.
375392

376393
## Notes <a name="notes"></a>
377394

378395
- Pipelines that use stencil MUST target a pass with a depth‑stencil attachment; otherwise, pipeline creation or draws will fail.
379396
- Mirroring across a plane flips winding. Either disable culling or adjust front‑face winding for the reflected draw; do not leave back‑face culling enabled with mirrored geometry.
397+
- This implementation culls front faces for the reflected pipeline to account for mirrored winding; the normal cube uses back‑face culling.
380398
- The mask pass SHOULD clear stencil to `0` and write `1` where the floor renders. Use `Replace` and a write mask of `0xFF`.
381399
- The reflected draw SHOULD use `read_mask = 0xFF`, `write_mask = 0x00`, and `reference = 1` to preserve the mask.
382400
- When depth testing is disabled, set `depth_compare = Always` and `depth_write = false` to avoid unintended depth interactions.
383401
- The pass and all pipelines in the pass MUST use the same MSAA sample count.
384402
- Transpose matrices before uploading when GLSL expects column‑major multiplication.
403+
- Metal (MSL) portability: avoid calling `inverse()` in shaders for normal transforms; compute the normal matrix on the CPU if needed. The example uses `mat3(model)` for rigid + mirror transforms.
385404

386405
## Conclusion <a name="conclusion"></a>
387406

@@ -403,4 +422,5 @@ The reflective floor combines a simple stencil mask with an optional depth test
403422

404423
## Changelog <a name="changelog"></a>
405424

425+
- 0.2.0 (2025‑11‑19): Updated for camera pitch, front‑face culling on reflection, lit translucent floor, unmasked reflection debug toggle, floor overlay toggle, and Metal portability note.
406426
- 0.1.0 (2025‑11‑17): Initial draft aligned with `crates/lambda-rs/examples/reflective_room.rs`, including stencil mask pass, reflected pipeline, and MSAA/depth toggles.

0 commit comments

Comments
 (0)