use bevy::prelude::*; use bevy::reflect::TypePath; use bevy::render::render_resource::{AsBindGroup, ShaderType}; use bevy::render::storage::ShaderBuffer; use bevy::shader::ShaderRef; use bevy::sprite_render::Material2d; /// GPU uniform: params + camera packed into one struct bound at binding 0. #[derive(Clone, ShaderType)] pub struct BlackHoleUniforms { // Camera basis + eye (4 vec3s = must align; pad each to vec4 via the shader struct) pub eye: Vec3, pub _pad0: f32, pub forward: Vec3, pub _pad1: f32, pub right: Vec3, pub _pad2: f32, pub up: Vec3, pub fov: f32, // Resolution pub resolution: Vec2, pub time: f32, pub _pad3: f32, // Physics + disk pub rs: f32, pub disk_inner: f32, pub disk_outer: f32, pub disk_tilt: f32, pub disk_brightness: f32, pub disk_rotation_speed: f32, pub doppler_strength: f32, pub star_intensity: f32, pub skybox_intensity: f32, pub grid_density: f32, // Flags packed as u32 (bools aren't valid uniform scalar types in WGSL) pub doppler_enabled: u32, pub grid_enabled: u32, pub planet_count: u32, pub steps: u32, pub spin: f32, // Phase 2: dimensionless Kerr spin χ = a/M ∈ [0,1]. pub star_aa: u32, pub bloom_threshold: f32, pub bloom_strength: f32, pub exposure: f32, pub _pad5: f32, // Disk volumetric (Phase 3.1) pub disk_half_thickness: f32, pub filament_freq: f32, pub filament_sharpness: f32, pub density_freq: f32, pub density_strength: f32, pub arm_count: f32, pub arm_tightness: f32, pub arm_strength: f32, pub disk_quality: u32, // Disk color mode + blackbody temp, relativistic jets. pub disk_color_mode: u32, // 0=gradient, 1=blackbody pub disk_temp: f32, // blackbody base temperature (Kelvin) pub jets_enabled: u32, pub jets_strength: f32, // Anti-aliasing (Phase 3.3): per-pixel supersample count for the lensed-image rings. pub aa_samples: u32, pub _pad6: f32, pub _pad7: f32, } impl Default for BlackHoleUniforms { fn default() -> Self { Self { eye: Vec3::new(0.0, 0.0, 30.0), _pad0: 0.0, forward: Vec3::new(0.0, 0.0, -1.0), _pad1: 0.0, right: Vec3::new(1.0, 0.0, 0.0), _pad2: 0.0, up: Vec3::new(0.0, 1.0, 0.0), fov: 1.0, resolution: Vec2::new(1280.0, 720.0), time: 0.0, _pad3: 0.0, rs: 1.0, disk_inner: 3.0, disk_outer: 25.0, disk_tilt: 0.45, disk_brightness: 1.0, disk_rotation_speed: 0.5, doppler_strength: 1.0, star_intensity: 1.0, skybox_intensity: 0.0, grid_density: 1.0, doppler_enabled: 1, grid_enabled: 0, planet_count: 0, steps: 300, spin: 0.0, star_aa: 1, bloom_threshold: 1.0, bloom_strength: 0.8, exposure: 1.0, _pad5: 0.0, disk_half_thickness: 0.15, filament_freq: 1.0, filament_sharpness: 2.0, density_freq: 2.0, density_strength: 1.2, arm_count: 2.0, arm_tightness: 2.0, arm_strength: 0.5, disk_quality: 3, // High disk_color_mode: 1, // Blackbody (Novikov-Thorne + Kerr Doppler) disk_temp: 6500.0, jets_enabled: 1, jets_strength: 1.0, aa_samples: 1, // overridden by params.aa_quality each frame _pad6: 0.0, _pad7: 0.0, } } } /// One planet's data, uploaded in a storage buffer (binding 3). #[derive(Clone, Copy, ShaderType, Default)] pub struct SphereData { pub center: Vec3, pub radius: f32, pub color: Vec3, pub emissive: u32, pub _pad0: f32, pub _pad1: f32, pub _pad2: f32, } pub const MAX_PLANETS: usize = 32; #[derive(Asset, TypePath, AsBindGroup, Clone, Default)] pub struct BlackHoleMaterial { #[uniform(0)] pub uniforms: BlackHoleUniforms, // Texture at binding 1 + its matching sampler at binding 2. The derive // requires the texture and sampler attributes to live on the same field. // `dimension = "cube"` is REQUIRED: skybox.wgsl declares this binding as // `texture_cube`. The derive defaults to D2, which made the bind-group // layout (D2) disagree with the shader (Cube) — the pipeline failed to // specialize and the fullscreen quad silently drew nothing, leaving only // the camera clear color. Matching the dimension to Cube lets the pipeline // compile; when no cubemap is set, Bevy binds its 1x1 cube fallback (gated // out by `skybox_intensity > 0` in the shader anyway). #[texture(1, dimension = "cube")] #[sampler(2)] pub skybox: Option>, #[storage(3, read_only)] pub planets: Handle, } impl Material2d for BlackHoleMaterial { fn fragment_shader() -> ShaderRef { "shaders/black_hole.wgsl".into() } } /// Uniform for the bright-pass (bloom stage [2]). /// Must be a struct (not bare f32) so the Rust binding size matches the /// WGSL `BrightPassUniform` (16 bytes: threshold + 3 pads). A bare f32 /// registers a 4-byte min_binding_size, causing a pipeline-layout mismatch. #[derive(Clone, ShaderType, Default)] pub struct BrightPassUniform { pub threshold: f32, pub _pad0: f32, pub _pad1: f32, pub _pad2: f32, } /// Extracts luminance above a threshold from the HDR offscreen into a /// half-res float texture (bloom stage [2]). Soft-knee, not hard cut. #[derive(Asset, TypePath, AsBindGroup, Clone)] pub struct BrightPassMaterial { #[uniform(0)] pub uniform: BrightPassUniform, #[texture(1)] #[sampler(2)] pub source: Handle, } impl Material2d for BrightPassMaterial { fn fragment_shader() -> ShaderRef { "shaders/brightpass.wgsl".into() } } /// Uniform for one blur pass (bloom stages [3]/[4]). #[derive(Clone, ShaderType)] pub struct BlurUniform { pub mode: u32, // 0 = downsample, 1 = upsample pub texel_size: Vec2, pub blend: f32, // upsample blend factor (ignored for down) pub _pad0: f32, } /// One pass of the bloom down/up pyramid. One material instance per pass /// (Bevy binds uniforms once per material, not per entity). #[derive(Asset, TypePath, AsBindGroup, Clone)] pub struct BlurMaterial { #[uniform(0)] pub uniform: BlurUniform, #[texture(1)] #[sampler(2)] pub source: Handle, } impl Material2d for BlurMaterial { fn fragment_shader() -> ShaderRef { "shaders/blur.wgsl".into() } } /// Uniform for the composite pass (bloom stage [5]). #[derive(Clone, ShaderType)] pub struct CompositeUniform { pub bloom_strength: f32, pub exposure: f32, pub _pad0: f32, pub _pad1: f32, } /// Final stage: tone-maps the HDR scene + bloom to LDR for the window. /// Replaces UpscaleMaterial. ACES (Narkowicz) tone mapping. #[derive(Asset, TypePath, AsBindGroup, Clone)] pub struct CompositeMaterial { #[uniform(0)] pub uniform: CompositeUniform, #[texture(1)] #[sampler(2)] pub scene: Handle, #[texture(3)] #[sampler(4)] pub bloom: Handle, } impl Material2d for CompositeMaterial { fn fragment_shader() -> ShaderRef { "shaders/composite.wgsl".into() } }