feat: BlackHoleUniforms + camera/params mirror system

This commit is contained in:
xfy 2026-07-10 10:33:10 +08:00
parent 1a2b157ef3
commit 265ec9925d
3 changed files with 183 additions and 18 deletions

View File

@ -1,11 +1,36 @@
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> time: f32;
struct BlackHoleUniforms {
eye: vec4<f32>,
forward: vec4<f32>,
right: vec4<f32>,
up: vec4<f32>,
resolution: vec2<f32>,
time: f32,
_pad3: f32,
rs: f32,
disk_inner: f32,
disk_outer: f32,
disk_tilt: f32,
disk_brightness: f32,
disk_rotation_speed: f32,
doppler_strength: f32,
star_intensity: f32,
skybox_intensity: f32,
grid_density: f32,
doppler_enabled: u32,
grid_enabled: u32,
planet_count: u32,
steps: u32,
_pad4: f32,
_pad5: f32,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> uniforms: BlackHoleUniforms;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// Gradient from time, just to prove the uniform flows.
let t = (sin(time) + 1.0) * 0.5;
let col = mix(vec3<f32>(0.02, 0.02, 0.05), vec3<f32>(0.08, 0.04, 0.12), t);
return vec4<f32>(col, 1.0);
// Prove the camera basis + uniforms flow: tint by eye direction.
let n = normalize(uniforms.eye.xyz);
return vec4<f32>(abs(n) * 0.5 + 0.3, 1.0);
}

View File

@ -1,13 +1,105 @@
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::render_resource::AsBindGroup;
use bevy::render::render_resource::{AsBindGroup, ShaderType};
use bevy::render::storage::ShaderBuffer;
use bevy::shader::ShaderRef;
use bevy::sprite_render::Material2d;
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
/// 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 _pad4: 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: 1.318,
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,
_pad4: 0.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)]
pub struct BlackHoleMaterial {
#[uniform(0)]
pub time: f32,
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.
#[texture(1)]
#[sampler(2)]
pub skybox: Option<Handle<Image>>,
#[storage(3, read_only)]
pub planets: Handle<ShaderBuffer>,
}
impl Material2d for BlackHoleMaterial {
@ -15,3 +107,13 @@ impl Material2d for BlackHoleMaterial {
"shaders/black_hole.wgsl".into()
}
}
impl Default for BlackHoleMaterial {
fn default() -> Self {
Self {
uniforms: BlackHoleUniforms::default(),
skybox: None,
planets: Handle::default(),
}
}
}

View File

@ -7,12 +7,11 @@ pub struct BlackHolePlugin;
impl Plugin for BlackHolePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(Material2dPlugin::<BlackHoleMaterial>::default())
.add_systems(Startup, spawn_fullscreen_quad)
.add_systems(Update, update_time)
.init_resource::<crate::camera::OrbitCamera>()
app.init_resource::<crate::camera::OrbitCamera>()
.init_resource::<crate::params::BlackHoleParams>()
.add_systems(Update, crate::camera::orbit_controller);
.add_plugins(Material2dPlugin::<BlackHoleMaterial>::default())
.add_systems(Startup, spawn_fullscreen_quad)
.add_systems(Update, (crate::camera::orbit_controller, mirror_params));
}
}
@ -20,17 +19,56 @@ fn spawn_fullscreen_quad(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<BlackHoleMaterial>>,
window: Query<&Window>,
) {
commands.spawn(Camera2d);
let win = match window.single() {
Ok(w) => w,
Err(_) => return,
};
let aspect = win.width() / win.height();
commands.spawn((
Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))),
MeshMaterial2d(materials.add(BlackHoleMaterial { time: 0.0 })),
Mesh2d(meshes.add(Rectangle::new(2.0 * aspect, 2.0))),
MeshMaterial2d(materials.add(BlackHoleMaterial::default())),
Transform::default(),
));
commands.spawn(Camera2d);
}
fn update_time(time: Res<Time>, mut materials: ResMut<Assets<BlackHoleMaterial>>) {
fn mirror_params(
camera: Res<crate::camera::OrbitCamera>,
params: Res<crate::params::BlackHoleParams>,
time: Res<Time>,
window: Query<&Window>,
mut materials: ResMut<Assets<BlackHoleMaterial>>,
) {
let win = match window.single() {
Ok(w) => w,
Err(_) => return,
};
// Only update when something changed.
let (eye, forward, right, up) = camera.basis();
for (_, mat) in materials.iter_mut() {
mat.time = time.elapsed_secs();
let u = &mut mat.uniforms;
u.eye = eye;
u.forward = forward;
u.right = right;
u.up = up;
u.fov = camera.fov;
u.resolution = Vec2::new(win.width(), win.height());
u.time = time.elapsed_secs();
u.rs = params.rs;
u.disk_inner = params.disk_inner;
u.disk_outer = params.disk_outer;
u.disk_tilt = params.disk_tilt;
u.disk_brightness = params.disk_brightness;
u.disk_rotation_speed = params.disk_rotation_speed;
u.doppler_strength = params.doppler_strength;
u.star_intensity = params.star_intensity;
u.skybox_intensity = params.skybox_intensity;
u.grid_density = params.grid_density;
u.doppler_enabled = params.doppler_enabled as u32;
u.grid_enabled = params.grid_enabled as u32;
u.planet_count = params.planet_count;
u.steps = params.steps;
}
}