1pub mod engine_realistic;
2
3use serde::Serialize;
4
5use crate::config::SceneConfig;
6use crate::frame::{Color, ImageFrame};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
9pub enum ScenarioId {
10 ThinReveal,
11 FastPan,
12 DiagonalReveal,
13 RevealBand,
14 MotionBiasBand,
15 LayeredSlats,
16 NoisyReprojection,
17 HeuristicFriendlyPan,
18 ContrastPulse,
19 StabilityHoldout,
20}
21
22impl ScenarioId {
23 pub fn as_str(self) -> &'static str {
24 match self {
25 Self::ThinReveal => "thin_reveal",
26 Self::FastPan => "fast_pan",
27 Self::DiagonalReveal => "diagonal_reveal",
28 Self::RevealBand => "reveal_band",
29 Self::MotionBiasBand => "motion_bias_band",
30 Self::LayeredSlats => "layered_slats",
31 Self::NoisyReprojection => "noisy_reprojection",
32 Self::HeuristicFriendlyPan => "heuristic_friendly_pan",
33 Self::ContrastPulse => "contrast_pulse",
34 Self::StabilityHoldout => "stability_holdout",
35 }
36 }
37
38 pub fn title(self) -> &'static str {
39 match self {
40 Self::ThinReveal => "Thin-Structure Reveal",
41 Self::FastPan => "Fast Lateral Reveal",
42 Self::DiagonalReveal => "Diagonal Subpixel Reveal",
43 Self::RevealBand => "Textured Reveal Band",
44 Self::MotionBiasBand => "Motion-Bias Reveal Band",
45 Self::LayeredSlats => "Layered Slat Reveal",
46 Self::NoisyReprojection => "Noisy Reprojection Reveal",
47 Self::HeuristicFriendlyPan => "Heuristic-Friendly Pan",
48 Self::ContrastPulse => "Contrast Pulse Stress",
49 Self::StabilityHoldout => "Stability Holdout",
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
55pub enum ScenarioExpectation {
56 BenefitExpected,
57 NeutralExpected,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
61pub enum ScenarioSupportCategory {
62 PointLikeRoi,
63 RegionRoi,
64 NegativeControl,
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
68pub enum SurfaceTag {
69 Background,
70 ThinStructure,
71 ForegroundObject,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
75pub struct MotionVector {
76 pub to_prev_x: f32,
77 pub to_prev_y: f32,
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
81pub struct Rect {
82 pub x: i32,
83 pub y: i32,
84 pub width: i32,
85 pub height: i32,
86}
87
88impl Rect {
89 pub fn contains(self, x: i32, y: i32) -> bool {
90 x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
91 }
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
95pub struct Normal3 {
96 pub x: f32,
97 pub y: f32,
98 pub z: f32,
99}
100
101impl Normal3 {
102 pub const fn new(x: f32, y: f32, z: f32) -> Self {
103 Self { x, y, z }
104 }
105
106 pub fn normalized(self) -> Self {
107 let norm = (self.x * self.x + self.y * self.y + self.z * self.z)
108 .sqrt()
109 .max(f32::EPSILON);
110 Self::new(self.x / norm, self.y / norm, self.z / norm)
111 }
112
113 pub fn dot(self, other: Self) -> f32 {
114 self.x * other.x + self.y * other.y + self.z * other.z
115 }
116}
117
118#[derive(Clone, Debug)]
119pub struct SceneFrame {
120 pub index: usize,
121 pub ground_truth: ImageFrame,
122 pub layers: Vec<SurfaceTag>,
123 pub motion: Vec<MotionVector>,
124 pub disocclusion_mask: Vec<bool>,
125 pub object_rect: Rect,
126 pub depth: Vec<f32>,
127 pub normals: Vec<Normal3>,
128}
129
130#[derive(Clone, Debug)]
131pub struct SceneSequence {
132 pub config: SceneConfig,
133 pub scenario_id: ScenarioId,
134 pub scenario_title: String,
135 pub scenario_description: String,
136 pub expectation: ScenarioExpectation,
137 pub support_category: ScenarioSupportCategory,
138 pub roi_note: String,
139 pub sampling_taxonomy: String,
140 pub realism_stress: bool,
141 pub competitive_baseline_case: bool,
142 pub bounded_loss_disclosure: bool,
143 pub demo_b_taxonomy: String,
144 pub onset_frame: usize,
145 pub target_label: String,
146 pub target_mask: Vec<bool>,
147 pub frames: Vec<SceneFrame>,
148}
149
150#[derive(Clone, Debug, Serialize)]
151pub struct SceneManifest {
152 pub scenario_id: String,
153 pub scenario_title: String,
154 pub scenario_description: String,
155 pub expectation: ScenarioExpectation,
156 pub support_category: ScenarioSupportCategory,
157 pub roi_note: String,
158 pub sampling_taxonomy: String,
159 pub realism_stress: bool,
160 pub competitive_baseline_case: bool,
161 pub bounded_loss_disclosure: bool,
162 pub demo_b_taxonomy: String,
163 pub target_label: String,
164 pub config: SceneConfig,
165 pub frame_count: usize,
166 pub onset_frame: usize,
167}
168
169#[derive(Clone, Debug, Serialize)]
170pub struct ScenarioDefinition {
171 pub id: ScenarioId,
172 pub title: &'static str,
173 pub description: &'static str,
174 pub expectation: ScenarioExpectation,
175 pub support_category: ScenarioSupportCategory,
176 pub roi_note: &'static str,
177 pub sampling_taxonomy: &'static str,
178 pub realism_stress: bool,
179 pub competitive_baseline_case: bool,
180 pub bounded_loss_disclosure: bool,
181 pub demo_b_taxonomy: &'static str,
182 pub target_label: &'static str,
183 pub scene: SceneConfig,
184 pub onset_frame: usize,
185}
186
187#[derive(Clone, Copy, Debug)]
188enum BackgroundStyle {
189 Default,
190 Textured,
191 HighContrast,
192 Calm,
193}
194
195#[derive(Clone, Copy, Debug)]
196enum ThinStyle {
197 VerticalAndDiagonal,
198 DiagonalOnly,
199 MixedWidthBand,
200 MixedWidthBandBiased,
201 None,
202}
203
204#[derive(Clone, Copy, Debug)]
205enum MotionProfile {
206 EaseOut,
207 FastPan,
208 Static,
209}
210
211#[derive(Clone, Copy, Debug)]
212struct PulseSpec {
213 rect: Rect,
214 start_frame: usize,
215 intensity: f32,
216}
217
218#[derive(Clone, Debug)]
219struct InternalScenarioSpec {
220 id: ScenarioId,
221 title: &'static str,
222 description: &'static str,
223 expectation: ScenarioExpectation,
224 support_category: ScenarioSupportCategory,
225 roi_note: &'static str,
226 sampling_taxonomy: &'static str,
227 realism_stress: bool,
228 competitive_baseline_case: bool,
229 bounded_loss_disclosure: bool,
230 demo_b_taxonomy: &'static str,
231 target_label: &'static str,
232 scene: SceneConfig,
233 onset_frame: usize,
234 background_style: BackgroundStyle,
235 thin_style: ThinStyle,
236 motion_profile: MotionProfile,
237 pulse: Option<PulseSpec>,
238}
239
240pub fn canonical_scenario(config: &SceneConfig) -> ScenarioDefinition {
241 let spec = internal_canonical_spec(config);
242 ScenarioDefinition {
243 id: spec.id,
244 title: spec.title,
245 description: spec.description,
246 expectation: spec.expectation,
247 support_category: spec.support_category,
248 roi_note: spec.roi_note,
249 sampling_taxonomy: spec.sampling_taxonomy,
250 realism_stress: spec.realism_stress,
251 competitive_baseline_case: spec.competitive_baseline_case,
252 bounded_loss_disclosure: spec.bounded_loss_disclosure,
253 demo_b_taxonomy: spec.demo_b_taxonomy,
254 target_label: spec.target_label,
255 scene: spec.scene,
256 onset_frame: spec.onset_frame,
257 }
258}
259
260pub fn scenario_suite(config: &SceneConfig) -> Vec<ScenarioDefinition> {
261 internal_scenario_suite(config)
262 .into_iter()
263 .map(|spec| ScenarioDefinition {
264 id: spec.id,
265 title: spec.title,
266 description: spec.description,
267 expectation: spec.expectation,
268 support_category: spec.support_category,
269 roi_note: spec.roi_note,
270 sampling_taxonomy: spec.sampling_taxonomy,
271 realism_stress: spec.realism_stress,
272 competitive_baseline_case: spec.competitive_baseline_case,
273 bounded_loss_disclosure: spec.bounded_loss_disclosure,
274 demo_b_taxonomy: spec.demo_b_taxonomy,
275 target_label: spec.target_label,
276 scene: spec.scene,
277 onset_frame: spec.onset_frame,
278 })
279 .collect()
280}
281
282pub fn scenario_by_id(config: &SceneConfig, scenario_id: ScenarioId) -> Option<ScenarioDefinition> {
283 internal_scenario_suite(config)
284 .into_iter()
285 .find(|spec| spec.id == scenario_id)
286 .map(|spec| ScenarioDefinition {
287 id: spec.id,
288 title: spec.title,
289 description: spec.description,
290 expectation: spec.expectation,
291 support_category: spec.support_category,
292 roi_note: spec.roi_note,
293 sampling_taxonomy: spec.sampling_taxonomy,
294 realism_stress: spec.realism_stress,
295 competitive_baseline_case: spec.competitive_baseline_case,
296 bounded_loss_disclosure: spec.bounded_loss_disclosure,
297 demo_b_taxonomy: spec.demo_b_taxonomy,
298 target_label: spec.target_label,
299 scene: spec.scene,
300 onset_frame: spec.onset_frame,
301 })
302}
303
304pub fn generate_sequence(config: &SceneConfig) -> SceneSequence {
305 generate_sequence_for_scenario(&internal_canonical_spec(config))
306}
307
308pub fn generate_sequence_for_definition(definition: &ScenarioDefinition) -> SceneSequence {
309 let spec = internal_scenario_suite(&definition.scene)
310 .into_iter()
311 .find(|candidate| candidate.id == definition.id)
312 .unwrap_or_else(|| InternalScenarioSpec {
313 id: definition.id,
314 title: definition.title,
315 description: definition.description,
316 expectation: definition.expectation,
317 support_category: definition.support_category,
318 roi_note: definition.roi_note,
319 sampling_taxonomy: definition.sampling_taxonomy,
320 realism_stress: definition.realism_stress,
321 competitive_baseline_case: definition.competitive_baseline_case,
322 bounded_loss_disclosure: definition.bounded_loss_disclosure,
323 demo_b_taxonomy: definition.demo_b_taxonomy,
324 target_label: definition.target_label,
325 scene: definition.scene.clone(),
326 onset_frame: definition.onset_frame,
327 background_style: BackgroundStyle::Default,
328 thin_style: ThinStyle::VerticalAndDiagonal,
329 motion_profile: MotionProfile::EaseOut,
330 pulse: None,
331 });
332 generate_sequence_for_scenario(&spec)
333}
334
335pub fn build_manifest(sequence: &SceneSequence) -> SceneManifest {
336 SceneManifest {
337 scenario_id: sequence.scenario_id.as_str().to_string(),
338 scenario_title: sequence.scenario_title.clone(),
339 scenario_description: sequence.scenario_description.clone(),
340 expectation: sequence.expectation,
341 support_category: sequence.support_category,
342 roi_note: sequence.roi_note.clone(),
343 sampling_taxonomy: sequence.sampling_taxonomy.clone(),
344 realism_stress: sequence.realism_stress,
345 competitive_baseline_case: sequence.competitive_baseline_case,
346 bounded_loss_disclosure: sequence.bounded_loss_disclosure,
347 demo_b_taxonomy: sequence.demo_b_taxonomy.clone(),
348 target_label: sequence.target_label.clone(),
349 config: sequence.config.clone(),
350 frame_count: sequence.frames.len(),
351 onset_frame: sequence.onset_frame,
352 }
353}
354
355fn generate_sequence_for_scenario(spec: &InternalScenarioSpec) -> SceneSequence {
356 let object_positions = build_object_positions(&spec.scene, spec.motion_profile);
357 let mut frames: Vec<SceneFrame> = Vec::with_capacity(spec.scene.frame_count);
358
359 for frame_index in 0..spec.scene.frame_count {
360 let object_rect = Rect {
361 x: object_positions[frame_index],
362 y: spec.scene.object_top_y,
363 width: spec.scene.object_width as i32,
364 height: spec.scene.object_height as i32,
365 };
366 let previous_object_x = if frame_index == 0 {
367 object_rect.x
368 } else {
369 object_positions[frame_index - 1]
370 };
371 let object_dx = object_rect.x - previous_object_x;
372
373 let mut ground_truth = ImageFrame::new(spec.scene.width, spec.scene.height);
374 let mut layers = vec![SurfaceTag::Background; spec.scene.width * spec.scene.height];
375 let mut motion = vec![
376 MotionVector {
377 to_prev_x: 0.0,
378 to_prev_y: 0.0,
379 };
380 spec.scene.width * spec.scene.height
381 ];
382 let mut depth = vec![0.0f32; spec.scene.width * spec.scene.height];
383 let mut normals = vec![Normal3::new(0.0, 0.0, 1.0); spec.scene.width * spec.scene.height];
384
385 for y in 0..spec.scene.height {
386 for x in 0..spec.scene.width {
387 let x_i = x as i32;
388 let y_i = y as i32;
389 let pixel_index = y * spec.scene.width + x;
390 let background_base = background_color(x, y, &spec.scene, spec.background_style);
391 let mut color = apply_pulse(background_base, frame_index, x_i, y_i, spec.pulse);
392 let mut layer = SurfaceTag::Background;
393 let mut depth_value = background_depth(x, y, &spec.scene, spec.background_style);
394 let mut normal_value = background_normal(x, y, &spec.scene, spec.background_style);
395
396 if is_thin_structure(x_i, y_i, &spec.scene, spec.thin_style) {
397 color = apply_pulse(
398 thin_structure_color(x_i, y_i, &spec.scene, spec.thin_style),
399 frame_index,
400 x_i,
401 y_i,
402 spec.pulse,
403 );
404 layer = SurfaceTag::ThinStructure;
405 depth_value = thin_structure_depth(x_i, y_i, &spec.scene, spec.thin_style);
406 normal_value = thin_structure_normal(x_i, y_i, &spec.scene, spec.thin_style);
407 }
408
409 if !matches!(spec.motion_profile, MotionProfile::Static)
410 && object_rect.contains(x_i, y_i)
411 {
412 color = object_color(x_i, y_i, object_rect);
413 layer = SurfaceTag::ForegroundObject;
414 depth_value = object_depth(x_i, y_i, object_rect);
415 normal_value = object_normal(x_i, y_i, object_rect);
416 }
417
418 ground_truth.set(x, y, color);
419 layers[pixel_index] = layer;
420 depth[pixel_index] = depth_value;
421 normals[pixel_index] = normal_value.normalized();
422 if matches!(layer, SurfaceTag::ForegroundObject) {
423 motion[pixel_index] = MotionVector {
424 to_prev_x: -(object_dx as f32),
425 to_prev_y: 0.0,
426 };
427 }
428 }
429 }
430
431 if matches!(
432 spec.id,
433 ScenarioId::MotionBiasBand | ScenarioId::NoisyReprojection
434 ) {
435 apply_motion_bias_band(
436 frame_index,
437 spec,
438 &mut motion,
439 &mut depth,
440 &mut normals,
441 &layers,
442 );
443 }
444 if matches!(spec.id, ScenarioId::NoisyReprojection) {
445 apply_noisy_reprojection(
446 frame_index,
447 spec,
448 &mut motion,
449 &mut depth,
450 &mut normals,
451 &layers,
452 );
453 }
454
455 let disocclusion_mask = if frame_index == 0 {
456 vec![false; spec.scene.width * spec.scene.height]
457 } else {
458 let previous_layers = &frames[frame_index - 1].layers;
459 let mut mask = vec![false; spec.scene.width * spec.scene.height];
460 for y in 0..spec.scene.height {
461 for x in 0..spec.scene.width {
462 let index = y * spec.scene.width + x;
463 let motion_vector = motion[index];
464 let prev_x = ((x as f32 + motion_vector.to_prev_x).round() as i32)
465 .clamp(0, spec.scene.width as i32 - 1)
466 as usize;
467 let prev_y = ((y as f32 + motion_vector.to_prev_y).round() as i32)
468 .clamp(0, spec.scene.height as i32 - 1)
469 as usize;
470 let previous_layer = previous_layers[prev_y * spec.scene.width + prev_x];
471 mask[index] = previous_layer != layers[index]
472 && !matches!(layers[index], SurfaceTag::ForegroundObject);
473 }
474 }
475 mask
476 };
477
478 frames.push(SceneFrame {
479 index: frame_index,
480 ground_truth,
481 layers,
482 motion,
483 disocclusion_mask,
484 object_rect,
485 depth,
486 normals,
487 });
488 }
489
490 let target_mask = build_target_mask(spec, &frames);
491
492 SceneSequence {
493 config: spec.scene.clone(),
494 scenario_id: spec.id,
495 scenario_title: spec.title.to_string(),
496 scenario_description: spec.description.to_string(),
497 expectation: spec.expectation,
498 support_category: spec.support_category,
499 roi_note: spec.roi_note.to_string(),
500 sampling_taxonomy: spec.sampling_taxonomy.to_string(),
501 realism_stress: spec.realism_stress,
502 competitive_baseline_case: spec.competitive_baseline_case,
503 bounded_loss_disclosure: spec.bounded_loss_disclosure,
504 demo_b_taxonomy: spec.demo_b_taxonomy.to_string(),
505 onset_frame: spec.onset_frame,
506 target_label: spec.target_label.to_string(),
507 target_mask,
508 frames,
509 }
510}
511
512fn build_target_mask(spec: &InternalScenarioSpec, frames: &[SceneFrame]) -> Vec<bool> {
513 let width = spec.scene.width;
514 let height = spec.scene.height;
515 let frame = &frames[spec.onset_frame.min(frames.len().saturating_sub(1))];
516
517 match spec.id {
518 ScenarioId::ThinReveal | ScenarioId::DiagonalReveal => frame
519 .layers
520 .iter()
521 .zip(frame.disocclusion_mask.iter().copied())
522 .map(|(layer, disoccluded)| disoccluded && matches!(*layer, SurfaceTag::ThinStructure))
523 .collect(),
524 ScenarioId::FastPan => frame
525 .layers
526 .iter()
527 .zip(frame.disocclusion_mask.iter().copied())
528 .map(|(layer, disoccluded)| {
529 disoccluded && matches!(*layer, SurfaceTag::ThinStructure | SurfaceTag::Background)
530 })
531 .collect(),
532 ScenarioId::RevealBand
533 | ScenarioId::MotionBiasBand
534 | ScenarioId::LayeredSlats
535 | ScenarioId::NoisyReprojection
536 | ScenarioId::HeuristicFriendlyPan => {
537 let mut mask = vec![false; width * height];
538 let band = Rect {
539 x: 28,
540 y: 20,
541 width: (width as i32 - 56).max(24),
542 height: (height as i32 - 40).max(18),
543 };
544 for y in 0..height {
545 for x in 0..width {
546 let index = y * width + x;
547 if frame.disocclusion_mask[index]
548 && band.contains(x as i32, y as i32)
549 && !matches!(frame.layers[index], SurfaceTag::ForegroundObject)
550 {
551 mask[index] = true;
552 }
553 }
554 }
555 mask
556 }
557 ScenarioId::ContrastPulse => {
558 let pulse = spec
559 .pulse
560 .expect("contrast-pulse scenarios require a pulse region");
561 let mut mask = vec![false; width * height];
562 for y in 0..height {
563 for x in 0..width {
564 let x_i = x as i32;
565 let y_i = y as i32;
566 if pulse.rect.contains(x_i, y_i)
567 && !matches!(frame.layers[y * width + x], SurfaceTag::ForegroundObject)
568 {
569 mask[y * width + x] = true;
570 }
571 }
572 }
573 mask
574 }
575 ScenarioId::StabilityHoldout => {
576 let mut mask = vec![false; width * height];
577 let band = Rect {
578 x: (width as i32 / 2) - 18,
579 y: (height as i32 / 2) - 14,
580 width: 36,
581 height: 28,
582 };
583 for y in 0..height {
584 for x in 0..width {
585 let x_i = x as i32;
586 let y_i = y as i32;
587 if band.contains(x_i, y_i)
588 && !matches!(frame.layers[y * width + x], SurfaceTag::ForegroundObject)
589 {
590 mask[y * width + x] = true;
591 }
592 }
593 }
594 mask
595 }
596 }
597}
598
599fn internal_canonical_spec(config: &SceneConfig) -> InternalScenarioSpec {
600 InternalScenarioSpec {
601 id: ScenarioId::ThinReveal,
602 title: ScenarioId::ThinReveal.title(),
603 description: "Moving occluder reveals thin vertical and diagonal structure on a deterministic patterned background.",
604 expectation: ScenarioExpectation::BenefitExpected,
605 support_category: ScenarioSupportCategory::PointLikeRoi,
606 roi_note: "Canonical reveal ROI collapses to a single disoccluded thin-structure pixel at the default resolution. It remains mechanically relevant but statistically weak and must be reported as point-like evidence.",
607 sampling_taxonomy: "coverage-dominated point reveal",
608 realism_stress: false,
609 competitive_baseline_case: false,
610 bounded_loss_disclosure: false,
611 demo_b_taxonomy: "aliasing_limited",
612 target_label: "revealed thin structure",
613 scene: config.clone(),
614 onset_frame: config.move_frames.min(config.frame_count.saturating_sub(2)),
615 background_style: BackgroundStyle::Default,
616 thin_style: ThinStyle::VerticalAndDiagonal,
617 motion_profile: MotionProfile::EaseOut,
618 pulse: None,
619 }
620}
621
622fn internal_scenario_suite(config: &SceneConfig) -> Vec<InternalScenarioSpec> {
623 let base_onset = config.move_frames.min(config.frame_count.saturating_sub(2));
624
625 let mut fast_pan_scene = config.clone();
626 fast_pan_scene.object_width = 26;
627 fast_pan_scene.object_height = 46;
628 fast_pan_scene.object_start_x = 14;
629 fast_pan_scene.object_stop_x = 86;
630 fast_pan_scene.move_frames = 4;
631
632 let mut diagonal_scene = config.clone();
633 diagonal_scene.object_width = 24;
634 diagonal_scene.object_height = 42;
635 diagonal_scene.object_start_x = 44;
636 diagonal_scene.object_stop_x = 70;
637 diagonal_scene.move_frames = 5;
638 diagonal_scene.thin_vertical_x = 70;
639
640 let mut reveal_band_scene = config.clone();
641 reveal_band_scene.object_width = 28;
642 reveal_band_scene.object_height = 52;
643 reveal_band_scene.object_start_x = 12;
644 reveal_band_scene.object_stop_x = 88;
645 reveal_band_scene.object_top_y = 22;
646 reveal_band_scene.move_frames = 5;
647 reveal_band_scene.thin_vertical_x = 40;
648
649 let mut motion_bias_scene = reveal_band_scene.clone();
650 motion_bias_scene.object_width = 24;
651 motion_bias_scene.object_start_x = 18;
652 motion_bias_scene.object_stop_x = 84;
653 motion_bias_scene.move_frames = 6;
654
655 let mut layered_slats_scene = reveal_band_scene.clone();
656 layered_slats_scene.object_width = 34;
657 layered_slats_scene.object_height = 56;
658 layered_slats_scene.object_start_x = 10;
659 layered_slats_scene.object_stop_x = 92;
660 layered_slats_scene.move_frames = 5;
661
662 let mut noisy_reprojection_scene = reveal_band_scene.clone();
663 noisy_reprojection_scene.object_width = 26;
664 noisy_reprojection_scene.object_height = 54;
665 noisy_reprojection_scene.object_start_x = 16;
666 noisy_reprojection_scene.object_stop_x = 86;
667 noisy_reprojection_scene.move_frames = 6;
668
669 let mut heuristic_friendly_scene = reveal_band_scene.clone();
670 heuristic_friendly_scene.object_width = 30;
671 heuristic_friendly_scene.object_height = 48;
672 heuristic_friendly_scene.object_start_x = 18;
673 heuristic_friendly_scene.object_stop_x = 78;
674 heuristic_friendly_scene.move_frames = 4;
675
676 let mut contrast_scene = config.clone();
677 contrast_scene.object_start_x = 20;
678 contrast_scene.object_stop_x = 20;
679 contrast_scene.move_frames = 0;
680
681 let mut holdout_scene = config.clone();
682 holdout_scene.object_start_x = 8;
683 holdout_scene.object_stop_x = 8;
684 holdout_scene.move_frames = 0;
685
686 vec![
687 internal_canonical_spec(config),
688 InternalScenarioSpec {
689 id: ScenarioId::FastPan,
690 title: ScenarioId::FastPan.title(),
691 description: "Faster occluder motion over a textured backdrop stresses motion disagreement, depth rejection, and neighborhood stability.",
692 expectation: ScenarioExpectation::BenefitExpected,
693 support_category: ScenarioSupportCategory::RegionRoi,
694 roi_note: "The ROI is a small but regional disocclusion strip rather than a single point. It is still sparse and should not be mixed with large-band ROI results without disclosure.",
695 sampling_taxonomy: "thin-band reveal with textured background",
696 realism_stress: false,
697 competitive_baseline_case: true,
698 bounded_loss_disclosure: false,
699 demo_b_taxonomy: "mixed",
700 target_label: "fast-pan reveal region",
701 scene: fast_pan_scene.clone(),
702 onset_frame: fast_pan_scene.move_frames.min(fast_pan_scene.frame_count.saturating_sub(2)),
703 background_style: BackgroundStyle::Textured,
704 thin_style: ThinStyle::VerticalAndDiagonal,
705 motion_profile: MotionProfile::FastPan,
706 pulse: None,
707 },
708 InternalScenarioSpec {
709 id: ScenarioId::DiagonalReveal,
710 title: ScenarioId::DiagonalReveal.title(),
711 description: "Diagonal subpixel structure on a high-contrast background stresses neighborhood clamping and thin-structure proxies.",
712 expectation: ScenarioExpectation::BenefitExpected,
713 support_category: ScenarioSupportCategory::PointLikeRoi,
714 roi_note: "At default resolution the diagonal reveal also reduces to point-like support. It is useful for aliasing behavior, but not as a region-sized aggregate claim.",
715 sampling_taxonomy: "subpixel diagonal coverage case",
716 realism_stress: false,
717 competitive_baseline_case: false,
718 bounded_loss_disclosure: false,
719 demo_b_taxonomy: "aliasing_limited",
720 target_label: "diagonal thin reveal",
721 scene: diagonal_scene.clone(),
722 onset_frame: diagonal_scene.move_frames.min(diagonal_scene.frame_count.saturating_sub(2)),
723 background_style: BackgroundStyle::HighContrast,
724 thin_style: ThinStyle::DiagonalOnly,
725 motion_profile: MotionProfile::EaseOut,
726 pulse: None,
727 },
728 InternalScenarioSpec {
729 id: ScenarioId::RevealBand,
730 title: ScenarioId::RevealBand.title(),
731 description: "Mixed-width slats and textured disocclusion band reveal a materially larger ROI and reduce the canonical point-measurement weakness.",
732 expectation: ScenarioExpectation::BenefitExpected,
733 support_category: ScenarioSupportCategory::RegionRoi,
734 roi_note: "This scenario is intentionally region-sized so cumulative ROI metrics are not driven by a single pixel.",
735 sampling_taxonomy: "mixed-width reveal band with aliasing and texture",
736 realism_stress: false,
737 competitive_baseline_case: false,
738 bounded_loss_disclosure: false,
739 demo_b_taxonomy: "mixed",
740 target_label: "textured reveal band",
741 scene: reveal_band_scene.clone(),
742 onset_frame: reveal_band_scene.move_frames.min(reveal_band_scene.frame_count.saturating_sub(2)),
743 background_style: BackgroundStyle::Textured,
744 thin_style: ThinStyle::MixedWidthBand,
745 motion_profile: MotionProfile::EaseOut,
746 pulse: None,
747 },
748 InternalScenarioSpec {
749 id: ScenarioId::MotionBiasBand,
750 title: ScenarioId::MotionBiasBand.title(),
751 description: "A region-sized reveal with biased background motion and reprojection mismatch stresses whether motion disagreement contributes beyond residual and neighborhood cues.",
752 expectation: ScenarioExpectation::BenefitExpected,
753 support_category: ScenarioSupportCategory::RegionRoi,
754 roi_note: "This is a region ROI with deliberately imperfect motion information. It is the main scenario used to decide whether motion disagreement belongs in the minimum path.",
755 sampling_taxonomy: "motion-mismatch reveal band",
756 realism_stress: true,
757 competitive_baseline_case: false,
758 bounded_loss_disclosure: false,
759 demo_b_taxonomy: "mixed",
760 target_label: "motion-bias reveal band",
761 scene: motion_bias_scene.clone(),
762 onset_frame: motion_bias_scene.move_frames.min(motion_bias_scene.frame_count.saturating_sub(2)),
763 background_style: BackgroundStyle::Textured,
764 thin_style: ThinStyle::MixedWidthBandBiased,
765 motion_profile: MotionProfile::FastPan,
766 pulse: None,
767 },
768 InternalScenarioSpec {
769 id: ScenarioId::LayeredSlats,
770 title: ScenarioId::LayeredSlats.title(),
771 description: "A broader layered-slat reveal mixes thin, medium, and wide structures across a larger ROI so the suite includes a second materially sized benefit-expected region case.",
772 expectation: ScenarioExpectation::BenefitExpected,
773 support_category: ScenarioSupportCategory::RegionRoi,
774 roi_note: "This region ROI is intentionally wider than the canonical band so cumulative claims are not dominated by sparse support.",
775 sampling_taxonomy: "layered slat reveal with mixed stable and unstable zones",
776 realism_stress: false,
777 competitive_baseline_case: false,
778 bounded_loss_disclosure: false,
779 demo_b_taxonomy: "mixed",
780 target_label: "layered slat reveal",
781 scene: layered_slats_scene.clone(),
782 onset_frame: layered_slats_scene
783 .move_frames
784 .min(layered_slats_scene.frame_count.saturating_sub(2)),
785 background_style: BackgroundStyle::Textured,
786 thin_style: ThinStyle::MixedWidthBand,
787 motion_profile: MotionProfile::EaseOut,
788 pulse: None,
789 },
790 InternalScenarioSpec {
791 id: ScenarioId::NoisyReprojection,
792 title: ScenarioId::NoisyReprojection.title(),
793 description: "A region reveal with subpixel-biased motion, reprojection noise, and depth-boundary disagreement makes the suite more engine-adjacent without pretending to be a real capture.",
794 expectation: ScenarioExpectation::BenefitExpected,
795 support_category: ScenarioSupportCategory::RegionRoi,
796 roi_note: "This is a realism-stress region ROI with deliberately imperfect reprojection cues.",
797 sampling_taxonomy: "realism-stress reveal with noisy reprojection",
798 realism_stress: true,
799 competitive_baseline_case: false,
800 bounded_loss_disclosure: false,
801 demo_b_taxonomy: "variance_limited",
802 target_label: "noisy reprojection reveal",
803 scene: noisy_reprojection_scene.clone(),
804 onset_frame: noisy_reprojection_scene
805 .move_frames
806 .min(noisy_reprojection_scene.frame_count.saturating_sub(2)),
807 background_style: BackgroundStyle::Textured,
808 thin_style: ThinStyle::MixedWidthBandBiased,
809 motion_profile: MotionProfile::FastPan,
810 pulse: None,
811 },
812 InternalScenarioSpec {
813 id: ScenarioId::HeuristicFriendlyPan,
814 title: ScenarioId::HeuristicFriendlyPan.title(),
815 description: "A cleaner, wider reveal pan is included specifically as a competitive-baseline case where strong heuristic guidance should remain competitive rather than being treated as an embarrassment.",
816 expectation: ScenarioExpectation::BenefitExpected,
817 support_category: ScenarioSupportCategory::RegionRoi,
818 roi_note: "This case is reported explicitly as a competitive-baseline disclosure rather than a universal-win setup.",
819 sampling_taxonomy: "competitive baseline reveal",
820 realism_stress: false,
821 competitive_baseline_case: true,
822 bounded_loss_disclosure: false,
823 demo_b_taxonomy: "edge_trap",
824 target_label: "heuristic-friendly reveal",
825 scene: heuristic_friendly_scene.clone(),
826 onset_frame: heuristic_friendly_scene
827 .move_frames
828 .min(heuristic_friendly_scene.frame_count.saturating_sub(2)),
829 background_style: BackgroundStyle::HighContrast,
830 thin_style: ThinStyle::MixedWidthBand,
831 motion_profile: MotionProfile::FastPan,
832 pulse: None,
833 },
834 InternalScenarioSpec {
835 id: ScenarioId::ContrastPulse,
836 title: ScenarioId::ContrastPulse.title(),
837 description: "A bounded lighting change with no geometry reveal stresses false positives and is intended as a low-benefit honesty case rather than a DSFB win scenario.",
838 expectation: ScenarioExpectation::NeutralExpected,
839 support_category: ScenarioSupportCategory::NegativeControl,
840 roi_note: "This negative control uses a large ROI on purpose, but it is not a benefit-expected disocclusion case.",
841 sampling_taxonomy: "negative control",
842 realism_stress: false,
843 competitive_baseline_case: false,
844 bounded_loss_disclosure: true,
845 demo_b_taxonomy: "variance_limited",
846 target_label: "pulse region",
847 scene: contrast_scene.clone(),
848 onset_frame: base_onset,
849 background_style: BackgroundStyle::Calm,
850 thin_style: ThinStyle::None,
851 motion_profile: MotionProfile::Static,
852 pulse: Some(PulseSpec {
853 rect: Rect {
854 x: (contrast_scene.width as i32 / 2) - 18,
855 y: (contrast_scene.height as i32 / 2) - 18,
856 width: 52,
857 height: 36,
858 },
859 start_frame: base_onset,
860 intensity: 1.22,
861 }),
862 },
863 InternalScenarioSpec {
864 id: ScenarioId::StabilityHoldout,
865 title: ScenarioId::StabilityHoldout.title(),
866 description: "Static holdout case with no reveal event. Useful for verifying low false-positive intervention and bounded neutral behavior.",
867 expectation: ScenarioExpectation::NeutralExpected,
868 support_category: ScenarioSupportCategory::NegativeControl,
869 roi_note: "This is a negative-control background patch used to bound non-ROI damage and false-positive intervention.",
870 sampling_taxonomy: "negative control",
871 realism_stress: false,
872 competitive_baseline_case: false,
873 bounded_loss_disclosure: true,
874 demo_b_taxonomy: "variance_limited",
875 target_label: "holdout background patch",
876 scene: holdout_scene,
877 onset_frame: base_onset,
878 background_style: BackgroundStyle::Default,
879 thin_style: ThinStyle::VerticalAndDiagonal,
880 motion_profile: MotionProfile::Static,
881 pulse: None,
882 },
883 ]
884}
885
886fn build_object_positions(config: &SceneConfig, profile: MotionProfile) -> Vec<i32> {
887 let mut positions = Vec::with_capacity(config.frame_count);
888 for frame_index in 0..config.frame_count {
889 let position = match profile {
890 MotionProfile::Static => config.object_start_x as f32,
891 MotionProfile::EaseOut => {
892 if frame_index < config.move_frames.max(1) {
893 let t = frame_index as f32 / config.move_frames.max(1) as f32;
894 let eased = 1.0 - (1.0 - t).powi(2);
895 config.object_start_x as f32
896 + (config.object_stop_x - config.object_start_x) as f32 * eased
897 } else {
898 config.object_stop_x as f32
899 }
900 }
901 MotionProfile::FastPan => {
902 if frame_index < config.move_frames.max(1) {
903 let t = frame_index as f32 / config.move_frames.max(1) as f32;
904 let eased = t.powf(0.75);
905 config.object_start_x as f32
906 + (config.object_stop_x - config.object_start_x) as f32 * eased
907 } else {
908 config.object_stop_x as f32
909 }
910 }
911 };
912 positions.push(position.round() as i32);
913 }
914 positions
915}
916
917fn background_color(x: usize, y: usize, config: &SceneConfig, style: BackgroundStyle) -> Color {
918 let xf = x as f32 / (config.width.saturating_sub(1).max(1)) as f32;
919 let yf = y as f32 / (config.height.saturating_sub(1).max(1)) as f32;
920 let checker = if ((x / 12) + (y / 12)) % 2 == 0 {
921 1.0
922 } else {
923 0.0
924 };
925 let diagonal = if (x + 2 * y) % 22 < 6 { 1.0 } else { 0.0 };
926 let stripes = if (3 * x + y) % 17 < 5 { 1.0 } else { 0.0 };
927 let vignette_x = (xf - 0.5).abs();
928 let vignette_y = (yf - 0.5).abs();
929 let vignette = 1.0 - (vignette_x * 0.35 + vignette_y * 0.4);
930
931 match style {
932 BackgroundStyle::Default => Color::rgb(
933 (0.12 + 0.16 * xf + 0.05 * checker + 0.03 * diagonal) * vignette,
934 (0.15 + 0.11 * yf + 0.04 * diagonal) * vignette,
935 (0.22 + 0.18 * (1.0 - xf) + 0.03 * checker) * vignette,
936 ),
937 BackgroundStyle::Textured => Color::rgb(
938 (0.10 + 0.18 * xf + 0.08 * checker + 0.05 * stripes) * vignette,
939 (0.11 + 0.15 * yf + 0.10 * diagonal + 0.04 * stripes) * vignette,
940 (0.18 + 0.20 * (1.0 - xf) + 0.06 * checker) * vignette,
941 ),
942 BackgroundStyle::HighContrast => Color::rgb(
943 (0.08 + 0.24 * checker + 0.20 * diagonal + 0.05 * xf) * vignette,
944 (0.08 + 0.18 * stripes + 0.07 * yf) * vignette,
945 (0.12 + 0.25 * (1.0 - checker) + 0.04 * xf) * vignette,
946 ),
947 BackgroundStyle::Calm => {
948 Color::rgb(0.18 + 0.06 * xf, 0.18 + 0.05 * yf, 0.24 + 0.06 * (1.0 - xf))
949 }
950 }
951}
952
953fn background_depth(x: usize, y: usize, config: &SceneConfig, style: BackgroundStyle) -> f32 {
954 let xf = x as f32 / config.width.max(1) as f32;
955 let yf = y as f32 / config.height.max(1) as f32;
956 let base = 0.78 + 0.06 * xf + 0.04 * yf;
957 match style {
958 BackgroundStyle::Default | BackgroundStyle::Calm => base,
959 BackgroundStyle::Textured => base + 0.01 * ((x / 8 + y / 7) % 3) as f32,
960 BackgroundStyle::HighContrast => base + 0.015 * ((x / 6 + y / 5) % 2) as f32,
961 }
962}
963
964fn background_normal(x: usize, y: usize, config: &SceneConfig, style: BackgroundStyle) -> Normal3 {
965 let xf = x as f32 / config.width.max(1) as f32;
966 let yf = y as f32 / config.height.max(1) as f32;
967 let tilt = match style {
968 BackgroundStyle::Default => 0.03,
969 BackgroundStyle::Textured => 0.08,
970 BackgroundStyle::HighContrast => 0.10,
971 BackgroundStyle::Calm => 0.01,
972 };
973 Normal3::new((xf - 0.5) * tilt, (0.5 - yf) * tilt, 1.0).normalized()
974}
975
976fn is_thin_structure(x: i32, y: i32, config: &SceneConfig, style: ThinStyle) -> bool {
977 let vertical = x == config.thin_vertical_x && y >= 14 && y <= config.height as i32 - 14;
978 let diagonal_line = {
979 let diagonal = 0.58 * x as f32 + 10.0;
980 (y as f32 - diagonal).abs() <= 0.55 && (28..=118).contains(&x)
981 };
982 let mixed_width_band = {
983 let in_band = (18..=(config.height as i32 - 18)).contains(&y)
984 && (26..=(config.width as i32 - 24)).contains(&x);
985 let thin_slats = (x - 28).rem_euclid(11) == 0;
986 let medium_slats = (x - 34).rem_euclid(19) <= 1;
987 let wide_slats = (x - 48).rem_euclid(29) <= 2;
988 let diagonal = (y as f32 - (0.44 * x as f32 + 12.0)).abs() <= 1.15
989 && (38..=(config.width as i32 - 32)).contains(&x);
990 in_band && (thin_slats || medium_slats || wide_slats || diagonal)
991 };
992 match style {
993 ThinStyle::VerticalAndDiagonal => vertical || diagonal_line,
994 ThinStyle::DiagonalOnly => diagonal_line,
995 ThinStyle::MixedWidthBand | ThinStyle::MixedWidthBandBiased => mixed_width_band,
996 ThinStyle::None => false,
997 }
998}
999
1000fn thin_structure_color(x: i32, y: i32, config: &SceneConfig, style: ThinStyle) -> Color {
1001 match style {
1002 ThinStyle::VerticalAndDiagonal if x == config.thin_vertical_x => {
1003 let pulse = if y % 6 < 3 { 1.0 } else { 0.82 };
1004 Color::rgb(0.95 * pulse, 0.96 * pulse, 0.98)
1005 }
1006 ThinStyle::DiagonalOnly => Color::rgb(0.24, 0.29, 0.35),
1007 ThinStyle::VerticalAndDiagonal => Color::rgb(0.64, 0.90, 0.96),
1008 ThinStyle::MixedWidthBand => {
1009 let phase = ((x + 2 * y) % 9) as f32 / 8.0;
1010 Color::rgb(
1011 0.22 + 0.48 * phase,
1012 0.58 + 0.26 * phase,
1013 0.84 + 0.12 * (1.0 - phase),
1014 )
1015 }
1016 ThinStyle::MixedWidthBandBiased => {
1017 let phase = ((2 * x + y) % 13) as f32 / 12.0;
1018 Color::rgb(
1019 0.78 + 0.16 * phase,
1020 0.74 + 0.10 * (1.0 - phase),
1021 0.26 + 0.18 * phase,
1022 )
1023 }
1024 ThinStyle::None => Color::rgb(0.0, 0.0, 0.0),
1025 }
1026}
1027
1028fn thin_structure_depth(x: i32, _y: i32, config: &SceneConfig, style: ThinStyle) -> f32 {
1029 match style {
1030 ThinStyle::VerticalAndDiagonal if x == config.thin_vertical_x => 0.70,
1031 ThinStyle::DiagonalOnly => 0.68,
1032 ThinStyle::VerticalAndDiagonal => 0.72,
1033 ThinStyle::MixedWidthBand => 0.69,
1034 ThinStyle::MixedWidthBandBiased => 0.67,
1035 ThinStyle::None => 0.80,
1036 }
1037}
1038
1039fn thin_structure_normal(x: i32, _y: i32, config: &SceneConfig, style: ThinStyle) -> Normal3 {
1040 match style {
1041 ThinStyle::VerticalAndDiagonal if x == config.thin_vertical_x => {
1042 Normal3::new(0.05, 0.0, 0.998).normalized()
1043 }
1044 ThinStyle::DiagonalOnly => Normal3::new(0.24, -0.08, 0.96).normalized(),
1045 ThinStyle::VerticalAndDiagonal => Normal3::new(0.16, -0.06, 0.98).normalized(),
1046 ThinStyle::MixedWidthBand => Normal3::new(0.18, -0.04, 0.98).normalized(),
1047 ThinStyle::MixedWidthBandBiased => {
1048 if (x - 48).rem_euclid(29) <= 2 {
1049 Normal3::new(0.30, -0.10, 0.95).normalized()
1050 } else {
1051 Normal3::new(0.18, -0.04, 0.98).normalized()
1052 }
1053 }
1054 ThinStyle::None => Normal3::new(0.0, 0.0, 1.0),
1055 }
1056}
1057
1058fn object_color(x: i32, y: i32, rect: Rect) -> Color {
1059 let local_x = (x - rect.x) as f32 / rect.width.max(1) as f32;
1060 let local_y = (y - rect.y) as f32 / rect.height.max(1) as f32;
1061 let stripe = if local_x > 0.36 && local_x < 0.46 {
1062 0.55
1063 } else {
1064 1.0
1065 };
1066 let rim = if !(2..=(rect.width - 3)).contains(&(x - rect.x))
1067 || !(2..=(rect.height - 3)).contains(&(y - rect.y))
1068 {
1069 1.12
1070 } else {
1071 1.0
1072 };
1073
1074 Color::rgb(
1075 (0.82 + 0.10 * local_y) * stripe * rim,
1076 (0.35 + 0.12 * (1.0 - local_y)) * stripe * rim,
1077 (0.20 + 0.08 * local_x) * stripe * rim,
1078 )
1079 .clamp01()
1080}
1081
1082fn object_depth(x: i32, y: i32, rect: Rect) -> f32 {
1083 let local_x = (x - rect.x) as f32 / rect.width.max(1) as f32;
1084 let local_y = (y - rect.y) as f32 / rect.height.max(1) as f32;
1085 0.30 + 0.05 * local_x + 0.03 * local_y
1086}
1087
1088fn object_normal(x: i32, y: i32, rect: Rect) -> Normal3 {
1089 let local_x = (x - rect.x) as f32 / rect.width.max(1) as f32 - 0.5;
1090 let local_y = (y - rect.y) as f32 / rect.height.max(1) as f32 - 0.5;
1091 Normal3::new(local_x * 0.24, -local_y * 0.12, 1.0).normalized()
1092}
1093
1094fn apply_pulse(
1095 color: Color,
1096 frame_index: usize,
1097 x: i32,
1098 y: i32,
1099 pulse: Option<PulseSpec>,
1100) -> Color {
1101 let Some(pulse) = pulse else {
1102 return color;
1103 };
1104 if frame_index < pulse.start_frame || !pulse.rect.contains(x, y) {
1105 return color;
1106 }
1107 Color::rgb(
1108 color.r * pulse.intensity,
1109 color.g * pulse.intensity,
1110 color.b * pulse.intensity,
1111 )
1112 .clamp01()
1113}
1114
1115fn apply_motion_bias_band(
1116 frame_index: usize,
1117 spec: &InternalScenarioSpec,
1118 motion: &mut [MotionVector],
1119 depth: &mut [f32],
1120 normals: &mut [Normal3],
1121 layers: &[SurfaceTag],
1122) {
1123 let width = spec.scene.width;
1124 let height = spec.scene.height;
1125 for y in 0..height {
1126 for x in 0..width {
1127 let index = y * width + x;
1128 if matches!(layers[index], SurfaceTag::ForegroundObject) {
1129 continue;
1130 }
1131 let in_band = (20..=(height.saturating_sub(20))).contains(&y)
1132 && (24..=(width.saturating_sub(24))).contains(&x);
1133 if !in_band {
1134 continue;
1135 }
1136 let jitter_seed = ((x * 13 + y * 7 + frame_index * 11) % 17) as f32 / 16.0;
1137 let x_bias = -0.45 + 0.20 * (jitter_seed - 0.5);
1138 let y_bias = 0.08 * (((x + frame_index) % 5) as f32 - 2.0) / 2.0;
1139 motion[index] = MotionVector {
1140 to_prev_x: x_bias,
1141 to_prev_y: y_bias,
1142 };
1143 depth[index] += 0.006 * (jitter_seed - 0.5);
1144 normals[index] = Normal3::new(
1145 normals[index].x + 0.05 * (jitter_seed - 0.5),
1146 normals[index].y - 0.03 * (jitter_seed - 0.5),
1147 normals[index].z,
1148 )
1149 .normalized();
1150 }
1151 }
1152}
1153
1154fn apply_noisy_reprojection(
1155 frame_index: usize,
1156 spec: &InternalScenarioSpec,
1157 motion: &mut [MotionVector],
1158 depth: &mut [f32],
1159 normals: &mut [Normal3],
1160 layers: &[SurfaceTag],
1161) {
1162 let width = spec.scene.width;
1163 let height = spec.scene.height;
1164 for y in 0..height {
1165 for x in 0..width {
1166 let index = y * width + x;
1167 if matches!(layers[index], SurfaceTag::ForegroundObject) {
1168 continue;
1169 }
1170 let in_band = (16..=(height.saturating_sub(16))).contains(&y)
1171 && (18..=(width.saturating_sub(18))).contains(&x);
1172 if !in_band {
1173 continue;
1174 }
1175 let seed = ((x * 19 + y * 23 + frame_index * 29) % 37) as f32 / 36.0;
1176 let subpixel_x = -0.65 + 0.55 * seed;
1177 let subpixel_y = 0.22 * (((2 * x + y + frame_index) % 9) as f32 - 4.0) / 4.0;
1178 motion[index] = MotionVector {
1179 to_prev_x: motion[index].to_prev_x + subpixel_x,
1180 to_prev_y: motion[index].to_prev_y + subpixel_y,
1181 };
1182 depth[index] += 0.014 * (seed - 0.5);
1183 normals[index] = Normal3::new(
1184 normals[index].x + 0.08 * (seed - 0.5),
1185 normals[index].y + 0.05 * (0.5 - seed),
1186 normals[index].z - 0.03 * (seed - 0.5).abs(),
1187 )
1188 .normalized();
1189 }
1190 }
1191}