singularity-rs/src/render/material.rs
xfy c699d9891b feat(disk): raise density_freq default to 2.0
Higher default turbulence frequency gives the volumetric disk more
trackable texture features at startup, making the Keplerian rotation
(rot ∝ 1/r^1.5) visually readable without needing to open the egui
panel. The old 0.8 default produced a near-featureless smooth slab at
default settings — the disk was rotating but the eye had nothing to lock
onto. 2.0 sits in the upper half of the slider range (0.2–3.0) while
still leaving room to push higher for debugging.
2026-07-17 14:08:08 +08:00

244 lines
7.4 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,
// 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<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()
}
}