Merge gfx/leftovers (skybox flags 0x8/0x10, shaft water mask, shadow-caster warm, torch terrain census) into integration
This commit is contained in:
+2
-2
@@ -20,7 +20,7 @@ behind each constant.
|
||||
| Synthesised lights | torches, braziers, lanterns, campfires and lampposts emit a light in the colour of their flame, with flicker | `fireLightGain`, `fireFlicker` |
|
||||
| Torch shadows | cube-map shadows from point lights onto buildings, models and terrain, with a contact-hardening penumbra and a shadow floor | `interiorShadows`, `exteriorShadows`, `torchShadowStrength`, `interiorShadowSoft` |
|
||||
| Daylight and doorways | calibrated fixtures at doors, windows and open boundaries carry daylight into a room, and doorways between rooms bleed light | `interiorDaylight`, env `WOW_DAYLIGHT`, `WOW_BLEED` |
|
||||
| Terrain in torch shadows | the ground casts into an outdoor fire's cube map, so a hill or bank blocks the fire (settled exterior slots only; `torch_terrain.rs` gathers the resident MCNK chunks inside the cube's 48 yd) | `torchTerrainShadows` (default 0, High 1), env `WOW_TORCH_TERRAIN=0\|1` |
|
||||
| Terrain in torch shadows | the ground casts into an outdoor fire's cube map, so a hill or bank blocks the fire (settled exterior slots only; `torch_terrain.rs` gathers the resident MCNK chunks inside the cube's 48 yd; the gather and the ground receiver both work, but stock Elwynn lamps/campfires show no visible change: the terrain-blocked ground is grazing (N·L ≈ 0, inside the 0.15 yd normal offset) or at the pool rim, below one 8-bit step — measured by the `lamp_terrain_occlusion_scan` census) | `torchTerrainShadows` (default 0, High 1), env `WOW_TORCH_TERRAIN=0\|1` |
|
||||
| City day floor | `interiorDaylight`'s room floor also reaches Stormwind/Ironforge rooms that the portal graph connects to the sky (an exterior-facing portal, or one hop from one; `district_sky_rooms` in `daylight.rs`); before this no city room could take it | `interiorDaylight` (default 0, suggested High 0.10) |
|
||||
| Spell and firework lights | fire, holy and fel effects light their surroundings for their lifetime; frost, nature, arcane and shadow do not | `spellLightGain`, env `WOW_SPELL_LIGHT=0` |
|
||||
| Moon shadows | at night the same shadow rig re-aims at the moon and casts a faint shadow; dims only the night sky term, never point lights | `moonShadowStrength` |
|
||||
@@ -35,7 +35,7 @@ behind each constant.
|
||||
| Ambient occlusion | soft contact shadows under props, in wall-floor corners and around feet; depth-only SSAO at half resolution (depth-reconstructed normals, 6/12 spiral taps, 4x4 depth-aware blur, bilateral upsample) multiplied into the opaque scene after the opaque pass and before the water copy, so water, particles and UI are untouched; fades out 45-90 yd (Low) / 60-120 yd (High), spares sky and bright pixels; on alpha-tested foliage (a dense window of two-sided depth spikes) the blur widens its depth tolerance and the AO drops 40 %, which removes the leaf grain while planes, silhouettes and contact creases keep full AO (`benilla-app/src/ssao.rs`, `benilla-app/src/shaders/ssao.wgsl`) | Advanced Graphics → Ambient Occlusion (Off/Low/High; default Off, High preset 2), cvar `ambientOcclusion`, env `WOW_AO=0\|1\|2`, `WOW_AO_DEBUG=1..5` diagnostic views (factor, protection, distance, raw occlusion, foliage mask), `WOW_AO_GAIN`/`_RADIUS`/`_BIAS`/`_STRENGTH` tuning |
|
||||
| Lamp fog | MONKEY (lampfog): the fog fullscreen pass selects the nearest resolved exterior and interior point-table sources and analytically integrates their softened inverse-square light over the camera-to-scene-depth ray. A near-field haze floor makes lamps visible inside the ordinary volumetric haze's 10 yd clear zone; it is zero by day, stronger in rain/fog, and deliberately unshadowed. The table colour already includes intensity, flame flicker and `fireLightGain`. Halo output uses destination headroom and stays below 1.0, so a later bloom pass cannot blow it out | Advanced Graphics → Lamp Fog, cvar `lampFog` 0 Off (default, unchanged) / 1 Low (16 lamps) / 2 High (32 lamps; Graphics preset High = 2), env `WOW_LAMPFOG=0\|1\|2`; `benilla-app/src/volumetric_fog.rs` |
|
||||
| City window daylight | MONKEY (fix-daylight/integration): a district WMO's oversized window batch (e.g. the Cathedral of Light's stained-glass walls) is split into window-sized daylight apertures, so the nave takes daylight pane by pane. Applies to WMOs loaded after a change | Advanced Graphics → City Window Daylight, cvar `daylightWindowSplit` 0/1 (default 1, the merged behaviour); `benilla-world/src/lighting/daylight.rs` (`set_window_split`), bridge `benilla-app/src/dynamic_interior.rs` |
|
||||
| Zone skyboxes | the painted sky a zone's `LightParams` names (clear-weather slot, storm slot lerped by the storm blend) for the living, fading in by the Light sphere falloff with the modern client's crossfade (ghost > WMO > zone); extended `LightSkybox.dbc` flags `0x1` day cycle, `0x2` keep sun/moon/stars/clouds, `0x4` fog-colour horizon cone, plus a celestial second model; every skybox now plays its bones, texture transforms, colour and alpha tracks (`benilla-world/src/skybox.rs`, `skybox_anim.rs`, `benilla-formats/src/light/skybox.rs`) | Advanced Graphics → Zone Skyboxes, cvar `zoneSkyboxes` (0 Off default, 1 On; High = 1), env `WOW_ZONE_SKYBOXES=0\|1` |
|
||||
| Zone skyboxes | the painted sky a zone's `LightParams` names (clear-weather slot, storm slot lerped by the storm blend) for the living, fading in by the Light sphere falloff with the modern client's crossfade (ghost > WMO > zone); extended `LightSkybox.dbc` flags `0x1` day cycle, `0x2` keep sun/moon/stars/clouds, `0x4` fog-colour horizon cone, `0x8` forces the screen-space sun shafts through a low or just-set sun (the sky's weight lifts their daylight fade; `post/sun_shafts.rs`), `0x10` fades the Modern fog's sun-colour lobe out by the sky's weight (`lighting/fog_model.rs`), plus a celestial second model; every skybox now plays its bones, texture transforms, colour and alpha tracks (`benilla-world/src/skybox.rs`, `skybox_anim.rs`, `benilla-formats/src/light/skybox.rs`) | Advanced Graphics → Zone Skyboxes, cvar `zoneSkyboxes` (0 Off default, 1 On; High = 1), env `WOW_ZONE_SKYBOXES=0\|1` |
|
||||
| Night and interior level | global dimming of the night sky term and of interior ambient | `nightGain`, `interiorGain`, `interiorBakeFloor` |
|
||||
|
||||
`zoneSkyboxes = 0` disables only the added living-player zone slot. WMO and ghost skyboxes are
|
||||
|
||||
@@ -1808,6 +1808,16 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
minute: 0,
|
||||
ui: None,
|
||||
},
|
||||
// MONKEY (leftovers): the Westfall sea with the visible sun ~5° up on its fixed 45° bearing
|
||||
// (minute 1200), framed so the sun sits above the open water: the sun-shaft sky mask test.
|
||||
Scenario {
|
||||
name: "leftovers-sea-sunset",
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-10500.0, 2112.0, 6.0],
|
||||
look: [-10420.0, 2192.0, 8.0],
|
||||
minute: 1200,
|
||||
ui: None,
|
||||
},
|
||||
];
|
||||
|
||||
/// MONKEY (p0 baseline): the Elwynn programme view, above the canopy east of Goldshire looking
|
||||
|
||||
@@ -63,6 +63,11 @@ pub(super) struct WarmLanes<'w> {
|
||||
/// The UI quad store (2262): the minimap interior composite's tile material is built here
|
||||
/// through its own production builder, for the tile-quad warm rig below.
|
||||
ui_quads: ResMut<'w, Assets<crate::ui_pass::UiQuadMaterial>>,
|
||||
/// MONKEY (leftovers): the two sun-shadow proxy lanes, `ShadowCasterMaterial` (solid) and
|
||||
/// `CutoutShadowCasterMaterial` (alpha-tested leaves).
|
||||
/// `Option`: an app without the shadow plugins (tests, the glue) has no store.
|
||||
shadow_solid: Option<ResMut<'w, Assets<crate::shadow_core::ShadowCasterMaterial>>>,
|
||||
shadow_cutout: Option<ResMut<'w, Assets<crate::world_shadow::CutoutShadowCasterMaterial>>>,
|
||||
}
|
||||
|
||||
/// One portrait/paperdoll booth camera + its layer ([`crate::portrait`], 0938): the booths run
|
||||
@@ -700,6 +705,52 @@ pub(super) fn spawn_menagerie(
|
||||
));
|
||||
count += 1;
|
||||
|
||||
// MONKEY (leftovers): the sun-shadow proxy lanes. Production proxies live on the private
|
||||
// layer 31, which the sun rig's directional light casts from and the world camera also draws
|
||||
// (forward discards every fragment). The solid proxy usually compiles at entry anyway (the
|
||||
// player's own caster), but the cutout one first appears with the first leaf card in reach,
|
||||
// which can be long after the cover lifts. Each rig sits on layers 0 and 31: the world camera
|
||||
// mints the forward pipeline, the shadow views (when a sun rig is live) the depth one. Meshes
|
||||
// are the production attribute sets (`empty_shadow_mesh` / `empty_cutout_mesh`), non-empty.
|
||||
let proxy_layers = bevy::camera::visibility::RenderLayers::from_layers(&[
|
||||
0,
|
||||
crate::shadow_core::PLAYER_SHADOW_LAYER,
|
||||
]);
|
||||
let tri = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
|
||||
let mut solid_mesh = crate::shadow_core::empty_shadow_mesh();
|
||||
solid_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, tri.to_vec());
|
||||
solid_mesh.insert_indices(Indices::U32(vec![0, 1, 2]));
|
||||
let mut cutout_mesh = crate::shadow_core::empty_cutout_mesh();
|
||||
cutout_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, tri.to_vec());
|
||||
cutout_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0f32, 0.0], [1.0, 0.0], [1.0, 1.0]]);
|
||||
cutout_mesh.insert_indices(Indices::U32(vec![0, 1, 2]));
|
||||
if let Some(store) = lanes.shadow_solid.as_mut() {
|
||||
let solid = store.add(crate::shadow_core::ShadowCasterMaterial {});
|
||||
spawn_lane_rig(
|
||||
commands,
|
||||
cam,
|
||||
Some(proxy_layers.clone()),
|
||||
&meshes.add(solid_mesh),
|
||||
None,
|
||||
solid,
|
||||
&mut count,
|
||||
);
|
||||
}
|
||||
if let Some(store) = lanes.shadow_cutout.as_mut() {
|
||||
// A real stand-in leaf, so the `#[texture]` binding never lands on the retry path.
|
||||
let leaf = lanes.images.add(Image::default());
|
||||
let cutout = store.add(crate::world_shadow::CutoutShadowCasterMaterial { leaf });
|
||||
spawn_lane_rig(
|
||||
commands,
|
||||
cam,
|
||||
Some(proxy_layers),
|
||||
&meshes.add(cutout_mesh),
|
||||
None,
|
||||
cutout,
|
||||
&mut count,
|
||||
);
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
use super::bloom::BloomLabel;
|
||||
use crate::video::VideoConfig;
|
||||
use benilla_world::{lighting::WowLighting, view::WorldCamera, wmo_portal::CameraInteriorClaim};
|
||||
// MONKEY (leftovers): LightSkybox flag 0x8.
|
||||
use benilla_world::skybox::CameraSkybox;
|
||||
use bevy::{
|
||||
core_pipeline::{
|
||||
core_3d::graph::{Core3d, Node3d},
|
||||
@@ -36,6 +38,12 @@ struct ShaftView {
|
||||
sun: Vec4,
|
||||
// Gamma-space sun colour; the framebuffer is still gamma-space here.
|
||||
color: Vec4,
|
||||
// MONKEY (leftovers): the view ray's world height is `dot(horizon.xyz, (ndc.x, ndc.y, 1))`
|
||||
// over `length((ndc.x * ray.x, ndc.y * ray.y, 1))`; the mask counts a depth-0 pixel as sky
|
||||
// only above `horizon.w` (sin elevation). Water writes no depth, so the sea read as sky.
|
||||
horizon: Vec4,
|
||||
// xy = tan half-FOV per axis (1/P00, 1/P11); zw unused.
|
||||
ray: Vec4,
|
||||
}
|
||||
|
||||
impl Plugin for SunShaftsPlugin {
|
||||
@@ -79,6 +87,7 @@ fn update_views(
|
||||
video: Res<VideoConfig>,
|
||||
lighting: Res<WowLighting>,
|
||||
interior: Res<CameraInteriorClaim>,
|
||||
sky: Option<Res<CameraSkybox>>,
|
||||
mut cameras: Query<
|
||||
(
|
||||
Entity,
|
||||
@@ -92,6 +101,11 @@ fn update_views(
|
||||
>,
|
||||
) {
|
||||
let to_sun = lighting.celestial_dir().normalize_or_zero();
|
||||
// MONKEY (leftovers): a `0x8` sky forces the shafts: its weight lifts the daylight ramp, so
|
||||
// a low or just-set sun keeps full shafts (a sun far below the horizon still ends them).
|
||||
let force = sky.map_or(0.0, |s| {
|
||||
s.flag_weight(benilla_formats::SKYBOX_FORCE_SUN_SHAFTS)
|
||||
});
|
||||
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;
|
||||
@@ -121,18 +135,22 @@ fn update_views(
|
||||
)
|
||||
.max_element();
|
||||
let edge_fade = 1.0 - smoothstep(0.0, 0.18, outside);
|
||||
let daylight = smoothstep(-0.02, 0.16, to_sun.y);
|
||||
let daylight = smoothstep(-0.02, 0.16, to_sun.y)
|
||||
.max(force.clamp(0.0, 1.0) * smoothstep(-0.12, -0.02, 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 (horizon, ray) = horizon_row(transform, projection);
|
||||
let next = ShaftView {
|
||||
sun: uv.extend(strength).extend(28.0),
|
||||
color: Vec3::from_array(lighting.diffuse)
|
||||
.lerp(Vec3::ONE, 0.18)
|
||||
.extend(0.0),
|
||||
horizon,
|
||||
ray,
|
||||
};
|
||||
if old != Some(&next) {
|
||||
commands.entity(entity).insert(next);
|
||||
@@ -140,6 +158,22 @@ fn update_views(
|
||||
}
|
||||
}
|
||||
|
||||
/// MONKEY (leftovers): the view ray's world-up component as a linear form in NDC, plus the per-axis
|
||||
/// ray scale to normalise it. A pixel is sky only above `-0.01` (≈ 0.6° below the horizon): a sun
|
||||
/// just under the horizon still finds sky next to it, the sea below it never does.
|
||||
fn horizon_row(transform: &GlobalTransform, projection: &Projection) -> (Vec4, Vec4) {
|
||||
let Projection::Perspective(p) = projection else {
|
||||
// No horizon test: `(0, 0, 1)` is always "up".
|
||||
return (Vec4::new(0.0, 0.0, 1.0, -2.0), Vec4::new(0.0, 0.0, 0.0, 0.0));
|
||||
};
|
||||
let ty = (p.fov * 0.5).tan();
|
||||
let tx = ty * p.aspect_ratio;
|
||||
let rot = transform.affine().matrix3;
|
||||
// View dir = right·x·tx + up·y·ty − back; only its world Y matters.
|
||||
let row = Vec3::new(rot.x_axis.y * tx, rot.y_axis.y * ty, -rot.z_axis.y);
|
||||
(row.extend(-0.01), Vec4::new(tx, ty, 0.0, 0.0))
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -197,13 +231,17 @@ fn init_pipeline(
|
||||
let mask_layouts = [false, true].map(|multisampled| {
|
||||
BindGroupLayoutDescriptor::new(
|
||||
"post_sun_shafts_mask_layout",
|
||||
&BindGroupLayoutEntries::single(
|
||||
// MONKEY (leftovers): + the view uniform, for the horizon row.
|
||||
&BindGroupLayoutEntries::sequential(
|
||||
ShaderStages::FRAGMENT,
|
||||
if multisampled {
|
||||
texture_depth_2d_multisampled()
|
||||
} else {
|
||||
texture_depth_2d()
|
||||
},
|
||||
(
|
||||
if multisampled {
|
||||
texture_depth_2d_multisampled()
|
||||
} else {
|
||||
texture_depth_2d()
|
||||
},
|
||||
uniform_buffer::<ShaftView>(false),
|
||||
),
|
||||
),
|
||||
)
|
||||
});
|
||||
@@ -302,7 +340,12 @@ fn prepare_masks(
|
||||
let Some(size) = camera.physical_viewport_size else {
|
||||
continue;
|
||||
};
|
||||
let rows = [shaft.sun.to_array(), shaft.color.to_array()];
|
||||
let rows = [
|
||||
shaft.sun.to_array(),
|
||||
shaft.color.to_array(),
|
||||
shaft.horizon.to_array(),
|
||||
shaft.ray.to_array(),
|
||||
];
|
||||
match uniform {
|
||||
Some(uniform) => queue.write_buffer(&uniform.0, 0, bytemuck::cast_slice(&rows)),
|
||||
None => {
|
||||
@@ -376,7 +419,7 @@ impl ViewNode for ShaftNode {
|
||||
let mask_bind = device.create_bind_group(
|
||||
"post_sun_shafts_mask",
|
||||
&cache.get_bind_group_layout(&settings.mask_layouts[multisampled]),
|
||||
&BindGroupEntries::single(depth.view()),
|
||||
&BindGroupEntries::sequential((depth.view(), uniform.0.as_entire_binding())),
|
||||
);
|
||||
let out = target.post_process_write();
|
||||
let bind = device.create_bind_group(
|
||||
@@ -444,4 +487,29 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
|
||||
/// MONKEY (leftovers): the shader's horizon form, evaluated on the CPU, is the view ray's
|
||||
/// world-up sine: the screen centre of a level camera sits on the horizon, a pitched-down one
|
||||
/// sees below it, and the top/bottom edges sit at ±half the vertical FOV.
|
||||
#[test]
|
||||
fn horizon_row_is_the_view_ray_elevation() {
|
||||
let p = PerspectiveProjection {
|
||||
fov: 1.0,
|
||||
aspect_ratio: 16.0 / 9.0,
|
||||
..default()
|
||||
};
|
||||
let proj = Projection::Perspective(p.clone());
|
||||
let up = |t: &Transform, ndc: Vec2| {
|
||||
let (h, r) = horizon_row(&GlobalTransform::from(*t), &proj);
|
||||
h.truncate().dot(ndc.extend(1.0)) / (ndc * r.truncate().truncate()).extend(1.0).length()
|
||||
};
|
||||
let level = Transform::default().looking_to(Vec3::X, Vec3::Y);
|
||||
assert!(up(&level, Vec2::ZERO).abs() < 1e-5);
|
||||
assert!((up(&level, Vec2::new(0.0, 1.0)) - 0.5f32.sin()).abs() < 1e-4);
|
||||
assert!((up(&level, Vec2::new(0.0, -1.0)) + 0.5f32.sin()).abs() < 1e-4);
|
||||
let down = Transform::default().looking_to(Vec3::new(1.0, -0.3, 0.0), Vec3::Y);
|
||||
let dir = Vec3::new(1.0, -0.3, 0.0).normalize();
|
||||
assert!((up(&down, Vec2::ZERO) - dir.y).abs() < 1e-5);
|
||||
assert!(up(&down, Vec2::ZERO) < -0.01, "sea below the horizon is not sky");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
|
||||
|
||||
struct Shafts {
|
||||
sun: vec4<f32>,
|
||||
color: vec4<f32>,
|
||||
// MONKEY (leftovers): world-up of the view ray in NDC (xyz) and the sky threshold (w).
|
||||
horizon: vec4<f32>,
|
||||
ray: vec4<f32>,
|
||||
};
|
||||
|
||||
#ifdef SHAFT_MASK
|
||||
|
||||
#ifdef MULTISAMPLED
|
||||
@@ -11,6 +19,7 @@
|
||||
#else
|
||||
@group(0) @binding(0) var depth: texture_depth_2d;
|
||||
#endif
|
||||
@group(0) @binding(1) var<uniform> shafts: Shafts;
|
||||
|
||||
@fragment
|
||||
fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
@@ -19,7 +28,12 @@ fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
// 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);
|
||||
// MONKEY (leftovers): water writes no depth, so depth 0 under the horizon is the sea (or the
|
||||
// far-clipped ground), never sky.
|
||||
let ndc = (vec2<f32>(p) + 0.5) / vec2<f32>(dims) * vec2(2.0, -2.0) + vec2(-1.0, 1.0);
|
||||
let up = dot(shafts.horizon.xyz, vec3(ndc, 1.0))
|
||||
/ length(vec3(ndc * shafts.ray.xy, 1.0));
|
||||
let sky = select(0.0, 1.0, textureLoad(depth, p, 0) <= 1.0e-7 && up > shafts.horizon.w);
|
||||
return vec4(sky, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -28,11 +42,6 @@ fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||
@group(0) @binding(0) var scene: texture_2d<f32>;
|
||||
@group(0) @binding(1) var scene_sampler: sampler;
|
||||
@group(0) @binding(2) var mask: texture_2d<f32>;
|
||||
|
||||
struct Shafts {
|
||||
sun: vec4<f32>,
|
||||
color: vec4<f32>,
|
||||
};
|
||||
@group(0) @binding(3) var<uniform> shafts: Shafts;
|
||||
|
||||
@fragment
|
||||
|
||||
@@ -84,13 +84,14 @@ fn refresh_streamed_shadows(
|
||||
/// `prepass_fragment_shader` makes the otherwise depth-only directional-shadow pass run a fragment —
|
||||
/// the exact pair Bevy's prepass specializer requires to alpha-test in the shadow map.
|
||||
#[derive(Asset, AsBindGroup, TypePath, Debug, Clone)]
|
||||
struct CutoutShadowCasterMaterial {
|
||||
// MONKEY (leftovers): crate-visible for the pipe_warm menagerie.
|
||||
pub(crate) struct CutoutShadowCasterMaterial {
|
||||
/// The leaf sheet, bound at material binding 0/1. Reusing the SAME image handle the forward pass
|
||||
/// draws makes the sampler inherit that image's clamp/repeat address mode, so the shadow
|
||||
/// silhouette matches the visible foliage (a cutout card's out-of-range UVs must clamp).
|
||||
#[texture(0)]
|
||||
#[sampler(1)]
|
||||
leaf: Handle<Image>,
|
||||
pub(crate) leaf: Handle<Image>,
|
||||
}
|
||||
|
||||
impl Material for CutoutShadowCasterMaterial {
|
||||
|
||||
@@ -135,7 +135,11 @@ pub use light::{Atmosphere, LightCatalog, Submersion, ZERO_KEY_COLOR, ZERO_KEY_S
|
||||
// MONKEY (fog): `LightFogBand.dbc`.
|
||||
pub use light::{FogBand, FogBandCatalog, FOG_BANDS_PER_PARAM};
|
||||
// MONKEY (skybox): the skybox row and the zone walk's entries.
|
||||
pub use light::{SkyboxDef, ZoneSkybox, SKYBOX_FOG_BLEND, SKYBOX_FULL_DAY, SKYBOX_KEEP_CELESTIAL};
|
||||
// MONKEY (leftovers): + the 0x8 / 0x10 flags.
|
||||
pub use light::{
|
||||
SkyboxDef, ZoneSkybox, SKYBOX_FOG_BLEND, SKYBOX_FORCE_SUN_SHAFTS, SKYBOX_FULL_DAY,
|
||||
SKYBOX_KEEP_CELESTIAL, SKYBOX_NO_SUN_FOG,
|
||||
};
|
||||
mod loading_screen;
|
||||
pub use loading_screen::{load_loading_screens, LoadingScreenCatalog};
|
||||
mod liquid;
|
||||
|
||||
@@ -19,7 +19,11 @@ mod tables;
|
||||
|
||||
pub use atmosphere::Atmosphere;
|
||||
pub use fog_band::{FogBand, FogBandCatalog, FOG_BANDS_PER_PARAM};
|
||||
pub use skybox::{SkyboxDef, ZoneSkybox, SKYBOX_FOG_BLEND, SKYBOX_FULL_DAY, SKYBOX_KEEP_CELESTIAL};
|
||||
// MONKEY (leftovers): + the 0x8 / 0x10 flags.
|
||||
pub use skybox::{
|
||||
SkyboxDef, ZoneSkybox, SKYBOX_FOG_BLEND, SKYBOX_FORCE_SUN_SHAFTS, SKYBOX_FULL_DAY,
|
||||
SKYBOX_KEEP_CELESTIAL, SKYBOX_NO_SUN_FOG,
|
||||
};
|
||||
use atmosphere::{
|
||||
FB_CLOUD_DENSITY, FB_FOG_END, FB_FOG_START_MULT, IB_AMBIENT, IB_CLOUD_GBASE, IB_CLOUD_SLOPE,
|
||||
IB_CLOUD_SUN, IB_DIFFUSE, IB_FOG_COLOR, IB_OCEAN_DEEP, IB_OCEAN_SHALLOW, IB_RIVER_DEEP,
|
||||
|
||||
@@ -26,6 +26,10 @@ pub const SKYBOX_FULL_DAY: u32 = 0x1;
|
||||
pub const SKYBOX_KEEP_CELESTIAL: u32 = 0x2;
|
||||
/// Flag `0x4`: an alpha cone in the final fog colour draws over the model's horizon.
|
||||
pub const SKYBOX_FOG_BLEND: u32 = 0x4;
|
||||
/// MONKEY (leftovers): flag `0x8`: sun shafts run under this sky even where they would fade.
|
||||
pub const SKYBOX_FORCE_SUN_SHAFTS: u32 = 0x8;
|
||||
/// MONKEY (leftovers): flag `0x10`: the fog's sun-colour lobe is off under this sky.
|
||||
pub const SKYBOX_NO_SUN_FOG: u32 = 0x10;
|
||||
|
||||
/// One `LightSkybox.dbc` row.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -306,10 +306,18 @@ fn lamp_terrain_occlusion_scan() {
|
||||
.find_map(|c| benilla_formats::terrain_height_at(c, [x, y, 1.0e4]))
|
||||
};
|
||||
let mut rows = Vec::new();
|
||||
// MONKEY (leftovers): optional 6th field = light height above the doodad origin (default the
|
||||
// lamppost's 3.5 yd; a campfire's flame sits near 1 yd).
|
||||
let lift = p.get(5).and_then(|v| v.parse::<f32>().ok()).unwrap_or(3.5);
|
||||
// 7th field = how far the ground must rise above the ray to count (default 5 cm; the receiver's
|
||||
// normal offset is 0.15 yd, so a grazing 5 cm crest is below what a cube map can resolve).
|
||||
let clear = p.get(6).and_then(|v| v.parse::<f32>().ok()).unwrap_or(0.05);
|
||||
for l in lamps {
|
||||
let head = Vec3::new(l[0], l[1], l[2] + 3.5);
|
||||
let head = Vec3::new(l[0], l[1], l[2] + lift);
|
||||
let (mut total, mut blocked) = (0u32, 0u32);
|
||||
let mut best = (0.0f32, 0.0f32);
|
||||
// MONKEY (leftovers): blocked samples inside 15 yd, where a fire's pool is still visible.
|
||||
let mut near = 0u32;
|
||||
for ring in 1..=10 {
|
||||
let d = ring as f32 * 2.5;
|
||||
for k in 0..36 {
|
||||
@@ -321,22 +329,23 @@ fn lamp_terrain_occlusion_scan() {
|
||||
let steps = (d / 0.5) as i32;
|
||||
let hit = (1..steps).any(|s| {
|
||||
let q = head.lerp(target, s as f32 / steps as f32);
|
||||
h(q.x, q.y).is_some_and(|g| g > q.z + 0.05)
|
||||
h(q.x, q.y).is_some_and(|g| g > q.z + clear)
|
||||
});
|
||||
if hit {
|
||||
blocked += 1;
|
||||
best = (a.to_degrees(), d);
|
||||
near += (d <= 15.0) as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
if total > 0 {
|
||||
rows.push((blocked as f32 / total as f32, l, best));
|
||||
rows.push((blocked as f32 / total as f32, l, best, near, h(l[0], l[1]).unwrap_or(f32::NAN)));
|
||||
}
|
||||
}
|
||||
rows.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
for (f, l, (a, d)) in rows.iter().take(12) {
|
||||
rows.sort_by(|a, b| b.3.cmp(&a.3).then(b.0.total_cmp(&a.0)));
|
||||
for (f, l, (a, d), near, g) in rows.iter().take(12) {
|
||||
println!(
|
||||
"lamp ({:8.1},{:7.1},{:6.1}) terrain-blocked {:5.1}% e.g. bearing {a:5.0} deg at {d:4.1} yd",
|
||||
"lamp ({:8.1},{:7.1},{:6.1}) ground {g:6.2} terrain-blocked {:5.1}% ({near} within 15 yd) e.g. bearing {a:5.0} deg at {d:4.1} yd",
|
||||
l[0], l[1], l[2], f * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -204,6 +204,8 @@ pub(super) fn update_fog_model(
|
||||
eye_liquid: crate::liquid::EyeLiquid,
|
||||
viewer: Res<crate::view::Viewer>,
|
||||
weather: Option<Res<crate::weather::WeatherState>>,
|
||||
// MONKEY (leftovers): LightSkybox flag 0x10 (last frame's backdrop; the skybox resolves later).
|
||||
sky: Option<Res<crate::skybox::CameraSkybox>>,
|
||||
mut frame: ResMut<MonkeyFrame>,
|
||||
) {
|
||||
let derived = derived_band(&lighting);
|
||||
@@ -233,6 +235,9 @@ pub(super) fn update_fog_model(
|
||||
};
|
||||
let mut next = *frame;
|
||||
fill_frame(&mut next, setting.0, &lighting, &band);
|
||||
// MONKEY (leftovers): a `0x10` skybox fades the sun-fog lobe out by its weight.
|
||||
let no_sun = sky.map_or(0.0, |s| s.flag_weight(benilla_formats::SKYBOX_NO_SUN_FOG));
|
||||
next.sun_fog_strength *= 1.0 - no_sun.clamp(0.0, 1.0);
|
||||
if *frame != next {
|
||||
*frame = next;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,15 @@ impl CameraSkybox {
|
||||
})
|
||||
}
|
||||
|
||||
/// MONKEY (leftovers): the heaviest main layer's weight among those carrying `flag`, 0 if none.
|
||||
pub fn flag_weight(&self, flag: u32) -> f32 {
|
||||
self.0
|
||||
.iter()
|
||||
.filter(|l| !l.celestial && l.flags & flag != 0)
|
||||
.map(|l| l.weight)
|
||||
.fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
fn layer(&self, path: &str) -> Option<&SkyboxLayer> {
|
||||
self.0.iter().find(|l| l.path == path)
|
||||
}
|
||||
@@ -1084,4 +1093,23 @@ mod tests {
|
||||
});
|
||||
assert_eq!(sky.primary().map(|l| l.path.as_str()), Some("b"));
|
||||
}
|
||||
|
||||
/// MONKEY (leftovers): a flag's weight is its heaviest main carrier; celestial layers and
|
||||
/// layers without the flag do not count.
|
||||
#[test]
|
||||
fn flag_weight_is_the_heaviest_main_carrier() {
|
||||
let with = |path, w, flags| SkyboxLayer {
|
||||
flags,
|
||||
..layer(path, w)
|
||||
};
|
||||
let mut sky = CameraSkybox(vec![with("a", 0.3, 0x8), with("b", 0.7, 0x1), with("c", 0.5, 0x18)]);
|
||||
sky.0.push(SkyboxLayer {
|
||||
celestial: true,
|
||||
..with("d", 1.0, 0x18)
|
||||
});
|
||||
assert_eq!(sky.flag_weight(benilla_formats::SKYBOX_FORCE_SUN_SHAFTS), 0.5);
|
||||
assert_eq!(sky.flag_weight(benilla_formats::SKYBOX_NO_SUN_FOG), 0.5);
|
||||
assert_eq!(sky.flag_weight(benilla_formats::SKYBOX_FOG_BLEND), 0.0);
|
||||
assert_eq!(CameraSkybox::default().flag_weight(0x8), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user