Skip to main content

dsfb_computer_graphics/scene/
mod.rs

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}