Skip to main content

nightshade_renderer/
config.rs

1//! Graphics settings resources.
2
3use serde::{Deserialize, Serialize};
4
5/// Controls how the renderer treats the focused viewport when the
6/// camera is in a non-`Always` update mode.
7///
8/// - `FocusedAlways` (default): the focused viewport (active camera, or
9///   the camera tile under the mouse) re-renders every frame regardless
10///   of its `ViewportUpdateMode`. Background tiles still respect their
11///   own update mode.
12/// - `Manual`: every viewport, focused or not, respects its own
13///   `ViewportUpdateMode` strictly. Useful when the user wants
14///   pixel-perfect control over which tiles render each frame.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
16pub enum ViewportFocusPolicy {
17    #[default]
18    FocusedAlways,
19    Manual,
20}
21
22/// Debug visualization mode for PBR rendering components.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
24pub enum PbrDebugMode {
25    #[default]
26    None,
27    BaseColor,
28    Normal,
29    Metallic,
30    Roughness,
31    Occlusion,
32    Emissive,
33    F,
34    G,
35    D,
36    Diffuse,
37    Specular,
38    MipRainbow,
39}
40
41impl PbrDebugMode {
42    pub const ALL: &'static [PbrDebugMode] = &[
43        PbrDebugMode::None,
44        PbrDebugMode::BaseColor,
45        PbrDebugMode::Normal,
46        PbrDebugMode::Metallic,
47        PbrDebugMode::Roughness,
48        PbrDebugMode::Occlusion,
49        PbrDebugMode::Emissive,
50        PbrDebugMode::F,
51        PbrDebugMode::G,
52        PbrDebugMode::D,
53        PbrDebugMode::Diffuse,
54        PbrDebugMode::Specular,
55        PbrDebugMode::MipRainbow,
56    ];
57
58    pub fn name(&self) -> &'static str {
59        match self {
60            PbrDebugMode::None => "None",
61            PbrDebugMode::BaseColor => "Base Color",
62            PbrDebugMode::Normal => "Normal",
63            PbrDebugMode::Metallic => "Metallic",
64            PbrDebugMode::Roughness => "Roughness",
65            PbrDebugMode::Occlusion => "Occlusion",
66            PbrDebugMode::Emissive => "Emissive",
67            PbrDebugMode::F => "Fresnel (F)",
68            PbrDebugMode::G => "Geometry (G)",
69            PbrDebugMode::D => "Distribution (D)",
70            PbrDebugMode::Diffuse => "Diffuse",
71            PbrDebugMode::Specular => "Specular",
72            PbrDebugMode::MipRainbow => "Mip Rainbow",
73        }
74    }
75
76    pub fn as_u32(&self) -> u32 {
77        match self {
78            PbrDebugMode::None => 0,
79            PbrDebugMode::BaseColor => 1,
80            PbrDebugMode::Normal => 2,
81            PbrDebugMode::Metallic => 3,
82            PbrDebugMode::Roughness => 4,
83            PbrDebugMode::Occlusion => 5,
84            PbrDebugMode::Emissive => 6,
85            PbrDebugMode::F => 7,
86            PbrDebugMode::G => 8,
87            PbrDebugMode::D => 9,
88            PbrDebugMode::Diffuse => 10,
89            PbrDebugMode::Specular => 11,
90            PbrDebugMode::MipRainbow => 12,
91        }
92    }
93}
94
95/// Quality level for depth of field effect.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
97pub enum DepthOfFieldQuality {
98    /// 8 samples per pixel.
99    Low,
100    /// 16 samples per pixel.
101    #[default]
102    Medium,
103    /// 32 samples per pixel.
104    High,
105}
106
107impl DepthOfFieldQuality {
108    pub const ALL: &'static [DepthOfFieldQuality] = &[
109        DepthOfFieldQuality::Low,
110        DepthOfFieldQuality::Medium,
111        DepthOfFieldQuality::High,
112    ];
113
114    pub fn name(&self) -> &'static str {
115        match self {
116            DepthOfFieldQuality::Low => "Low",
117            DepthOfFieldQuality::Medium => "Medium",
118            DepthOfFieldQuality::High => "High",
119        }
120    }
121
122    pub fn sample_count(&self) -> u32 {
123        match self {
124            DepthOfFieldQuality::Low => 8,
125            DepthOfFieldQuality::Medium => 16,
126            DepthOfFieldQuality::High => 32,
127        }
128    }
129}
130
131/// Depth of field post-processing settings.
132#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
133pub struct DepthOfField {
134    /// Whether DOF is active.
135    pub enabled: bool,
136    /// Distance to the focal plane in world units.
137    pub focus_distance: f32,
138    /// Range around focus distance that remains sharp.
139    pub focus_range: f32,
140    /// Maximum blur radius in pixels.
141    pub max_blur_radius: f32,
142    /// Brightness threshold for bokeh highlights.
143    pub bokeh_threshold: f32,
144    /// Intensity multiplier for bokeh highlights.
145    pub bokeh_intensity: f32,
146    /// Sample count quality level.
147    pub quality: DepthOfFieldQuality,
148    /// Debug visualization of circle of confusion.
149    pub visualize_coc: bool,
150    /// Enable tilt-shift miniature effect.
151    pub tilt_shift_enabled: bool,
152    /// Angle of the tilt-shift band in radians.
153    pub tilt_shift_angle: f32,
154    /// Vertical position of the sharp band center (-1 to 1).
155    pub tilt_shift_center: f32,
156    /// Blur strength outside the sharp band.
157    pub tilt_shift_blur_amount: f32,
158    /// Debug visualization of tilt-shift mask.
159    pub visualize_tilt_shift: bool,
160}
161
162impl Default for DepthOfField {
163    fn default() -> Self {
164        Self {
165            enabled: false,
166            focus_distance: 10.0,
167            focus_range: 5.0,
168            max_blur_radius: 8.0,
169            bokeh_threshold: 0.8,
170            bokeh_intensity: 1.0,
171            quality: DepthOfFieldQuality::Medium,
172            visualize_coc: false,
173            tilt_shift_enabled: false,
174            tilt_shift_angle: 0.0,
175            tilt_shift_center: 0.0,
176            tilt_shift_blur_amount: 1.0,
177            visualize_tilt_shift: false,
178        }
179    }
180}
181
182impl DepthOfField {
183    /// Preset for close-up portraits with strong background blur.
184    pub fn portrait() -> Self {
185        Self {
186            enabled: true,
187            focus_distance: 3.0,
188            focus_range: 1.5,
189            max_blur_radius: 12.0,
190            bokeh_threshold: 0.6,
191            bokeh_intensity: 1.2,
192            quality: DepthOfFieldQuality::High,
193            visualize_coc: false,
194            tilt_shift_enabled: false,
195            tilt_shift_angle: 0.0,
196            tilt_shift_center: 0.0,
197            tilt_shift_blur_amount: 1.0,
198            visualize_tilt_shift: false,
199        }
200    }
201
202    /// Preset for cinematic medium-distance focus.
203    pub fn cinematic() -> Self {
204        Self {
205            enabled: true,
206            focus_distance: 8.0,
207            focus_range: 4.0,
208            max_blur_radius: 10.0,
209            bokeh_threshold: 0.7,
210            bokeh_intensity: 1.0,
211            quality: DepthOfFieldQuality::Medium,
212            visualize_coc: false,
213            tilt_shift_enabled: false,
214            tilt_shift_angle: 0.0,
215            tilt_shift_center: 0.0,
216            tilt_shift_blur_amount: 1.0,
217            visualize_tilt_shift: false,
218        }
219    }
220
221    pub fn macro_shot() -> Self {
222        Self {
223            enabled: true,
224            focus_distance: 0.5,
225            focus_range: 0.2,
226            max_blur_radius: 16.0,
227            bokeh_threshold: 0.5,
228            bokeh_intensity: 1.5,
229            quality: DepthOfFieldQuality::High,
230            visualize_coc: false,
231            tilt_shift_enabled: false,
232            tilt_shift_angle: 0.0,
233            tilt_shift_center: 0.0,
234            tilt_shift_blur_amount: 1.0,
235            visualize_tilt_shift: false,
236        }
237    }
238
239    pub fn landscape() -> Self {
240        Self {
241            enabled: true,
242            focus_distance: 50.0,
243            focus_range: 100.0,
244            max_blur_radius: 4.0,
245            bokeh_threshold: 0.9,
246            bokeh_intensity: 0.5,
247            quality: DepthOfFieldQuality::Low,
248            visualize_coc: false,
249            tilt_shift_enabled: false,
250            tilt_shift_angle: 0.0,
251            tilt_shift_center: 0.0,
252            tilt_shift_blur_amount: 1.0,
253            visualize_tilt_shift: false,
254        }
255    }
256
257    pub fn tilt_shift() -> Self {
258        Self {
259            enabled: true,
260            focus_distance: 10.0,
261            focus_range: 5.0,
262            max_blur_radius: 12.0,
263            bokeh_threshold: 0.8,
264            bokeh_intensity: 0.8,
265            quality: DepthOfFieldQuality::Medium,
266            visualize_coc: false,
267            tilt_shift_enabled: true,
268            tilt_shift_angle: 0.0,
269            tilt_shift_center: 0.0,
270            tilt_shift_blur_amount: 1.0,
271            visualize_tilt_shift: false,
272        }
273    }
274}
275
276/// HDR to LDR tonemapping algorithm.
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
278pub enum TonemapAlgorithm {
279    /// Academy Color Encoding System (Narkowicz approximation).
280    #[default]
281    Aces,
282    /// Fitted ACES (Stephen Hill) with AP1 color transforms.
283    Aces2,
284    /// Simple Reinhard curve.
285    Reinhard,
286    /// Extended Reinhard with white point.
287    ReinhardExtended,
288    /// Filmic curve from Uncharted 2.
289    Uncharted2,
290    /// AgX display transform.
291    AgX,
292    /// Neutral (minimal color shift).
293    Neutral,
294    /// No tonemapping (clamp only).
295    None,
296}
297
298impl TonemapAlgorithm {
299    pub const ALL: &'static [TonemapAlgorithm] = &[
300        TonemapAlgorithm::Aces,
301        TonemapAlgorithm::Aces2,
302        TonemapAlgorithm::Reinhard,
303        TonemapAlgorithm::ReinhardExtended,
304        TonemapAlgorithm::Uncharted2,
305        TonemapAlgorithm::AgX,
306        TonemapAlgorithm::Neutral,
307        TonemapAlgorithm::None,
308    ];
309
310    pub fn as_u32(&self) -> u32 {
311        match self {
312            TonemapAlgorithm::Aces => 0,
313            TonemapAlgorithm::Aces2 => 7,
314            TonemapAlgorithm::Reinhard => 1,
315            TonemapAlgorithm::ReinhardExtended => 2,
316            TonemapAlgorithm::Uncharted2 => 3,
317            TonemapAlgorithm::AgX => 4,
318            TonemapAlgorithm::Neutral => 5,
319            TonemapAlgorithm::None => 6,
320        }
321    }
322
323    pub fn name(&self) -> &'static str {
324        match self {
325            TonemapAlgorithm::Aces => "ACES",
326            TonemapAlgorithm::Aces2 => "ACES (Fitted)",
327            TonemapAlgorithm::Reinhard => "Reinhard",
328            TonemapAlgorithm::ReinhardExtended => "Reinhard Extended",
329            TonemapAlgorithm::Uncharted2 => "Uncharted 2",
330            TonemapAlgorithm::AgX => "AgX",
331            TonemapAlgorithm::Neutral => "Neutral",
332            TonemapAlgorithm::None => "None",
333        }
334    }
335}
336
337/// Pre-configured color grading style.
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
339pub enum ColorGradingPreset {
340    /// Neutral default settings.
341    #[default]
342    Default,
343    /// High saturation and brightness.
344    Vibrant,
345    /// Film-like with lifted blacks.
346    Cinematic,
347    /// Low saturation and contrast.
348    Muted,
349    /// Strong contrast with deep blacks.
350    HighContrast,
351    /// Orange-shifted warm tones.
352    Warm,
353    /// Blue-shifted cool tones.
354    Cool,
355    /// Faded vintage look.
356    Retro,
357    /// Nearly black and white.
358    Desaturated,
359    /// User-defined values.
360    Custom,
361}
362
363impl ColorGradingPreset {
364    pub const ALL: &'static [ColorGradingPreset] = &[
365        ColorGradingPreset::Default,
366        ColorGradingPreset::Vibrant,
367        ColorGradingPreset::Cinematic,
368        ColorGradingPreset::Muted,
369        ColorGradingPreset::HighContrast,
370        ColorGradingPreset::Warm,
371        ColorGradingPreset::Cool,
372        ColorGradingPreset::Retro,
373        ColorGradingPreset::Desaturated,
374        ColorGradingPreset::Custom,
375    ];
376
377    pub fn name(&self) -> &'static str {
378        match self {
379            ColorGradingPreset::Default => "Default",
380            ColorGradingPreset::Vibrant => "Vibrant",
381            ColorGradingPreset::Cinematic => "Cinematic",
382            ColorGradingPreset::Muted => "Muted",
383            ColorGradingPreset::HighContrast => "High Contrast",
384            ColorGradingPreset::Warm => "Warm",
385            ColorGradingPreset::Cool => "Cool",
386            ColorGradingPreset::Retro => "Retro",
387            ColorGradingPreset::Desaturated => "Desaturated",
388            ColorGradingPreset::Custom => "Custom",
389        }
390    }
391
392    pub fn to_color_grading(&self) -> ColorGrading {
393        let preset = *self;
394        match self {
395            ColorGradingPreset::Default => ColorGrading {
396                preset,
397                ..ColorGrading::default()
398            },
399            ColorGradingPreset::Vibrant => ColorGrading {
400                saturation: 1.3,
401                brightness: 0.02,
402                contrast: 1.1,
403                vibrance: 0.4,
404                preset,
405                ..ColorGrading::default()
406            },
407            ColorGradingPreset::Cinematic => ColorGrading {
408                gamma: 2.4,
409                saturation: 0.9,
410                brightness: -0.02,
411                contrast: 1.15,
412                preset,
413                ..ColorGrading::default()
414            },
415            ColorGradingPreset::Muted => ColorGrading {
416                saturation: 0.7,
417                contrast: 0.9,
418                tonemap_algorithm: TonemapAlgorithm::Reinhard,
419                preset,
420                ..ColorGrading::default()
421            },
422            ColorGradingPreset::HighContrast => ColorGrading {
423                saturation: 1.1,
424                contrast: 1.4,
425                preset,
426                ..ColorGrading::default()
427            },
428            ColorGradingPreset::Warm => ColorGrading {
429                gamma: 2.1,
430                saturation: 1.1,
431                brightness: 0.03,
432                contrast: 1.05,
433                preset,
434                ..ColorGrading::default()
435            },
436            ColorGradingPreset::Cool => ColorGrading {
437                gamma: 2.3,
438                saturation: 0.95,
439                brightness: -0.01,
440                contrast: 1.05,
441                tonemap_algorithm: TonemapAlgorithm::Neutral,
442                preset,
443                ..ColorGrading::default()
444            },
445            ColorGradingPreset::Retro => ColorGrading {
446                gamma: 2.0,
447                saturation: 0.8,
448                brightness: 0.05,
449                contrast: 1.2,
450                tonemap_algorithm: TonemapAlgorithm::Reinhard,
451                preset,
452                ..ColorGrading::default()
453            },
454            ColorGradingPreset::Desaturated => ColorGrading {
455                saturation: 0.3,
456                preset,
457                ..ColorGrading::default()
458            },
459            ColorGradingPreset::Custom => ColorGrading::default(),
460        }
461    }
462}
463
464/// Color grading and tonemapping settings.
465#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
466pub struct ColorGrading {
467    /// Linear exposure multiplier applied before tonemapping. Use small values
468    /// (e.g. 0.001) when the scene uses physical glTF light units (lux/candela).
469    pub exposure: f32,
470    /// Exposure compensation in EV stops applied on top of `exposure` and
471    /// auto-exposure. Multiplies the final color by `2^ev`.
472    pub exposure_compensation_ev: f32,
473    /// When true, derive exposure each frame from average HDR scene luminance
474    /// and use `exposure` as a manual compensation multiplier on top.
475    pub auto_exposure: bool,
476    /// Target luminance the auto-exposure tries to map to middle gray
477    /// (typical 0.18 = perceptual middle gray).
478    pub auto_exposure_target: f32,
479    /// Adaptation speed for the auto-exposure smoothing (1/seconds). 1.0 lerps
480    /// ~63% toward the new target each second; 5.0 is snappy; 0.2 is cinematic.
481    pub auto_exposure_rate: f32,
482    /// Lower bound on the exposure multiplier resolved from auto-exposure,
483    /// expressed in EV stops relative to `auto_exposure_target` mapping to
484    /// middle gray. Prevents night-vision overshoot in dark scenes.
485    pub auto_exposure_min_ev: f32,
486    /// Upper bound on the exposure multiplier resolved from auto-exposure,
487    /// expressed in EV stops. Prevents specular hot-spots from crushing
488    /// bright scenes.
489    pub auto_exposure_max_ev: f32,
490    /// Gamma correction exponent (typically 2.2).
491    pub gamma: f32,
492    /// Color saturation multiplier (1.0 = neutral).
493    pub saturation: f32,
494    /// Brightness offset (-1 to 1).
495    pub brightness: f32,
496    /// Contrast multiplier (1.0 = neutral).
497    pub contrast: f32,
498    /// Vibrance: saturation that spares already-saturated colors (0 = off).
499    pub vibrance: f32,
500    /// Vignette darkening strength at the frame edges (0 = off).
501    pub vignette_intensity: f32,
502    /// Normalized radius where the vignette begins (0 at center, 1 at corner).
503    pub vignette_radius: f32,
504    /// Falloff width of the vignette past its radius.
505    pub vignette_smoothness: f32,
506    /// Chromatic aberration strength: per-channel radial offset scaled by the
507    /// squared distance from the center (0 = off).
508    pub chromatic_aberration: f32,
509    /// Blend weight for the 3D color grading lookup table (0 = off, 1 = full).
510    /// Upload the table itself with a `SetColorLut` render command.
511    pub color_lut_weight: f32,
512    /// HDR tonemapping algorithm.
513    pub tonemap_algorithm: TonemapAlgorithm,
514    /// Active preset (Custom if manually adjusted).
515    pub preset: ColorGradingPreset,
516}
517
518impl Default for ColorGrading {
519    fn default() -> Self {
520        Self {
521            exposure: 1.0,
522            exposure_compensation_ev: 0.0,
523            auto_exposure: false,
524            auto_exposure_target: 0.18,
525            auto_exposure_rate: 1.5,
526            auto_exposure_min_ev: -3.0,
527            auto_exposure_max_ev: 5.0,
528            gamma: 2.2,
529            saturation: 1.0,
530            brightness: 0.0,
531            contrast: 1.0,
532            vibrance: 0.0,
533            vignette_intensity: 0.0,
534            vignette_radius: 0.6,
535            vignette_smoothness: 0.4,
536            chromatic_aberration: 0.0,
537            color_lut_weight: 0.0,
538            tonemap_algorithm: TonemapAlgorithm::Aces,
539            preset: ColorGradingPreset::Default,
540        }
541    }
542}
543
544/// PS1-style vertex snapping for retro rendering.
545#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
546pub struct VertexSnap {
547    /// Grid resolution to snap vertices to (lower = more pixelated).
548    pub resolution: [f32; 2],
549}
550
551impl Default for VertexSnap {
552    fn default() -> Self {
553        Self {
554            resolution: [160.0, 120.0],
555        }
556    }
557}
558
559/// Distance-based fog settings.
560#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
561pub struct Fog {
562    /// Fog color (linear RGB).
563    pub color: [f32; 3],
564    /// Distance where fog begins. For exponential modes this is the offset
565    /// where fog starts accumulating.
566    pub start: f32,
567    /// Distance where linear fog is fully opaque. For exponential modes this
568    /// is the distance at which fog is nearly opaque (drives the density).
569    pub end: f32,
570    /// How fog density grows with distance.
571    #[serde(default)]
572    pub mode: FogMode,
573}
574
575/// How fog density accumulates with distance.
576#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
577pub enum FogMode {
578    /// Linear ramp between `start` and `end`.
579    #[default]
580    Linear,
581    /// Exponential falloff (`1 - exp(-d)`), softer near the camera.
582    Exponential,
583    /// Exponential-squared falloff (`1 - exp(-d*d)`), a tighter band.
584    ExponentialSquared,
585}
586
587impl FogMode {
588    /// Shader mode index. 0 is reserved for "fog disabled".
589    pub fn as_u32(self) -> u32 {
590        match self {
591            FogMode::Linear => 1,
592            FogMode::Exponential => 2,
593            FogMode::ExponentialSquared => 3,
594        }
595    }
596
597    /// Build from a mode index, falling back to `Linear` for unknown values.
598    pub fn from_u32(value: u32) -> Self {
599        match value {
600            2 => FogMode::Exponential,
601            3 => FogMode::ExponentialSquared,
602            _ => FogMode::Linear,
603        }
604    }
605}
606
607impl Default for Fog {
608    fn default() -> Self {
609        Self {
610            color: [0.5, 0.5, 0.55],
611            start: 2.0,
612            end: 15.0,
613            mode: FogMode::Linear,
614        }
615    }
616}
617
618#[derive(Clone)]
619pub struct DayNightState {
620    pub hour: f32,
621    pub speed: f32,
622    pub auto_cycle: bool,
623    pub sun_entity: Option<crate::entity::RenderEntity>,
624    /// Hours the renderer bakes image based lighting snapshots at when the
625    /// day/night hour starts changing, so lighting blends smoothly between them.
626    pub ibl_snapshot_hours: Vec<f32>,
627}
628
629impl Default for DayNightState {
630    fn default() -> Self {
631        Self {
632            hour: 12.0,
633            speed: 0.0,
634            auto_cycle: false,
635            sun_entity: None,
636            ibl_snapshot_hours: vec![0.0, 4.0, 7.0, 10.0, 14.0, 17.0, 20.0],
637        }
638    }
639}
640
641/// Skybox and environment map selection.
642#[derive(
643    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, enum2schema::Schema,
644)]
645#[schema(string_enum)]
646pub enum Atmosphere {
647    /// Solid background color (no skybox).
648    #[default]
649    None,
650    /// Procedural clear sky gradient.
651    Sky,
652    /// Procedural sky with volumetric clouds.
653    CloudySky,
654    /// Procedural starfield.
655    Space,
656    /// Procedural nebula with stars.
657    Nebula,
658    /// Procedural sunset gradient.
659    Sunset,
660    /// Procedural day/night cycle driven by hour parameter.
661    DayNight,
662    /// HDR environment cubemap.
663    Hdr,
664    /// Debug: HDR mip level 0.
665    HdrMip0,
666    /// Debug: HDR mip level 1.
667    HdrMip1,
668    /// Debug: HDR mip level 2.
669    HdrMip2,
670    /// Debug: HDR mip level 3.
671    HdrMip3,
672    /// Debug: HDR mip level 4.
673    HdrMip4,
674    /// Debug: Diffuse irradiance map.
675    Irradiance,
676    /// Debug: Prefiltered specular mip 0.
677    PrefilterMip0,
678    /// Debug: Prefiltered specular mip 1.
679    PrefilterMip1,
680    /// Debug: Prefiltered specular mip 2.
681    PrefilterMip2,
682    /// Debug: Prefiltered specular mip 3.
683    PrefilterMip3,
684    /// Debug: Prefiltered specular mip 4.
685    PrefilterMip4,
686}
687
688impl Atmosphere {
689    pub const ALL: &'static [Atmosphere] = &[
690        Atmosphere::None,
691        Atmosphere::Sky,
692        Atmosphere::CloudySky,
693        Atmosphere::Space,
694        Atmosphere::Nebula,
695        Atmosphere::Sunset,
696        Atmosphere::DayNight,
697        Atmosphere::Hdr,
698        Atmosphere::HdrMip0,
699        Atmosphere::HdrMip1,
700        Atmosphere::HdrMip2,
701        Atmosphere::HdrMip3,
702        Atmosphere::HdrMip4,
703        Atmosphere::Irradiance,
704        Atmosphere::PrefilterMip0,
705        Atmosphere::PrefilterMip1,
706        Atmosphere::PrefilterMip2,
707        Atmosphere::PrefilterMip3,
708        Atmosphere::PrefilterMip4,
709    ];
710
711    pub fn mip_level(&self) -> f32 {
712        match self {
713            Atmosphere::HdrMip0 | Atmosphere::PrefilterMip0 => 0.0,
714            Atmosphere::HdrMip1 | Atmosphere::PrefilterMip1 => 1.0,
715            Atmosphere::HdrMip2 | Atmosphere::PrefilterMip2 => 2.0,
716            Atmosphere::HdrMip3 | Atmosphere::PrefilterMip3 => 3.0,
717            Atmosphere::HdrMip4 | Atmosphere::PrefilterMip4 => 4.0,
718            _ => 0.0,
719        }
720    }
721
722    pub fn next(self) -> Self {
723        let all = Self::ALL;
724        let current_index = all.iter().position(|&a| a == self).unwrap_or(0);
725        let next_index = (current_index + 1) % all.len();
726        all[next_index]
727    }
728
729    pub fn previous(self) -> Self {
730        let all = Self::ALL;
731        let current_index = all.iter().position(|&a| a == self).unwrap_or(0);
732        let prev_index = if current_index == 0 {
733            all.len() - 1
734        } else {
735            current_index - 1
736        };
737        all[prev_index]
738    }
739
740    pub fn is_procedural(&self) -> bool {
741        matches!(
742            self,
743            Atmosphere::Sky
744                | Atmosphere::CloudySky
745                | Atmosphere::Space
746                | Atmosphere::Nebula
747                | Atmosphere::Sunset
748                | Atmosphere::DayNight
749        )
750    }
751
752    pub fn as_procedural_cubemap_type(&self) -> Option<u32> {
753        match self {
754            Atmosphere::Sky => Some(0),
755            Atmosphere::CloudySky => Some(1),
756            Atmosphere::Space => Some(2),
757            Atmosphere::Nebula => Some(3),
758            Atmosphere::Sunset => Some(4),
759            Atmosphere::DayNight => Some(5),
760            _ => None,
761        }
762    }
763}
764
765#[derive(Default)]
766pub struct IblViews {
767    pub brdf_lut_view: Option<wgpu::TextureView>,
768    pub irradiance_view: Option<wgpu::TextureView>,
769    pub prefiltered_view: Option<wgpu::TextureView>,
770}
771
772impl Clone for IblViews {
773    fn clone(&self) -> Self {
774        Self {
775            brdf_lut_view: self.brdf_lut_view.clone(),
776            irradiance_view: self.irradiance_view.clone(),
777            prefiltered_view: self.prefiltered_view.clone(),
778        }
779    }
780}
781
782#[derive(Clone)]
783pub struct MeshLodLevel {
784    pub mesh_name: String,
785    pub min_screen_pixels: f32,
786}
787
788#[derive(Clone)]
789pub struct MeshLodChain {
790    pub base_mesh: String,
791    pub levels: Vec<MeshLodLevel>,
792}
793
794/// GPU uniform layout for the configurable post-process effects pass.
795/// 38 packed `f32`s mapped 1:1 to the WGSL uniform binding in
796/// `crates/nightshade/src/render/wgpu/shaders/effects.wgsl`. The
797/// `EffectsPass` writes this buffer each frame from
798/// `Graphics::effects.uniforms` plus the live frame time.
799#[repr(C)]
800#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
801pub struct EffectsUniforms {
802    pub time: f32,
803    pub chromatic_aberration: f32,
804    pub wave_distortion: f32,
805    pub color_shift: f32,
806    pub kaleidoscope_segments: f32,
807    pub crt_scanlines: f32,
808    pub vignette: f32,
809    pub plasma_intensity: f32,
810    pub glitch_intensity: f32,
811    pub mirror_mode: f32,
812    pub invert: f32,
813    pub hue_rotation: f32,
814    pub raymarch_mode: f32,
815    pub raymarch_blend: f32,
816    pub film_grain: f32,
817    pub sharpen: f32,
818    pub pixelate: f32,
819    pub color_posterize: f32,
820    pub radial_blur: f32,
821    pub tunnel_speed: f32,
822    pub fractal_iterations: f32,
823    pub glow_intensity: f32,
824    pub screen_shake: f32,
825    pub zoom_pulse: f32,
826    pub speed_lines: f32,
827    pub color_grade_mode: f32,
828    pub vhs_distortion: f32,
829    pub lens_flare: f32,
830    pub edge_glow: f32,
831    pub saturation: f32,
832    pub warp_speed: f32,
833    pub pulse_rings: f32,
834    pub heat_distortion: f32,
835    pub digital_rain: f32,
836    pub strobe: f32,
837    pub color_cycle_speed: f32,
838    pub feedback_amount: f32,
839    pub ascii_mode: f32,
840}
841
842impl Default for EffectsUniforms {
843    fn default() -> Self {
844        Self {
845            time: 0.0,
846            chromatic_aberration: 0.0,
847            wave_distortion: 0.0,
848            color_shift: 0.0,
849            kaleidoscope_segments: 0.0,
850            crt_scanlines: 0.0,
851            vignette: 0.0,
852            plasma_intensity: 0.0,
853            glitch_intensity: 0.0,
854            mirror_mode: 0.0,
855            invert: 0.0,
856            hue_rotation: 0.0,
857            raymarch_mode: 0.0,
858            raymarch_blend: 0.0,
859            film_grain: 0.0,
860            sharpen: 0.0,
861            pixelate: 0.0,
862            color_posterize: 0.0,
863            radial_blur: 0.0,
864            tunnel_speed: 1.0,
865            fractal_iterations: 4.0,
866            glow_intensity: 0.0,
867            screen_shake: 0.0,
868            zoom_pulse: 0.0,
869            speed_lines: 0.0,
870            color_grade_mode: 0.0,
871            vhs_distortion: 0.0,
872            lens_flare: 0.0,
873            edge_glow: 0.0,
874            saturation: 1.0,
875            warp_speed: 0.0,
876            pulse_rings: 0.0,
877            heat_distortion: 0.0,
878            digital_rain: 0.0,
879            strobe: 0.0,
880            color_cycle_speed: 1.0,
881            feedback_amount: 0.0,
882            ascii_mode: 0.0,
883        }
884    }
885}
886
887#[derive(Clone, Copy, Debug, PartialEq, Eq)]
888pub enum RaymarchMode {
889    Off = 0,
890    Tunnel = 1,
891    Fractal = 2,
892    Mandelbulb = 3,
893    PlasmaVortex = 4,
894    Geometric = 5,
895}
896
897impl From<u32> for RaymarchMode {
898    fn from(value: u32) -> Self {
899        match value {
900            1 => RaymarchMode::Tunnel,
901            2 => RaymarchMode::Fractal,
902            3 => RaymarchMode::Mandelbulb,
903            4 => RaymarchMode::PlasmaVortex,
904            5 => RaymarchMode::Geometric,
905            _ => RaymarchMode::Off,
906        }
907    }
908}
909
910#[derive(Clone, Copy, Debug, PartialEq, Eq)]
911pub enum ColorGradeMode {
912    None = 0,
913    Cyberpunk = 1,
914    Sunset = 2,
915    Grayscale = 3,
916    Sepia = 4,
917    Matrix = 5,
918    HotMetal = 6,
919}
920
921impl From<u32> for ColorGradeMode {
922    fn from(value: u32) -> Self {
923        match value {
924            1 => ColorGradeMode::Cyberpunk,
925            2 => ColorGradeMode::Sunset,
926            3 => ColorGradeMode::Grayscale,
927            4 => ColorGradeMode::Sepia,
928            5 => ColorGradeMode::Matrix,
929            6 => ColorGradeMode::HotMetal,
930            _ => ColorGradeMode::None,
931        }
932    }
933}
934
935/// Post-process effects pass settings. Held in
936/// `Graphics::effects` as plain data; the `EffectsPass` reads it from
937/// `RendererState::effects` each frame and uploads
938/// `uniforms` (with `time`, and optionally `hue_rotation`, overridden
939/// for the current frame) to the GPU.
940#[derive(Clone, Debug)]
941pub struct EffectsState {
942    pub uniforms: EffectsUniforms,
943    pub enabled: bool,
944    pub animate_hue: bool,
945}
946
947impl Default for EffectsState {
948    fn default() -> Self {
949        Self {
950            uniforms: EffectsUniforms::default(),
951            enabled: true,
952            animate_hue: false,
953        }
954    }
955}
956
957/// User-tunable rendering settings. This is the subset of former
958/// `Graphics` state that materially controls how a scene is shaded and
959/// is safe to persist. Carried in `RenderInputs::settings`.
960#[derive(Clone, Serialize, Deserialize)]
961pub struct RenderSettings {
962    /// Software frame rate cap in frames per second. `None` lets the
963    /// present mode pace the loop (typically vsync or unbounded under
964    /// Mailbox). `Some(fps)` sleeps after each frame to hold the loop
965    /// near the target rate.
966    ///
967    /// On macOS this defaults to `refresh_rate - 1` at window creation
968    /// (and re-applies when the window moves to a monitor with a
969    /// different refresh rate) when the value still matches what the
970    /// engine auto-applied.
971    pub frame_rate_limit: Option<f32>,
972    /// Active skybox/environment map.
973    pub atmosphere: Atmosphere,
974    /// When false, the sky pass is skipped so the atmosphere does not render
975    /// to the color buffer. IBL contributions are unaffected.
976    pub show_sky: bool,
977    /// Enable world render layer.
978    pub render_layer_world_enabled: bool,
979    /// Enable overlay render layer.
980    pub render_layer_overlay_enabled: bool,
981    /// When true, the world renders to the swapchain even when no viewport tile is requesting
982    /// a camera. Default true, which is the normal "game" behaviour: the world fills the window
983    /// and any retained UI overlays on top of it. UI-first apps (asset viewers, gallery demos)
984    /// can set this to false so an idle frame skips all scene passes and only paints the UI.
985    pub render_world_to_swapchain: bool,
986    /// Background clear color.
987    pub clear_color: [f32; 4],
988    /// Override UI scale factor (None = automatic).
989    pub ui_scale: Option<f32>,
990    /// Disable all lighting calculations.
991    pub unlit_mode: bool,
992    /// PS1-style vertex snapping settings.
993    pub vertex_snap: Option<VertexSnap>,
994    /// PS1-style affine texture mapping.
995    pub affine_texture_mapping: bool,
996    /// Anisotropic filtering clamp for material textures (1 = disabled,
997    /// max 16). Default 16 for crisp PBR textures at grazing angles.
998    pub material_anisotropy_filtering: u16,
999    /// Distance fog settings.
1000    pub fog: Option<Fog>,
1001    /// Color grading and tonemapping.
1002    pub color_grading: ColorGrading,
1003    /// Enable bloom post-processing.
1004    pub bloom_enabled: bool,
1005    /// Bloom intensity multiplier.
1006    pub bloom_intensity: f32,
1007    /// Brightness threshold for bloom extraction (pixels below this luminance are excluded).
1008    pub bloom_threshold: f32,
1009    /// Soft-knee width around `bloom_threshold` (0 = hard cut).
1010    pub bloom_knee: f32,
1011    /// Upsample blur radius in normalized UV units.
1012    pub bloom_filter_radius: f32,
1013    /// Depth of field settings.
1014    pub depth_of_field: DepthOfField,
1015    /// Enable screen-space ambient occlusion.
1016    pub ssao_enabled: bool,
1017    /// SSAO sample radius in world units.
1018    pub ssao_radius: f32,
1019    /// SSAO depth bias to reduce self-occlusion.
1020    pub ssao_bias: f32,
1021    /// SSAO darkening intensity.
1022    pub ssao_intensity: f32,
1023    pub ssao_sample_count: u32,
1024    pub ssao_blur_depth_threshold: f32,
1025    pub ssao_blur_normal_power: f32,
1026    pub ssgi_enabled: bool,
1027    pub ssgi_radius: f32,
1028    pub ssgi_intensity: f32,
1029    pub ssgi_max_steps: u32,
1030    pub ssr_enabled: bool,
1031    pub ssr_max_steps: u32,
1032    pub ssr_thickness: f32,
1033    pub ssr_max_distance: f32,
1034    pub ssr_stride: f32,
1035    pub ssr_fade_start: f32,
1036    pub ssr_fade_end: f32,
1037    pub ssr_intensity: f32,
1038    /// Global ambient light color and intensity.
1039    pub ambient_light: [f32; 4],
1040    /// Enables temporal antialiasing. Off by default; apps and scripts turn it
1041    /// on when they want it. When off, the temporal resolve passes the image
1042    /// through unchanged and camera jitter is disabled.
1043    pub taa_enabled: bool,
1044    /// Temporal resolve blend toward the current frame each frame. Lower keeps
1045    /// more history (steadier, more ghost-prone), higher favors the current
1046    /// frame (sharper, noisier).
1047    pub taa_blend: f32,
1048    /// Post-resolve sharpening strength applied to the displayed image only.
1049    pub taa_sharpness: f32,
1050    /// Master switch for the water surface pass. The pass also no-ops when no
1051    /// `Water` entities exist, so this only matters when bodies are present.
1052    pub water_enabled: bool,
1053    /// When true, the swapchain runs in `Fifo` (vsync). When false, the
1054    /// renderer picks the lowest-latency present mode the adapter
1055    /// supports. The renderer compares this against the active swapchain
1056    /// configuration each frame and reconfigures live. No restart required.
1057    pub vsync_enabled: bool,
1058    /// Per-camera render-resolution multiplier. Clamped to `[0.25, 4.0]` at use.
1059    pub render_scale: f32,
1060    /// IBL blend factor for interpolating between snapshot cubemaps (0.0 - 1.0).
1061    pub ibl_blend_factor: f32,
1062}
1063
1064/// Debug visualization toggles. Carried in `RenderInputs::debug_draw`.
1065#[derive(Clone)]
1066pub struct DebugDraw {
1067    /// Show debug grid overlay.
1068    pub show_grid: bool,
1069    /// Show bounding volumes for all entities.
1070    pub show_bounding_volumes: bool,
1071    /// Show bounding volume for selected entity only.
1072    pub show_selected_bounding_volume: bool,
1073    /// Show vertex normal debug lines.
1074    pub show_normals: bool,
1075    /// Length of normal debug lines.
1076    pub normal_line_length: f32,
1077    /// Color for normal debug lines.
1078    pub normal_line_color: [f32; 4],
1079    /// PBR debug visualization mode.
1080    pub pbr_debug_mode: PbrDebugMode,
1081    /// Show animated debug stripes on textures.
1082    pub texture_debug_stripes: bool,
1083    /// Animation speed for debug stripes.
1084    pub texture_debug_stripes_speed: f32,
1085    pub ssao_visualization: bool,
1086}
1087
1088/// The GPU class the renderer ended up on, mirrored from `wgpu::DeviceType`
1089/// so apps can pick quality presets without depending on wgpu directly.
1090#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1091pub enum GpuDeviceType {
1092    #[default]
1093    Other,
1094    IntegratedGpu,
1095    DiscreteGpu,
1096    VirtualGpu,
1097    Cpu,
1098}
1099
1100/// The graphics backend the renderer is running on. `WebGl` is the constrained
1101/// browser fallback and is the strongest signal that a mobile-class budget is
1102/// appropriate.
1103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1104pub enum GpuBackend {
1105    #[default]
1106    Other,
1107    Vulkan,
1108    Metal,
1109    Dx12,
1110    Gl,
1111    WebGpu,
1112    WebGl,
1113}
1114
1115/// A snapshot of the adapter the renderer selected, published into
1116/// `RendererState` so game code can adapt quality and controls to the device.
1117#[derive(Debug, Clone, Default)]
1118pub struct GpuProfile {
1119    pub backend: GpuBackend,
1120    pub device_type: GpuDeviceType,
1121    pub name: String,
1122}
1123
1124impl GpuProfile {
1125    /// True when the renderer fell back to the browser WebGL path, the clearest
1126    /// indication of a constrained mobile-class GPU budget.
1127    pub fn is_webgl(&self) -> bool {
1128        self.backend == GpuBackend::WebGl
1129    }
1130
1131    /// True when the adapter is an integrated, virtual, or software device, the
1132    /// classes that benefit from a reduced render budget.
1133    pub fn is_low_power(&self) -> bool {
1134        matches!(
1135            self.device_type,
1136            GpuDeviceType::IntegratedGpu | GpuDeviceType::VirtualGpu | GpuDeviceType::Cpu
1137        )
1138    }
1139}
1140
1141/// Per-dispatch camera state the renderer extracts once at the start of each
1142/// per-camera render and every pass reads, replacing scattered
1143/// `query_active_camera_matrices` calls and their per-pass re-derivations.
1144/// `projection` carries the TAA jitter, matching `query_active_camera_matrices`.
1145#[derive(Clone, Debug)]
1146pub struct RenderView {
1147    pub view: nalgebra_glm::Mat4,
1148    pub projection: nalgebra_glm::Mat4,
1149    pub view_projection: nalgebra_glm::Mat4,
1150    pub inverse_view: nalgebra_glm::Mat4,
1151    pub inverse_projection: nalgebra_glm::Mat4,
1152    pub inverse_view_projection: nalgebra_glm::Mat4,
1153    pub camera_position: nalgebra_glm::Vec3,
1154    pub camera_right: nalgebra_glm::Vec3,
1155    pub camera_up: nalgebra_glm::Vec3,
1156    pub frustum_planes: [nalgebra_glm::Vec4; 6],
1157    pub z_near: f32,
1158    pub z_far: f32,
1159    pub y_fov_rad: f32,
1160    pub aspect: f32,
1161    pub orthographic: Option<(f32, f32)>,
1162    pub screen_size: (u32, u32),
1163    pub constrained_aspect: Option<f32>,
1164}
1165
1166/// Scene lighting the renderer collects once per dispatch and every lit pass
1167/// reads, replacing per-pass `collect_lights`, `collect_area_lights`,
1168/// `calculate_cascade_shadows`, and `query_sun` calls. The base light list is
1169/// carried without per-pass shadow-index application; each pass still resolves
1170/// its own spotlight, point, and cookie indices against its texture arrays.
1171#[cfg(feature = "wgpu")]
1172#[derive(Clone, Default)]
1173pub struct RenderLighting {
1174    pub lights_data: Vec<crate::wgpu::passes::geometry::projection::LightData>,
1175    pub num_directional_lights: u32,
1176    pub directional_light_direction: [f32; 4],
1177    pub has_directional_light: bool,
1178    pub entity_to_lights_index: std::collections::HashMap<crate::entity::RenderEntity, usize>,
1179    pub cascade_view_projections: [[[f32; 4]; 4]; crate::wgpu::passes::NUM_SHADOW_CASCADES],
1180    pub cascade_diameters: [f32; crate::wgpu::passes::NUM_SHADOW_CASCADES],
1181    pub cascade_split_distances: [f32; crate::wgpu::passes::NUM_SHADOW_CASCADES],
1182    pub light_view_projection: [[f32; 4]; 4],
1183    pub shadow_bias: f32,
1184    pub shadow_normal_bias: f32,
1185    pub directional_light_size: f32,
1186    pub shadows_enabled: f32,
1187    pub area_lights_data: Vec<crate::wgpu::passes::geometry::projection::AreaLightData>,
1188    pub area_entity_to_index: std::collections::HashMap<crate::entity::RenderEntity, usize>,
1189    pub sun_direction: nalgebra_glm::Vec3,
1190    pub sun_color: nalgebra_glm::Vec3,
1191}
1192
1193/// Per-entity dynamic render state the renderer refreshes each frame without a
1194/// full rebuild: whether the object is visible and its current morph weights.
1195#[derive(Clone, Copy, Debug)]
1196pub struct DynamicObjectState {
1197    pub visible: u32,
1198    pub morph_weights: [f32; 8],
1199    pub transform: nalgebra_glm::Mat4,
1200    pub culling_mask: u32,
1201    pub is_overlay: u32,
1202    pub render_layer: u32,
1203}
1204
1205/// Expanded per-instance data for one instanced-mesh entity, snapshotted once
1206/// per frame so the renderer builds instanced draws without reading the
1207/// `InstancedMesh` component.
1208#[derive(Clone, Default)]
1209pub struct InstancedObjectData {
1210    pub world_models: Vec<[[f32; 4]; 4]>,
1211    pub world_normals: Vec<[[f32; 4]; 3]>,
1212    pub local_matrices: Vec<nalgebra_glm::Mat4>,
1213    pub custom_tints: Vec<[f32; 4]>,
1214    pub mesh_name: String,
1215    pub render_layer: u32,
1216    pub visible: u32,
1217    pub parent_transform: nalgebra_glm::Mat4,
1218}
1219
1220/// A single animation channel flattened to GPU-ready arrays, produced by the
1221/// engine so the renderer never clones keyframe data per frame.
1222#[derive(Clone)]
1223pub struct SkinnedChannelData {
1224    pub property: u32,
1225    pub interpolation: u32,
1226    pub input: Vec<f32>,
1227    pub values: Vec<[f32; 4]>,
1228    pub stride: u32,
1229}
1230
1231/// One joint of a skinned skeleton in the renderer's dispatch order, with its
1232/// rest pose and resolved animation channels for the current clip, the
1233/// cross-fade source clip, and each animation layer.
1234#[derive(Clone)]
1235pub struct SkinnedJointData {
1236    pub local_index: u32,
1237    pub parent_local: Option<u32>,
1238    pub rest_translation: [f32; 3],
1239    pub rest_rotation: [f32; 4],
1240    pub rest_scale: [f32; 3],
1241    pub cur_channels: Vec<SkinnedChannelData>,
1242    pub blend_channels: Vec<SkinnedChannelData>,
1243    pub layer_channels: Vec<Vec<SkinnedChannelData>>,
1244}
1245
1246/// One animated skeleton resolved by the engine: its skin and player entities,
1247/// depth-ordered joints, and how many animation layers the player exposes.
1248#[derive(Clone)]
1249pub struct SkinnedSkeletonData {
1250    pub skin_entity: crate::entity::RenderEntity,
1251    pub player_entity: crate::entity::RenderEntity,
1252    /// The entity above the skeleton's root joints, captured at build time so
1253    /// the per-frame armature-root refresh is one transform read instead of a
1254    /// joint-parent walk.
1255    pub armature_parent: Option<crate::entity::RenderEntity>,
1256    pub joint_count: u32,
1257    pub layer_count: u32,
1258    pub joints_ordered: Vec<SkinnedJointData>,
1259}
1260
1261/// Skinning palette resolved once per frame by the engine so the skinned-mesh
1262/// and shadow passes build their GPU buffers without reading Skin components or
1263/// bone transforms. The static layout (`cache`) is rebuilt only when the skinned
1264/// set changes, bumping `static_generation`; `bone_transforms` refreshes every
1265/// frame.
1266#[derive(Default, Clone)]
1267pub struct RenderSkinning {
1268    pub cache: crate::skinning::SkinningCache,
1269    pub bone_transforms: Vec<nalgebra_glm::Mat4>,
1270    pub static_generation: u64,
1271    /// Bumped whenever any bone transform changed since the last sync, so
1272    /// consumers can gate uploads on a counter instead of comparing the
1273    /// whole palette.
1274    pub bone_transforms_generation: u64,
1275}
1276
1277/// Animation state resolved once per frame by the engine so the skinned-mesh
1278/// compute driver builds its GPU buffers without reading ECS animation, skin,
1279/// parent, or transform components. The heavy `skeletons` payload is rebuilt
1280/// only when the engine's animation gate sees a change, bumping `signature`;
1281/// the runtime maps refresh every frame.
1282#[derive(Clone, Default)]
1283pub struct SkinnedAnimationSnapshot {
1284    pub signature: u64,
1285    pub skeletons: Vec<SkinnedSkeletonData>,
1286    pub player_runtime: std::collections::HashMap<crate::entity::RenderEntity, [f32; 3]>,
1287    pub layer_runtime: std::collections::HashMap<(crate::entity::RenderEntity, usize), [f32; 2]>,
1288    pub armature_roots: std::collections::HashMap<crate::entity::RenderEntity, nalgebra_glm::Mat4>,
1289}
1290
1291/// A resolved material for the renderer: the engine-side material data plus its
1292/// resolved texture ids. The renderer converts this to GPU material data with
1293/// its own bindless texture-layer map. Material id `N` maps to `entries[N - 1]`;
1294/// id 0 is the built-in default material.
1295#[derive(Clone, Default)]
1296pub struct RenderMaterialEntry {
1297    pub material: crate::material::Material,
1298    pub texture_ids: crate::material::MaterialTextureIds,
1299}
1300
1301/// The scene's materials resolved once per frame by the engine, so the render
1302/// passes read materials here instead of walking the material registry and each
1303/// entity's `MaterialRef` themselves.
1304#[derive(Clone, Default)]
1305pub struct RenderMaterials {
1306    pub entries: Vec<RenderMaterialEntry>,
1307    pub name_to_id: std::collections::HashMap<String, u32>,
1308    pub entity_to_id: std::collections::HashMap<crate::entity::RenderEntity, u32>,
1309    pub entity_to_name: std::collections::HashMap<crate::entity::RenderEntity, String>,
1310    pub transparent_ids: std::collections::HashSet<u32>,
1311    pub mask_ids: std::collections::HashSet<u32>,
1312    pub double_sided_ids: std::collections::HashSet<u32>,
1313    /// Bumped whenever the table or the entity maps change, so consumers can
1314    /// gate their material conversions on a counter.
1315    pub generation: u64,
1316}
1317
1318/// One scene light with its world transform, snapshotted once per frame so the
1319/// lighting, shadow, and projection passes read lights here instead of walking
1320/// the ECS themselves.
1321/// A light's kind, mirrored from the `LightType` component enum so the render
1322/// passes do not read that type.
1323#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1324pub enum RenderLightType {
1325    #[default]
1326    Directional,
1327    Point,
1328    Spot,
1329    Area,
1330}
1331
1332/// An area light's shape, mirrored from the `AreaLightShape` component enum so
1333/// the render passes do not read that type.
1334#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1335pub enum RenderAreaLightShape {
1336    #[default]
1337    Rectangle,
1338    Disk,
1339    Sphere,
1340    Tube,
1341}
1342
1343/// A light's render parameters snapshotted once per frame from the `Light`
1344/// component, so the lighting, shadow, and projection passes read them here
1345/// instead of that component.
1346#[derive(Clone)]
1347pub struct RenderLightData {
1348    pub light_type: RenderLightType,
1349    pub color: nalgebra_glm::Vec3,
1350    pub intensity: f32,
1351    pub range: f32,
1352    pub inner_cone_angle: f32,
1353    pub outer_cone_angle: f32,
1354    pub cast_shadows: bool,
1355    pub shadow_bias: f32,
1356    pub shadow_resolution: u32,
1357    pub shadow_distance: f32,
1358    pub cookie_texture: Option<String>,
1359    pub area_shape: RenderAreaLightShape,
1360    pub area_width: f32,
1361    pub area_height: f32,
1362    pub area_radius: f32,
1363    pub area_two_sided: bool,
1364    pub area_emissive_texture: Option<String>,
1365    pub shadow_normal_bias: f32,
1366    pub shadow_softness: f32,
1367}
1368
1369#[derive(Clone)]
1370pub struct RenderLight {
1371    pub entity: crate::entity::RenderEntity,
1372    pub light: RenderLightData,
1373    pub transform: nalgebra_glm::Mat4,
1374}
1375
1376/// The scene's lights collected once per frame in query order, with an
1377/// entity-to-index map for point lookups. Passes that shade, shadow, or project
1378/// lights read this instead of `query_entities(LIGHT | GLOBAL_TRANSFORM)`.
1379#[derive(Clone, Default)]
1380pub struct RenderLights {
1381    pub lights: Vec<RenderLight>,
1382    pub entity_to_index: std::collections::HashMap<crate::entity::RenderEntity, usize>,
1383}
1384
1385/// A projected decal's render parameters snapshotted once per frame from the
1386/// `Decal` component, so the decal pass reads them here instead of that
1387/// component.
1388#[derive(Clone)]
1389pub struct RenderDecalData {
1390    pub texture: Option<String>,
1391    pub emissive_texture: Option<String>,
1392    pub emissive_strength: f32,
1393    pub color: [f32; 4],
1394    pub size: nalgebra_glm::Vec2,
1395    pub depth: f32,
1396    pub normal_threshold: f32,
1397    pub fade_start: f32,
1398    pub fade_end: f32,
1399}
1400
1401/// One projected decal with its world transform and visibility, snapshotted once
1402/// per frame so the decal pass reads it here instead of walking the ECS.
1403#[derive(Clone)]
1404pub struct RenderDecal {
1405    pub decal: RenderDecalData,
1406    pub transform: nalgebra_glm::Mat4,
1407    pub visible: bool,
1408}
1409
1410/// A water body's render parameters snapshotted once per frame from the `Water`
1411/// component, so the water pass reads them here instead of that component.
1412#[derive(Clone, Debug)]
1413pub struct RenderWaterData {
1414    pub half_extents: nalgebra_glm::Vec2,
1415    pub tessellation: u32,
1416    pub wave_amplitude: f32,
1417    pub wave_steepness: f32,
1418    pub wave_length: f32,
1419    pub wave_speed: f32,
1420    pub wave_direction_radians: f32,
1421    pub shallow_color: [f32; 3],
1422    pub deep_color: [f32; 3],
1423    pub depth_fade_distance: f32,
1424    pub edge_foam_distance: f32,
1425    pub foam_amount: f32,
1426    pub foam_color: [f32; 3],
1427    pub roughness: f32,
1428    pub fresnel_power: f32,
1429    pub reflection_strength: f32,
1430    pub refraction_strength: f32,
1431    pub specular_strength: f32,
1432}
1433
1434/// One water body with its world transform, snapshotted once per frame so the
1435/// water pass reads it here instead of walking `WATER | GLOBAL_TRANSFORM`.
1436#[derive(Clone)]
1437pub struct RenderWater {
1438    pub water: RenderWaterData,
1439    pub transform: nalgebra_glm::Mat4,
1440}
1441
1442/// Which cloth particles anchor to the entity transform, mirrored from the
1443/// `ClothPinning` component enum so the cloth pass does not read that type.
1444#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1445pub enum RenderClothPinning {
1446    #[default]
1447    TopRow,
1448    TopCorners,
1449    None,
1450}
1451
1452/// A cloth body's simulation parameters snapshotted once per frame from the
1453/// `Cloth` component, so the cloth pass reads them here instead of that
1454/// component. Carries every field so the pass's config-change comparison still
1455/// detects rebuilds and resets.
1456#[derive(Clone, Debug, PartialEq)]
1457pub struct RenderClothData {
1458    pub columns: u32,
1459    pub rows: u32,
1460    pub width: f32,
1461    pub height: f32,
1462    pub pinning: RenderClothPinning,
1463    pub stiffness: f32,
1464    pub damping: f32,
1465    pub substeps: u32,
1466    pub solver_iterations: u32,
1467    pub gravity: nalgebra_glm::Vec3,
1468    pub wind_response: f32,
1469    pub ground_height: Option<f32>,
1470    pub texture_tiling: nalgebra_glm::Vec2,
1471    pub reset_epoch: u32,
1472}
1473
1474/// One cloth body with its anchor transform and mesh name, snapshotted once per
1475/// frame so the cloth pass reads it here instead of walking the ECS.
1476#[derive(Clone)]
1477pub struct RenderCloth {
1478    pub entity: crate::entity::RenderEntity,
1479    pub cloth: RenderClothData,
1480    pub transform: nalgebra_glm::Mat4,
1481    pub mesh_name: String,
1482}
1483
1484/// One particle emitter snapshotted once per frame so the particle pass reads it
1485/// here instead of the `ParticleEmitter` component. Every emitter is captured in
1486/// query order, disabled ones included, so an emitter's index is stable.
1487#[derive(Clone)]
1488pub struct RenderParticleEmitter {
1489    pub entity: crate::entity::RenderEntity,
1490    pub emitter: crate::particles::ParticleEmitter,
1491}
1492
1493/// The shadow-casting entities collected once per frame in query order, split by
1494/// draw kind, so the shadow pass enumerates occluders from here instead of
1495/// walking the ECS for each `CASTS_SHADOW` archetype.
1496#[derive(Clone, Default)]
1497pub struct RenderShadowCasters {
1498    pub meshes: Vec<crate::entity::RenderEntity>,
1499    pub instanced: Vec<crate::entity::RenderEntity>,
1500    pub skinned: Vec<crate::entity::RenderEntity>,
1501}
1502
1503/// One visible 3D text entity snapshotted once per frame, with its world
1504/// transform, resolved character count, and per-character colors, so the text
1505/// pass reads it here instead of the `Text`, `GlobalTransform`, and
1506/// `TextCharacterColors` components. Only visible text with a cached mesh is
1507/// captured.
1508#[derive(Clone)]
1509pub struct RenderText {
1510    pub mesh: crate::text_data::TextMesh,
1511    pub color: nalgebra_glm::Vec4,
1512    pub outline_color: nalgebra_glm::Vec4,
1513    pub outline_width: f32,
1514    pub smoothing: f32,
1515    pub billboard: bool,
1516    pub transform: nalgebra_glm::Mat4,
1517    pub char_count: usize,
1518    pub character_colors: Option<Vec<Option<nalgebra_glm::Vec4>>>,
1519}
1520
1521/// A renderable's bounding volume reduced to the data the renderer needs:
1522/// the local-space sphere center and radius. Snapshotted once per frame from
1523/// the `BoundingVolume` component so the renderer never reads that component.
1524#[derive(Clone, Copy, Debug, Default)]
1525pub struct RenderBounds {
1526    pub center: nalgebra_glm::Vec3,
1527    pub sphere_radius: f32,
1528}
1529
1530/// Runtime-only renderer bookkeeping that apps generally do not edit
1531/// directly. Carried in `RenderInputs::scene`.
1532#[derive(Clone)]
1533pub struct RendererState {
1534    /// Internal bookkeeping for the macOS frame-rate auto-default.
1535    pub auto_frame_rate_limit_baseline: Option<f32>,
1536    /// Sub-pixel offset applied to the active-camera projection each frame for
1537    /// temporal antialiasing, expressed in normalized device coordinates. The
1538    /// renderer advances it along a low-discrepancy sequence so the temporal
1539    /// resolve accumulates a supersampled image.
1540    pub taa_jitter: [f32; 2],
1541    /// Active-camera view-projection from the current frame, unjittered, used to
1542    /// derive screen-space motion vectors.
1543    pub view_projection: [[f32; 4]; 4],
1544    /// Active-camera view-projection from the previous frame, unjittered.
1545    pub prev_view_projection: [[f32; 4]; 4],
1546    /// Enable GPU frustum culling.
1547    pub gpu_culling_enabled: bool,
1548    /// Enable GPU hi-z occlusion culling for the mesh pass. Also gates the
1549    /// machinery that feeds it: the depth prepass, the hi-z pyramid build,
1550    /// and the second cull dispatch. Defaults off when the renderer publishes
1551    /// a Metal profile, because Apple's tile based GPUs already eliminate
1552    /// hidden opaque surfaces in hardware and the prepass costs a full extra
1553    /// geometry rasterization there. The `cull occlusion on` shell command
1554    /// re-enables it at runtime.
1555    pub occlusion_culling_enabled: bool,
1556    /// Enable the GPU-driven batch table build (classification + combo table on
1557    /// the GPU). When false the CPU builds the batch tables.
1558    pub gpu_batching_enabled: bool,
1559    pub min_screen_pixel_size: f32,
1560    /// Minimum window size in logical pixels. `None` disables the floor.
1561    pub min_window_size: Option<(u32, u32)>,
1562    /// Use fullscreen mode.
1563    pub use_fullscreen: bool,
1564    /// Show the mouse cursor.
1565    pub show_cursor: bool,
1566    /// Current letterbox amount (0-1).
1567    pub letterbox_amount: f32,
1568    /// Target letterbox amount for animation.
1569    pub letterbox_target: f32,
1570    pub day_night: DayNightState,
1571    pub mesh_lod_chains: Vec<MeshLodChain>,
1572    /// View-local shading derived for the camera the renderer is currently
1573    /// executing the graph against. Renderer writes this at the start of
1574    /// each per-camera render. Passes read from this for view-local decisions.
1575    pub active_view: EffectiveShading,
1576    /// Camera state for the view the renderer is currently executing the graph
1577    /// against, extracted once per dispatch. `None` before the first render or
1578    /// when there is no active camera. Passes read this instead of calling
1579    /// `query_active_camera_matrices`.
1580    pub render_view: Option<RenderView>,
1581    /// Per-entity dynamic render state (visibility and morph weights) extracted
1582    /// once per frame, so the mesh pass reads it here instead of scanning the
1583    /// ECS per tracked object. A step toward world-free geometry passes.
1584    pub render_dynamic_objects:
1585        std::collections::HashMap<crate::entity::RenderEntity, DynamicObjectState>,
1586    /// Scene materials resolved once per frame by the engine. Render passes read
1587    /// materials here instead of walking the material registry themselves.
1588    pub render_materials: RenderMaterials,
1589    /// Per-entity mesh name, extracted once per frame so the mesh pass resolves
1590    /// geometry from its own registry without reading the `RenderMesh` component.
1591    pub render_object_meshes: std::collections::HashMap<crate::entity::RenderEntity, String>,
1592    /// Per-entity expanded instanced-mesh data, extracted once per frame.
1593    pub render_instanced_objects:
1594        std::collections::HashMap<crate::entity::RenderEntity, InstancedObjectData>,
1595    /// The scene's lights collected once per frame in query order. The lighting,
1596    /// shadow, and projection passes read lights here instead of scanning the ECS.
1597    pub render_lights: RenderLights,
1598    /// The scene's projected decals collected once per frame in query order. The
1599    /// decal pass reads this instead of walking `DECAL | GLOBAL_TRANSFORM`.
1600    pub render_decals: Vec<RenderDecal>,
1601    /// The scene's particle emitters collected once per frame in query order. The
1602    /// particle pass reads this instead of walking `PARTICLE_EMITTER`.
1603    pub render_particle_emitters: Vec<RenderParticleEmitter>,
1604    /// The scene's enabled water bodies collected once per frame in query order,
1605    /// so the water pass reads them here instead of walking the ECS.
1606    pub render_water: Vec<RenderWater>,
1607    /// The scene's cloth bodies collected once per frame in query order, so the
1608    /// cloth pass reads them here instead of walking the ECS.
1609    pub render_cloth: Vec<RenderCloth>,
1610    /// The entity ids the selection-outline mask covers (selected seeds plus
1611    /// their descendants), resolved once per frame so the selection-mask pass
1612    /// reads them here instead of walking the transform hierarchy. Empty when the
1613    /// outline is disabled or nothing is selected.
1614    pub render_selection_outline_ids: Vec<u32>,
1615    /// The color the outline pass draws the selection outline with, resolved
1616    /// once per frame alongside `render_selection_outline_ids`.
1617    pub render_selection_outline_color: [f32; 4],
1618    /// Per-entity bounding volumes collected once per frame, so culling, the
1619    /// wireframe pass, and shadow bounds read them here instead of the ECS.
1620    pub render_bounds: std::collections::HashMap<crate::entity::RenderEntity, RenderBounds>,
1621    /// Shadow-casting entities collected once per frame, split by draw kind, so
1622    /// the shadow pass enumerates occluders here instead of walking the ECS.
1623    pub render_shadow_casters: RenderShadowCasters,
1624    /// Skinned-mesh entities collected once per frame in query order, so the
1625    /// skinned-mesh pass enumerates draws here instead of walking the ECS.
1626    pub render_skinned_meshes: Vec<crate::entity::RenderEntity>,
1627    /// Regular (non-skinned) render-mesh entities collected once per frame in
1628    /// query order, so the mesh pass builds its scene from this list instead of
1629    /// querying `RENDER_MESH` and filtering `SKIN` against the ECS.
1630    pub render_regular_mesh_entities: Vec<crate::entity::RenderEntity>,
1631    /// When set, the mesh pass derives its cull frustum planes from this
1632    /// view-projection instead of the active camera's, pinning culling to a
1633    /// fixed viewpoint.
1634    pub culling_camera_view_projection: Option<nalgebra_glm::Mat4>,
1635    /// Skinning palette resolved once per frame, so the skinned-mesh and shadow
1636    /// passes read bone data here instead of walking Skin and bone transforms.
1637    pub render_skinning: RenderSkinning,
1638    /// Visible 3D text entities collected once per frame in query order, so the
1639    /// text pass reads them here instead of the ECS.
1640    pub render_texts: Vec<RenderText>,
1641    /// Skinned-animation state resolved once per frame by the engine, so the
1642    /// skinned-mesh compute driver builds its GPU buffers without reading ECS
1643    /// animation, skin, parent, or transform components.
1644    pub render_animation: SkinnedAnimationSnapshot,
1645    /// Scene lighting for the current dispatch, collected once by the renderer.
1646    /// `None` before the first render. Lit passes read this instead of scanning
1647    /// the ECS for lights themselves.
1648    #[cfg(feature = "wgpu")]
1649    pub render_lighting: Option<RenderLighting>,
1650    /// Bumped whenever a skinned entity's object state changed (visibility,
1651    /// morph weights, transform, bounds), so the skinned pass can gate its
1652    /// instance rebuild on a counter.
1653    pub skinned_objects_generation: u64,
1654    /// Monotonic version bumped by the renderer whenever
1655    /// `render_settings_signature` changes between frames.
1656    pub settings_version: u64,
1657    /// Controls whether the focused viewport overrides its
1658    /// `ViewportUpdateMode` and re-renders every frame.
1659    pub focus_policy: ViewportFocusPolicy,
1660    /// Frame-time budget that scales optional post-process sample counts.
1661    pub adaptive_sampling: AdaptiveSamplingState,
1662    /// Post-process effects pass settings. The `EffectsPass` reads this each frame.
1663    pub effects: EffectsState,
1664    /// The adapter the renderer selected, published once the renderer has a
1665    /// device. `None` until the first rendered frame.
1666    pub gpu_profile: Option<GpuProfile>,
1667}
1668
1669#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1670pub enum PerformanceTarget {
1671    #[default]
1672    Unbounded,
1673    Interactive,
1674    Balanced,
1675    Quality,
1676}
1677
1678impl PerformanceTarget {
1679    pub fn target_frame_time_ms(self) -> Option<f32> {
1680        match self {
1681            PerformanceTarget::Unbounded => None,
1682            PerformanceTarget::Interactive => Some(1000.0 / 60.0),
1683            PerformanceTarget::Balanced => Some(1000.0 / 30.0),
1684            PerformanceTarget::Quality => Some(1000.0 / 15.0),
1685        }
1686    }
1687}
1688
1689#[derive(Debug, Clone)]
1690pub struct AdaptiveSamplingState {
1691    pub target: PerformanceTarget,
1692    pub frame_time_rolling_ms: f32,
1693    pub frame_time_sample_count: u32,
1694    pub ssao_sample_scale: f32,
1695    pub ssgi_sample_scale: f32,
1696    pub ssr_step_scale: f32,
1697}
1698
1699impl Default for AdaptiveSamplingState {
1700    fn default() -> Self {
1701        Self {
1702            target: PerformanceTarget::Unbounded,
1703            frame_time_rolling_ms: 16.67,
1704            frame_time_sample_count: 0,
1705            ssao_sample_scale: 1.0,
1706            ssgi_sample_scale: 1.0,
1707            ssr_step_scale: 1.0,
1708        }
1709    }
1710}
1711
1712impl AdaptiveSamplingState {
1713    pub fn record_frame_time(&mut self, frame_time_ms: f32) {
1714        const WINDOW: u32 = 100;
1715        if self.frame_time_sample_count == 0 {
1716            self.frame_time_rolling_ms = frame_time_ms;
1717            self.frame_time_sample_count = 1;
1718        } else {
1719            let alpha = 1.0 / (self.frame_time_sample_count.min(WINDOW) as f32 + 1.0);
1720            self.frame_time_rolling_ms =
1721                self.frame_time_rolling_ms * (1.0 - alpha) + frame_time_ms * alpha;
1722            self.frame_time_sample_count = (self.frame_time_sample_count + 1).min(WINDOW);
1723        }
1724
1725        let Some(target_ms) = self.target.target_frame_time_ms() else {
1726            self.ssao_sample_scale = 1.0;
1727            self.ssgi_sample_scale = 1.0;
1728            self.ssr_step_scale = 1.0;
1729            return;
1730        };
1731
1732        let error = self.frame_time_rolling_ms - target_ms;
1733        let sensitivity = 0.002;
1734        let max_delta = 0.05;
1735        let adjustment = (error * sensitivity).clamp(-max_delta, max_delta);
1736
1737        self.ssao_sample_scale = (self.ssao_sample_scale - adjustment).clamp(0.25, 1.0);
1738        self.ssgi_sample_scale = (self.ssgi_sample_scale - adjustment).clamp(0.25, 1.0);
1739        self.ssr_step_scale = (self.ssr_step_scale - adjustment).clamp(0.25, 1.0);
1740    }
1741}
1742
1743/// Hash of every setting that materially affects what a rendered tile
1744/// looks like, spanning the render and debug buckets plus the resolved
1745/// selection-outline coverage. Fields that
1746/// only affect performance, window chrome, or day/night driver state are
1747/// excluded. The renderer compares this signature against the previous
1748/// frame's value and bumps `RendererState::settings_version` when they differ.
1749pub fn render_settings_signature(
1750    render: &RenderSettings,
1751    debug: &DebugDraw,
1752    outline_ids: &[u32],
1753    outline_color: [f32; 4],
1754) -> u64 {
1755    use std::hash::{Hash, Hasher};
1756    let mut hasher = std::collections::hash_map::DefaultHasher::new();
1757    outline_ids.hash(&mut hasher);
1758    for component in outline_color {
1759        component.to_bits().hash(&mut hasher);
1760    }
1761    debug.show_grid.hash(&mut hasher);
1762    debug.show_bounding_volumes.hash(&mut hasher);
1763    debug.show_normals.hash(&mut hasher);
1764    debug.normal_line_length.to_bits().hash(&mut hasher);
1765    for component in debug.normal_line_color {
1766        component.to_bits().hash(&mut hasher);
1767    }
1768    (render.atmosphere as u32).hash(&mut hasher);
1769    render.show_sky.hash(&mut hasher);
1770    render.render_layer_world_enabled.hash(&mut hasher);
1771    render.render_layer_overlay_enabled.hash(&mut hasher);
1772    render.render_world_to_swapchain.hash(&mut hasher);
1773    for component in render.clear_color {
1774        component.to_bits().hash(&mut hasher);
1775    }
1776    render.unlit_mode.hash(&mut hasher);
1777    render.vertex_snap.is_some().hash(&mut hasher);
1778    if let Some(ref snap) = render.vertex_snap {
1779        for component in snap.resolution {
1780            component.to_bits().hash(&mut hasher);
1781        }
1782    }
1783    render.affine_texture_mapping.hash(&mut hasher);
1784    render.fog.is_some().hash(&mut hasher);
1785    if let Some(ref fog) = render.fog {
1786        for component in fog.color {
1787            component.to_bits().hash(&mut hasher);
1788        }
1789        fog.start.to_bits().hash(&mut hasher);
1790        fog.end.to_bits().hash(&mut hasher);
1791    }
1792    let cg = &render.color_grading;
1793    cg.exposure.to_bits().hash(&mut hasher);
1794    cg.exposure_compensation_ev.to_bits().hash(&mut hasher);
1795    cg.auto_exposure.hash(&mut hasher);
1796    cg.auto_exposure_target.to_bits().hash(&mut hasher);
1797    cg.auto_exposure_rate.to_bits().hash(&mut hasher);
1798    cg.auto_exposure_min_ev.to_bits().hash(&mut hasher);
1799    cg.auto_exposure_max_ev.to_bits().hash(&mut hasher);
1800    cg.gamma.to_bits().hash(&mut hasher);
1801    cg.saturation.to_bits().hash(&mut hasher);
1802    cg.brightness.to_bits().hash(&mut hasher);
1803    cg.contrast.to_bits().hash(&mut hasher);
1804    (cg.tonemap_algorithm as u32).hash(&mut hasher);
1805    render.bloom_enabled.hash(&mut hasher);
1806    render.bloom_intensity.to_bits().hash(&mut hasher);
1807    render.bloom_threshold.to_bits().hash(&mut hasher);
1808    render.bloom_knee.to_bits().hash(&mut hasher);
1809    render.bloom_filter_radius.to_bits().hash(&mut hasher);
1810    let dof = &render.depth_of_field;
1811    dof.enabled.hash(&mut hasher);
1812    dof.focus_distance.to_bits().hash(&mut hasher);
1813    dof.focus_range.to_bits().hash(&mut hasher);
1814    dof.max_blur_radius.to_bits().hash(&mut hasher);
1815    dof.bokeh_threshold.to_bits().hash(&mut hasher);
1816    dof.bokeh_intensity.to_bits().hash(&mut hasher);
1817    (dof.quality as u32).hash(&mut hasher);
1818    dof.visualize_coc.hash(&mut hasher);
1819    dof.tilt_shift_enabled.hash(&mut hasher);
1820    dof.tilt_shift_angle.to_bits().hash(&mut hasher);
1821    dof.tilt_shift_center.to_bits().hash(&mut hasher);
1822    dof.tilt_shift_blur_amount.to_bits().hash(&mut hasher);
1823    dof.visualize_tilt_shift.hash(&mut hasher);
1824    render.ssao_enabled.hash(&mut hasher);
1825    render.ssao_radius.to_bits().hash(&mut hasher);
1826    render.ssao_bias.to_bits().hash(&mut hasher);
1827    render.ssao_intensity.to_bits().hash(&mut hasher);
1828    render.ssao_sample_count.hash(&mut hasher);
1829    debug.ssao_visualization.hash(&mut hasher);
1830    render.ssao_blur_depth_threshold.to_bits().hash(&mut hasher);
1831    render.ssao_blur_normal_power.to_bits().hash(&mut hasher);
1832    render.ssgi_enabled.hash(&mut hasher);
1833    render.ssgi_radius.to_bits().hash(&mut hasher);
1834    render.ssgi_intensity.to_bits().hash(&mut hasher);
1835    render.ssgi_max_steps.hash(&mut hasher);
1836    render.ssr_enabled.hash(&mut hasher);
1837    render.ssr_max_steps.hash(&mut hasher);
1838    render.ssr_thickness.to_bits().hash(&mut hasher);
1839    render.ssr_max_distance.to_bits().hash(&mut hasher);
1840    render.ssr_stride.to_bits().hash(&mut hasher);
1841    render.ssr_fade_start.to_bits().hash(&mut hasher);
1842    render.ssr_fade_end.to_bits().hash(&mut hasher);
1843    render.ssr_intensity.to_bits().hash(&mut hasher);
1844    for component in render.ambient_light {
1845        component.to_bits().hash(&mut hasher);
1846    }
1847    debug.pbr_debug_mode.as_u32().hash(&mut hasher);
1848    debug.texture_debug_stripes.hash(&mut hasher);
1849    debug
1850        .texture_debug_stripes_speed
1851        .to_bits()
1852        .hash(&mut hasher);
1853    render.taa_enabled.hash(&mut hasher);
1854    render.render_scale.to_bits().hash(&mut hasher);
1855    render.ibl_blend_factor.to_bits().hash(&mut hasher);
1856    hasher.finish()
1857}
1858
1859impl Default for RenderSettings {
1860    fn default() -> Self {
1861        Self {
1862            frame_rate_limit: None,
1863            atmosphere: Atmosphere::None,
1864            show_sky: true,
1865            render_layer_world_enabled: true,
1866            render_layer_overlay_enabled: true,
1867            render_world_to_swapchain: true,
1868            clear_color: [0.0, 0.0, 0.0, 1.0],
1869            ui_scale: None,
1870            unlit_mode: false,
1871            vertex_snap: None,
1872            affine_texture_mapping: false,
1873            material_anisotropy_filtering: 16,
1874            fog: None,
1875            color_grading: ColorGrading::default(),
1876            bloom_enabled: true,
1877            bloom_intensity: 0.04,
1878            bloom_threshold: 1.0,
1879            bloom_knee: 0.5,
1880            bloom_filter_radius: 0.005,
1881            depth_of_field: DepthOfField::default(),
1882            ssao_enabled: false,
1883            ssao_radius: 0.5,
1884            ssao_bias: 0.025,
1885            ssao_intensity: 1.0,
1886            ssao_sample_count: 64,
1887            ssao_blur_depth_threshold: 0.005,
1888            ssao_blur_normal_power: 8.0,
1889            ssgi_enabled: false,
1890            ssgi_radius: 2.0,
1891            ssgi_intensity: 1.0,
1892            ssgi_max_steps: 16,
1893            ssr_enabled: false,
1894            ssr_max_steps: 64,
1895            ssr_thickness: 0.3,
1896            ssr_max_distance: 50.0,
1897            ssr_stride: 1.0,
1898            ssr_fade_start: 0.8,
1899            ssr_fade_end: 1.0,
1900            ssr_intensity: 1.0,
1901            ambient_light: [0.1, 0.1, 0.1, 1.0],
1902            taa_enabled: false,
1903            taa_blend: 0.12,
1904            taa_sharpness: 0.5,
1905            water_enabled: true,
1906            vsync_enabled: true,
1907            render_scale: 1.0,
1908            ibl_blend_factor: 0.0,
1909        }
1910    }
1911}
1912
1913impl Default for DebugDraw {
1914    fn default() -> Self {
1915        Self {
1916            show_grid: false,
1917            show_bounding_volumes: false,
1918            show_selected_bounding_volume: false,
1919            show_normals: false,
1920            normal_line_length: 0.1,
1921            normal_line_color: [0.0, 1.0, 0.0, 1.0],
1922            pbr_debug_mode: PbrDebugMode::None,
1923            texture_debug_stripes: false,
1924            texture_debug_stripes_speed: 100.0,
1925            ssao_visualization: false,
1926        }
1927    }
1928}
1929
1930impl Default for RendererState {
1931    fn default() -> Self {
1932        Self {
1933            auto_frame_rate_limit_baseline: None,
1934            taa_jitter: [0.0, 0.0],
1935            view_projection: nalgebra_glm::Mat4::identity().into(),
1936            prev_view_projection: nalgebra_glm::Mat4::identity().into(),
1937            gpu_culling_enabled: true,
1938            occlusion_culling_enabled: true,
1939            gpu_batching_enabled: true,
1940            min_screen_pixel_size: 0.0,
1941            min_window_size: None,
1942            use_fullscreen: false,
1943            show_cursor: true,
1944            letterbox_amount: 0.0,
1945            letterbox_target: 0.0,
1946            day_night: DayNightState::default(),
1947            mesh_lod_chains: Vec::new(),
1948            active_view: EffectiveShading::default(),
1949            render_view: None,
1950            render_dynamic_objects: std::collections::HashMap::new(),
1951            render_materials: RenderMaterials::default(),
1952            render_object_meshes: std::collections::HashMap::new(),
1953            render_instanced_objects: std::collections::HashMap::new(),
1954            render_lights: RenderLights::default(),
1955            render_decals: Vec::new(),
1956            render_particle_emitters: Vec::new(),
1957            render_water: Vec::new(),
1958            render_cloth: Vec::new(),
1959            render_selection_outline_ids: Vec::new(),
1960            render_selection_outline_color: [1.0, 0.45, 0.0, 1.0],
1961            render_bounds: std::collections::HashMap::new(),
1962            render_shadow_casters: RenderShadowCasters::default(),
1963            render_skinned_meshes: Vec::new(),
1964            render_regular_mesh_entities: Vec::new(),
1965            culling_camera_view_projection: None,
1966            render_skinning: RenderSkinning::default(),
1967            render_texts: Vec::new(),
1968            render_animation: SkinnedAnimationSnapshot::default(),
1969            #[cfg(feature = "wgpu")]
1970            render_lighting: None,
1971            skinned_objects_generation: 0,
1972            settings_version: 0,
1973            focus_policy: ViewportFocusPolicy::default(),
1974            adaptive_sampling: AdaptiveSamplingState::default(),
1975            effects: EffectsState::default(),
1976            gpu_profile: None,
1977        }
1978    }
1979}
1980
1981pub const FLAT_SHADING_COLOR: nalgebra_glm::Vec4 = nalgebra_glm::Vec4::new(0.72, 0.72, 0.72, 1.0);
1982
1983/// Update mode for a camera's viewport tile.
1984///
1985/// - `Always`: re-render every frame regardless of dirty state.
1986/// - `WhenVisible`: same as `Always` for now (any dispatched camera is
1987///   treated as visible). Reserved for future visibility/focus tracking.
1988/// - `WhenDirty`: re-render only when the camera moved, when first becoming
1989///   visible (no cached frame yet), or when the frame's coarse dirty signal
1990///   indicates a transform/scene mutation. Otherwise blit the cached
1991///   viewport texture from the previous render.
1992/// - `Once`: render exactly the first time the camera is visible, then
1993///   reuse the cached texture forever.
1994/// - `Disabled`: never render after the first frame; the cached texture
1995///   is whatever the first render produced (or zero-initialized memory if
1996///   the camera was never rendered).
1997#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
1998pub enum ViewportUpdateMode {
1999    #[default]
2000    Always,
2001    WhenVisible,
2002    WhenDirty,
2003    Once,
2004    Disabled,
2005}
2006
2007/// Sentinel `flat_color` used to tell the mesh fragment shaders that the
2008/// camera is in wireframe mode and that the surface should be discarded.
2009/// `flat_color.a >= 2.0` is impossible for a regular color and is reserved
2010/// for this purpose.
2011pub const WIREFRAME_SENTINEL_COLOR: nalgebra_glm::Vec4 =
2012    nalgebra_glm::Vec4::new(0.0, 0.0, 0.0, 2.0);
2013
2014#[derive(Debug, Clone, Copy, PartialEq)]
2015pub struct EffectiveShading {
2016    pub unlit_mode: bool,
2017    pub bloom_enabled: bool,
2018    pub ssao_enabled: bool,
2019    pub ssgi_enabled: bool,
2020    pub ssr_enabled: bool,
2021    pub show_normals: bool,
2022    pub show_bounding_volumes: bool,
2023    pub show_wireframe: bool,
2024    pub selection_outline_enabled: bool,
2025    pub flat_shading_color: Option<nalgebra_glm::Vec4>,
2026    pub shadow_depth_enabled: bool,
2027    pub lines_enabled: bool,
2028    pub color_grading: ColorGrading,
2029    pub depth_of_field: DepthOfField,
2030    pub bloom_intensity: f32,
2031    pub bloom_threshold: f32,
2032    pub bloom_knee: f32,
2033    pub bloom_filter_radius: f32,
2034    pub ssao_radius: f32,
2035    pub ssao_bias: f32,
2036    pub ssao_intensity: f32,
2037    pub ssao_sample_count: u32,
2038    pub ssao_visualization: bool,
2039    pub ssao_blur_depth_threshold: f32,
2040    pub ssao_blur_normal_power: f32,
2041    pub ssgi_radius: f32,
2042    pub ssgi_intensity: f32,
2043    pub ssgi_max_steps: u32,
2044    pub ssr_max_steps: u32,
2045    pub ssr_thickness: f32,
2046    pub ssr_max_distance: f32,
2047    pub ssr_stride: f32,
2048    pub ssr_fade_start: f32,
2049    pub ssr_fade_end: f32,
2050    pub ssr_intensity: f32,
2051    pub ambient_light: [f32; 4],
2052    /// Bitmask of "culling layers" this view will render. Compared
2053    /// against each entity's `CullingMask`
2054    /// at render-collection time; an entity is skipped when
2055    /// `(entity_layers & culling_mask) == 0`. Defaults to `!0` (all
2056    /// layers) so cameras without a `CameraCullingMask` component
2057    /// continue to render every entity.
2058    pub culling_mask: u32,
2059    /// Resolved fog state for this view. `None` disables fog; `Some`
2060    /// uses the contained Fog parameters. Inherits from `Graphics::fog`
2061    /// unless the camera carries a `CameraEnvironment` override.
2062    pub fog: Option<Fog>,
2063    /// Resolved atmosphere selection for this view, used by the sky
2064    /// pass and the IBL bracket selection. Defaults to
2065    /// `Graphics::atmosphere` and can be overridden per camera via
2066    /// `CameraEnvironment`.
2067    pub atmosphere: Atmosphere,
2068}
2069
2070impl Default for EffectiveShading {
2071    fn default() -> Self {
2072        Self {
2073            unlit_mode: false,
2074            bloom_enabled: true,
2075            ssao_enabled: false,
2076            ssgi_enabled: false,
2077            ssr_enabled: false,
2078            show_normals: false,
2079            show_bounding_volumes: false,
2080            show_wireframe: false,
2081            selection_outline_enabled: false,
2082            flat_shading_color: None,
2083            shadow_depth_enabled: true,
2084            lines_enabled: false,
2085            color_grading: ColorGrading::default(),
2086            depth_of_field: DepthOfField::default(),
2087            bloom_intensity: 0.5,
2088            bloom_threshold: 1.0,
2089            bloom_knee: 0.5,
2090            bloom_filter_radius: 0.005,
2091            ssao_radius: 0.5,
2092            ssao_bias: 0.025,
2093            ssao_intensity: 1.0,
2094            ssao_sample_count: 64,
2095            ssao_visualization: false,
2096            ssao_blur_depth_threshold: 0.005,
2097            ssao_blur_normal_power: 8.0,
2098            ssgi_radius: 2.0,
2099            ssgi_intensity: 1.0,
2100            ssgi_max_steps: 16,
2101            ssr_max_steps: 64,
2102            ssr_thickness: 0.3,
2103            ssr_max_distance: 50.0,
2104            ssr_stride: 1.0,
2105            ssr_fade_start: 0.8,
2106            ssr_fade_end: 1.0,
2107            ssr_intensity: 1.0,
2108            ambient_light: [0.1, 0.1, 0.1, 1.0],
2109            culling_mask: !0,
2110            fog: None,
2111            atmosphere: Atmosphere::None,
2112        }
2113    }
2114}
2115
2116#[derive(Debug, Clone, Copy, Default)]
2117pub struct ViewportRect {
2118    pub x: f32,
2119    pub y: f32,
2120    pub width: f32,
2121    pub height: f32,
2122}
2123
2124impl ViewportRect {
2125    pub fn contains(&self, screen_pos: nalgebra_glm::Vec2) -> bool {
2126        screen_pos.x >= self.x
2127            && screen_pos.x <= self.x + self.width
2128            && screen_pos.y >= self.y
2129            && screen_pos.y <= self.y + self.height
2130    }
2131
2132    pub fn to_local(&self, screen_pos: nalgebra_glm::Vec2) -> nalgebra_glm::Vec2 {
2133        nalgebra_glm::Vec2::new(screen_pos.x - self.x, screen_pos.y - self.y)
2134    }
2135}