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    /// Enable selection outline effect.
1080    pub selection_outline_enabled: bool,
1081    /// Selection outline color.
1082    pub selection_outline_color: [f32; 4],
1083    /// PBR debug visualization mode.
1084    pub pbr_debug_mode: PbrDebugMode,
1085    /// Show animated debug stripes on textures.
1086    pub texture_debug_stripes: bool,
1087    /// Animation speed for debug stripes.
1088    pub texture_debug_stripes_speed: f32,
1089    pub ssao_visualization: bool,
1090}
1091
1092/// Editor/selection state used by tools that highlight entities. Held
1093/// in `RenderInputs::editor_selection`.
1094#[derive(Clone, Default)]
1095pub struct EditorSelection {
1096    /// RenderEntity to highlight with bounding volume.
1097    pub bounding_volume_selected_entity: Option<crate::entity::RenderEntity>,
1098    /// Additional entities to include in the selection outline. The
1099    /// outline pass renders these alongside `bounding_volume_selected_entity`
1100    /// and their descendants.
1101    pub selected_entities: Vec<crate::entity::RenderEntity>,
1102    /// When set, frustum culling uses this camera's frustum instead of the active camera's.
1103    pub culling_camera_override: Option<crate::entity::RenderEntity>,
1104}
1105
1106/// The GPU class the renderer ended up on, mirrored from `wgpu::DeviceType`
1107/// so apps can pick quality presets without depending on wgpu directly.
1108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1109pub enum GpuDeviceType {
1110    #[default]
1111    Other,
1112    IntegratedGpu,
1113    DiscreteGpu,
1114    VirtualGpu,
1115    Cpu,
1116}
1117
1118/// The graphics backend the renderer is running on. `WebGl` is the constrained
1119/// browser fallback and is the strongest signal that a mobile-class budget is
1120/// appropriate.
1121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1122pub enum GpuBackend {
1123    #[default]
1124    Other,
1125    Vulkan,
1126    Metal,
1127    Dx12,
1128    Gl,
1129    WebGpu,
1130    WebGl,
1131}
1132
1133/// A snapshot of the adapter the renderer selected, published into
1134/// `RendererState` so game code can adapt quality and controls to the device.
1135#[derive(Debug, Clone, Default)]
1136pub struct GpuProfile {
1137    pub backend: GpuBackend,
1138    pub device_type: GpuDeviceType,
1139    pub name: String,
1140}
1141
1142impl GpuProfile {
1143    /// True when the renderer fell back to the browser WebGL path, the clearest
1144    /// indication of a constrained mobile-class GPU budget.
1145    pub fn is_webgl(&self) -> bool {
1146        self.backend == GpuBackend::WebGl
1147    }
1148
1149    /// True when the adapter is an integrated, virtual, or software device, the
1150    /// classes that benefit from a reduced render budget.
1151    pub fn is_low_power(&self) -> bool {
1152        matches!(
1153            self.device_type,
1154            GpuDeviceType::IntegratedGpu | GpuDeviceType::VirtualGpu | GpuDeviceType::Cpu
1155        )
1156    }
1157}
1158
1159/// Per-dispatch camera state the renderer extracts once at the start of each
1160/// per-camera render and every pass reads, replacing scattered
1161/// `query_active_camera_matrices` calls and their per-pass re-derivations.
1162/// `projection` carries the TAA jitter, matching `query_active_camera_matrices`.
1163#[derive(Clone, Debug)]
1164pub struct RenderView {
1165    pub view: nalgebra_glm::Mat4,
1166    pub projection: nalgebra_glm::Mat4,
1167    pub view_projection: nalgebra_glm::Mat4,
1168    pub inverse_view: nalgebra_glm::Mat4,
1169    pub inverse_projection: nalgebra_glm::Mat4,
1170    pub inverse_view_projection: nalgebra_glm::Mat4,
1171    pub camera_position: nalgebra_glm::Vec3,
1172    pub camera_right: nalgebra_glm::Vec3,
1173    pub camera_up: nalgebra_glm::Vec3,
1174    pub frustum_planes: [nalgebra_glm::Vec4; 6],
1175    pub z_near: f32,
1176    pub z_far: f32,
1177    pub y_fov_rad: f32,
1178    pub aspect: f32,
1179    pub orthographic: Option<(f32, f32)>,
1180    pub screen_size: (u32, u32),
1181    pub constrained_aspect: Option<f32>,
1182}
1183
1184/// Scene lighting the renderer collects once per dispatch and every lit pass
1185/// reads, replacing per-pass `collect_lights`, `collect_area_lights`,
1186/// `calculate_cascade_shadows`, and `query_sun` calls. The base light list is
1187/// carried without per-pass shadow-index application; each pass still resolves
1188/// its own spotlight, point, and cookie indices against its texture arrays.
1189#[cfg(feature = "wgpu")]
1190#[derive(Clone, Default)]
1191pub struct RenderLighting {
1192    pub lights_data: Vec<crate::wgpu::passes::geometry::projection::LightData>,
1193    pub num_directional_lights: u32,
1194    pub directional_light_direction: [f32; 4],
1195    pub has_directional_light: bool,
1196    pub entity_to_lights_index: std::collections::HashMap<crate::entity::RenderEntity, usize>,
1197    pub cascade_view_projections: [[[f32; 4]; 4]; crate::wgpu::passes::NUM_SHADOW_CASCADES],
1198    pub cascade_diameters: [f32; crate::wgpu::passes::NUM_SHADOW_CASCADES],
1199    pub cascade_split_distances: [f32; crate::wgpu::passes::NUM_SHADOW_CASCADES],
1200    pub light_view_projection: [[f32; 4]; 4],
1201    pub shadow_bias: f32,
1202    pub shadow_normal_bias: f32,
1203    pub directional_light_size: f32,
1204    pub shadows_enabled: f32,
1205    pub area_lights_data: Vec<crate::wgpu::passes::geometry::projection::AreaLightData>,
1206    pub area_entity_to_index: std::collections::HashMap<crate::entity::RenderEntity, usize>,
1207    pub sun_direction: nalgebra_glm::Vec3,
1208    pub sun_color: nalgebra_glm::Vec3,
1209}
1210
1211/// Per-entity dynamic render state the renderer refreshes each frame without a
1212/// full rebuild: whether the object is visible and its current morph weights.
1213#[derive(Clone, Copy, Debug)]
1214pub struct DynamicObjectState {
1215    pub visible: u32,
1216    pub morph_weights: [f32; 8],
1217    pub transform: nalgebra_glm::Mat4,
1218    pub culling_mask: u32,
1219    pub is_overlay: u32,
1220    pub render_layer: u32,
1221}
1222
1223/// Expanded per-instance data for one instanced-mesh entity, snapshotted once
1224/// per frame so the renderer builds instanced draws without reading the
1225/// `InstancedMesh` component.
1226#[derive(Clone, Default)]
1227pub struct InstancedObjectData {
1228    pub world_models: Vec<[[f32; 4]; 4]>,
1229    pub world_normals: Vec<[[f32; 4]; 3]>,
1230    pub local_matrices: Vec<nalgebra_glm::Mat4>,
1231    pub custom_tints: Vec<[f32; 4]>,
1232    pub mesh_name: String,
1233    pub render_layer: u32,
1234    pub visible: u32,
1235    pub parent_transform: nalgebra_glm::Mat4,
1236}
1237
1238/// A single animation channel flattened to GPU-ready arrays, produced by the
1239/// engine so the renderer never clones keyframe data per frame.
1240#[derive(Clone)]
1241pub struct SkinnedChannelData {
1242    pub property: u32,
1243    pub interpolation: u32,
1244    pub input: Vec<f32>,
1245    pub values: Vec<[f32; 4]>,
1246    pub stride: u32,
1247}
1248
1249/// One joint of a skinned skeleton in the renderer's dispatch order, with its
1250/// rest pose and resolved animation channels for the current clip, the
1251/// cross-fade source clip, and each animation layer.
1252#[derive(Clone)]
1253pub struct SkinnedJointData {
1254    pub local_index: u32,
1255    pub parent_local: Option<u32>,
1256    pub rest_translation: [f32; 3],
1257    pub rest_rotation: [f32; 4],
1258    pub rest_scale: [f32; 3],
1259    pub cur_channels: Vec<SkinnedChannelData>,
1260    pub blend_channels: Vec<SkinnedChannelData>,
1261    pub layer_channels: Vec<Vec<SkinnedChannelData>>,
1262}
1263
1264/// One animated skeleton resolved by the engine: its skin and player entities,
1265/// depth-ordered joints, and how many animation layers the player exposes.
1266#[derive(Clone)]
1267pub struct SkinnedSkeletonData {
1268    pub skin_entity: crate::entity::RenderEntity,
1269    pub player_entity: crate::entity::RenderEntity,
1270    /// The entity above the skeleton's root joints, captured at build time so
1271    /// the per-frame armature-root refresh is one transform read instead of a
1272    /// joint-parent walk.
1273    pub armature_parent: Option<crate::entity::RenderEntity>,
1274    pub joint_count: u32,
1275    pub layer_count: u32,
1276    pub joints_ordered: Vec<SkinnedJointData>,
1277}
1278
1279/// Skinning palette resolved once per frame by the engine so the skinned-mesh
1280/// and shadow passes build their GPU buffers without reading Skin components or
1281/// bone transforms. The static layout (`cache`) is rebuilt only when the skinned
1282/// set changes, bumping `static_generation`; `bone_transforms` refreshes every
1283/// frame.
1284#[derive(Default, Clone)]
1285pub struct RenderSkinning {
1286    pub cache: crate::skinning::SkinningCache,
1287    pub bone_transforms: Vec<nalgebra_glm::Mat4>,
1288    pub static_generation: u64,
1289    /// Bumped whenever any bone transform changed since the last sync, so
1290    /// consumers can gate uploads on a counter instead of comparing the
1291    /// whole palette.
1292    pub bone_transforms_generation: u64,
1293}
1294
1295/// Animation state resolved once per frame by the engine so the skinned-mesh
1296/// compute driver builds its GPU buffers without reading ECS animation, skin,
1297/// parent, or transform components. The heavy `skeletons` payload is rebuilt
1298/// only when the engine's animation gate sees a change, bumping `signature`;
1299/// the runtime maps refresh every frame.
1300#[derive(Clone, Default)]
1301pub struct SkinnedAnimationSnapshot {
1302    pub signature: u64,
1303    pub skeletons: Vec<SkinnedSkeletonData>,
1304    pub player_runtime: std::collections::HashMap<crate::entity::RenderEntity, [f32; 3]>,
1305    pub layer_runtime: std::collections::HashMap<(crate::entity::RenderEntity, usize), [f32; 2]>,
1306    pub armature_roots: std::collections::HashMap<crate::entity::RenderEntity, nalgebra_glm::Mat4>,
1307}
1308
1309/// A resolved material for the renderer: the engine-side material data plus its
1310/// resolved texture ids. The renderer converts this to GPU material data with
1311/// its own bindless texture-layer map. Material id `N` maps to `entries[N - 1]`;
1312/// id 0 is the built-in default material.
1313#[derive(Clone, Default)]
1314pub struct RenderMaterialEntry {
1315    pub material: crate::material::Material,
1316    pub texture_ids: crate::material::MaterialTextureIds,
1317}
1318
1319/// The scene's materials resolved once per frame by the engine, so the render
1320/// passes read materials here instead of walking the material registry and each
1321/// entity's `MaterialRef` themselves.
1322#[derive(Clone, Default)]
1323pub struct RenderMaterials {
1324    pub entries: Vec<RenderMaterialEntry>,
1325    pub name_to_id: std::collections::HashMap<String, u32>,
1326    pub entity_to_id: std::collections::HashMap<crate::entity::RenderEntity, u32>,
1327    pub entity_to_name: std::collections::HashMap<crate::entity::RenderEntity, String>,
1328    pub transparent_ids: std::collections::HashSet<u32>,
1329    pub mask_ids: std::collections::HashSet<u32>,
1330    pub double_sided_ids: std::collections::HashSet<u32>,
1331    /// Bumped whenever the table or the entity maps change, so consumers can
1332    /// gate their material conversions on a counter.
1333    pub generation: u64,
1334}
1335
1336/// One scene light with its world transform, snapshotted once per frame so the
1337/// lighting, shadow, and projection passes read lights here instead of walking
1338/// the ECS themselves.
1339/// A light's kind, mirrored from the `LightType` component enum so the render
1340/// passes do not read that type.
1341#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1342pub enum RenderLightType {
1343    #[default]
1344    Directional,
1345    Point,
1346    Spot,
1347    Area,
1348}
1349
1350/// An area light's shape, mirrored from the `AreaLightShape` component enum so
1351/// the render passes do not read that type.
1352#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1353pub enum RenderAreaLightShape {
1354    #[default]
1355    Rectangle,
1356    Disk,
1357    Sphere,
1358    Tube,
1359}
1360
1361/// A light's render parameters snapshotted once per frame from the `Light`
1362/// component, so the lighting, shadow, and projection passes read them here
1363/// instead of that component.
1364#[derive(Clone)]
1365pub struct RenderLightData {
1366    pub light_type: RenderLightType,
1367    pub color: nalgebra_glm::Vec3,
1368    pub intensity: f32,
1369    pub range: f32,
1370    pub inner_cone_angle: f32,
1371    pub outer_cone_angle: f32,
1372    pub cast_shadows: bool,
1373    pub shadow_bias: f32,
1374    pub shadow_resolution: u32,
1375    pub shadow_distance: f32,
1376    pub cookie_texture: Option<String>,
1377    pub area_shape: RenderAreaLightShape,
1378    pub area_width: f32,
1379    pub area_height: f32,
1380    pub area_radius: f32,
1381    pub area_two_sided: bool,
1382    pub area_emissive_texture: Option<String>,
1383    pub shadow_normal_bias: f32,
1384    pub shadow_softness: f32,
1385}
1386
1387#[derive(Clone)]
1388pub struct RenderLight {
1389    pub entity: crate::entity::RenderEntity,
1390    pub light: RenderLightData,
1391    pub transform: nalgebra_glm::Mat4,
1392}
1393
1394/// The scene's lights collected once per frame in query order, with an
1395/// entity-to-index map for point lookups. Passes that shade, shadow, or project
1396/// lights read this instead of `query_entities(LIGHT | GLOBAL_TRANSFORM)`.
1397#[derive(Clone, Default)]
1398pub struct RenderLights {
1399    pub lights: Vec<RenderLight>,
1400    pub entity_to_index: std::collections::HashMap<crate::entity::RenderEntity, usize>,
1401}
1402
1403/// A projected decal's render parameters snapshotted once per frame from the
1404/// `Decal` component, so the decal pass reads them here instead of that
1405/// component.
1406#[derive(Clone)]
1407pub struct RenderDecalData {
1408    pub texture: Option<String>,
1409    pub emissive_texture: Option<String>,
1410    pub emissive_strength: f32,
1411    pub color: [f32; 4],
1412    pub size: nalgebra_glm::Vec2,
1413    pub depth: f32,
1414    pub normal_threshold: f32,
1415    pub fade_start: f32,
1416    pub fade_end: f32,
1417}
1418
1419/// One projected decal with its world transform and visibility, snapshotted once
1420/// per frame so the decal pass reads it here instead of walking the ECS.
1421#[derive(Clone)]
1422pub struct RenderDecal {
1423    pub decal: RenderDecalData,
1424    pub transform: nalgebra_glm::Mat4,
1425    pub visible: bool,
1426}
1427
1428/// A water body's render parameters snapshotted once per frame from the `Water`
1429/// component, so the water pass reads them here instead of that component.
1430#[derive(Clone, Debug)]
1431pub struct RenderWaterData {
1432    pub half_extents: nalgebra_glm::Vec2,
1433    pub tessellation: u32,
1434    pub wave_amplitude: f32,
1435    pub wave_steepness: f32,
1436    pub wave_length: f32,
1437    pub wave_speed: f32,
1438    pub wave_direction_radians: f32,
1439    pub shallow_color: [f32; 3],
1440    pub deep_color: [f32; 3],
1441    pub depth_fade_distance: f32,
1442    pub edge_foam_distance: f32,
1443    pub foam_amount: f32,
1444    pub foam_color: [f32; 3],
1445    pub roughness: f32,
1446    pub fresnel_power: f32,
1447    pub reflection_strength: f32,
1448    pub refraction_strength: f32,
1449    pub specular_strength: f32,
1450}
1451
1452/// One water body with its world transform, snapshotted once per frame so the
1453/// water pass reads it here instead of walking `WATER | GLOBAL_TRANSFORM`.
1454#[derive(Clone)]
1455pub struct RenderWater {
1456    pub water: RenderWaterData,
1457    pub transform: nalgebra_glm::Mat4,
1458}
1459
1460/// Which cloth particles anchor to the entity transform, mirrored from the
1461/// `ClothPinning` component enum so the cloth pass does not read that type.
1462#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1463pub enum RenderClothPinning {
1464    #[default]
1465    TopRow,
1466    TopCorners,
1467    None,
1468}
1469
1470/// A cloth body's simulation parameters snapshotted once per frame from the
1471/// `Cloth` component, so the cloth pass reads them here instead of that
1472/// component. Carries every field so the pass's config-change comparison still
1473/// detects rebuilds and resets.
1474#[derive(Clone, Debug, PartialEq)]
1475pub struct RenderClothData {
1476    pub columns: u32,
1477    pub rows: u32,
1478    pub width: f32,
1479    pub height: f32,
1480    pub pinning: RenderClothPinning,
1481    pub stiffness: f32,
1482    pub damping: f32,
1483    pub substeps: u32,
1484    pub solver_iterations: u32,
1485    pub gravity: nalgebra_glm::Vec3,
1486    pub wind_response: f32,
1487    pub ground_height: Option<f32>,
1488    pub texture_tiling: nalgebra_glm::Vec2,
1489    pub reset_epoch: u32,
1490}
1491
1492/// One cloth body with its anchor transform and mesh name, snapshotted once per
1493/// frame so the cloth pass reads it here instead of walking the ECS.
1494#[derive(Clone)]
1495pub struct RenderCloth {
1496    pub entity: crate::entity::RenderEntity,
1497    pub cloth: RenderClothData,
1498    pub transform: nalgebra_glm::Mat4,
1499    pub mesh_name: String,
1500}
1501
1502/// One particle emitter snapshotted once per frame so the particle pass reads it
1503/// here instead of the `ParticleEmitter` component. Every emitter is captured in
1504/// query order, disabled ones included, so an emitter's index is stable.
1505#[derive(Clone)]
1506pub struct RenderParticleEmitter {
1507    pub entity: crate::entity::RenderEntity,
1508    pub emitter: crate::particles::ParticleEmitter,
1509}
1510
1511/// The shadow-casting entities collected once per frame in query order, split by
1512/// draw kind, so the shadow pass enumerates occluders from here instead of
1513/// walking the ECS for each `CASTS_SHADOW` archetype.
1514#[derive(Clone, Default)]
1515pub struct RenderShadowCasters {
1516    pub meshes: Vec<crate::entity::RenderEntity>,
1517    pub instanced: Vec<crate::entity::RenderEntity>,
1518    pub skinned: Vec<crate::entity::RenderEntity>,
1519}
1520
1521/// One visible 3D text entity snapshotted once per frame, with its world
1522/// transform, resolved character count, and per-character colors, so the text
1523/// pass reads it here instead of the `Text`, `GlobalTransform`, and
1524/// `TextCharacterColors` components. Only visible text with a cached mesh is
1525/// captured.
1526#[derive(Clone)]
1527pub struct RenderText {
1528    pub mesh: crate::text_data::TextMesh,
1529    pub color: nalgebra_glm::Vec4,
1530    pub outline_color: nalgebra_glm::Vec4,
1531    pub outline_width: f32,
1532    pub smoothing: f32,
1533    pub billboard: bool,
1534    pub transform: nalgebra_glm::Mat4,
1535    pub char_count: usize,
1536    pub character_colors: Option<Vec<Option<nalgebra_glm::Vec4>>>,
1537}
1538
1539/// A renderable's bounding volume reduced to the data the renderer needs:
1540/// the local-space sphere center and radius. Snapshotted once per frame from
1541/// the `BoundingVolume` component so the renderer never reads that component.
1542#[derive(Clone, Copy, Debug, Default)]
1543pub struct RenderBounds {
1544    pub center: nalgebra_glm::Vec3,
1545    pub sphere_radius: f32,
1546}
1547
1548/// Runtime-only renderer bookkeeping that apps generally do not edit
1549/// directly. Carried in `RenderInputs::scene`.
1550#[derive(Clone)]
1551pub struct RendererState {
1552    /// Internal bookkeeping for the macOS frame-rate auto-default.
1553    pub auto_frame_rate_limit_baseline: Option<f32>,
1554    /// Sub-pixel offset applied to the active-camera projection each frame for
1555    /// temporal antialiasing, expressed in normalized device coordinates. The
1556    /// renderer advances it along a low-discrepancy sequence so the temporal
1557    /// resolve accumulates a supersampled image.
1558    pub taa_jitter: [f32; 2],
1559    /// Active-camera view-projection from the current frame, unjittered, used to
1560    /// derive screen-space motion vectors.
1561    pub view_projection: [[f32; 4]; 4],
1562    /// Active-camera view-projection from the previous frame, unjittered.
1563    pub prev_view_projection: [[f32; 4]; 4],
1564    /// Enable GPU frustum culling.
1565    pub gpu_culling_enabled: bool,
1566    /// Enable GPU hi-z occlusion culling for the mesh pass. Also gates the
1567    /// machinery that feeds it: the depth prepass, the hi-z pyramid build,
1568    /// and the second cull dispatch. Defaults off when the renderer publishes
1569    /// a Metal profile, because Apple's tile based GPUs already eliminate
1570    /// hidden opaque surfaces in hardware and the prepass costs a full extra
1571    /// geometry rasterization there. The `cull occlusion on` shell command
1572    /// re-enables it at runtime.
1573    pub occlusion_culling_enabled: bool,
1574    /// Enable the GPU-driven batch table build (classification + combo table on
1575    /// the GPU). When false the CPU builds the batch tables.
1576    pub gpu_batching_enabled: bool,
1577    pub min_screen_pixel_size: f32,
1578    /// Minimum window size in logical pixels. `None` disables the floor.
1579    pub min_window_size: Option<(u32, u32)>,
1580    /// Use fullscreen mode.
1581    pub use_fullscreen: bool,
1582    /// Show the mouse cursor.
1583    pub show_cursor: bool,
1584    /// Current letterbox amount (0-1).
1585    pub letterbox_amount: f32,
1586    /// Target letterbox amount for animation.
1587    pub letterbox_target: f32,
1588    pub day_night: DayNightState,
1589    pub mesh_lod_chains: Vec<MeshLodChain>,
1590    /// View-local shading derived for the camera the renderer is currently
1591    /// executing the graph against. Renderer writes this at the start of
1592    /// each per-camera render. Passes read from this for view-local decisions.
1593    pub active_view: EffectiveShading,
1594    /// Camera state for the view the renderer is currently executing the graph
1595    /// against, extracted once per dispatch. `None` before the first render or
1596    /// when there is no active camera. Passes read this instead of calling
1597    /// `query_active_camera_matrices`.
1598    pub render_view: Option<RenderView>,
1599    /// Per-entity dynamic render state (visibility and morph weights) extracted
1600    /// once per frame, so the mesh pass reads it here instead of scanning the
1601    /// ECS per tracked object. A step toward world-free geometry passes.
1602    pub render_dynamic_objects:
1603        std::collections::HashMap<crate::entity::RenderEntity, DynamicObjectState>,
1604    /// Scene materials resolved once per frame by the engine. Render passes read
1605    /// materials here instead of walking the material registry themselves.
1606    pub render_materials: RenderMaterials,
1607    /// Per-entity mesh name, extracted once per frame so the mesh pass resolves
1608    /// geometry from its own registry without reading the `RenderMesh` component.
1609    pub render_object_meshes: std::collections::HashMap<crate::entity::RenderEntity, String>,
1610    /// Per-entity expanded instanced-mesh data, extracted once per frame.
1611    pub render_instanced_objects:
1612        std::collections::HashMap<crate::entity::RenderEntity, InstancedObjectData>,
1613    /// The scene's lights collected once per frame in query order. The lighting,
1614    /// shadow, and projection passes read lights here instead of scanning the ECS.
1615    pub render_lights: RenderLights,
1616    /// The scene's projected decals collected once per frame in query order. The
1617    /// decal pass reads this instead of walking `DECAL | GLOBAL_TRANSFORM`.
1618    pub render_decals: Vec<RenderDecal>,
1619    /// The scene's particle emitters collected once per frame in query order. The
1620    /// particle pass reads this instead of walking `PARTICLE_EMITTER`.
1621    pub render_particle_emitters: Vec<RenderParticleEmitter>,
1622    /// The scene's enabled water bodies collected once per frame in query order,
1623    /// so the water pass reads them here instead of walking the ECS.
1624    pub render_water: Vec<RenderWater>,
1625    /// The scene's cloth bodies collected once per frame in query order, so the
1626    /// cloth pass reads them here instead of walking the ECS.
1627    pub render_cloth: Vec<RenderCloth>,
1628    /// The entity ids the selection-outline mask covers (selected seeds plus
1629    /// their descendants), resolved once per frame so the selection-mask pass
1630    /// reads them here instead of walking the transform hierarchy. Empty when the
1631    /// outline is disabled or nothing is selected.
1632    pub render_selection_outline_ids: Vec<u32>,
1633    /// Per-entity bounding volumes collected once per frame, so culling, the
1634    /// wireframe pass, and shadow bounds read them here instead of the ECS.
1635    pub render_bounds: std::collections::HashMap<crate::entity::RenderEntity, RenderBounds>,
1636    /// Shadow-casting entities collected once per frame, split by draw kind, so
1637    /// the shadow pass enumerates occluders here instead of walking the ECS.
1638    pub render_shadow_casters: RenderShadowCasters,
1639    /// Skinned-mesh entities collected once per frame in query order, so the
1640    /// skinned-mesh pass enumerates draws here instead of walking the ECS.
1641    pub render_skinned_meshes: Vec<crate::entity::RenderEntity>,
1642    /// Regular (non-skinned) render-mesh entities collected once per frame in
1643    /// query order, so the mesh pass builds its scene from this list instead of
1644    /// querying `RENDER_MESH` and filtering `SKIN` against the ECS.
1645    pub render_regular_mesh_entities: Vec<crate::entity::RenderEntity>,
1646    /// View-projection of the frozen culling-override camera when the editor
1647    /// pins culling to a specific camera, so the mesh pass derives cull frustum
1648    /// planes here instead of querying that camera's matrices from the ECS.
1649    pub culling_camera_view_projection: Option<nalgebra_glm::Mat4>,
1650    /// Skinning palette resolved once per frame, so the skinned-mesh and shadow
1651    /// passes read bone data here instead of walking Skin and bone transforms.
1652    pub render_skinning: RenderSkinning,
1653    /// Visible 3D text entities collected once per frame in query order, so the
1654    /// text pass reads them here instead of the ECS.
1655    pub render_texts: Vec<RenderText>,
1656    /// Skinned-animation state resolved once per frame by the engine, so the
1657    /// skinned-mesh compute driver builds its GPU buffers without reading ECS
1658    /// animation, skin, parent, or transform components.
1659    pub render_animation: SkinnedAnimationSnapshot,
1660    /// Scene lighting for the current dispatch, collected once by the renderer.
1661    /// `None` before the first render. Lit passes read this instead of scanning
1662    /// the ECS for lights themselves.
1663    #[cfg(feature = "wgpu")]
1664    pub render_lighting: Option<RenderLighting>,
1665    /// Bumped whenever a skinned entity's object state changed (visibility,
1666    /// morph weights, transform, bounds), so the skinned pass can gate its
1667    /// instance rebuild on a counter.
1668    pub skinned_objects_generation: u64,
1669    /// Monotonic version bumped by the renderer whenever
1670    /// `render_settings_signature` changes between frames.
1671    pub settings_version: u64,
1672    /// Controls whether the focused viewport overrides its
1673    /// `ViewportUpdateMode` and re-renders every frame.
1674    pub focus_policy: ViewportFocusPolicy,
1675    /// Frame-time budget that scales optional post-process sample counts.
1676    pub adaptive_sampling: AdaptiveSamplingState,
1677    /// Post-process effects pass settings. The `EffectsPass` reads this each frame.
1678    pub effects: EffectsState,
1679    /// The adapter the renderer selected, published once the renderer has a
1680    /// device. `None` until the first rendered frame.
1681    pub gpu_profile: Option<GpuProfile>,
1682}
1683
1684#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1685pub enum PerformanceTarget {
1686    #[default]
1687    Unbounded,
1688    Interactive,
1689    Balanced,
1690    Quality,
1691}
1692
1693impl PerformanceTarget {
1694    pub fn target_frame_time_ms(self) -> Option<f32> {
1695        match self {
1696            PerformanceTarget::Unbounded => None,
1697            PerformanceTarget::Interactive => Some(1000.0 / 60.0),
1698            PerformanceTarget::Balanced => Some(1000.0 / 30.0),
1699            PerformanceTarget::Quality => Some(1000.0 / 15.0),
1700        }
1701    }
1702}
1703
1704#[derive(Debug, Clone)]
1705pub struct AdaptiveSamplingState {
1706    pub target: PerformanceTarget,
1707    pub frame_time_rolling_ms: f32,
1708    pub frame_time_sample_count: u32,
1709    pub ssao_sample_scale: f32,
1710    pub ssgi_sample_scale: f32,
1711    pub ssr_step_scale: f32,
1712}
1713
1714impl Default for AdaptiveSamplingState {
1715    fn default() -> Self {
1716        Self {
1717            target: PerformanceTarget::Unbounded,
1718            frame_time_rolling_ms: 16.67,
1719            frame_time_sample_count: 0,
1720            ssao_sample_scale: 1.0,
1721            ssgi_sample_scale: 1.0,
1722            ssr_step_scale: 1.0,
1723        }
1724    }
1725}
1726
1727impl AdaptiveSamplingState {
1728    pub fn record_frame_time(&mut self, frame_time_ms: f32) {
1729        const WINDOW: u32 = 100;
1730        if self.frame_time_sample_count == 0 {
1731            self.frame_time_rolling_ms = frame_time_ms;
1732            self.frame_time_sample_count = 1;
1733        } else {
1734            let alpha = 1.0 / (self.frame_time_sample_count.min(WINDOW) as f32 + 1.0);
1735            self.frame_time_rolling_ms =
1736                self.frame_time_rolling_ms * (1.0 - alpha) + frame_time_ms * alpha;
1737            self.frame_time_sample_count = (self.frame_time_sample_count + 1).min(WINDOW);
1738        }
1739
1740        let Some(target_ms) = self.target.target_frame_time_ms() else {
1741            self.ssao_sample_scale = 1.0;
1742            self.ssgi_sample_scale = 1.0;
1743            self.ssr_step_scale = 1.0;
1744            return;
1745        };
1746
1747        let error = self.frame_time_rolling_ms - target_ms;
1748        let sensitivity = 0.002;
1749        let max_delta = 0.05;
1750        let adjustment = (error * sensitivity).clamp(-max_delta, max_delta);
1751
1752        self.ssao_sample_scale = (self.ssao_sample_scale - adjustment).clamp(0.25, 1.0);
1753        self.ssgi_sample_scale = (self.ssgi_sample_scale - adjustment).clamp(0.25, 1.0);
1754        self.ssr_step_scale = (self.ssr_step_scale - adjustment).clamp(0.25, 1.0);
1755    }
1756}
1757
1758/// Hash of every setting that materially affects what a rendered tile
1759/// looks like, spanning the render/debug/selection buckets. Fields that
1760/// only affect performance, window chrome, or day/night driver state are
1761/// excluded. The renderer compares this signature against the previous
1762/// frame's value and bumps `RendererState::settings_version` when they differ.
1763pub fn render_settings_signature(
1764    render: &RenderSettings,
1765    debug: &DebugDraw,
1766    selection: &EditorSelection,
1767) -> u64 {
1768    use std::hash::{Hash, Hasher};
1769    let mut hasher = std::collections::hash_map::DefaultHasher::new();
1770    selection
1771        .bounding_volume_selected_entity
1772        .map(|entity| (entity.id, entity.generation))
1773        .hash(&mut hasher);
1774    for entity in &selection.selected_entities {
1775        (entity.id, entity.generation).hash(&mut hasher);
1776    }
1777    debug.show_grid.hash(&mut hasher);
1778    debug.show_bounding_volumes.hash(&mut hasher);
1779    debug.show_normals.hash(&mut hasher);
1780    debug.normal_line_length.to_bits().hash(&mut hasher);
1781    for component in debug.normal_line_color {
1782        component.to_bits().hash(&mut hasher);
1783    }
1784    (render.atmosphere as u32).hash(&mut hasher);
1785    render.show_sky.hash(&mut hasher);
1786    render.render_layer_world_enabled.hash(&mut hasher);
1787    render.render_layer_overlay_enabled.hash(&mut hasher);
1788    render.render_world_to_swapchain.hash(&mut hasher);
1789    for component in render.clear_color {
1790        component.to_bits().hash(&mut hasher);
1791    }
1792    render.unlit_mode.hash(&mut hasher);
1793    debug.selection_outline_enabled.hash(&mut hasher);
1794    for component in debug.selection_outline_color {
1795        component.to_bits().hash(&mut hasher);
1796    }
1797    render.vertex_snap.is_some().hash(&mut hasher);
1798    if let Some(ref snap) = render.vertex_snap {
1799        for component in snap.resolution {
1800            component.to_bits().hash(&mut hasher);
1801        }
1802    }
1803    render.affine_texture_mapping.hash(&mut hasher);
1804    render.fog.is_some().hash(&mut hasher);
1805    if let Some(ref fog) = render.fog {
1806        for component in fog.color {
1807            component.to_bits().hash(&mut hasher);
1808        }
1809        fog.start.to_bits().hash(&mut hasher);
1810        fog.end.to_bits().hash(&mut hasher);
1811    }
1812    let cg = &render.color_grading;
1813    cg.exposure.to_bits().hash(&mut hasher);
1814    cg.exposure_compensation_ev.to_bits().hash(&mut hasher);
1815    cg.auto_exposure.hash(&mut hasher);
1816    cg.auto_exposure_target.to_bits().hash(&mut hasher);
1817    cg.auto_exposure_rate.to_bits().hash(&mut hasher);
1818    cg.auto_exposure_min_ev.to_bits().hash(&mut hasher);
1819    cg.auto_exposure_max_ev.to_bits().hash(&mut hasher);
1820    cg.gamma.to_bits().hash(&mut hasher);
1821    cg.saturation.to_bits().hash(&mut hasher);
1822    cg.brightness.to_bits().hash(&mut hasher);
1823    cg.contrast.to_bits().hash(&mut hasher);
1824    (cg.tonemap_algorithm as u32).hash(&mut hasher);
1825    render.bloom_enabled.hash(&mut hasher);
1826    render.bloom_intensity.to_bits().hash(&mut hasher);
1827    render.bloom_threshold.to_bits().hash(&mut hasher);
1828    render.bloom_knee.to_bits().hash(&mut hasher);
1829    render.bloom_filter_radius.to_bits().hash(&mut hasher);
1830    let dof = &render.depth_of_field;
1831    dof.enabled.hash(&mut hasher);
1832    dof.focus_distance.to_bits().hash(&mut hasher);
1833    dof.focus_range.to_bits().hash(&mut hasher);
1834    dof.max_blur_radius.to_bits().hash(&mut hasher);
1835    dof.bokeh_threshold.to_bits().hash(&mut hasher);
1836    dof.bokeh_intensity.to_bits().hash(&mut hasher);
1837    (dof.quality as u32).hash(&mut hasher);
1838    dof.visualize_coc.hash(&mut hasher);
1839    dof.tilt_shift_enabled.hash(&mut hasher);
1840    dof.tilt_shift_angle.to_bits().hash(&mut hasher);
1841    dof.tilt_shift_center.to_bits().hash(&mut hasher);
1842    dof.tilt_shift_blur_amount.to_bits().hash(&mut hasher);
1843    dof.visualize_tilt_shift.hash(&mut hasher);
1844    render.ssao_enabled.hash(&mut hasher);
1845    render.ssao_radius.to_bits().hash(&mut hasher);
1846    render.ssao_bias.to_bits().hash(&mut hasher);
1847    render.ssao_intensity.to_bits().hash(&mut hasher);
1848    render.ssao_sample_count.hash(&mut hasher);
1849    debug.ssao_visualization.hash(&mut hasher);
1850    render.ssao_blur_depth_threshold.to_bits().hash(&mut hasher);
1851    render.ssao_blur_normal_power.to_bits().hash(&mut hasher);
1852    render.ssgi_enabled.hash(&mut hasher);
1853    render.ssgi_radius.to_bits().hash(&mut hasher);
1854    render.ssgi_intensity.to_bits().hash(&mut hasher);
1855    render.ssgi_max_steps.hash(&mut hasher);
1856    render.ssr_enabled.hash(&mut hasher);
1857    render.ssr_max_steps.hash(&mut hasher);
1858    render.ssr_thickness.to_bits().hash(&mut hasher);
1859    render.ssr_max_distance.to_bits().hash(&mut hasher);
1860    render.ssr_stride.to_bits().hash(&mut hasher);
1861    render.ssr_fade_start.to_bits().hash(&mut hasher);
1862    render.ssr_fade_end.to_bits().hash(&mut hasher);
1863    render.ssr_intensity.to_bits().hash(&mut hasher);
1864    for component in render.ambient_light {
1865        component.to_bits().hash(&mut hasher);
1866    }
1867    debug.pbr_debug_mode.as_u32().hash(&mut hasher);
1868    debug.texture_debug_stripes.hash(&mut hasher);
1869    debug
1870        .texture_debug_stripes_speed
1871        .to_bits()
1872        .hash(&mut hasher);
1873    render.taa_enabled.hash(&mut hasher);
1874    render.render_scale.to_bits().hash(&mut hasher);
1875    render.ibl_blend_factor.to_bits().hash(&mut hasher);
1876    hasher.finish()
1877}
1878
1879impl Default for RenderSettings {
1880    fn default() -> Self {
1881        Self {
1882            frame_rate_limit: None,
1883            atmosphere: Atmosphere::None,
1884            show_sky: true,
1885            render_layer_world_enabled: true,
1886            render_layer_overlay_enabled: true,
1887            render_world_to_swapchain: true,
1888            clear_color: [0.0, 0.0, 0.0, 1.0],
1889            ui_scale: None,
1890            unlit_mode: false,
1891            vertex_snap: None,
1892            affine_texture_mapping: false,
1893            material_anisotropy_filtering: 16,
1894            fog: None,
1895            color_grading: ColorGrading::default(),
1896            bloom_enabled: true,
1897            bloom_intensity: 0.04,
1898            bloom_threshold: 1.0,
1899            bloom_knee: 0.5,
1900            bloom_filter_radius: 0.005,
1901            depth_of_field: DepthOfField::default(),
1902            ssao_enabled: false,
1903            ssao_radius: 0.5,
1904            ssao_bias: 0.025,
1905            ssao_intensity: 1.0,
1906            ssao_sample_count: 64,
1907            ssao_blur_depth_threshold: 0.005,
1908            ssao_blur_normal_power: 8.0,
1909            ssgi_enabled: false,
1910            ssgi_radius: 2.0,
1911            ssgi_intensity: 1.0,
1912            ssgi_max_steps: 16,
1913            ssr_enabled: false,
1914            ssr_max_steps: 64,
1915            ssr_thickness: 0.3,
1916            ssr_max_distance: 50.0,
1917            ssr_stride: 1.0,
1918            ssr_fade_start: 0.8,
1919            ssr_fade_end: 1.0,
1920            ssr_intensity: 1.0,
1921            ambient_light: [0.1, 0.1, 0.1, 1.0],
1922            taa_enabled: false,
1923            taa_blend: 0.12,
1924            taa_sharpness: 0.5,
1925            water_enabled: true,
1926            vsync_enabled: true,
1927            render_scale: 1.0,
1928            ibl_blend_factor: 0.0,
1929        }
1930    }
1931}
1932
1933impl Default for DebugDraw {
1934    fn default() -> Self {
1935        Self {
1936            show_grid: false,
1937            show_bounding_volumes: false,
1938            show_selected_bounding_volume: false,
1939            show_normals: false,
1940            normal_line_length: 0.1,
1941            normal_line_color: [0.0, 1.0, 0.0, 1.0],
1942            selection_outline_enabled: false,
1943            selection_outline_color: [1.0, 0.45, 0.0, 1.0],
1944            pbr_debug_mode: PbrDebugMode::None,
1945            texture_debug_stripes: false,
1946            texture_debug_stripes_speed: 100.0,
1947            ssao_visualization: false,
1948        }
1949    }
1950}
1951
1952impl Default for RendererState {
1953    fn default() -> Self {
1954        Self {
1955            auto_frame_rate_limit_baseline: None,
1956            taa_jitter: [0.0, 0.0],
1957            view_projection: nalgebra_glm::Mat4::identity().into(),
1958            prev_view_projection: nalgebra_glm::Mat4::identity().into(),
1959            gpu_culling_enabled: true,
1960            occlusion_culling_enabled: true,
1961            gpu_batching_enabled: true,
1962            min_screen_pixel_size: 0.0,
1963            min_window_size: None,
1964            use_fullscreen: false,
1965            show_cursor: true,
1966            letterbox_amount: 0.0,
1967            letterbox_target: 0.0,
1968            day_night: DayNightState::default(),
1969            mesh_lod_chains: Vec::new(),
1970            active_view: EffectiveShading::default(),
1971            render_view: None,
1972            render_dynamic_objects: std::collections::HashMap::new(),
1973            render_materials: RenderMaterials::default(),
1974            render_object_meshes: std::collections::HashMap::new(),
1975            render_instanced_objects: std::collections::HashMap::new(),
1976            render_lights: RenderLights::default(),
1977            render_decals: Vec::new(),
1978            render_particle_emitters: Vec::new(),
1979            render_water: Vec::new(),
1980            render_cloth: Vec::new(),
1981            render_selection_outline_ids: Vec::new(),
1982            render_bounds: std::collections::HashMap::new(),
1983            render_shadow_casters: RenderShadowCasters::default(),
1984            render_skinned_meshes: Vec::new(),
1985            render_regular_mesh_entities: Vec::new(),
1986            culling_camera_view_projection: None,
1987            render_skinning: RenderSkinning::default(),
1988            render_texts: Vec::new(),
1989            render_animation: SkinnedAnimationSnapshot::default(),
1990            #[cfg(feature = "wgpu")]
1991            render_lighting: None,
1992            skinned_objects_generation: 0,
1993            settings_version: 0,
1994            focus_policy: ViewportFocusPolicy::default(),
1995            adaptive_sampling: AdaptiveSamplingState::default(),
1996            effects: EffectsState::default(),
1997            gpu_profile: None,
1998        }
1999    }
2000}
2001
2002pub const FLAT_SHADING_COLOR: nalgebra_glm::Vec4 = nalgebra_glm::Vec4::new(0.72, 0.72, 0.72, 1.0);
2003
2004/// Update mode for a camera's viewport tile.
2005///
2006/// - `Always`: re-render every frame regardless of dirty state.
2007/// - `WhenVisible`: same as `Always` for now (any dispatched camera is
2008///   treated as visible). Reserved for future visibility/focus tracking.
2009/// - `WhenDirty`: re-render only when the camera moved, when first becoming
2010///   visible (no cached frame yet), or when the frame's coarse dirty signal
2011///   indicates a transform/scene mutation. Otherwise blit the cached
2012///   viewport texture from the previous render.
2013/// - `Once`: render exactly the first time the camera is visible, then
2014///   reuse the cached texture forever.
2015/// - `Disabled`: never render after the first frame; the cached texture
2016///   is whatever the first render produced (or zero-initialized memory if
2017///   the camera was never rendered).
2018#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
2019pub enum ViewportUpdateMode {
2020    #[default]
2021    Always,
2022    WhenVisible,
2023    WhenDirty,
2024    Once,
2025    Disabled,
2026}
2027
2028/// Sentinel `flat_color` used to tell the mesh fragment shaders that the
2029/// camera is in wireframe mode and that the surface should be discarded.
2030/// `flat_color.a >= 2.0` is impossible for a regular color and is reserved
2031/// for this purpose.
2032pub const WIREFRAME_SENTINEL_COLOR: nalgebra_glm::Vec4 =
2033    nalgebra_glm::Vec4::new(0.0, 0.0, 0.0, 2.0);
2034
2035#[derive(Debug, Clone, Copy, PartialEq)]
2036pub struct EffectiveShading {
2037    pub unlit_mode: bool,
2038    pub bloom_enabled: bool,
2039    pub ssao_enabled: bool,
2040    pub ssgi_enabled: bool,
2041    pub ssr_enabled: bool,
2042    pub show_normals: bool,
2043    pub show_bounding_volumes: bool,
2044    pub show_wireframe: bool,
2045    pub selection_outline_enabled: bool,
2046    pub flat_shading_color: Option<nalgebra_glm::Vec4>,
2047    pub shadow_depth_enabled: bool,
2048    pub lines_enabled: bool,
2049    pub color_grading: ColorGrading,
2050    pub depth_of_field: DepthOfField,
2051    pub bloom_intensity: f32,
2052    pub bloom_threshold: f32,
2053    pub bloom_knee: f32,
2054    pub bloom_filter_radius: f32,
2055    pub ssao_radius: f32,
2056    pub ssao_bias: f32,
2057    pub ssao_intensity: f32,
2058    pub ssao_sample_count: u32,
2059    pub ssao_visualization: bool,
2060    pub ssao_blur_depth_threshold: f32,
2061    pub ssao_blur_normal_power: f32,
2062    pub ssgi_radius: f32,
2063    pub ssgi_intensity: f32,
2064    pub ssgi_max_steps: u32,
2065    pub ssr_max_steps: u32,
2066    pub ssr_thickness: f32,
2067    pub ssr_max_distance: f32,
2068    pub ssr_stride: f32,
2069    pub ssr_fade_start: f32,
2070    pub ssr_fade_end: f32,
2071    pub ssr_intensity: f32,
2072    pub ambient_light: [f32; 4],
2073    /// Bitmask of "culling layers" this view will render. Compared
2074    /// against each entity's `CullingMask`
2075    /// at render-collection time; an entity is skipped when
2076    /// `(entity_layers & culling_mask) == 0`. Defaults to `!0` (all
2077    /// layers) so cameras without a `CameraCullingMask` component
2078    /// continue to render every entity.
2079    pub culling_mask: u32,
2080    /// Resolved fog state for this view. `None` disables fog; `Some`
2081    /// uses the contained Fog parameters. Inherits from `Graphics::fog`
2082    /// unless the camera carries a `CameraEnvironment` override.
2083    pub fog: Option<Fog>,
2084    /// Resolved atmosphere selection for this view, used by the sky
2085    /// pass and the IBL bracket selection. Defaults to
2086    /// `Graphics::atmosphere` and can be overridden per camera via
2087    /// `CameraEnvironment`.
2088    pub atmosphere: Atmosphere,
2089}
2090
2091impl Default for EffectiveShading {
2092    fn default() -> Self {
2093        Self {
2094            unlit_mode: false,
2095            bloom_enabled: true,
2096            ssao_enabled: false,
2097            ssgi_enabled: false,
2098            ssr_enabled: false,
2099            show_normals: false,
2100            show_bounding_volumes: false,
2101            show_wireframe: false,
2102            selection_outline_enabled: false,
2103            flat_shading_color: None,
2104            shadow_depth_enabled: true,
2105            lines_enabled: false,
2106            color_grading: ColorGrading::default(),
2107            depth_of_field: DepthOfField::default(),
2108            bloom_intensity: 0.5,
2109            bloom_threshold: 1.0,
2110            bloom_knee: 0.5,
2111            bloom_filter_radius: 0.005,
2112            ssao_radius: 0.5,
2113            ssao_bias: 0.025,
2114            ssao_intensity: 1.0,
2115            ssao_sample_count: 64,
2116            ssao_visualization: false,
2117            ssao_blur_depth_threshold: 0.005,
2118            ssao_blur_normal_power: 8.0,
2119            ssgi_radius: 2.0,
2120            ssgi_intensity: 1.0,
2121            ssgi_max_steps: 16,
2122            ssr_max_steps: 64,
2123            ssr_thickness: 0.3,
2124            ssr_max_distance: 50.0,
2125            ssr_stride: 1.0,
2126            ssr_fade_start: 0.8,
2127            ssr_fade_end: 1.0,
2128            ssr_intensity: 1.0,
2129            ambient_light: [0.1, 0.1, 0.1, 1.0],
2130            culling_mask: !0,
2131            fog: None,
2132            atmosphere: Atmosphere::None,
2133        }
2134    }
2135}
2136
2137#[derive(Debug, Clone, Copy, Default)]
2138pub struct ViewportRect {
2139    pub x: f32,
2140    pub y: f32,
2141    pub width: f32,
2142    pub height: f32,
2143}
2144
2145impl ViewportRect {
2146    pub fn contains(&self, screen_pos: nalgebra_glm::Vec2) -> bool {
2147        screen_pos.x >= self.x
2148            && screen_pos.x <= self.x + self.width
2149            && screen_pos.y >= self.y
2150            && screen_pos.y <= self.y + self.height
2151    }
2152
2153    pub fn to_local(&self, screen_pos: nalgebra_glm::Vec2) -> nalgebra_glm::Vec2 {
2154        nalgebra_glm::Vec2::new(screen_pos.x - self.x, screen_pos.y - self.y)
2155    }
2156}