render: composite + ACES tone-map (bloom stage [5]), replaces upscale
Adds CompositeMaterial + composite.wgsl: combines the HDR scene with the bloom pyramid output, applies ACES (Narkowicz) tone mapping, and writes LDR to the window. Removes UpscaleMaterial + upscale.wgsl. mirror_params now updates composite (bloom_strength, exposure) and brightpass (threshold) uniforms each frame. resize_offscreen rebuilds the full bloom pyramid targets on window resize.
This commit is contained in:
parent
ed6639738f
commit
a7490c0c2f
33
assets/shaders/composite.wgsl
Normal file
33
assets/shaders/composite.wgsl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
|
||||||
|
|
||||||
|
struct CompositeUniform {
|
||||||
|
bloom_strength: f32,
|
||||||
|
exposure: f32,
|
||||||
|
_pad0: f32,
|
||||||
|
_pad1: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> u: CompositeUniform;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var scene_tex: texture_2d<f32>;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var scene_samp: sampler;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(3) var bloom_tex: texture_2d<f32>;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(4) var bloom_samp: sampler;
|
||||||
|
|
||||||
|
// ACES Narkowicz fit (5 ops, clamped to [0,1]).
|
||||||
|
fn aces_tonemap(x: vec3<f32>) -> vec3<f32> {
|
||||||
|
let a = 2.51;
|
||||||
|
let b = 0.03;
|
||||||
|
let c = 2.43;
|
||||||
|
let d = 0.59;
|
||||||
|
let e = 0.14;
|
||||||
|
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3<f32>(0.0), vec3<f32>(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let hdr = textureSample(scene_tex, scene_samp, in.uv).rgb;
|
||||||
|
let bloom = textureSample(bloom_tex, bloom_samp, in.uv).rgb;
|
||||||
|
let combined = hdr + bloom * u.bloom_strength;
|
||||||
|
let mapped = aces_tonemap(combined * u.exposure);
|
||||||
|
return vec4<f32>(mapped, 1.0);
|
||||||
|
}
|
||||||
@ -1,10 +0,0 @@
|
|||||||
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
|
|
||||||
|
|
||||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var tex: texture_2d<f32>;
|
|
||||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var samp: sampler;
|
|
||||||
|
|
||||||
@fragment
|
|
||||||
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
||||||
// in.uv is [0,1]; sample the offscreen image directly (linear sampler upscales).
|
|
||||||
return textureSample(tex, samp, in.uv);
|
|
||||||
}
|
|
||||||
@ -123,21 +123,6 @@ impl Material2d for BlackHoleMaterial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Samples the sub-resolution offscreen render and blits it fullscreen.
|
|
||||||
/// Bound to a second Camera2d that draws after the offscreen camera.
|
|
||||||
#[derive(Asset, TypePath, AsBindGroup, Clone)]
|
|
||||||
pub struct UpscaleMaterial {
|
|
||||||
#[texture(0)]
|
|
||||||
#[sampler(1)]
|
|
||||||
pub source: Handle<Image>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Material2d for UpscaleMaterial {
|
|
||||||
fn fragment_shader() -> ShaderRef {
|
|
||||||
"shaders/upscale.wgsl".into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts luminance above a threshold from the HDR offscreen into a
|
/// Extracts luminance above a threshold from the HDR offscreen into a
|
||||||
/// half-res float texture (bloom stage [2]). Soft-knee, not hard cut.
|
/// half-res float texture (bloom stage [2]). Soft-knee, not hard cut.
|
||||||
#[derive(Asset, TypePath, AsBindGroup, Clone)]
|
#[derive(Asset, TypePath, AsBindGroup, Clone)]
|
||||||
@ -181,3 +166,31 @@ impl Material2d for BlurMaterial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -83,7 +83,7 @@ impl Plugin for BlackHolePlugin {
|
|||||||
.init_resource::<crate::camera::WantsPointer>()
|
.init_resource::<crate::camera::WantsPointer>()
|
||||||
.init_resource::<crate::params::BlackHoleParams>()
|
.init_resource::<crate::params::BlackHoleParams>()
|
||||||
.add_plugins(Material2dPlugin::<BlackHoleMaterial>::default())
|
.add_plugins(Material2dPlugin::<BlackHoleMaterial>::default())
|
||||||
.add_plugins(Material2dPlugin::<crate::render::material::UpscaleMaterial>::default())
|
.add_plugins(Material2dPlugin::<crate::render::material::CompositeMaterial>::default())
|
||||||
.add_plugins(Material2dPlugin::<crate::render::material::BrightPassMaterial>::default())
|
.add_plugins(Material2dPlugin::<crate::render::material::BrightPassMaterial>::default())
|
||||||
.add_plugins(Material2dPlugin::<crate::render::material::BlurMaterial>::default())
|
.add_plugins(Material2dPlugin::<crate::render::material::BlurMaterial>::default())
|
||||||
.add_plugins(bevy_egui::EguiPlugin::default())
|
.add_plugins(bevy_egui::EguiPlugin::default())
|
||||||
@ -110,7 +110,7 @@ fn spawn_fullscreen_quad(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<BlackHoleMaterial>>,
|
mut materials: ResMut<Assets<BlackHoleMaterial>>,
|
||||||
mut upscale_materials: ResMut<Assets<crate::render::material::UpscaleMaterial>>,
|
mut composite_materials: ResMut<Assets<crate::render::material::CompositeMaterial>>,
|
||||||
mut bp_materials: ResMut<Assets<crate::render::material::BrightPassMaterial>>,
|
mut bp_materials: ResMut<Assets<crate::render::material::BrightPassMaterial>>,
|
||||||
mut blur_materials: ResMut<Assets<crate::render::material::BlurMaterial>>,
|
mut blur_materials: ResMut<Assets<crate::render::material::BlurMaterial>>,
|
||||||
mut buffers: ResMut<Assets<ShaderBuffer>>,
|
mut buffers: ResMut<Assets<ShaderBuffer>>,
|
||||||
@ -307,12 +307,18 @@ fn spawn_fullscreen_quad(
|
|||||||
RenderTarget::Image(bloom_final.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(6),
|
RenderTarget::Image(bloom_final.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(6),
|
||||||
));
|
));
|
||||||
|
|
||||||
// --- Upscale quad (draws offscreen Image to the window) ---
|
// --- Composite quad (draws HDR scene + bloom to the window, ACES tone-mapped) ---
|
||||||
|
// bloom_final comes from the blur pyramid (Task 5).
|
||||||
|
let composite_mat = composite_materials.add(crate::render::material::CompositeMaterial {
|
||||||
|
uniform: crate::render::material::CompositeUniform {
|
||||||
|
bloom_strength: 0.8, exposure: 1.0, _pad0: 0.0, _pad1: 0.0,
|
||||||
|
},
|
||||||
|
scene: offscreen.clone(),
|
||||||
|
bloom: bloom_final.clone(),
|
||||||
|
});
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))),
|
Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))),
|
||||||
MeshMaterial2d(upscale_materials.add(crate::render::material::UpscaleMaterial {
|
MeshMaterial2d(composite_mat),
|
||||||
source: offscreen.clone(),
|
|
||||||
})),
|
|
||||||
Transform::default().with_scale(Vec3::new(win.width() / 2.0, win.height() / 2.0, 1.0)),
|
Transform::default().with_scale(Vec3::new(win.width() / 2.0, win.height() / 2.0, 1.0)),
|
||||||
UpscaleQuad,
|
UpscaleQuad,
|
||||||
CompositeQuad,
|
CompositeQuad,
|
||||||
@ -328,6 +334,10 @@ fn resize_offscreen(
|
|||||||
mut images: ResMut<Assets<Image>>,
|
mut images: ResMut<Assets<Image>>,
|
||||||
params: Res<crate::params::BlackHoleParams>,
|
params: Res<crate::params::BlackHoleParams>,
|
||||||
target: Query<&OffscreenTarget>,
|
target: Query<&OffscreenTarget>,
|
||||||
|
bloom0: Query<&BloomTarget0>,
|
||||||
|
bloom1: Query<&BloomTarget1>,
|
||||||
|
bloom2: Query<&BloomTarget2>,
|
||||||
|
bloom_final: Query<&BloomFinalTarget>,
|
||||||
window: Query<&Window>,
|
window: Query<&Window>,
|
||||||
mut resized: MessageReader<bevy::window::WindowResized>,
|
mut resized: MessageReader<bevy::window::WindowResized>,
|
||||||
// Both queries borrow `&mut Transform`. Bevy's conflict checker does not
|
// Both queries borrow `&mut Transform`. Bevy's conflict checker does not
|
||||||
@ -354,6 +364,25 @@ fn resize_offscreen(
|
|||||||
// insert is harmless — it'll succeed on the next WindowResized.
|
// insert is harmless — it'll succeed on the next WindowResized.
|
||||||
let _ = images.insert(handle.0.id(), img);
|
let _ = images.insert(handle.0.id(), img);
|
||||||
}
|
}
|
||||||
|
// Bloom pyramid (queries return empty when bloom is off).
|
||||||
|
let bw = ((w as f32 * 0.5) as u32).max(1);
|
||||||
|
let bh = ((h as f32 * 0.5) as u32).max(1);
|
||||||
|
let b1w = ((w as f32 * 0.25) as u32).max(1);
|
||||||
|
let b1h = ((h as f32 * 0.25) as u32).max(1);
|
||||||
|
let b2w = ((w as f32 * 0.125) as u32).max(1);
|
||||||
|
let b2h = ((h as f32 * 0.125) as u32).max(1);
|
||||||
|
if let Ok(t) = bloom0.single() {
|
||||||
|
let _ = images.insert(t.0.id(), Image::new_target_texture(bw, bh, TextureFormat::Rgba16Float, None));
|
||||||
|
}
|
||||||
|
if let Ok(t) = bloom1.single() {
|
||||||
|
let _ = images.insert(t.0.id(), Image::new_target_texture(b1w, b1h, TextureFormat::Rgba16Float, None));
|
||||||
|
}
|
||||||
|
if let Ok(t) = bloom2.single() {
|
||||||
|
let _ = images.insert(t.0.id(), Image::new_target_texture(b2w, b2h, TextureFormat::Rgba16Float, None));
|
||||||
|
}
|
||||||
|
if let Ok(t) = bloom_final.single() {
|
||||||
|
let _ = images.insert(t.0.id(), Image::new_target_texture(bw, bh, TextureFormat::Rgba16Float, None));
|
||||||
|
}
|
||||||
// Rescale offscreen + bloom quads against the offscreen resolution.
|
// Rescale offscreen + bloom quads against the offscreen resolution.
|
||||||
for (mut t, f) in &mut quads.p0() {
|
for (mut t, f) in &mut quads.p0() {
|
||||||
t.scale = Vec3::new(w as f32 * f.0 / 2.0, h as f32 * f.1 / 2.0, 1.0);
|
t.scale = Vec3::new(w as f32 * f.0 / 2.0, h as f32 * f.1 / 2.0, 1.0);
|
||||||
@ -389,6 +418,8 @@ fn mirror_params(
|
|||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
window: Query<&Window>,
|
window: Query<&Window>,
|
||||||
mut materials: ResMut<Assets<BlackHoleMaterial>>,
|
mut materials: ResMut<Assets<BlackHoleMaterial>>,
|
||||||
|
mut brightpass_materials: ResMut<Assets<crate::render::material::BrightPassMaterial>>,
|
||||||
|
mut composite_materials: ResMut<Assets<crate::render::material::CompositeMaterial>>,
|
||||||
) {
|
) {
|
||||||
let win = match window.single() {
|
let win = match window.single() {
|
||||||
Ok(w) => w,
|
Ok(w) => w,
|
||||||
@ -426,4 +457,13 @@ fn mirror_params(
|
|||||||
u.bloom_strength = params.bloom_strength;
|
u.bloom_strength = params.bloom_strength;
|
||||||
u.exposure = params.exposure;
|
u.exposure = params.exposure;
|
||||||
}
|
}
|
||||||
|
// Update brightpass threshold (live-tunable).
|
||||||
|
for (_, mat) in brightpass_materials.iter_mut() {
|
||||||
|
mat.threshold = params.bloom_threshold;
|
||||||
|
}
|
||||||
|
// Update composite material uniforms (bloom strength + exposure live-tunable).
|
||||||
|
for (_, mat) in composite_materials.iter_mut() {
|
||||||
|
mat.uniform.bloom_strength = params.bloom_strength;
|
||||||
|
mat.uniform.exposure = params.exposure;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user