MONKEY (post): add depth-occluded sun shafts
Co-Authored-By: GPT-5.6 Sol (Codex)
This commit is contained in:
co-authored by
GPT-5.6 Sol (Codex)
parent
fe94f670ab
commit
57220f558f
@@ -496,6 +496,7 @@
|
||||
volumetricFog = "Volumetric Fog",
|
||||
-- MONKEY (post): HDR excess + halo quality.
|
||||
bloom = "Bloom",
|
||||
sunShafts = "Sun Shafts",
|
||||
fogOff = "Off",
|
||||
waterQuality = "Water Quality",
|
||||
waterClassic = "Classic",
|
||||
@@ -537,6 +538,8 @@
|
||||
-- MONKEY (post): Off guarantees the pre-HDR image.
|
||||
BLOOM =
|
||||
"Makes fire, lava, spells and lit windows carry a soft halo. Low uses a smaller blur chain; High is smoother. Off leaves the original image unchanged.",
|
||||
SUN_SHAFTS =
|
||||
"Draws sunlight through gaps in trees and buildings. Scene depth blocks the rays; they fade at night, off-screen and indoors.",
|
||||
VOLUMETRIC_FOG =
|
||||
"Off disables volumetric fog. Low adds nearby haze and sun or moon shafts with fewer steps; High uses more steps for smoother shafts. Mist is heavier at dawn and in bad weather, and subtle indoors. Shafts require shadows; with shadows off only haze remains.",
|
||||
WATER_QUALITY =
|
||||
@@ -623,6 +626,7 @@
|
||||
lightingQuality = "Качество освещения",
|
||||
-- MONKEY (post): Russian HDR excess + halo quality row.
|
||||
bloom = "Свечение",
|
||||
sunShafts = "Солнечные лучи",
|
||||
-- MONKEY (volumetric fog): Russian atmospheric quality row.
|
||||
volumetricFog = "Объёмный туман",
|
||||
fogOff = "Выкл",
|
||||
@@ -667,6 +671,8 @@
|
||||
-- MONKEY (post): localised bloom effect and Off contract.
|
||||
BLOOM =
|
||||
"Добавляет мягкое сияние огню, лаве, заклинаниям и освещённым окнам. Низкое качество использует короткое размытие, высокое — более плавное. Выключение сохраняет исходное изображение.",
|
||||
SUN_SHAFTS =
|
||||
"Показывает солнечные лучи сквозь деревья и здания. Рельеф и здания перекрывают свет; ночью и в помещениях эффект исчезает.",
|
||||
-- MONKEY (volumetric fog): localised effect and cost explanation.
|
||||
VOLUMETRIC_FOG =
|
||||
"Выкл отключает объёмный туман. Низкое добавляет дымку вблизи и лучи солнца или луны с меньшим числом шагов; Высокое использует больше шагов для более плавных лучей. Туман гуще на рассвете и в непогоду, а в помещениях слабее. Для лучей нужны тени; без теней остаётся только дымка.",
|
||||
@@ -805,7 +811,7 @@
|
||||
-- three of eighteen shadow settings and no hint that the other fifteen existed.
|
||||
AdvancedGraphics = {
|
||||
-- MONKEY (volumetric fog): include in refresh, defaults and search.
|
||||
"RowLightingQuality", "RowWaterQuality", "RowVolumetricFog", "RowBloom",
|
||||
"RowLightingQuality", "RowWaterQuality", "RowVolumetricFog", "RowBloom", "RowSunShafts",
|
||||
"RowCharacterShadows", "RowWorldShadows", "RowShadowDistance",
|
||||
"RowShadowResolution", "RowMoonShadows",
|
||||
"RowInteriorLight", "RowInteriorShadows", "RowExteriorShadows", "RowTorchSoft",
|
||||
@@ -2628,12 +2634,23 @@
|
||||
})
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowCharacterShadows" inherits="OptionsCheckboxRowTemplate">
|
||||
<!-- MONKEY (post): depth-occluded radial sunlight, world only. -->
|
||||
<Frame name="$parentRowSunShafts" inherits="OptionsCheckboxRowTemplate">
|
||||
<Anchors>
|
||||
<!-- MONKEY (post): bloom is the new row immediately above this one. -->
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowBloom" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowBloom" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
OptionsRow_OnLoad(self, "sunShafts", BENILLA_ADVGFX.sunShafts,
|
||||
"BENILLA_TOOLTIP_SUN_SHAFTS")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowCharacterShadows" inherits="OptionsCheckboxRowTemplate">
|
||||
<Anchors>
|
||||
<!-- MONKEY (post): screen effects sit immediately above this one. -->
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowSunShafts" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowSunShafts" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
OptionsRow_OnLoad(self, "characterShadows",
|
||||
BENILLA_ADVGFX.characterShadows,
|
||||
|
||||
@@ -923,6 +923,7 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
ours("volumetricFog", "1", "benilla's own: near-field volumetric fog, 0 Off / 1 Low / 2 High"),
|
||||
// MONKEY (post): tier 0 leaves every emissive site and the frame byte-identical.
|
||||
ours("bloom", "2", "benilla's own: HDR emissive bloom, 0 Off / 1 Low / 2 High"),
|
||||
ours("sunShafts", "1", "benilla's own: depth-occluded screen-space sun shafts"),
|
||||
ours(
|
||||
"waterQuality",
|
||||
"1",
|
||||
@@ -2012,6 +2013,7 @@ pub(crate) const LIGHTING_PRESETS: &[(&str, &[(&str, &str)])] = &[
|
||||
("volumetricFog", "0"),
|
||||
// MONKEY (post): no HDR lift or post pass is the byte-identical baseline.
|
||||
("bloom", "0"),
|
||||
("sunShafts", "0"),
|
||||
("lavaLightGain", "0"),
|
||||
("fireLightGain", "0"),
|
||||
("nightGain", "1.0"),
|
||||
@@ -2037,6 +2039,7 @@ pub(crate) const LIGHTING_PRESETS: &[(&str, &[(&str, &str)])] = &[
|
||||
("volumetricFog", "1"),
|
||||
// MONKEY (post): quarter-resolution halo.
|
||||
("bloom", "1"),
|
||||
("sunShafts", "0"),
|
||||
("lavaLightGain", "1"),
|
||||
("fireLightGain", "1"),
|
||||
("nightGain", "0.45"),
|
||||
@@ -2065,6 +2068,7 @@ pub(crate) const LIGHTING_PRESETS: &[(&str, &[(&str, &str)])] = &[
|
||||
("volumetricFog", "1"),
|
||||
// MONKEY (post): quarter-resolution halo.
|
||||
("bloom", "1"),
|
||||
("sunShafts", "0"),
|
||||
("lavaLightGain", "1"),
|
||||
("fireLightGain", "1"),
|
||||
("nightGain", "0.45"),
|
||||
@@ -2094,6 +2098,7 @@ pub(crate) const LIGHTING_PRESETS: &[(&str, &[(&str, &str)])] = &[
|
||||
("volumetricFog", "1"),
|
||||
// MONKEY (post): half-resolution halo.
|
||||
("bloom", "2"),
|
||||
("sunShafts", "1"),
|
||||
("lavaLightGain", "1"),
|
||||
("fireLightGain", "1"),
|
||||
("nightGain", "0.45"),
|
||||
@@ -2908,12 +2913,13 @@ mod tests {
|
||||
let shadows = VideoConfig::default();
|
||||
let flag = |b: bool| if b { 1.0 } else { 0.0 };
|
||||
// MONKEY (volumetric fog): include the atmospheric tier in this fixed-size default table.
|
||||
let lighting: [(&str, f32); 33] = [
|
||||
let lighting: [(&str, f32); 35] = [
|
||||
("waterQuality", shadows.water_quality as f32),
|
||||
// MONKEY (volumetric fog): weld registry and renderer defaults.
|
||||
("volumetricFog", shadows.volumetric_fog as f32),
|
||||
// MONKEY (post): weld registry and renderer defaults.
|
||||
("bloom", shadows.bloom as f32),
|
||||
("sunShafts", flag(shadows.sun_shafts)),
|
||||
("lavaLightGain", shadows.lava_light_gain),
|
||||
// The two sun lanes and the cascade they share.
|
||||
("worldShadows", flag(shadows.world_shadows)),
|
||||
@@ -2966,10 +2972,10 @@ mod tests {
|
||||
assert_eq!(d[name], want, "{name}: registered default left the knob");
|
||||
}
|
||||
// …and the census IS the row set. A name here that nothing registers would weld against a
|
||||
// row the client does not have; the length is the other half — 33 rows, 33 welds.
|
||||
// row the client does not have; the length is the other half — 35 rows, 35 welds.
|
||||
let welded: std::collections::BTreeSet<&str> = lighting.iter().map(|(n, _)| *n).collect();
|
||||
// MONKEY (volumetric fog): the atmospheric tier joins the default-consumer weld.
|
||||
assert_eq!(welded.len(), 33, "the lighting lane welds 33 distinct rows");
|
||||
assert_eq!(welded.len(), 35, "the lighting lane welds 35 distinct rows");
|
||||
for name in &welded {
|
||||
assert!(
|
||||
REGISTERED.iter().any(|r| r.name == *name),
|
||||
@@ -3147,6 +3153,7 @@ mod tests {
|
||||
"volumetricFog" => video.volumetric_fog as f32,
|
||||
// MONKEY (post): the post lane's live tier.
|
||||
"bloom" => video.bloom as f32,
|
||||
"sunShafts" => video.sun_shafts as u32 as f32,
|
||||
"lavaLightGain" => video.lava_light_gain,
|
||||
"fireLightGain" => video.fire_light_gain,
|
||||
"moonShadowStrength" => video.moon_shadow_strength,
|
||||
@@ -3999,7 +4006,7 @@ mod tests {
|
||||
strings.sort_unstable(); // the list is the claim, not where the rows sit in the table
|
||||
assert_eq!(
|
||||
strings,
|
||||
vec!["gxApi", "gxResolution", "realmList", "realmName"]
|
||||
vec!["gxApi", "gxResolution", "lightingQuality", "realmList", "realmName"]
|
||||
);
|
||||
let default_of = |name: &str| {
|
||||
REGISTERED
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//! MONKEY (post): optional world-camera effects ahead of FFXGlow's gamma clamp.
|
||||
|
||||
mod bloom;
|
||||
mod sun_shafts;
|
||||
|
||||
pub(crate) struct PostPlugin;
|
||||
|
||||
impl bevy::prelude::Plugin for PostPlugin {
|
||||
fn build(&self, app: &mut bevy::prelude::App) {
|
||||
app.add_plugins(bloom::BloomPlugin);
|
||||
app.add_plugins(sun_shafts::SunShaftsPlugin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
//! MONKEY (post): screen-space sun shafts from the visible sky depth.
|
||||
|
||||
use super::bloom::BloomLabel;
|
||||
use crate::video::VideoConfig;
|
||||
use benilla_world::{lighting::WowLighting, view::WorldCamera, wmo_portal::CameraInteriorClaim};
|
||||
use bevy::{
|
||||
core_pipeline::{
|
||||
core_3d::graph::{Core3d, Node3d},
|
||||
FullscreenShader,
|
||||
},
|
||||
ecs::query::QueryItem,
|
||||
prelude::*,
|
||||
render::{
|
||||
diagnostic::RecordDiagnostics,
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
render_graph::{
|
||||
NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner,
|
||||
},
|
||||
render_resource::{binding_types::*, *},
|
||||
renderer::{RenderContext, RenderDevice, RenderQueue},
|
||||
view::{Msaa, ViewDepthTexture, ViewTarget},
|
||||
Render, RenderApp, RenderStartup, RenderSystems,
|
||||
},
|
||||
};
|
||||
|
||||
pub(super) struct SunShaftsPlugin;
|
||||
|
||||
#[derive(Component, Clone, Copy, PartialEq, ExtractComponent, ShaderType)]
|
||||
struct ShaftView {
|
||||
// xy = sun UV, z = faded strength, w = sample count.
|
||||
sun: Vec4,
|
||||
// Gamma-space sun colour; the framebuffer is still gamma-space here.
|
||||
color: Vec4,
|
||||
}
|
||||
|
||||
impl Plugin for SunShaftsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(ExtractComponentPlugin::<ShaftView>::default())
|
||||
.add_systems(Last, update_views);
|
||||
if !app.is_plugin_added::<AssetPlugin>() {
|
||||
return;
|
||||
}
|
||||
let shader = app
|
||||
.world_mut()
|
||||
.resource_mut::<Assets<Shader>>()
|
||||
.add(Shader::from_wgsl(
|
||||
include_str!("sun_shafts.wgsl"),
|
||||
"post/sun_shafts.wgsl",
|
||||
));
|
||||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||
return;
|
||||
};
|
||||
render_app
|
||||
.insert_resource(ShaftShader(shader))
|
||||
.init_resource::<SpecializedRenderPipelines<ShaftPipeline>>()
|
||||
.add_systems(RenderStartup, init_pipeline)
|
||||
.add_systems(Render, prepare_pipelines.in_set(RenderSystems::Prepare))
|
||||
.add_render_graph_node::<ViewNodeRunner<ShaftNode>>(Core3d, ShaftLabel)
|
||||
.add_render_graph_edges(
|
||||
Core3d,
|
||||
(Node3d::StartMainPassPostProcessing, ShaftLabel, BloomLabel),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_views(
|
||||
mut commands: Commands,
|
||||
video: Res<VideoConfig>,
|
||||
lighting: Res<WowLighting>,
|
||||
interior: Res<CameraInteriorClaim>,
|
||||
mut cameras: Query<
|
||||
(
|
||||
Entity,
|
||||
&Camera,
|
||||
&GlobalTransform,
|
||||
&Projection,
|
||||
&mut Camera3d,
|
||||
Option<&ShaftView>,
|
||||
),
|
||||
With<WorldCamera>,
|
||||
>,
|
||||
) {
|
||||
let to_sun = lighting.celestial_dir().normalize_or_zero();
|
||||
for (entity, camera, transform, projection, mut camera3d, old) in &mut cameras {
|
||||
let disabled =
|
||||
!video.sun_shafts || !camera.is_active || interior.0.is_some() || to_sun == Vec3::ZERO;
|
||||
if disabled {
|
||||
if old.is_some() {
|
||||
commands.entity(entity).remove::<ShaftView>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let far = match projection {
|
||||
Projection::Perspective(p) => p.far,
|
||||
_ => 3000.0,
|
||||
};
|
||||
let world = transform.translation() + to_sun * far * 0.85;
|
||||
let (Ok(screen), Some(size)) = (
|
||||
camera.world_to_viewport(transform, world),
|
||||
camera.logical_viewport_size(),
|
||||
) else {
|
||||
commands.entity(entity).remove::<ShaftView>();
|
||||
continue;
|
||||
};
|
||||
let uv = screen / size;
|
||||
// Keep a small off-screen skirt so shafts leave the frame continuously.
|
||||
let outside = Vec2::new(
|
||||
(-uv.x).max(uv.x - 1.0).max(0.0),
|
||||
(-uv.y).max(uv.y - 1.0).max(0.0),
|
||||
)
|
||||
.max_element();
|
||||
let edge_fade = 1.0 - smoothstep(0.0, 0.18, outside);
|
||||
let daylight = smoothstep(-0.02, 0.16, to_sun.y);
|
||||
let strength = edge_fade * daylight;
|
||||
if strength <= 0.001 {
|
||||
commands.entity(entity).remove::<ShaftView>();
|
||||
continue;
|
||||
}
|
||||
camera3d.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
|
||||
let next = ShaftView {
|
||||
sun: uv.extend(strength).extend(28.0),
|
||||
color: Vec3::from_array(lighting.diffuse)
|
||||
.lerp(Vec3::ONE, 0.18)
|
||||
.extend(0.0),
|
||||
};
|
||||
if old != Some(&next) {
|
||||
commands.entity(entity).insert(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn smoothstep(a: f32, b: f32, value: f32) -> f32 {
|
||||
let t = ((value - a) / (b - a)).clamp(0.0, 1.0);
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ShaftShader(Handle<Shader>);
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ShaftPipeline {
|
||||
layouts: [BindGroupLayoutDescriptor; 2],
|
||||
shader: Handle<Shader>,
|
||||
fullscreen: FullscreenShader,
|
||||
sampler: Sampler,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct ViewShaftPipeline(CachedRenderPipelineId);
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
||||
struct ShaftLabel;
|
||||
|
||||
fn init_pipeline(
|
||||
mut commands: Commands,
|
||||
device: Res<RenderDevice>,
|
||||
shader: Res<ShaftShader>,
|
||||
fullscreen: Res<FullscreenShader>,
|
||||
) {
|
||||
let layouts = [false, true].map(|multisampled| {
|
||||
BindGroupLayoutDescriptor::new(
|
||||
"post_sun_shafts_layout",
|
||||
&BindGroupLayoutEntries::sequential(
|
||||
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),
|
||||
),
|
||||
),
|
||||
)
|
||||
});
|
||||
commands.insert_resource(ShaftPipeline {
|
||||
layouts,
|
||||
shader: shader.0.clone(),
|
||||
fullscreen: fullscreen.clone(),
|
||||
sampler: device.create_sampler(&SamplerDescriptor {
|
||||
min_filter: FilterMode::Linear,
|
||||
mag_filter: FilterMode::Linear,
|
||||
address_mode_u: AddressMode::ClampToEdge,
|
||||
address_mode_v: AddressMode::ClampToEdge,
|
||||
..default()
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
impl SpecializedRenderPipeline for ShaftPipeline {
|
||||
type Key = (TextureFormat, bool);
|
||||
|
||||
fn specialize(&self, (format, multisampled): Self::Key) -> RenderPipelineDescriptor {
|
||||
RenderPipelineDescriptor {
|
||||
label: Some("post_sun_shafts".into()),
|
||||
layout: vec![self.layouts[multisampled as usize].clone()],
|
||||
vertex: self.fullscreen.to_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: self.shader.clone(),
|
||||
shader_defs: multisampled
|
||||
.then(|| "MULTISAMPLED".into())
|
||||
.into_iter()
|
||||
.collect(),
|
||||
entry_point: Some("fragment".into()),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
..default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_pipelines(
|
||||
mut commands: Commands,
|
||||
cache: Res<PipelineCache>,
|
||||
pipeline: Res<ShaftPipeline>,
|
||||
mut specialized: ResMut<SpecializedRenderPipelines<ShaftPipeline>>,
|
||||
views: Query<(Entity, &ViewTarget, &Msaa), With<ShaftView>>,
|
||||
) {
|
||||
for (entity, target, msaa) in &views {
|
||||
let id = specialized.specialize(
|
||||
&cache,
|
||||
&pipeline,
|
||||
(target.main_texture_format(), msaa.samples() > 1),
|
||||
);
|
||||
commands.entity(entity).insert(ViewShaftPipeline(id));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ShaftNode;
|
||||
|
||||
impl ViewNode for ShaftNode {
|
||||
type ViewQuery = (
|
||||
&'static ViewTarget,
|
||||
&'static ViewDepthTexture,
|
||||
&'static ShaftView,
|
||||
&'static ViewShaftPipeline,
|
||||
);
|
||||
|
||||
fn run<'w>(
|
||||
&self,
|
||||
_graph: &mut RenderGraphContext,
|
||||
context: &mut RenderContext<'w>,
|
||||
(target, depth, shaft, id): QueryItem<'w, '_, Self::ViewQuery>,
|
||||
world: &'w World,
|
||||
) -> Result<(), NodeRunError> {
|
||||
let cache = world.resource::<PipelineCache>();
|
||||
let Some(pipeline) = cache.get_render_pipeline(id.0) else {
|
||||
return Ok(());
|
||||
};
|
||||
if !depth
|
||||
.texture
|
||||
.usage()
|
||||
.contains(TextureUsages::TEXTURE_BINDING)
|
||||
{
|
||||
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 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],
|
||||
),
|
||||
&BindGroupEntries::sequential((
|
||||
out.source,
|
||||
&settings.sampler,
|
||||
depth.view(),
|
||||
uniform.binding().unwrap(),
|
||||
)),
|
||||
);
|
||||
let diagnostics = context.diagnostic_recorder();
|
||||
let mut pass = context
|
||||
.command_encoder()
|
||||
.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("post_sun_shafts"),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: out.destination,
|
||||
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");
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(0, &bind, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
span.end(&mut pass);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn edge_and_night_fades_are_bounded() {
|
||||
assert_eq!(smoothstep(0.0, 1.0, -1.0), 0.0);
|
||||
assert_eq!(smoothstep(0.0, 1.0, 2.0), 1.0);
|
||||
assert!((smoothstep(0.0, 1.0, 0.5) - 0.5).abs() < 1.0e-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// MONKEY (post): screen-space radial blur through sky pixels in the scene depth.
|
||||
|
||||
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
|
||||
|
||||
@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
|
||||
|
||||
struct Shafts {
|
||||
sun: vec4<f32>,
|
||||
color: vec4<f32>,
|
||||
};
|
||||
@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
|
||||
var d = 0.0;
|
||||
for (var sample = 0u; sample < textureNumSamples(depth); sample++) {
|
||||
// Reversed Z: any covered sample is enough to block the sky ray.
|
||||
d = max(d, textureLoad(depth, p, i32(sample)));
|
||||
}
|
||||
return d;
|
||||
#else
|
||||
return textureLoad(depth, p, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
let base = textureSample(scene, scene_sampler, in.uv);
|
||||
let count = u32(shafts.sun.w);
|
||||
let step_uv = (shafts.sun.xy - in.uv) * (0.88 / shafts.sun.w);
|
||||
var uv = in.uv;
|
||||
var decay = 1.0;
|
||||
var light = 0.0;
|
||||
var norm = 0.0;
|
||||
for (var i = 0u; i < 32u; i++) {
|
||||
if (i >= count) { break; }
|
||||
uv += step_uv;
|
||||
decay *= 0.965;
|
||||
let sky = 1.0 - smoothstep(0.000002, 0.00008, scene_depth(uv));
|
||||
light += sky * decay;
|
||||
norm += decay;
|
||||
}
|
||||
let rays = light / max(norm, 0.0001);
|
||||
return vec4(base.rgb + shafts.color.rgb * rays * shafts.sun.z * 0.16, base.a);
|
||||
}
|
||||
@@ -224,6 +224,8 @@ pub(crate) struct VideoConfig {
|
||||
pub(crate) volumetric_fog: u8,
|
||||
// MONKEY (post): emissive HDR + bloom quality, 0 Off / 1 Low / 2 High.
|
||||
pub(crate) bloom: u8,
|
||||
// MONKEY (post): depth-occluded radial sun shafts.
|
||||
pub(crate) sun_shafts: bool,
|
||||
/// Brightness of lava lighting its surroundings, 0..4; 0 disables the glow.
|
||||
/// Published to `benilla_world::lighting::LavaLightGain` by `dynamic_interior::bridge`.
|
||||
pub(crate) lava_light_gain: f32,
|
||||
@@ -471,6 +473,8 @@ impl Default for VideoConfig {
|
||||
volumetric_fog: 1,
|
||||
// MONKEY (post): the shipped High graphics preset uses the full-resolution tier.
|
||||
bloom: 2,
|
||||
// MONKEY (post): part of the shipped High graphics preset.
|
||||
sun_shafts: true,
|
||||
lava_light_gain: 1.0,
|
||||
fire_flicker: 1.0,
|
||||
display: if windowed_env() {
|
||||
@@ -553,6 +557,8 @@ pub(crate) fn on_cvar(
|
||||
"volumetricfog" => cfg.volumetric_fog = v.clamp(0.0, 2.0) as u8,
|
||||
// MONKEY (post): constrain UI/console writes to the supported bloom tiers.
|
||||
"bloom" => cfg.bloom = v.clamp(0.0, 2.0) as u8,
|
||||
// MONKEY (post): the shafts lane is binary.
|
||||
"sunshafts" => cfg.sun_shafts = v != 0.0,
|
||||
"lavalightgain" => cfg.lava_light_gain = v.clamp(0.0, 4.0),
|
||||
"worldshadows" => cfg.world_shadows = ev.flag(),
|
||||
"charactershadows" => cfg.character_shadows = ev.flag(),
|
||||
|
||||
Reference in New Issue
Block a user