Skip to content

Commit bf0e90a

Browse files
committed
[update] camera to allow the angle to be adjusted.
1 parent a8ed539 commit bf0e90a

File tree

1 file changed

+179
-58
lines changed

1 file changed

+179
-58
lines changed

crates/lambda-rs/examples/reflective_room.rs

Lines changed: 179 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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
8687
void 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() {
109112
const FRAGMENT_FLOOR_TINT_SOURCE: &str = r#"
110113
#version 450
111114
115+
layout (location = 0) in vec3 v_world_normal;
112116
layout (location = 0) out vec4 fragment_color;
113117
114118
void 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

165184
impl 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

Comments
 (0)