Two runtime validation errors crashed the app on startup:
1. BrightPassMaterial used #[uniform(0)] threshold: f32 (bare f32 = 4-byte
binding), but the WGSL BrightPassUniform struct is 16 bytes (threshold +
3 pads). The pipeline-layout mismatch ("Buffer structure size 16...
greater than min_binding_size, which is 4") killed the opaque_mesh2d
pipeline. Fix: wrap threshold in a BrightPassUniform struct matching the
WGSL layout, same pattern as BlurUniform/CompositeUniform.
2. bevy_egui 0.41's auto-context assigns PrimaryEguiContext to the FIRST
Added<Camera> — with the 7-camera bloom pipeline, that's the offscreen
camera (renders to Rgba16Float Image). egui's pipeline expects the
window's Rgba8UnormSrgb surface → format mismatch crash. Fix: disable
auto_create_primary_context (PreStartup system) and explicitly tag the
composite (window) camera with PrimaryEguiContext.
Both fixes verified: the app now runs without validation errors for 8+
seconds. Physics tests (20/20) unaffected.
209 lines
6.1 KiB
Rust
209 lines
6.1 KiB
Rust
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,
|
|
}
|
|
|
|
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: 15.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,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<f32>`. 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<Handle<Image>>,
|
|
#[storage(3, read_only)]
|
|
pub planets: Handle<ShaderBuffer>,
|
|
}
|
|
|
|
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<Image>,
|
|
}
|
|
|
|
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<Image>,
|
|
}
|
|
|
|
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<Image>,
|
|
#[texture(3)]
|
|
#[sampler(4)]
|
|
pub bloom: Handle<Image>,
|
|
}
|
|
|
|
impl Material2d for CompositeMaterial {
|
|
fn fragment_shader() -> ShaderRef {
|
|
"shaders/composite.wgsl".into()
|
|
}
|
|
}
|