MONKEY (reviewfix-a): merge graphics-programme-polish
Conflicts: sun_shafts prepare keeps the integration warm loop on the polish format-only blur key (mask pipelines are queued at init); sky_fx keeps the clock ordered before LightingConsumeSet plus the polish clock buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ use bevy::{
|
||||
renderer::{RenderContext, RenderDevice, RenderQueue},
|
||||
texture::GpuImage,
|
||||
view::ViewTarget,
|
||||
RenderApp, RenderStartup,
|
||||
Render, RenderApp, RenderStartup, RenderSystems,
|
||||
},
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -43,10 +43,66 @@ struct GradeCatalog {
|
||||
struct GradeView {
|
||||
day: Handle<Image>,
|
||||
night: Handle<Image>,
|
||||
// x = authored strength, y = night blend.
|
||||
/// MONKEY (polish): the pair being faded out (identity once the fade is done).
|
||||
prev_day: Handle<Image>,
|
||||
prev_night: Handle<Image>,
|
||||
// x = authored strength, y = night blend, z = crossfade weight of the current pair (1 = done),
|
||||
// w = the previous pair's strength.
|
||||
control: Vec4,
|
||||
}
|
||||
|
||||
/// MONKEY (polish): the zone crossfade's length; a border crossing no longer snaps the grade.
|
||||
const GRADE_FADE_S: f32 = 2.0;
|
||||
|
||||
/// One zone's LUT pair at its authored strength; a zone with no row grades at strength 0.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
struct GradePair {
|
||||
day: Handle<Image>,
|
||||
night: Handle<Image>,
|
||||
strength: f32,
|
||||
}
|
||||
|
||||
/// MONKEY (polish): the crossfade from the previous zone's pair to the current one.
|
||||
#[derive(Resource, Default)]
|
||||
struct GradeBlend {
|
||||
current: Option<GradePair>,
|
||||
previous: Option<GradePair>,
|
||||
/// The current pair's weight, 0..1.
|
||||
fade: f32,
|
||||
primed: bool,
|
||||
}
|
||||
|
||||
impl GradeBlend {
|
||||
/// Steps toward `target`. The first graded zone after login (or re-enabling) and a disabled
|
||||
/// grade snap; a return to the
|
||||
/// pair still fading out reverses the fade instead of restarting it.
|
||||
fn step(&mut self, target: Option<GradePair>, dt: f32, snap: bool) {
|
||||
if snap || !self.primed {
|
||||
*self = Self {
|
||||
primed: !snap && target.is_some(),
|
||||
current: target,
|
||||
previous: None,
|
||||
fade: 1.0,
|
||||
};
|
||||
return;
|
||||
}
|
||||
if target != self.current {
|
||||
if self.fade < 1.0 && target == self.previous {
|
||||
std::mem::swap(&mut self.current, &mut self.previous);
|
||||
self.fade = 1.0 - self.fade;
|
||||
} else {
|
||||
self.previous = self.current.take();
|
||||
self.current = target;
|
||||
self.fade = 0.0;
|
||||
}
|
||||
}
|
||||
self.fade = (self.fade + dt / GRADE_FADE_S).min(1.0);
|
||||
if self.fade >= 1.0 {
|
||||
self.previous = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, ShaderType)]
|
||||
struct GradeUniform {
|
||||
control: Vec4,
|
||||
@@ -55,6 +111,7 @@ struct GradeUniform {
|
||||
impl Plugin for GradingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(ExtractComponentPlugin::<GradeView>::default())
|
||||
.init_resource::<GradeBlend>()
|
||||
.add_systems(Last, update_views);
|
||||
if !app.is_plugin_added::<AssetPlugin>() {
|
||||
return;
|
||||
@@ -73,6 +130,10 @@ impl Plugin for GradingPlugin {
|
||||
render_app
|
||||
.insert_resource(GradeShader(shader))
|
||||
.add_systems(RenderStartup, init_pipeline)
|
||||
.add_systems(
|
||||
Render,
|
||||
prepare_uniforms.in_set(RenderSystems::PrepareResources),
|
||||
)
|
||||
.add_render_graph_node::<ViewNodeRunner<GradeNode>>(Core3d, GradeLabel)
|
||||
.add_render_graph_edges(
|
||||
Core3d,
|
||||
@@ -215,6 +276,7 @@ fn volume_image(cube: Vec<u8>) -> Image {
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update_views(
|
||||
mut commands: Commands,
|
||||
video: Res<VideoConfig>,
|
||||
@@ -222,6 +284,8 @@ fn update_views(
|
||||
area: Res<CurrentArea>,
|
||||
areas: Option<Res<crate::area::AreaTableRes>>,
|
||||
time: Res<WorldTime>,
|
||||
clock: Res<Time>,
|
||||
mut blend: ResMut<GradeBlend>,
|
||||
cameras: Query<(Entity, &Camera, Option<&GradeView>), With<WorldCamera>>,
|
||||
) {
|
||||
let selected = catalog.as_ref().and_then(|catalog| {
|
||||
@@ -234,7 +298,7 @@ fn update_views(
|
||||
.rows
|
||||
.for_area(leaf)
|
||||
.or_else(|| catalog.rows.for_area(zone))?;
|
||||
(row.strength > 0.0).then(|| GradeView {
|
||||
(row.strength > 0.0).then(|| GradePair {
|
||||
day: catalog
|
||||
.luts
|
||||
.get(&row.day_lut)
|
||||
@@ -245,7 +309,33 @@ fn update_views(
|
||||
.get(&row.night_lut)
|
||||
.unwrap_or(&catalog.identity)
|
||||
.clone(),
|
||||
control: Vec4::new(row.strength, night_weight(time.minute_f), 0.0, 0.0),
|
||||
strength: row.strength,
|
||||
})
|
||||
});
|
||||
blend.step(selected, clock.delta_secs(), !video.color_grading);
|
||||
let selected = catalog.as_ref().and_then(|catalog| {
|
||||
if blend.current.is_none() && blend.previous.is_none() {
|
||||
return None;
|
||||
}
|
||||
let pair = |p: &Option<GradePair>| {
|
||||
p.as_ref().map_or_else(
|
||||
|| (catalog.identity.clone(), catalog.identity.clone(), 0.0),
|
||||
|p| (p.day.clone(), p.night.clone(), p.strength),
|
||||
)
|
||||
};
|
||||
let (day, night, strength) = pair(&blend.current);
|
||||
let (prev_day, prev_night, prev_strength) = pair(&blend.previous);
|
||||
Some(GradeView {
|
||||
day,
|
||||
night,
|
||||
prev_day,
|
||||
prev_night,
|
||||
control: Vec4::new(
|
||||
strength,
|
||||
night_weight(time.minute_f),
|
||||
blend.fade,
|
||||
prev_strength,
|
||||
),
|
||||
})
|
||||
});
|
||||
for (entity, camera, old) in &cameras {
|
||||
@@ -307,6 +397,9 @@ fn init_pipeline(
|
||||
texture_3d(TextureSampleType::Float { filterable: true }),
|
||||
sampler(SamplerBindingType::Filtering),
|
||||
uniform_buffer::<GradeUniform>(false),
|
||||
// MONKEY (polish): the previous zone's pair, crossfaded out.
|
||||
texture_3d(TextureSampleType::Float { filterable: true }),
|
||||
texture_3d(TextureSampleType::Float { filterable: true }),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -344,17 +437,47 @@ fn init_pipeline(
|
||||
});
|
||||
}
|
||||
|
||||
/// MONKEY (polish): the view's uniform, created once and rewritten in prepare, like bloom's.
|
||||
#[derive(Component)]
|
||||
struct GradeUniformBuffer(Buffer);
|
||||
|
||||
fn prepare_uniforms(
|
||||
mut commands: Commands,
|
||||
device: Res<RenderDevice>,
|
||||
queue: Res<RenderQueue>,
|
||||
views: Query<(Entity, &GradeView, Option<&GradeUniformBuffer>)>,
|
||||
) {
|
||||
for (entity, grade, buffer) in &views {
|
||||
let rows = [grade.control.to_array()];
|
||||
match buffer {
|
||||
Some(buffer) => queue.write_buffer(&buffer.0, 0, bytemuck::cast_slice(&rows)),
|
||||
None => {
|
||||
let buffer = device.create_buffer_with_data(&BufferInitDescriptor {
|
||||
label: Some("post_grading_uniform"),
|
||||
contents: bytemuck::cast_slice(&rows),
|
||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||
});
|
||||
commands.entity(entity).insert(GradeUniformBuffer(buffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GradeNode;
|
||||
|
||||
impl ViewNode for GradeNode {
|
||||
type ViewQuery = (&'static ViewTarget, &'static GradeView);
|
||||
type ViewQuery = (
|
||||
&'static ViewTarget,
|
||||
&'static GradeView,
|
||||
&'static GradeUniformBuffer,
|
||||
);
|
||||
|
||||
fn run<'w>(
|
||||
&self,
|
||||
_graph: &mut RenderGraphContext,
|
||||
context: &mut RenderContext<'w>,
|
||||
(target, grade): QueryItem<'w, '_, Self::ViewQuery>,
|
||||
(target, grade, uniform): QueryItem<'w, '_, Self::ViewQuery>,
|
||||
world: &'w World,
|
||||
) -> Result<(), NodeRunError> {
|
||||
let settings = world.resource::<GradePipeline>();
|
||||
@@ -363,14 +486,15 @@ impl ViewNode for GradeNode {
|
||||
return Ok(());
|
||||
};
|
||||
let images = world.resource::<RenderAssets<GpuImage>>();
|
||||
let (Some(day), Some(night)) = (images.get(&grade.day), images.get(&grade.night)) else {
|
||||
let (Some(day), Some(night), Some(prev_day), Some(prev_night)) = (
|
||||
images.get(&grade.day),
|
||||
images.get(&grade.night),
|
||||
images.get(&grade.prev_day),
|
||||
images.get(&grade.prev_night),
|
||||
) else {
|
||||
return Ok(());
|
||||
};
|
||||
let device = context.render_device();
|
||||
let mut uniform = UniformBuffer::from(GradeUniform {
|
||||
control: grade.control,
|
||||
});
|
||||
uniform.write_buffer(device, world.resource::<RenderQueue>());
|
||||
let out = target.post_process_write();
|
||||
let bind = device.create_bind_group(
|
||||
"post_grading",
|
||||
@@ -381,7 +505,9 @@ impl ViewNode for GradeNode {
|
||||
&day.texture_view,
|
||||
&night.texture_view,
|
||||
&settings.lut_sampler,
|
||||
uniform.binding().unwrap(),
|
||||
uniform.0.as_entire_binding(),
|
||||
&prev_day.texture_view,
|
||||
&prev_night.texture_view,
|
||||
)),
|
||||
);
|
||||
let diagnostics = context.diagnostic_recorder();
|
||||
@@ -422,6 +548,44 @@ mod tests {
|
||||
assert_eq!(at(0, 0, 31), &[0, 0, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_change_crossfades_and_a_quick_return_reverses() {
|
||||
let pair = |id: u128, strength| GradePair {
|
||||
day: Handle::Uuid(bevy::asset::uuid::Uuid::from_u128(id), default()),
|
||||
night: Handle::Uuid(bevy::asset::uuid::Uuid::from_u128(id + 1), default()),
|
||||
strength,
|
||||
};
|
||||
let (a, b) = (pair(10, 1.0), pair(20, 0.5));
|
||||
let mut blend = GradeBlend::default();
|
||||
// No zone yet, then the first graded zone snaps in.
|
||||
blend.step(None, 0.016, false);
|
||||
blend.step(Some(a.clone()), 0.016, false);
|
||||
assert_eq!((blend.current.clone(), blend.fade), (Some(a.clone()), 1.0));
|
||||
// A border crossing fades over GRADE_FADE_S.
|
||||
blend.step(Some(b.clone()), 0.5, false);
|
||||
assert_eq!(blend.previous, Some(a.clone()));
|
||||
assert!((blend.fade - 0.25).abs() < 1e-6);
|
||||
// Stepping back mid-fade reverses from the same image.
|
||||
blend.step(Some(a.clone()), 0.0, false);
|
||||
assert_eq!(
|
||||
(blend.current.clone(), blend.previous.clone()),
|
||||
(Some(a.clone()), Some(b))
|
||||
);
|
||||
assert!((blend.fade - 0.75).abs() < 1e-6);
|
||||
blend.step(Some(a.clone()), 1.0, false);
|
||||
assert_eq!((blend.fade, blend.previous.is_none()), (1.0, true));
|
||||
// An ungraded zone fades out to identity rather than cutting.
|
||||
blend.step(None, 1.0, false);
|
||||
assert_eq!(
|
||||
(blend.current.is_none(), blend.previous.clone()),
|
||||
(true, Some(a))
|
||||
);
|
||||
assert!((blend.fade - 0.5).abs() < 1e-6);
|
||||
// Disabling snaps.
|
||||
blend.step(None, 0.0, true);
|
||||
assert!(blend.previous.is_none() && blend.current.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_night_blend_is_continuous() {
|
||||
assert_eq!(night_weight(0.0), 1.0);
|
||||
|
||||
@@ -15,6 +15,9 @@ struct Grade {
|
||||
control: vec4<f32>,
|
||||
};
|
||||
@group(0) @binding(5) var<uniform> grade: Grade;
|
||||
// MONKEY (polish): the previous zone's pair, crossfaded out over `grade.control.z`.
|
||||
@group(0) @binding(6) var prev_day_lut: texture_3d<f32>;
|
||||
@group(0) @binding(7) var prev_night_lut: texture_3d<f32>;
|
||||
|
||||
fn grade_one(lut: texture_3d<f32>, color: vec3<f32>) -> vec3<f32> {
|
||||
// Half a texel in from each face, with 31 intervals across the 32-texel cube.
|
||||
@@ -29,7 +32,16 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
let day = grade_one(day_lut, base);
|
||||
let night = grade_one(night_lut, base);
|
||||
let authored = mix(day, night, grade.control.y);
|
||||
let graded = mix(base, authored, grade.control.x);
|
||||
var graded = mix(base, authored, grade.control.x);
|
||||
// MONKEY (polish): mid-crossfade, the previous zone's pair through the same day/night blend
|
||||
// at its own strength; a missing zone or LUT is the identity cube, so it fades to ungraded.
|
||||
if (grade.control.z < 1.0) {
|
||||
let prev_day = grade_one(prev_day_lut, base);
|
||||
let prev_night = grade_one(prev_night_lut, base);
|
||||
let prev_authored = mix(prev_day, prev_night, grade.control.y);
|
||||
let prev_graded = mix(base, prev_authored, grade.control.w);
|
||||
graded = mix(prev_graded, graded, grade.control.z);
|
||||
}
|
||||
// Bloom already consumed the HDR signal; preserve its remaining excess for the FFX clamp.
|
||||
return vec4(graded + max(source.rgb - vec3(1.0), vec3(0.0)), source.a);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
//! MONKEY (post): screen-space sun shafts from the visible sky depth.
|
||||
//!
|
||||
//! MONKEY (polish): the depth is read once per half-resolution texel into an R8 sky mask
|
||||
//! (`fs_mask`, the only MSAA-specialised stage); the 28-tap radial blur then samples the mask.
|
||||
|
||||
use super::bloom::BloomLabel;
|
||||
use crate::video::VideoConfig;
|
||||
@@ -11,6 +14,7 @@ use bevy::{
|
||||
ecs::query::QueryItem,
|
||||
prelude::*,
|
||||
render::{
|
||||
camera::ExtractedCamera,
|
||||
diagnostic::RecordDiagnostics,
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
render_graph::{
|
||||
@@ -18,7 +22,8 @@ use bevy::{
|
||||
},
|
||||
render_resource::{binding_types::*, *},
|
||||
renderer::{RenderContext, RenderDevice, RenderQueue},
|
||||
view::{Msaa, ViewDepthTexture, ViewTarget},
|
||||
texture::{CachedTexture, TextureCache},
|
||||
view::{ViewDepthTexture, ViewTarget},
|
||||
Render, RenderApp, RenderStartup, RenderSystems,
|
||||
},
|
||||
};
|
||||
@@ -54,7 +59,13 @@ impl Plugin for SunShaftsPlugin {
|
||||
.insert_resource(ShaftShader(shader))
|
||||
.init_resource::<SpecializedRenderPipelines<ShaftPipeline>>()
|
||||
.add_systems(RenderStartup, init_pipeline)
|
||||
.add_systems(Render, prepare_pipelines.in_set(RenderSystems::Prepare))
|
||||
.add_systems(
|
||||
Render,
|
||||
(
|
||||
prepare_pipelines.in_set(RenderSystems::Prepare),
|
||||
prepare_masks.in_set(RenderSystems::PrepareResources),
|
||||
),
|
||||
)
|
||||
.add_render_graph_node::<ViewNodeRunner<ShaftNode>>(Core3d, ShaftLabel)
|
||||
.add_render_graph_edges(
|
||||
Core3d,
|
||||
@@ -139,12 +150,25 @@ struct ShaftShader(Handle<Shader>);
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ShaftPipeline {
|
||||
layouts: [BindGroupLayoutDescriptor; 2],
|
||||
/// The blur's layout; the mask's per MSAA (`[single, multisampled]`).
|
||||
layout: BindGroupLayoutDescriptor,
|
||||
mask_layouts: [BindGroupLayoutDescriptor; 2],
|
||||
masks: [CachedRenderPipelineId; 2],
|
||||
shader: Handle<Shader>,
|
||||
fullscreen: FullscreenShader,
|
||||
sampler: Sampler,
|
||||
}
|
||||
|
||||
/// The half-resolution sky mask the blur marches through.
|
||||
const MASK_FORMAT: TextureFormat = TextureFormat::R8Unorm;
|
||||
|
||||
#[derive(Component)]
|
||||
struct ShaftMask(CachedTexture);
|
||||
|
||||
/// MONKEY (polish): the view's uniform, created once and rewritten in prepare, like bloom's.
|
||||
#[derive(Component)]
|
||||
struct ShaftUniform(Buffer);
|
||||
|
||||
#[derive(Component)]
|
||||
struct ViewShaftPipeline(CachedRenderPipelineId);
|
||||
|
||||
@@ -154,29 +178,61 @@ struct ShaftLabel;
|
||||
fn init_pipeline(
|
||||
mut commands: Commands,
|
||||
device: Res<RenderDevice>,
|
||||
cache: Res<PipelineCache>,
|
||||
shader: Res<ShaftShader>,
|
||||
fullscreen: Res<FullscreenShader>,
|
||||
) {
|
||||
let layouts = [false, true].map(|multisampled| {
|
||||
let layout = BindGroupLayoutDescriptor::new(
|
||||
"post_sun_shafts_layout",
|
||||
&BindGroupLayoutEntries::sequential(
|
||||
ShaderStages::FRAGMENT,
|
||||
(
|
||||
texture_2d(TextureSampleType::Float { filterable: true }),
|
||||
sampler(SamplerBindingType::Filtering),
|
||||
texture_2d(TextureSampleType::Float { filterable: true }),
|
||||
uniform_buffer::<ShaftView>(false),
|
||||
),
|
||||
),
|
||||
);
|
||||
let mask_layouts = [false, true].map(|multisampled| {
|
||||
BindGroupLayoutDescriptor::new(
|
||||
"post_sun_shafts_layout",
|
||||
&BindGroupLayoutEntries::sequential(
|
||||
"post_sun_shafts_mask_layout",
|
||||
&BindGroupLayoutEntries::single(
|
||||
ShaderStages::FRAGMENT,
|
||||
(
|
||||
texture_2d(TextureSampleType::Float { filterable: true }),
|
||||
sampler(SamplerBindingType::Filtering),
|
||||
if multisampled {
|
||||
texture_depth_2d_multisampled()
|
||||
} else {
|
||||
texture_depth_2d()
|
||||
},
|
||||
uniform_buffer::<ShaftView>(false),
|
||||
),
|
||||
if multisampled {
|
||||
texture_depth_2d_multisampled()
|
||||
} else {
|
||||
texture_depth_2d()
|
||||
},
|
||||
),
|
||||
)
|
||||
});
|
||||
let masks = [false, true].map(|multisampled| {
|
||||
let mut shader_defs = vec!["SHAFT_MASK".into()];
|
||||
if multisampled {
|
||||
shader_defs.push("MULTISAMPLED".into());
|
||||
}
|
||||
cache.queue_render_pipeline(RenderPipelineDescriptor {
|
||||
label: Some("post_sun_shafts_mask".into()),
|
||||
layout: vec![mask_layouts[multisampled as usize].clone()],
|
||||
vertex: fullscreen.to_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: shader.0.clone(),
|
||||
shader_defs,
|
||||
entry_point: Some("fs_mask".into()),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
format: MASK_FORMAT,
|
||||
blend: None,
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
..default()
|
||||
})
|
||||
});
|
||||
commands.insert_resource(ShaftPipeline {
|
||||
layouts,
|
||||
layout,
|
||||
mask_layouts,
|
||||
masks,
|
||||
shader: shader.0.clone(),
|
||||
fullscreen: fullscreen.clone(),
|
||||
sampler: device.create_sampler(&SamplerDescriptor {
|
||||
@@ -190,19 +246,16 @@ fn init_pipeline(
|
||||
}
|
||||
|
||||
impl SpecializedRenderPipeline for ShaftPipeline {
|
||||
type Key = (TextureFormat, bool);
|
||||
type Key = TextureFormat;
|
||||
|
||||
fn specialize(&self, (format, multisampled): Self::Key) -> RenderPipelineDescriptor {
|
||||
fn specialize(&self, format: Self::Key) -> RenderPipelineDescriptor {
|
||||
RenderPipelineDescriptor {
|
||||
label: Some("post_sun_shafts".into()),
|
||||
layout: vec![self.layouts[multisampled as usize].clone()],
|
||||
layout: vec![self.layout.clone()],
|
||||
vertex: self.fullscreen.to_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: self.shader.clone(),
|
||||
shader_defs: multisampled
|
||||
.then(|| "MULTISAMPLED".into())
|
||||
.into_iter()
|
||||
.collect(),
|
||||
shader_defs: vec![],
|
||||
entry_point: Some("fragment".into()),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
format,
|
||||
@@ -220,25 +273,68 @@ fn prepare_pipelines(
|
||||
cache: Res<PipelineCache>,
|
||||
pipeline: Res<ShaftPipeline>,
|
||||
mut specialized: ResMut<SpecializedRenderPipelines<ShaftPipeline>>,
|
||||
views: Query<(Entity, &ViewTarget, &Msaa), With<ShaftView>>,
|
||||
all_views: Query<(&ViewTarget, &Msaa), With<Camera3d>>,
|
||||
views: Query<(Entity, &ViewTarget), With<ShaftView>>,
|
||||
all_views: Query<&ViewTarget, With<Camera3d>>,
|
||||
) {
|
||||
// MONKEY (integration): warm every reachable key on every 3-D view, feature on or off, so the
|
||||
// compile happens under the entry cover and never live when the player turns the row on.
|
||||
// (Named in `pipe_warm/menagerie.rs`'s custom-lane census.)
|
||||
for (target, msaa) in &all_views {
|
||||
specialized.specialize(&cache, &pipeline, (target.main_texture_format(), msaa.samples() > 1));
|
||||
// (Named in `pipe_warm/menagerie.rs`'s custom-lane census.) The mask pipelines (per MSAA) are
|
||||
// queued once at init, so only the blur's format key needs warming here.
|
||||
for target in &all_views {
|
||||
specialized.specialize(&cache, &pipeline, target.main_texture_format());
|
||||
}
|
||||
for (entity, target, msaa) in &views {
|
||||
let id = specialized.specialize(
|
||||
&cache,
|
||||
&pipeline,
|
||||
(target.main_texture_format(), msaa.samples() > 1),
|
||||
);
|
||||
for (entity, target) in &views {
|
||||
let id = specialized.specialize(&cache, &pipeline, target.main_texture_format());
|
||||
commands.entity(entity).insert(ViewShaftPipeline(id));
|
||||
}
|
||||
}
|
||||
|
||||
/// The half-size mask target, rounded up so an odd edge column still has a texel, and the
|
||||
/// view's uniform, written in place.
|
||||
fn prepare_masks(
|
||||
mut commands: Commands,
|
||||
mut textures: ResMut<TextureCache>,
|
||||
device: Res<RenderDevice>,
|
||||
queue: Res<RenderQueue>,
|
||||
views: Query<(Entity, &ExtractedCamera, &ShaftView, Option<&ShaftUniform>)>,
|
||||
) {
|
||||
for (entity, camera, shaft, uniform) in &views {
|
||||
let Some(size) = camera.physical_viewport_size else {
|
||||
continue;
|
||||
};
|
||||
let rows = [shaft.sun.to_array(), shaft.color.to_array()];
|
||||
match uniform {
|
||||
Some(uniform) => queue.write_buffer(&uniform.0, 0, bytemuck::cast_slice(&rows)),
|
||||
None => {
|
||||
let buffer = device.create_buffer_with_data(&BufferInitDescriptor {
|
||||
label: Some("post_sun_shafts_uniform"),
|
||||
contents: bytemuck::cast_slice(&rows),
|
||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||
});
|
||||
commands.entity(entity).insert(ShaftUniform(buffer));
|
||||
}
|
||||
}
|
||||
let mask = textures.get(
|
||||
&device,
|
||||
TextureDescriptor {
|
||||
label: Some("post_sun_shafts_mask"),
|
||||
size: Extent3d {
|
||||
width: size.x.div_ceil(2).max(1),
|
||||
height: size.y.div_ceil(2).max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: MASK_FORMAT,
|
||||
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
);
|
||||
commands.entity(entity).insert(ShaftMask(mask));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ShaftNode;
|
||||
|
||||
@@ -246,19 +342,27 @@ impl ViewNode for ShaftNode {
|
||||
type ViewQuery = (
|
||||
&'static ViewTarget,
|
||||
&'static ViewDepthTexture,
|
||||
// Gates the pass: the mask and uniform outlive a disabled frame.
|
||||
&'static ShaftView,
|
||||
&'static ViewShaftPipeline,
|
||||
&'static ShaftMask,
|
||||
&'static ShaftUniform,
|
||||
);
|
||||
|
||||
fn run<'w>(
|
||||
&self,
|
||||
_graph: &mut RenderGraphContext,
|
||||
context: &mut RenderContext<'w>,
|
||||
(target, depth, shaft, id): QueryItem<'w, '_, Self::ViewQuery>,
|
||||
(target, depth, _, id, mask, uniform): QueryItem<'w, '_, Self::ViewQuery>,
|
||||
world: &'w World,
|
||||
) -> Result<(), NodeRunError> {
|
||||
let cache = world.resource::<PipelineCache>();
|
||||
let Some(pipeline) = cache.get_render_pipeline(id.0) else {
|
||||
let settings = world.resource::<ShaftPipeline>();
|
||||
let multisampled = (depth.texture.sample_count() > 1) as usize;
|
||||
let (Some(pipeline), Some(mask_pipeline)) = (
|
||||
cache.get_render_pipeline(id.0),
|
||||
cache.get_render_pipeline(settings.masks[multisampled]),
|
||||
) else {
|
||||
return Ok(());
|
||||
};
|
||||
if !depth
|
||||
@@ -268,24 +372,45 @@ impl ViewNode for ShaftNode {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let settings = world.resource::<ShaftPipeline>();
|
||||
let device = context.render_device();
|
||||
let mut uniform = UniformBuffer::from(*shaft);
|
||||
uniform.write_buffer(device, world.resource::<RenderQueue>());
|
||||
let mask_bind = device.create_bind_group(
|
||||
"post_sun_shafts_mask",
|
||||
&cache.get_bind_group_layout(&settings.mask_layouts[multisampled]),
|
||||
&BindGroupEntries::single(depth.view()),
|
||||
);
|
||||
let out = target.post_process_write();
|
||||
let bind = device.create_bind_group(
|
||||
"post_sun_shafts",
|
||||
&cache.get_bind_group_layout(
|
||||
&settings.layouts[(depth.texture.sample_count() > 1) as usize],
|
||||
),
|
||||
&cache.get_bind_group_layout(&settings.layout),
|
||||
&BindGroupEntries::sequential((
|
||||
out.source,
|
||||
&settings.sampler,
|
||||
depth.view(),
|
||||
uniform.binding().unwrap(),
|
||||
&mask.0.default_view,
|
||||
uniform.0.as_entire_binding(),
|
||||
)),
|
||||
);
|
||||
let diagnostics = context.diagnostic_recorder();
|
||||
{
|
||||
let mut pass = context
|
||||
.command_encoder()
|
||||
.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("post_sun_shafts_mask"),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &mask.0.default_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: Operations::default(),
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
let span = diagnostics.pass_span(&mut pass, "post_sun_shafts_mask");
|
||||
pass.set_pipeline(mask_pipeline);
|
||||
pass.set_bind_group(0, &mask_bind, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
span.end(&mut pass);
|
||||
}
|
||||
let mut pass = context
|
||||
.command_encoder()
|
||||
.begin_render_pass(&RenderPassDescriptor {
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
// MONKEY (post): screen-space radial blur through sky pixels in the scene depth.
|
||||
// MONKEY (polish): two passes. `SHAFT_MASK` reads the depth once per half-resolution texel into
|
||||
// a sky mask; the blur then samples that mask 28 times instead of the full-size depth.
|
||||
|
||||
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
|
||||
|
||||
#ifdef SHAFT_MASK
|
||||
|
||||
#ifdef MULTISAMPLED
|
||||
@group(0) @binding(0) var depth: texture_depth_multisampled_2d;
|
||||
#else
|
||||
@group(0) @binding(0) var depth: texture_depth_2d;
|
||||
#endif
|
||||
|
||||
@fragment
|
||||
fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
let dims = vec2<i32>(textureDimensions(depth));
|
||||
let p = min(vec2<i32>(in.position.xy) * 2, dims - vec2<i32>(1));
|
||||
// MONKEY (fix-post): one sample, not a max over all of them: the march is a blur.
|
||||
// Only the cleared / sky-pinned depth (exactly 0 on infinite reverse-Z) is sky; a ramp on
|
||||
// `near / z` would move with the live `nearclip` cvar and pour shafts through far terrain.
|
||||
let sky = select(0.0, 1.0, textureLoad(depth, p, 0) <= 1.0e-7);
|
||||
return vec4(sky, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
@group(0) @binding(0) var scene: texture_2d<f32>;
|
||||
@group(0) @binding(1) var scene_sampler: sampler;
|
||||
#ifdef MULTISAMPLED
|
||||
@group(0) @binding(2) var depth: texture_depth_multisampled_2d;
|
||||
#else
|
||||
@group(0) @binding(2) var depth: texture_depth_2d;
|
||||
#endif
|
||||
@group(0) @binding(2) var mask: texture_2d<f32>;
|
||||
|
||||
struct Shafts {
|
||||
sun: vec4<f32>,
|
||||
@@ -16,18 +35,6 @@ struct Shafts {
|
||||
};
|
||||
@group(0) @binding(3) var<uniform> shafts: Shafts;
|
||||
|
||||
fn scene_depth(uv: vec2<f32>) -> f32 {
|
||||
let dims = textureDimensions(depth);
|
||||
let p = vec2<i32>(clamp(uv, vec2(0.0), vec2(0.999999)) * vec2<f32>(dims));
|
||||
#ifdef MULTISAMPLED
|
||||
// MONKEY (fix-post): one sample, not a max over all of them: the march is a blur, and the
|
||||
// per-sample loop cost 28 x N loads per pixel (224 at 8x).
|
||||
return textureLoad(depth, p, 0);
|
||||
#else
|
||||
return textureLoad(depth, p, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
let base = textureSample(scene, scene_sampler, in.uv);
|
||||
@@ -41,10 +48,8 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
if (i >= count) { break; }
|
||||
uv += step_uv;
|
||||
decay *= 0.965;
|
||||
// MONKEY (fix-post): only the cleared / sky-pinned depth (exactly 0 on infinite reverse-Z)
|
||||
// is sky. The old ramp read d = near / z, so it moved with the live `nearclip` cvar and
|
||||
// let shafts pour through terrain beyond ~125 yd at nearclip 0.01.
|
||||
let sky = select(0.0, 1.0, scene_depth(uv) <= 1.0e-7);
|
||||
// The clamp-to-edge bilinear tap is the old clamped depth read, pre-thresholded.
|
||||
let sky = textureSampleLevel(mask, scene_sampler, uv, 0.0).r;
|
||||
light += sky * decay;
|
||||
norm += decay;
|
||||
}
|
||||
@@ -56,3 +61,5 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
let rays = light / max(norm, 0.0001) * radial * radial;
|
||||
return vec4(base.rgb + shafts.color.rgb * rays * shafts.sun.z * 0.16, base.a);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,7 +12,7 @@ use bevy::pbr::{
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::render_resource::{
|
||||
AsBindGroup, Extent3d, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
AsBindGroup, Buffer, Extent3d, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
TextureDimension, TextureFormat,
|
||||
};
|
||||
use bevy::shader::ShaderRef;
|
||||
@@ -35,13 +35,18 @@ pub struct CloudExt {
|
||||
#[texture(100)]
|
||||
#[sampler(101)]
|
||||
pub(crate) texels: Handle<Image>,
|
||||
/// MONKEY (sky): `x` the sky tier (detail at 2), `y` the sky clock, `zw` the unit direction
|
||||
/// toward the glow body on the sheet ([`update_cloud_fx`]).
|
||||
/// MONKEY (sky): `x` the sky tier (detail at 2), `y` reserved (0; the clock is
|
||||
/// [`Self::clock`]), `zw` the unit direction toward the glow body on the sheet
|
||||
/// ([`update_cloud_fx`]).
|
||||
#[uniform(102)]
|
||||
pub(crate) fx: Vec4,
|
||||
/// MONKEY (sky): the Light.dbc cloud sun colour (`rgb`, gamma) and the march strength (`w`).
|
||||
#[uniform(102)]
|
||||
pub(crate) lit: Vec4,
|
||||
/// MONKEY (polish): the shared sky clock ([`crate::sky_fx::SkyClockBuffer`]), outside the
|
||||
/// uniform so the drift never rewrites the material.
|
||||
#[storage(103, read_only, buffer)]
|
||||
pub(crate) clock: Buffer,
|
||||
}
|
||||
|
||||
/// MONKEY (sky): the pipeline key: High compiles `SKY_FX_CLOUDS` into `cloud.wgsl`; below High
|
||||
@@ -159,7 +164,11 @@ pub(super) fn setup_cloud_layer(
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<CloudMaterial>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
clock: Option<Res<crate::sky_fx::SkyClockBuffer>>,
|
||||
) {
|
||||
let Some(clock) = clock else {
|
||||
return;
|
||||
};
|
||||
let image = images.add(Image::new(
|
||||
Extent3d {
|
||||
width: COLS as u32,
|
||||
@@ -186,6 +195,7 @@ pub(super) fn setup_cloud_layer(
|
||||
// MONKEY (sky): Classic until `update_cloud_fx` reads the tier.
|
||||
fx: Vec4::ZERO,
|
||||
lit: Vec4::ZERO,
|
||||
clock: clock.0.clone(),
|
||||
},
|
||||
});
|
||||
commands.spawn((
|
||||
@@ -251,7 +261,6 @@ pub(super) fn follow_cloud_dome(
|
||||
pub(super) fn update_cloud_fx(
|
||||
light: Res<crate::lighting::WowLighting>,
|
||||
quality: Res<crate::sky_fx::SkyQuality>,
|
||||
clock: Res<crate::sky_fx::SkyClock>,
|
||||
layer: Option<Res<CloudLayer>>,
|
||||
mut materials: ResMut<Assets<CloudMaterial>>,
|
||||
) {
|
||||
@@ -268,7 +277,7 @@ pub(super) fn update_cloud_fx(
|
||||
let q = |v: f32| benilla_assets::quantize(v, 4096.0);
|
||||
let sun = light.cloud_colors[0];
|
||||
(
|
||||
Vec4::new(f32::from(quality.0), clock.secs, q(d.x), q(d.y)),
|
||||
Vec4::new(f32::from(quality.0), 0.0, q(d.x), q(d.y)),
|
||||
Vec4::new(q(sun[0]), q(sun[1]), q(sun[2]), q(strength.clamp(0.0, 1.0))),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var cloud_tex: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(101) var cloud_samp: sampler;
|
||||
|
||||
// MONKEY (sky): `fx.x` the sky tier, `.y` the sky clock (s), `.zw` the unit direction toward the
|
||||
// MONKEY (sky): `fx.x` the sky tier, `.y` reserved, `.zw` the unit direction toward the
|
||||
// glow body on the sheet (u = world x, v = world z); `lit.rgb` the Light.dbc cloud sun colour
|
||||
// (gamma), `.w` the march strength (the body's flatness × the glow envelope).
|
||||
struct CloudFx {
|
||||
@@ -22,6 +22,8 @@ struct CloudFx {
|
||||
lit: vec4<f32>,
|
||||
};
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var<uniform> cfx: CloudFx;
|
||||
// MONKEY (polish): x = the sky clock (s, wrapped at a day), written per frame outside the material.
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(103) var<storage, read> sky_clock: vec4<f32>;
|
||||
|
||||
#ifdef SKY_FX_CLOUDS
|
||||
// MONKEY (sky): coverage with detail at a sheet point; `oct` detail octaves.
|
||||
@@ -30,7 +32,7 @@ fn detailed_cover(uv: vec2<f32>, oct: i32) -> f32 {
|
||||
if (a <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return sky_fx::cloud_erode(a, sky_fx::cloud_detail(uv, cfx.fx.y, oct));
|
||||
return sky_fx::cloud_erode(a, sky_fx::cloud_detail(uv, sky_clock.x, oct));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -43,7 +45,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// kernel's texels as they are.
|
||||
#ifdef SKY_FX_CLOUDS
|
||||
if (cfx.fx.x >= 1.5) {
|
||||
let n = sky_fx::cloud_detail(in.uv, cfx.fx.y, 4);
|
||||
let n = sky_fx::cloud_detail(in.uv, sky_clock.x, 4);
|
||||
a = sky_fx::cloud_erode(a, n);
|
||||
// A painterly tone inside the mass from the same detail.
|
||||
rgb = rgb * (0.93 + 0.14 * n);
|
||||
|
||||
@@ -21,7 +21,7 @@ struct SkyColors {
|
||||
sky4: vec4<f32>, // 1.8°
|
||||
fog: vec4<f32>, // horizon (0°) and below: LightIntBand row 7
|
||||
warp: vec4<f32>, // x = dawn/dusk warp strength S (0 = off), y = sun azimuth (rad), zw reserved
|
||||
// MONKEY (sky): x = skyQuality (0 Classic), y = sky clock (s), z = night-sky alpha, w = glow
|
||||
// MONKEY (sky): x = skyQuality (0 Classic), y = reserved, z = night-sky alpha, w = glow
|
||||
// strength.
|
||||
fx: vec4<f32>,
|
||||
// MONKEY (sky): xyz = camera to the visible sun, w unused.
|
||||
@@ -34,6 +34,8 @@ struct SkyColors {
|
||||
mf_fog_d: vec4<f32>,
|
||||
};
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> sky: SkyColors;
|
||||
// MONKEY (polish): x = the sky clock (s, wrapped at a day), written per frame outside the material.
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(101) var<storage, read> sky_clock: vec4<f32>;
|
||||
|
||||
// Glow `g` for a sun-relative azimuth phase, sampled linearly like `0x6d0f50` from the reference's
|
||||
// six-keyframe wrap-around table at `0xce9af8` (written by `0x6ce210`); 0.125 is the sun bearing.
|
||||
@@ -137,7 +139,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let px = length(fwidth(dir)) * sky_fx::STAR_GRID * 0.5;
|
||||
if (night > 0.0 && dir.y > 0.0) {
|
||||
let band = sky_fx::galaxy_band(dir);
|
||||
let stars = sky_fx::star_field(dir, sky.fx.y, max(px, 1e-4), band)
|
||||
let stars = sky_fx::star_field(dir, sky_clock.x, max(px, 1e-4), band)
|
||||
* smoothstep(0.0, 0.2, dir.y);
|
||||
let milky = sky_fx::milky_way(dir, band) * 0.05 * smoothstep(0.03, 0.35, dir.y);
|
||||
lin += (stars * 0.85 + milky) * night;
|
||||
|
||||
@@ -54,14 +54,18 @@ fn hash_cell(p: vec2<i32>, salt: u32) -> f32 {
|
||||
|
||||
// Value noise with the quintic fade: the cubic's second-derivative seam shows as square plateaus
|
||||
// on a slowly drifting sky (the WarcraftXL note).
|
||||
fn value_noise_plane(p: vec2<f32>, salt: u32) -> f32 {
|
||||
// MONKEY (polish): the lattice repeats every `period` cells, so a shift by `period` is no change.
|
||||
fn value_noise_plane(p: vec2<f32>, salt: u32, period: i32) -> f32 {
|
||||
let i = vec2<i32>(floor(p));
|
||||
let f = fract(p);
|
||||
let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
||||
let a = hash_cell(i, salt);
|
||||
let b = hash_cell(i + vec2<i32>(1, 0), salt);
|
||||
let c = hash_cell(i + vec2<i32>(0, 1), salt);
|
||||
let d = hash_cell(i + vec2<i32>(1, 1), salt);
|
||||
let per = vec2<i32>(period);
|
||||
let lo = ((i % per) + per) % per;
|
||||
let hi = (lo + vec2<i32>(1)) % per;
|
||||
let a = hash_cell(lo, salt);
|
||||
let b = hash_cell(vec2<i32>(hi.x, lo.y), salt);
|
||||
let c = hash_cell(vec2<i32>(lo.x, hi.y), salt);
|
||||
let d = hash_cell(hi, salt);
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
@@ -244,6 +248,15 @@ fn milky_way(dir: vec3<f32>, band: f32) -> vec3<f32> {
|
||||
|
||||
// ---- S3: cloud detail -------------------------------------------------------------------------
|
||||
|
||||
// MONKEY (polish): the detail field loops over the sky clock's wrap. It tiles every `CLOUD_TILE`
|
||||
// cells of the detail plane (octave o every `CLOUD_TILE << o` of its own lattice; the half-rate
|
||||
// warp every `CLOUD_TILE / 2`), and the drift covers whole tiles per wrap, so the field at
|
||||
// `SKY_WRAP` is the field at 0. One tile is 64 / 36 of the sheet, wider than the dome's UV.
|
||||
const CLOUD_TILE: i32 = 64;
|
||||
// Tiles of drift per wrap on u and v: 0.01111 and 0.00444 cells/s, WarcraftXL's 0.011 and 0.0043
|
||||
// rounded to whole tiles.
|
||||
const CLOUD_DRIFT_TILES: vec2<f32> = vec2<f32>(15.0, 6.0);
|
||||
|
||||
// WarcraftXL's billow blend: fold each octave toward rounded puffs by `cotton` (0.6).
|
||||
fn billow(v: f32) -> f32 {
|
||||
return v + (1.0 - abs(2.0 * v - 1.0) - v) * 0.6;
|
||||
@@ -256,7 +269,7 @@ fn cloud_billow(p: vec2<f32>, octaves: i32) -> f32 {
|
||||
var tot = 0.0;
|
||||
var q = p;
|
||||
for (var o = 0; o < octaves; o++) {
|
||||
s += a * billow(value_noise_plane(q, u32(o) * 57u));
|
||||
s += a * billow(value_noise_plane(q, u32(o) * 57u, CLOUD_TILE << u32(o)));
|
||||
tot += a;
|
||||
a *= 0.55;
|
||||
q = q * 2.0 + vec2<f32>(3.7, 1.9);
|
||||
@@ -267,8 +280,14 @@ fn cloud_billow(p: vec2<f32>, octaves: i32) -> f32 {
|
||||
// The detail field in [0, 1] on the sheet: a broad warp twists the domain before the billowed fbm
|
||||
// is read, which turns thresholded blobs into cauliflower edges.
|
||||
fn cloud_detail(uv: vec2<f32>, t: f32, octaves: i32) -> f32 {
|
||||
let p = uv * 36.0 + vec2<f32>(0.011, 0.0043) * t;
|
||||
let w = vec2<f32>(value_noise_plane(p * 0.5, 173u), value_noise_plane(p * 0.5 + 5.3, 219u)) - 0.5;
|
||||
// The drift modulo one tile: seamless at the wrap, and small enough to keep f32 precision.
|
||||
let drift = fract(CLOUD_DRIFT_TILES * (t / SKY_WRAP)) * f32(CLOUD_TILE);
|
||||
let p = uv * 36.0 + drift;
|
||||
let half_tile = CLOUD_TILE / 2;
|
||||
let w = vec2<f32>(
|
||||
value_noise_plane(p * 0.5, 173u, half_tile),
|
||||
value_noise_plane(p * 0.5 + 5.3, 219u, half_tile),
|
||||
) - 0.5;
|
||||
return cloud_billow(p + w * 2.6, octaves);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use bevy::pbr::{
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::render_resource::{
|
||||
AsBindGroup, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
AsBindGroup, Buffer, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
};
|
||||
use bevy::shader::ShaderRef;
|
||||
|
||||
@@ -57,7 +57,8 @@ pub struct SkyExt {
|
||||
#[uniform(100)]
|
||||
pub(crate) warp: Vec4,
|
||||
/// MONKEY (sky): `x` the [`crate::sky_fx::SkyQuality`] tier (0 = Classic, the fields below
|
||||
/// unused), `y` the sky clock in seconds, `z` the night-sky alpha, `w` the sun-glow strength.
|
||||
/// unused), `y` reserved (0; the clock is [`Self::clock`]), `z` the night-sky alpha, `w` the
|
||||
/// sun-glow strength.
|
||||
#[uniform(100)]
|
||||
pub(crate) fx: Vec4,
|
||||
/// MONKEY (sky): camera to the visible sun (`xyz`).
|
||||
@@ -73,6 +74,10 @@ pub struct SkyExt {
|
||||
pub(crate) mf_fog_c: Vec4,
|
||||
#[uniform(100)]
|
||||
pub(crate) mf_fog_d: Vec4,
|
||||
/// MONKEY (polish): the shared sky clock ([`crate::sky_fx::SkyClockBuffer`]), outside the
|
||||
/// uniform so the clock never rewrites the material.
|
||||
#[storage(101, read_only, buffer)]
|
||||
pub(crate) clock: Buffer,
|
||||
}
|
||||
|
||||
/// MONKEY (sky): the pipeline key: Enhanced/High compile `SKY_FX` into `sky.wgsl`; Classic keeps
|
||||
@@ -206,7 +211,11 @@ fn setup_sky(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<SkyMaterial>>,
|
||||
clock: Option<Res<crate::sky_fx::SkyClockBuffer>>,
|
||||
) {
|
||||
let Some(clock) = clock else {
|
||||
return;
|
||||
};
|
||||
let mesh = meshes.add(dome_mesh());
|
||||
// Seeded with the neutral-day fallback; `update_sky_colors` overwrites from Light.dbc each frame.
|
||||
let material = materials.add(SkyMaterial {
|
||||
@@ -231,6 +240,7 @@ fn setup_sky(
|
||||
mf_fog_b: Vec4::ZERO,
|
||||
mf_fog_c: Vec4::ZERO,
|
||||
mf_fog_d: Vec4::ZERO,
|
||||
clock: clock.0.clone(),
|
||||
},
|
||||
});
|
||||
commands.spawn((
|
||||
@@ -293,9 +303,8 @@ fn apply_sky_visibility(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update_sky_colors(
|
||||
light: Res<WowLighting>,
|
||||
// MONKEY (sky): the tier, the sky clock and the cloud cover over the sun.
|
||||
// MONKEY (sky): the tier and the cloud cover over the sun.
|
||||
quality: Res<crate::sky_fx::SkyQuality>,
|
||||
clock: Res<crate::sky_fx::SkyClock>,
|
||||
clouds: Res<crate::clouds::CloudCoverage>,
|
||||
// MONKEY (fog): the Modern fog rows for the shared horizon colour.
|
||||
monkey: Res<crate::lighting::MonkeyFrame>,
|
||||
@@ -326,7 +335,7 @@ fn update_sky_colors(
|
||||
0.0,
|
||||
);
|
||||
// MONKEY (sky): the Enhanced/High inputs; all zero at Classic so the write gate stays quiet.
|
||||
let (fx, sun_v, glow) = sky_fx_inputs(&light, *quality, clock.secs, &clouds);
|
||||
let (fx, sun_v, glow) = sky_fx_inputs(&light, *quality, &clouds);
|
||||
// MONKEY (fog): rows B-D as packed for the light buffer; clock lanes are not read here.
|
||||
let rows = monkey.pack(0.0, 0.0);
|
||||
let mf_fog = [
|
||||
@@ -374,7 +383,6 @@ fn update_sky_colors(
|
||||
fn sky_fx_inputs(
|
||||
light: &WowLighting,
|
||||
quality: crate::sky_fx::SkyQuality,
|
||||
secs: f32,
|
||||
clouds: &crate::clouds::CloudCoverage,
|
||||
) -> (Vec4, Vec4, Vec4) {
|
||||
if !quality.enhanced() {
|
||||
@@ -393,7 +401,7 @@ fn sky_fx_inputs(
|
||||
// Quantized like every other lane here, so an idle frame writes nothing.
|
||||
let q = |v: f32| benilla_assets::quantize(v, 4096.0);
|
||||
(
|
||||
Vec4::new(f32::from(quality.0), secs, q(night), q(strength)),
|
||||
Vec4::new(f32::from(quality.0), 0.0, q(night), q(strength)),
|
||||
Vec4::new(q(sun.x), q(sun.y), q(sun.z), 0.0),
|
||||
Vec4::new(q(glow.x), q(glow.y), q(glow.z), 0.0),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
//! Ported from WarcraftXL (https://github.com/WarcraftXL) by iThorgrim — module wxl-retail-clouds, Clouds.cpp, Clouds.hpp.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::{
|
||||
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
||||
render_resource::{Buffer, BufferDescriptor, BufferUsages},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
Render, RenderApp, RenderSystems,
|
||||
};
|
||||
|
||||
/// The sky tier: 0 Classic, 1 Enhanced, 2 High. Written by the app from the `skyQuality` cvar;
|
||||
/// `$WOW_SKY_QUALITY` pins it for the session (captures).
|
||||
@@ -41,8 +47,8 @@ impl SkyQuality {
|
||||
///
|
||||
/// MONKEY (fix-sky): it wrapped hourly in f32, and every star's twinkle jumped at the wrap. Now it
|
||||
/// accumulates in f64 and wraps at [`SKY_CLOCK_WRAP_S`]; the shader's twinkle rates are whole
|
||||
/// cycles per wrap (seamless). The High cloud-detail drift still re-patterns at the wrap, once
|
||||
/// per 24 h of continuous play.
|
||||
/// cycles per wrap (seamless). MONKEY (polish): the High cloud detail tiles and drifts whole
|
||||
/// tiles per wrap (`CLOUD_TILE`, `CLOUD_DRIFT_TILES`), so it is seamless there too.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct SkyClock {
|
||||
pub secs: f32,
|
||||
@@ -53,6 +59,25 @@ pub struct SkyClock {
|
||||
/// The sky clock's wrap in seconds; `sky_fx.wgsl`'s `SKY_WRAP` must match.
|
||||
pub const SKY_CLOCK_WRAP_S: f64 = 86_400.0;
|
||||
|
||||
/// MONKEY (polish): the sky clock on the GPU, one `vec4` (`x` = [`SkyClock::secs`]) that the sky
|
||||
/// and cloud materials bind read-only. It is rewritten in place each frame in the render world,
|
||||
/// so the materials re-prepare only when a real input moves, not to advance the clock. Bevy's
|
||||
/// `globals.time` is not used: it wraps hourly, and the twinkle rates are whole cycles per day.
|
||||
#[derive(Resource, Clone, ExtractResource)]
|
||||
pub struct SkyClockBuffer(pub Buffer);
|
||||
|
||||
/// The clock value the render world uploads.
|
||||
#[derive(Resource, Clone, Copy)]
|
||||
struct SkyClockSecs(f32);
|
||||
|
||||
impl ExtractResource for SkyClockSecs {
|
||||
type Source = SkyClock;
|
||||
|
||||
fn extract_resource(source: &SkyClock) -> Self {
|
||||
Self(source.secs)
|
||||
}
|
||||
}
|
||||
|
||||
/// How strongly the glow shows: a broad halo at `GLOW_GAIN` × the sun colour at the sun itself.
|
||||
pub(crate) const GLOW_GAIN: f32 = 0.22;
|
||||
|
||||
@@ -92,10 +117,50 @@ impl Plugin for SkyFxPlugin {
|
||||
let quality = SkyQuality(SkyQuality::env_override().unwrap_or(0));
|
||||
app.insert_resource(quality)
|
||||
// MONKEY (integration): the clock ticks before its readers (sky colours, cloud FX).
|
||||
.add_systems(Update, tick_sky_clock.before(crate::lighting::LightingConsumeSet));
|
||||
.add_systems(Update, tick_sky_clock.before(crate::lighting::LightingConsumeSet))
|
||||
// Beside the shared light buffer, ahead of the sky and cloud materials built after it.
|
||||
.add_systems(
|
||||
Startup,
|
||||
init_sky_clock_buffer.in_set(benilla_assets::AssetSet::Open),
|
||||
)
|
||||
.add_plugins((
|
||||
ExtractResourcePlugin::<SkyClockBuffer>::default(),
|
||||
ExtractResourcePlugin::<SkyClockSecs>::default(),
|
||||
));
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.add_systems(
|
||||
Render,
|
||||
upload_sky_clock.in_set(RenderSystems::PrepareResources),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A headless build has no device, and then no sky or cloud dome either.
|
||||
fn init_sky_clock_buffer(mut commands: Commands, device: Option<Res<RenderDevice>>) {
|
||||
let Some(device) = device else {
|
||||
return;
|
||||
};
|
||||
commands.insert_resource(SkyClockBuffer(device.create_buffer(&BufferDescriptor {
|
||||
label: Some("sky_clock"),
|
||||
size: 16,
|
||||
usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})));
|
||||
}
|
||||
|
||||
/// Render-world: the clock into the shared buffer before any sky draw reads it.
|
||||
fn upload_sky_clock(
|
||||
queue: Res<RenderQueue>,
|
||||
buffer: Option<Res<SkyClockBuffer>>,
|
||||
secs: Option<Res<SkyClockSecs>>,
|
||||
) {
|
||||
let (Some(buffer), Some(secs)) = (buffer, secs) else {
|
||||
return;
|
||||
};
|
||||
queue.write_buffer(&buffer.0, 0, bytemuck::cast_slice(&[secs.0, 0.0, 0.0, 0.0]));
|
||||
}
|
||||
|
||||
fn tick_sky_clock(time: Res<Time>, mut clock: ResMut<SkyClock>) {
|
||||
if let Some(t) = clock.frozen {
|
||||
if clock.secs != t {
|
||||
@@ -111,6 +176,39 @@ fn tick_sky_clock(time: Res<Time>, mut clock: ResMut<SkyClock>) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A `const NAME ... = <value>;` line of `sky_fx.wgsl`, as text.
|
||||
fn wgsl_const(name: &str) -> String {
|
||||
let src = include_str!("shaders/sky_fx.wgsl");
|
||||
let line = src
|
||||
.lines()
|
||||
.find(|l| l.starts_with(&format!("const {name}:")))
|
||||
.unwrap_or_else(|| panic!("no const {name}"));
|
||||
let value = line.split('=').nth(1).unwrap().trim();
|
||||
value.trim_end_matches(';').trim().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shader_wrap_and_the_cloud_loop_match_the_clock() {
|
||||
let wrap: f64 = wgsl_const("SKY_WRAP").parse().unwrap();
|
||||
assert_eq!(wrap, SKY_CLOCK_WRAP_S);
|
||||
// The loop needs an even tile (the half-rate warp tiles at half of it) and whole tiles of
|
||||
// drift per wrap; the rates stay within 5% of WarcraftXL's 0.011 and 0.0043 cells/s.
|
||||
let tile: i32 = wgsl_const("CLOUD_TILE").parse().unwrap();
|
||||
assert!(tile > 0 && tile % 2 == 0);
|
||||
let drift = wgsl_const("CLOUD_DRIFT_TILES");
|
||||
let tiles: Vec<f64> = drift
|
||||
.trim_start_matches("vec2<f32>(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.map(|v| v.trim().parse().unwrap())
|
||||
.collect();
|
||||
for (n, rate) in tiles.iter().zip([0.011, 0.0043]) {
|
||||
assert_eq!(n.fract(), 0.0, "whole tiles per wrap");
|
||||
let actual = n * f64::from(tile) / SKY_CLOCK_WRAP_S;
|
||||
assert!((actual / rate - 1.0).abs() < 0.05, "drift {actual} vs {rate}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glow_fades_out_below_the_horizon_and_under_cloud() {
|
||||
assert_eq!(glow_strength(-0.2, 0.0, 0.0), 0.0);
|
||||
|
||||
Reference in New Issue
Block a user