The starburst was three defects, and none of them was the oblique projection I blamed it on in the last commit. What found them was looking at the mirror's own render target instead of reasoning about the composited frame — the water reads it through a fresnel weight, a rippled UV, a plane-match band and an SSR march that paints over it, so from the outside a broken mirror and a broken water shader look identical. WOW_MIRROR_DUMP=1 now writes that target beside the capture. (Bevy's own save_to_disk cannot: try_into_dynamic knows four 8-bit formats and answers UnsupportedFormat for Rgba16Float, logging and writing nothing.) - The oblique row's scale. Replacing the near plane with the water plane costs the FAR plane: `z >= 0` stops meaning "nearer than CAM_FAR" and becomes `depth >= a*d`, a plane through the eye tilted by a. Lengyel's own scale puts that plane exactly through the frustum's far corner, and the sky dome is pinned just inside the far plane, so it straddled the boundary and was sliced along its own tessellation. Scale by the supremum of d/depth over the frustum instead, with 2% of margin. - The water surface lay ON its own clip plane. This was the starburst and it was fatal rather than untidy: depth now means "distance above the plane", so a fragment at d=0 reads z/w=1, the nearest value reverse-Z has, and beats every other surface in the frame. The mesh is wave-displaced, so its triangles straddled the plane instead of lying in it, and each was clipped to a sliver that kept the winning depth. SURFACE_CLEARANCE lifts the plane a yard: the swell sums to 0.34 and the probed height answers for the surface under the eye, not this fragment. - The mirror was stored upside down. A true reflection is what a camera on the far side sees with its up vector reflected too — an improper basis no Transform can hold. The honest right-handed camera sees the same surfaces through the same frustum and stores the image flipped, so the shader samples 1 - v; that costs no winding flip, so backface culling stays correct. It read as a lake reflecting the ridge the RIGHT way up. Measured: Lake Everstill 2.87 -> 3.36 ms mean over 600 frames (+17%), the Stormwind canal inside the noise, CPU +0.2 to +1.1 ms. It is the first lane in docs/GRAPHICS.md that spends main thread, so it is a rung of its own above High and the default stays 2. Seated as a setting, per the standing rule that no graphics lane hides behind an env var: waterQuality gains tier 3, the Ultra preset takes it, and the Advanced Graphics dropdown gains a fourth entry in both locales. With the lane off, both scenarios are byte-identical to the build before it — which took two findings of its own: - The system could not live in Update at all. `&mut Transform` on a camera is shared with everything that moves anything; declaring it put 59 undeclared orders into Update and tripped the schedule ratchet, which is what kept this behind an env var. It belongs in PostUpdate with the skybox and the billboards, after propagation and before visibility, where it also reads the eye's settled pose instead of a frame-old one. (The comment blaming ResMut<Assets<Image>> for those 59 was wrong — dropping the asset handle changed the count by zero.) - A system that runs and returns on its first line is still not free. Holding &mut Camera and &mut GlobalTransform for a frame is enough to change which undeclared orders the executor picks: the canal moved 0.75% of its pixels by up to 96/255 with the lane switched off. So the whole system sits behind a run condition rather than an early return — and the same effect killed a .before(UpdateDirectionalLightCascades) order that was correct in itself. PostUpdate's ambiguity ceiling goes 466 -> 495 with the reason (the pairs are structurally impossible: nothing else in the schedule names the mirror camera); Update's ratchets down 4973 -> 4952. Also restores scripts/gates.sh's `where` example, which had gone missing. Its absence was silent — cargo's error went to /dev/null and `|| true` made it read as "no install" — so the memo key and the player-tests gate saw none even when WOW_DATA was set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
148 lines
6.7 KiB
Rust
148 lines
6.7 KiB
Rust
//! Shared water settings and image binding, independent of world streaming.
|
|
use bevy::prelude::*;
|
|
use bevy::render::render_resource::ShaderType;
|
|
|
|
/// The enhanced water module's uniform (binding 104, `WaterParams` in `enhanced_water.wgsl`).
|
|
/// Field order is the shader's; change the two together.
|
|
#[derive(ShaderType, Clone, Copy, Debug, PartialEq)]
|
|
pub struct WaterUniform {
|
|
/// x = quality (0 Classic, 1 Enhanced, 2 High); y = wave energy (ocean 1.0, ADT inland 0.18,
|
|
/// WMO pools 0.12); z = the pinned capture time; w = clock enable (0 on a deterministic run).
|
|
pub mode: Vec4,
|
|
/// x = which renderer (0 ADT MCLQ, 1 WMO exterior, 2 WMO interior); y = ocean; z = fullbright.
|
|
pub lane: Vec4,
|
|
/// Reflection endpoints, linear RGB from the dome's resolved LightIntBand 2 / 6.
|
|
pub sky_zenith: Vec4,
|
|
pub sky_horizon: Vec4,
|
|
/// xyz toward the visible sun by day, the white moon by night; w = 0 sun, 1 moon.
|
|
pub celestial: Vec4,
|
|
/// GFX (planar water): the mirror pass's state. `x` = 1 while the reflection target holds a
|
|
/// view for THIS surface's plane, 0 otherwise (the pass off, no plane found, or the camera
|
|
/// under the water). `y` = the plane's world height, so a surface can tell whether the mirror
|
|
/// was rendered for its own sheet or for a different body a few yards away — a lake and a
|
|
/// canal in one frame share one target, and only the one the pass was aimed at may use it.
|
|
/// `zw` unused.
|
|
pub mirror: Vec4,
|
|
}
|
|
|
|
impl Default for WaterUniform {
|
|
fn default() -> Self {
|
|
Self {
|
|
mode: Vec4::new(0.0, 0.0, 0.0, 1.0),
|
|
lane: Vec4::ZERO,
|
|
sky_zenith: Vec4::ZERO,
|
|
sky_horizon: Vec4::ZERO,
|
|
celestial: Vec3::Y.extend(0.0),
|
|
mirror: Vec4::ZERO,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 0 = Classic, 1 = Enhanced, 2 = High (Enhanced + screen-space reflection of the scenery).
|
|
#[derive(Resource, Clone, Copy, PartialEq)]
|
|
pub struct WaterQuality(pub u8);
|
|
|
|
impl Default for WaterQuality {
|
|
/// GFX (water): High. The scenery reflection was held back until its cost was measured, and
|
|
/// it measures as nothing — Lake Everstill runs 1.91 / 1.84 / 1.90 ms over 300 frames at the
|
|
/// three tiers and the ocean 1.89 / 1.88 / 1.89, inside the run-to-run noise. Kept welded to
|
|
/// `cvars::REGISTERED`'s `waterQuality` by `registered_defaults_mirror_the_code_truths`.
|
|
fn default() -> Self {
|
|
Self(2)
|
|
}
|
|
}
|
|
|
|
/// The world view's opaque depth, resolved to a sampleable R32Float image.
|
|
#[derive(Resource, Clone)]
|
|
pub struct WaterDepthImage(pub Handle<Image>);
|
|
|
|
impl FromWorld for WaterDepthImage {
|
|
fn from_world(world: &mut World) -> Self {
|
|
use bevy::{asset::RenderAssetUsages, render::render_resource::*};
|
|
let mut image = Image::new_fill(
|
|
Extent3d::default(),
|
|
TextureDimension::D2,
|
|
&0f32.to_le_bytes(),
|
|
TextureFormat::R32Float,
|
|
RenderAssetUsages::default(),
|
|
);
|
|
image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING
|
|
| TextureUsages::RENDER_ATTACHMENT
|
|
| TextureUsages::COPY_DST;
|
|
// Render-only: no CPU copy, so a resize does not allocate (and upload) a screen of zeros.
|
|
image.data = None;
|
|
Self(world.resource_mut::<Assets<Image>>().add(image))
|
|
}
|
|
}
|
|
|
|
/// GFX (planar water): the mirrored view of the world, rendered by a second camera placed on the
|
|
/// far side of the water plane (`benilla_world::liquid::reflect`).
|
|
///
|
|
/// `Rgba16Float` and half the viewport on each axis. Half because a reflection is read through a
|
|
/// rippled normal and across a surface the eye never sees square-on, so the resolution it can
|
|
/// actually show is far below the screen's — and because the pass costs a second scene draw, which
|
|
/// is the one thing this renderer cannot spend freely (`docs/GRAPHICS.md`).
|
|
#[derive(Resource, Clone)]
|
|
pub struct WaterReflectionImage(pub Handle<Image>);
|
|
|
|
impl FromWorld for WaterReflectionImage {
|
|
fn from_world(world: &mut World) -> Self {
|
|
use bevy::{asset::RenderAssetUsages, render::render_resource::*};
|
|
let mut image = Image::new_fill(
|
|
Extent3d::default(),
|
|
TextureDimension::D2,
|
|
&[0u8; 8],
|
|
TextureFormat::Rgba16Float,
|
|
RenderAssetUsages::default(),
|
|
);
|
|
image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING
|
|
| TextureUsages::RENDER_ATTACHMENT
|
|
| TextureUsages::COPY_DST;
|
|
// `$WOW_MIRROR_DUMP=1` reads this target back to a PNG (`capture::save_mirror`), which
|
|
// needs COPY_SRC — and a readback usage the shipped build never exercises is not one to
|
|
// declare for everyone.
|
|
if std::env::var_os("WOW_MIRROR_DUMP").is_some() {
|
|
image.texture_descriptor.usage |= TextureUsages::COPY_SRC;
|
|
}
|
|
// Filtered, unlike the depth and colour copies beside it: the reflection is sampled at a
|
|
// rippled UV, so a nearest fetch would alias the ripple into stair steps.
|
|
image.sampler =
|
|
bevy::image::ImageSampler::Descriptor(bevy::image::ImageSamplerDescriptor {
|
|
label: Some("water_reflection".to_owned()),
|
|
address_mode_u: bevy::image::ImageAddressMode::ClampToEdge,
|
|
address_mode_v: bevy::image::ImageAddressMode::ClampToEdge,
|
|
mag_filter: bevy::image::ImageFilterMode::Linear,
|
|
min_filter: bevy::image::ImageFilterMode::Linear,
|
|
..Default::default()
|
|
});
|
|
// Render-only: no CPU copy, so a resize does not allocate (and upload) a screen of zeros.
|
|
image.data = None;
|
|
Self(world.resource_mut::<Assets<Image>>().add(image))
|
|
}
|
|
}
|
|
|
|
/// The world view's opaque COLOUR, copied right after the main opaque pass (the frame as it stood
|
|
/// before any water drew), for the enhanced water's refraction. `Rgba16Float`: a load and a store,
|
|
/// never filtered, so the copy is exact whatever the view target's own format is.
|
|
#[derive(Resource, Clone)]
|
|
pub struct WaterColourImage(pub Handle<Image>);
|
|
|
|
impl FromWorld for WaterColourImage {
|
|
fn from_world(world: &mut World) -> Self {
|
|
use bevy::{asset::RenderAssetUsages, render::render_resource::*};
|
|
let mut image = Image::new_fill(
|
|
Extent3d::default(),
|
|
TextureDimension::D2,
|
|
&[0u8; 8],
|
|
TextureFormat::Rgba16Float,
|
|
RenderAssetUsages::default(),
|
|
);
|
|
image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING
|
|
| TextureUsages::RENDER_ATTACHMENT
|
|
| TextureUsages::COPY_DST;
|
|
// Render-only: no CPU copy, so a resize does not allocate (and upload) a screen of zeros.
|
|
image.data = None;
|
|
Self(world.resource_mut::<Assets<Image>>().add(image))
|
|
}
|
|
}
|