HD clouds composited by depth so terrain and vehicles inside or behind the layer are hidden and those in front stay in front (#67); world-anchored noise everywhere (cloud detail by height above the layer, fog, smoke and puddle scales dividing the wrap) (#68); the replay's time is visual only, gameplay sky darkening uses the world's time (#70)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1838,6 +1838,8 @@ struct Game<'a> {
|
||||
events: events::Events,
|
||||
/// The overworld's last `sky_darken` and its inputs (time, rain, thunder).
|
||||
sky_darken_memo: std::cell::Cell<Option<((i64, u32, u32), i32)>>,
|
||||
/// The same for the time a mod shows (a replay's): render only.
|
||||
shown_darken_memo: std::cell::Cell<Option<((i64, u32, u32), i32)>>,
|
||||
persist: persist::EntityMemory,
|
||||
/// Connected to a server: the world comes from the network (game/remote.rs).
|
||||
remote: Option<remote::Remote>,
|
||||
@@ -8020,7 +8022,7 @@ impl Game<'_> {
|
||||
.flatten();
|
||||
if let Some(sprite) = shadow_sprite {
|
||||
let ambient = if self.dim == Dim::Nether { 0.1 } else { 0.0 };
|
||||
let darken = self.sky_darken();
|
||||
let darken = self.shown_sky_darken();
|
||||
let a = alpha;
|
||||
let lerp = |p: Vec3, q: Vec3| {
|
||||
glam::DVec3::new(
|
||||
@@ -8378,33 +8380,43 @@ impl Game<'_> {
|
||||
.map_or(self.world.day_time, |t| t as i64)
|
||||
}
|
||||
|
||||
/// The sky's darkening for the game's rules (mob AI, spawning, block behaviour, stats): always
|
||||
/// the world's real time, never a mod's render-only override.
|
||||
fn sky_darken(&self) -> i32 {
|
||||
self.sky_darken_at(self.world.day_time, &self.sky_darken_memo)
|
||||
}
|
||||
|
||||
/// The sky's darkening as the player sees it (entity shadows, particle light): follows the mod's
|
||||
/// time override, so a replay's night looks dark without touching the rules.
|
||||
fn shown_sky_darken(&self) -> i32 {
|
||||
self.sky_darken_at(self.shown_day_time(), &self.shown_darken_memo)
|
||||
}
|
||||
|
||||
fn sky_darken_at(
|
||||
&self,
|
||||
time: i64,
|
||||
memo: &std::cell::Cell<Option<((i64, u32, u32), i32)>>,
|
||||
) -> i32 {
|
||||
if self.dim == Dim::Nether {
|
||||
return 11;
|
||||
}
|
||||
if self.dim == Dim::End {
|
||||
let sky = self
|
||||
.res
|
||||
.end_environment()
|
||||
.sample(self.shown_day_time(), None, None);
|
||||
let sky = self.res.end_environment().sample(time, None, None);
|
||||
return (15.0 - 15.0 * sky.sky_light_level).round() as i32;
|
||||
}
|
||||
let (rain, thunder) = self.world.weather.levels(1.0);
|
||||
// Sampled once per (time, weather): every mob asks each tick.
|
||||
let key = (self.shown_day_time(), rain.to_bits(), thunder.to_bits());
|
||||
if let Some((k, darken)) = self.sky_darken_memo.get()
|
||||
let key = (time, rain.to_bits(), thunder.to_bits());
|
||||
if let Some((k, darken)) = memo.get()
|
||||
&& k == key
|
||||
{
|
||||
return darken;
|
||||
}
|
||||
let mut sky = self
|
||||
.res
|
||||
.environment()
|
||||
.sample(self.shown_day_time(), None, None);
|
||||
let mut sky = self.res.environment().sample(time, None, None);
|
||||
horizon_client::environment::apply_weather(&mut sky, rain, thunder);
|
||||
// Level.updateSkyBrightness: darkening from the sky light level.
|
||||
let darken = (15.0 - 15.0 * sky.sky_light_level).round() as i32;
|
||||
self.sky_darken_memo.set(Some((key, darken)));
|
||||
memo.set(Some((key, darken)));
|
||||
darken
|
||||
}
|
||||
fn mob_hit(&self) -> Option<(usize, f64)> {
|
||||
@@ -13954,6 +13966,7 @@ impl<'a> Game<'a> {
|
||||
mods: Default::default(),
|
||||
native_shaders: Default::default(),
|
||||
sky_darken_memo: Default::default(),
|
||||
shown_darken_memo: Default::default(),
|
||||
persist: Default::default(),
|
||||
remote: None,
|
||||
lan: None,
|
||||
|
||||
@@ -241,7 +241,7 @@ impl Game<'_> {
|
||||
eye[2].floor() as i32,
|
||||
];
|
||||
let packed = self.world.light(b[0], b[1], b[2]);
|
||||
let sky = i32::from(packed >> 4) - self.sky_darken();
|
||||
let sky = i32::from(packed >> 4) - self.shown_sky_darken();
|
||||
let raw = sky.max(i32::from(packed & 15)).clamp(0, 15) as u8;
|
||||
lightmap_brightness(if self.dim == Dim::Nether { 0.1 } else { 0.0 }, raw)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
|
||||
@group(0) @binding(6) var history: texture_2d<f32>;
|
||||
@group(0) @binding(7) var smoke_tex: texture_2d<f32>;
|
||||
@group(0) @binding(8) var scene_depth: texture_depth_2d;
|
||||
@group(0) @binding(9) var history_dist: texture_2d<f32>;
|
||||
|
||||
// What the sky pixels report as the length of their ray (nothing stops them).
|
||||
const SKY_LEN: f32 = 20000.0;
|
||||
|
||||
struct CloudOut {
|
||||
@location(0) cloud: vec4<f32>,
|
||||
// How far the ray was marched: the surface behind this texel, or SKY_LEN for sky.
|
||||
@location(1) dist: vec4<f32>,
|
||||
}
|
||||
|
||||
fn ign(px: vec2<f32>) -> f32 {
|
||||
let p = px + vec2<f32>(5.588238 * frame.params.w);
|
||||
@@ -65,7 +76,26 @@ fn cloud_scatter(
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
|
||||
fn fs_cloud(in: FullOut) -> CloudOut {
|
||||
// The nearest of the four world pixels this texel stands for: the march ends at the surface
|
||||
// there (the sky counts as infinitely far), so what stands in front of a cloud stays in front
|
||||
// and what stands behind it is hidden by it.
|
||||
let size = vec2<i32>(frame.resolution.xy);
|
||||
let base = vec2<i32>(in.position.xy) * 2;
|
||||
var zmax = 0.0;
|
||||
var best = base;
|
||||
for (var k = 0; k < 4; k++) {
|
||||
let q = min(base + vec2<i32>(k & 1, k >> 1), size - 1);
|
||||
let z = textureLoad(scene_depth, q, 0);
|
||||
if z > zmax {
|
||||
zmax = z;
|
||||
best = q;
|
||||
}
|
||||
}
|
||||
let suv = (vec2<f32>(best) + 0.5) * frame.resolution.zw;
|
||||
let sndc = vec4<f32>(suv.x * 2.0 - 1.0, 1.0 - suv.y * 2.0, max(zmax, 0.000001), 1.0);
|
||||
let sh = frame.inverse_view_proj * sndc;
|
||||
let scene_len = select(SKY_LEN, length(sh.xyz / sh.w), zmax > 0.0);
|
||||
let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, 0.5, 1.0);
|
||||
let hh = frame.inverse_view_proj * ndc;
|
||||
let dir = normalize(hh.xyz / hh.w);
|
||||
@@ -73,11 +103,13 @@ fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
|
||||
var hit_t = 900.0;
|
||||
let y0 = frame.clouds.x;
|
||||
let y1 = y0 + frame.clouds.y;
|
||||
if abs(dir.y) > 0.004 {
|
||||
let ta = y0 / dir.y;
|
||||
let tb = y1 / dir.y;
|
||||
// A level ray stays in the slab if the camera is in it (it never leaves), else misses it.
|
||||
let dy = select(dir.y, select(-1.0e-4, 1.0e-4, dir.y >= 0.0), abs(dir.y) < 1.0e-4);
|
||||
{
|
||||
let ta = y0 / dy;
|
||||
let tb = y1 / dy;
|
||||
let lo = max(min(ta, tb), 0.0);
|
||||
let hi = min(max(ta, tb), 4200.0);
|
||||
let hi = min(max(ta, tb), min(4200.0, scene_len));
|
||||
if hi > lo {
|
||||
let dither = ign(in.position.xy);
|
||||
let n = clamp(i32(ceil((hi - lo) / 14.0)), 8, 26);
|
||||
@@ -131,6 +163,8 @@ fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
|
||||
cur = vec4<f32>(colour, trans);
|
||||
}
|
||||
}
|
||||
var out: CloudOut;
|
||||
out.dist = vec4<f32>(scene_len, 0.0, 0.0, 0.0);
|
||||
// Reproject the history by where the clouds are (rotation and the camera's step) and blend.
|
||||
if frame.cloud_cam.w > 0.5 {
|
||||
let hp = frame.prev_view_proj * vec4<f32>(dir * hit_t + frame.taa.xyz, 1.0);
|
||||
@@ -138,12 +172,18 @@ fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let huv = vec2<f32>(hp.x / hp.w * 0.5 + 0.5, 0.5 - hp.y / hp.w * 0.5);
|
||||
if all(huv > vec2<f32>(0.0)) && all(huv < vec2<f32>(1.0)) {
|
||||
let old = textureSampleLevel(history, linear, huv, 0.0);
|
||||
// A history that was marched to another surface (an edge, something that moved in
|
||||
// front) does not describe this ray.
|
||||
let hs = vec2<f32>(textureDimensions(history_dist));
|
||||
let old_len = textureLoad(history_dist, vec2<i32>(huv * hs), 0).x;
|
||||
let same_len = 1.0 - smoothstep(0.5, 1.0, abs(old_len - scene_len) / (0.1 * min(scene_len, old_len) + 2.0));
|
||||
// Not too much of a history that disagrees by a lot (a gust, a fast turn).
|
||||
let diff = length(old.rgb - cur.rgb) + abs(old.a - cur.a);
|
||||
let keep = 0.9 * (1.0 - smoothstep(0.35, 1.2, diff));
|
||||
let keep = 0.9 * (1.0 - smoothstep(0.35, 1.2, diff)) * same_len;
|
||||
cur = mix(cur, old, keep);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cur;
|
||||
out.cloud = cur;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ fn cloud_density(pos: vec3<f32>, detail: f32) -> f32 {
|
||||
d *= smoothstep(0.0, 0.2, h);
|
||||
if d < 0.002 { return 0.0; }
|
||||
if detail > 0.0 {
|
||||
let p3 = vec3<f32>(w.x, pos.y, w.y);
|
||||
// Height above the layer's base, not the camera's height: the noise stays put in the world.
|
||||
let p3 = vec3<f32>(w.x, pos.y - frame.clouds.x, w.y);
|
||||
let warp = (textureSampleLevel(noise3, repeat_s, p3 / 1024.0, 0.0).b - 0.5) * 0.4;
|
||||
let w0 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 512.0 + vec3<f32>(warp), 0.0).r;
|
||||
let w1 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 64.0 + vec3<f32>(warp * 3.0), 0.0).g;
|
||||
|
||||
@@ -40,7 +40,7 @@ fn sun_visible(p: vec3<f32>) -> f32 {
|
||||
}
|
||||
|
||||
// The air's extinction per block at a camera-relative point: a haze that thins with height above
|
||||
// the sea, a morning mist hugging the ground, rain, patches from a 3D noise that drifts; none
|
||||
// the sea, a morning mist hugging the ground, rain, patches from a 3D noise that drifts (its scale divides 65536, the wrap of the camera's x, z, so it does not jump at x or z = 0); none
|
||||
// deep underground.
|
||||
fn fog_sigma(p: vec3<f32>) -> f32 {
|
||||
let wy = p.y + frame.vfog.y;
|
||||
@@ -50,7 +50,7 @@ fn fog_sigma(p: vec3<f32>) -> f32 {
|
||||
s += frame.sky_extra.z * 0.003;
|
||||
let w = vec3<f32>(p.x + frame.cloud_cam.x, wy, p.z + frame.cloud_cam.y)
|
||||
+ vec3<f32>(0.5, 0.0, 0.35) * frame.clouds.w;
|
||||
let n = textureSampleLevel(noise3, repeat_s, w / 170.0, 0.0).b;
|
||||
let n = textureSampleLevel(noise3, repeat_s, w / 128.0, 0.0).b;
|
||||
let patchy = 0.35 + 1.3 * smoothstep(0.25, 0.75, n);
|
||||
// Fade out far below the sea's height: caves keep their own dark air.
|
||||
return s * patchy * smoothstep(28.0, 58.0, wy);
|
||||
@@ -70,7 +70,7 @@ fn smoke_detail(pos: vec3<f32>, core: f32) -> f32 {
|
||||
let w = vec3<f32>(pos.x + frame.cloud_cam.x, pos.y + frame.vfog.y, pos.z + frame.cloud_cam.y)
|
||||
- vec3<f32>(SMOKE_WIND.x, 0.0, SMOKE_WIND.y) * frame.smoke.z;
|
||||
let n1 = textureSampleLevel(noise3, repeat_s, w / 64.0, 0.0).r;
|
||||
let n2 = textureSampleLevel(noise3, repeat_s, w / 22.0, 0.0).g;
|
||||
let n2 = textureSampleLevel(noise3, repeat_s, w / 32.0, 0.0).g;
|
||||
let n = 0.55 * n1 + 0.45 * n2;
|
||||
// Thin smoke breaks up into wisps, thick smoke stays whole.
|
||||
let keep = smoothstep(0.18, 0.62, n + 0.45 * min(core, 1.5) - 0.12);
|
||||
|
||||
@@ -30,6 +30,9 @@ struct Light {
|
||||
@group(0) @binding(17) var fog_dist_tex: texture_2d<f32>;
|
||||
@group(0) @binding(18) var cloud_tex: texture_2d<f32>;
|
||||
@group(0) @binding(19) var cloud_shadow_tex: texture_2d<f32>;
|
||||
// How far each cloud texel's ray was marched (the surface behind it, or SKY_RAY for sky).
|
||||
@group(0) @binding(20) var cloud_dist_tex: texture_2d<f32>;
|
||||
const SKY_RAY: f32 = 20000.0;
|
||||
const CLOUD_SHADOW_R: f32 = 1200.0;
|
||||
// The pixel being lit, for the shadow filter's noise.
|
||||
var<private> frag_px: vec2<f32>;
|
||||
@@ -194,6 +197,34 @@ fn fog_at(px: vec2<i32>, dist: f32) -> vec4<f32> {
|
||||
return sum / total;
|
||||
}
|
||||
|
||||
// The volumetric clouds between the camera and a surface `dist` away (SKY_RAY for sky): the four
|
||||
// nearest half-resolution texels weighted by closeness and by how near the length they were
|
||||
// marched to is, so a cloud in front of a mountain does not bleed onto the sky beside it.
|
||||
fn cloud_at(px: vec2<i32>, dist: f32) -> vec4<f32> {
|
||||
let size = vec2<i32>(textureDimensions(cloud_tex));
|
||||
let f = vec2<f32>(px) * 0.5 - vec2<f32>(0.25);
|
||||
let base = vec2<i32>(floor(f));
|
||||
let t = f - floor(f);
|
||||
var sum = vec4<f32>(0.0);
|
||||
var total = 0.0;
|
||||
for (var j = 0; j < 2; j++) {
|
||||
for (var i = 0; i < 2; i++) {
|
||||
let q = clamp(base + vec2<i32>(i, j), vec2<i32>(0), size - 1);
|
||||
let s = textureLoad(cloud_tex, q, 0);
|
||||
let sd = textureLoad(cloud_dist_tex, q, 0).x;
|
||||
let bw = select(1.0 - t.x, t.x, i == 1) * select(1.0 - t.y, t.y, j == 1);
|
||||
let dz = (sd - dist) / (0.06 * min(sd, dist) + 0.4);
|
||||
let w = bw / (1.0 + dz * dz * dz * dz);
|
||||
sum += s * w;
|
||||
total += w;
|
||||
}
|
||||
}
|
||||
if total < 0.0001 {
|
||||
return textureLoad(cloud_tex, clamp(px / 2, vec2<i32>(0), size - 1), 0);
|
||||
}
|
||||
return sum / total;
|
||||
}
|
||||
|
||||
struct LightOut {
|
||||
// The lit colour; alpha 0.5 marks water (the floor under it, lit without any air), 0.6 an
|
||||
// object in front of the water (the scene as drawn).
|
||||
@@ -246,7 +277,7 @@ fn shade_surface(px: vec2<i32>, p: vec3<f32>, water: bool) -> vec3<f32> {
|
||||
let flat_up = smoothstep(0.9, 0.98, n.y);
|
||||
let soak = wet * open_sky * (0.35 + 0.65 * flat_up);
|
||||
let wp = frame.cloud_cam.xy + p.xz;
|
||||
let nz = 0.6 * wet_noise(wp * 0.45) + 0.4 * wet_noise(wp * 1.3 + vec2<f32>(17.0, 5.0));
|
||||
let nz = 0.6 * wet_noise(wp * 0.5) + 0.4 * wet_noise(wp * 1.25 + vec2<f32>(17.0, 5.0));
|
||||
puddle = smoothstep(0.66 - 0.16 * wet, 0.74 - 0.16 * wet, nz) * smoothstep(0.3, 0.7, wet) * open_sky * flat_up;
|
||||
surface_rgb *= mix(1.0, 0.7, soak) * mix(1.0, 0.6, puddle);
|
||||
rough = mix(rough, 0.12, soak * (1.0 - 0.5 * metal));
|
||||
@@ -377,7 +408,7 @@ fn fs_light(in: FullOut) -> LightOut {
|
||||
c = sky + kept;
|
||||
// The volumetric clouds over the sky and the sun.
|
||||
if frame.clouds.z > 0.5 {
|
||||
let cl = textureSampleLevel(cloud_tex, linear, in.uv, 0.0);
|
||||
let cl = cloud_at(px, SKY_RAY);
|
||||
c = c * cl.a + cl.rgb;
|
||||
}
|
||||
}
|
||||
@@ -421,6 +452,13 @@ fn fs_light(in: FullOut) -> LightOut {
|
||||
air_t = fg.a;
|
||||
air_s = fg.rgb;
|
||||
}
|
||||
// The clouds between the camera and the surface (inside the layer, or in front of a mountain
|
||||
// or a flying thing): laid over the surface and under the fog, as the sky's are.
|
||||
if d > 0.0 && atmosphere && frame.clouds.z > 0.5 {
|
||||
let cl = cloud_at(px, length(ps));
|
||||
air_s += air_t * cl.rgb;
|
||||
air_t *= cl.a;
|
||||
}
|
||||
if !volumetric && sun_on > 0.0 && density > 0.0 {
|
||||
let steps = 14;
|
||||
let jitter = ign(in.position.xy);
|
||||
|
||||
@@ -34,7 +34,7 @@ pub const MAX_LIGHTS: usize = 512;
|
||||
|
||||
pub(super) const LIGHT: &str = include_str!("light.wgsl");
|
||||
/// The lighting's group 0, binding by binding (see the contract above).
|
||||
pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 20] = [
|
||||
pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 21] = [
|
||||
UNIFORM,
|
||||
UNFILTERED,
|
||||
DEPTH,
|
||||
@@ -55,6 +55,7 @@ pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 20] = [
|
||||
UNFILTERED,
|
||||
FLOAT,
|
||||
FLOAT,
|
||||
UNFILTERED,
|
||||
];
|
||||
|
||||
/// The outside views and sampler the lighting group reads.
|
||||
@@ -219,6 +220,7 @@ impl Hd {
|
||||
wgpu::BindingResource::TextureView(&self.volumes.fog_dist),
|
||||
wgpu::BindingResource::TextureView(self.volumes.cloud_view()),
|
||||
wgpu::BindingResource::TextureView(&self.volumes.cloud_shadow),
|
||||
wgpu::BindingResource::TextureView(self.volumes.cloud_dist_view()),
|
||||
];
|
||||
let entries: Vec<_> = resources
|
||||
.into_iter()
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
use super::noise as hd_noise;
|
||||
use super::{
|
||||
DEPTH, FLOAT, Hd, SAMPLER, SHADOW_ARRAY, SHADOW_SAMPLER, UNIFORM, entries, fullscreen, module,
|
||||
stamp, stamped_pass, target,
|
||||
DEPTH, FLOAT, Hd, SAMPLER, SHADOW_ARRAY, SHADOW_SAMPLER, UNFILTERED, UNIFORM, entries,
|
||||
fullscreen, module, stamp, stamped_pass, target,
|
||||
};
|
||||
|
||||
pub const CLOUD_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
@@ -86,6 +86,9 @@ pub struct HdVolumes {
|
||||
shadow_sampler: wgpu::Sampler,
|
||||
/// The clouds' half-resolution target and its history (read one, write the other).
|
||||
pub clouds: [wgpu::TextureView; 2],
|
||||
/// How far each cloud texel's ray was marched (ping-pong with `clouds`).
|
||||
cloud_dist: [wgpu::TextureView; 2],
|
||||
cloud_depth: Option<wgpu::TextureView>,
|
||||
flip: usize,
|
||||
history_ready: bool,
|
||||
pub cloud_shadow: wgpu::TextureView,
|
||||
@@ -111,9 +114,10 @@ const D3: wgpu::BindingType = wgpu::BindingType::Texture {
|
||||
};
|
||||
|
||||
/// `fs_cloud`'s group 0: frame, 3D noise, 2D noise, clamped and repeating samplers, the sky
|
||||
/// texture, the previous cloud frame.
|
||||
pub(super) const CLOUD_BINDINGS: [wgpu::BindingType; 8] =
|
||||
[UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT, FLOAT, FLOAT];
|
||||
/// texture, the previous cloud frame, the smoke atlas, the world's depth, the previous march lengths.
|
||||
pub(super) const CLOUD_BINDINGS: [wgpu::BindingType; 10] = [
|
||||
UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT, FLOAT, FLOAT, DEPTH, UNFILTERED,
|
||||
];
|
||||
/// `fs_cloud_shadow`'s group 0: as the clouds' without the history.
|
||||
pub(super) const CLOUD_SHADOW_BINDINGS: [wgpu::BindingType; 6] =
|
||||
[UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT];
|
||||
@@ -218,15 +222,40 @@ impl HdVolumes {
|
||||
let cloud_module = module(device, "HD clouds", &cloud_src);
|
||||
let shadow_module = module(device, "HD cloud shadows", &shadow_src);
|
||||
let fog_module = module(device, "HD fog", &fog_body());
|
||||
let cloud_pipeline = fullscreen(
|
||||
device,
|
||||
"HD clouds",
|
||||
&cloud_layout,
|
||||
&cloud_module,
|
||||
"fs_cloud",
|
||||
CLOUD_FORMAT,
|
||||
None,
|
||||
);
|
||||
let cloud_pipeline = {
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("HD clouds"),
|
||||
bind_group_layouts: &[Some(&cloud_layout)],
|
||||
..Default::default()
|
||||
});
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("HD clouds"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &cloud_module,
|
||||
entry_point: Some("vs_full"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &cloud_module,
|
||||
entry_point: Some("fs_cloud"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[CLOUD_FORMAT, FOG_DIST_FORMAT].map(|format| {
|
||||
Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})
|
||||
}),
|
||||
}),
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let shadow_pipeline = fullscreen(
|
||||
device,
|
||||
"HD cloud shadows",
|
||||
@@ -286,6 +315,11 @@ impl HdVolumes {
|
||||
one("HD clouds", [1, 1], CLOUD_FORMAT),
|
||||
one("HD clouds", [1, 1], CLOUD_FORMAT),
|
||||
],
|
||||
cloud_dist: [
|
||||
one("HD cloud distance", [1, 1], FOG_DIST_FORMAT),
|
||||
one("HD cloud distance", [1, 1], FOG_DIST_FORMAT),
|
||||
],
|
||||
cloud_depth: None,
|
||||
flip: 0,
|
||||
history_ready: false,
|
||||
cloud_shadow: one(
|
||||
@@ -316,6 +350,10 @@ impl HdVolumes {
|
||||
one("HD clouds", CLOUD_FORMAT),
|
||||
one("HD clouds", CLOUD_FORMAT),
|
||||
];
|
||||
self.cloud_dist = [
|
||||
one("HD cloud distance", FOG_DIST_FORMAT),
|
||||
one("HD cloud distance", FOG_DIST_FORMAT),
|
||||
];
|
||||
self.fog = one("HD fog", FOG_FORMAT);
|
||||
self.fog_dist = one("HD fog distance", FOG_DIST_FORMAT);
|
||||
self.history_ready = false;
|
||||
@@ -342,6 +380,11 @@ impl HdVolumes {
|
||||
&self.clouds[self.flip]
|
||||
}
|
||||
|
||||
/// The march lengths that go with `cloud_view`.
|
||||
pub fn cloud_dist_view(&self) -> &wgpu::TextureView {
|
||||
&self.cloud_dist[self.flip]
|
||||
}
|
||||
|
||||
fn group(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -372,7 +415,12 @@ impl HdVolumes {
|
||||
frame: &wgpu::Buffer,
|
||||
sky_lut: &wgpu::TextureView,
|
||||
atlas: &wgpu::TextureView,
|
||||
depth: &wgpu::TextureView,
|
||||
) {
|
||||
if self.cloud_depth.as_ref() != Some(depth) {
|
||||
self.cloud_groups = None;
|
||||
self.cloud_depth = Some(depth.clone());
|
||||
}
|
||||
if self.shadow_group.is_none() {
|
||||
let layout = self.shadow_layout.clone();
|
||||
let (n3, n2, clamp, repeat) = (
|
||||
@@ -405,6 +453,7 @@ impl HdVolumes {
|
||||
self.repeat.clone(),
|
||||
);
|
||||
let hist = self.clouds.clone();
|
||||
let hist_dist = self.cloud_dist.clone();
|
||||
let groups = [0, 1].map(|read| {
|
||||
self.group(
|
||||
device,
|
||||
@@ -419,6 +468,8 @@ impl HdVolumes {
|
||||
wgpu::BindingResource::TextureView(sky_lut),
|
||||
wgpu::BindingResource::TextureView(&hist[read]),
|
||||
wgpu::BindingResource::TextureView(atlas),
|
||||
wgpu::BindingResource::TextureView(depth),
|
||||
wgpu::BindingResource::TextureView(&hist_dist[read]),
|
||||
],
|
||||
)
|
||||
});
|
||||
@@ -455,7 +506,7 @@ impl HdVolumes {
|
||||
stamped_pass(
|
||||
encoder,
|
||||
"HD clouds",
|
||||
&[&self.clouds[write]],
|
||||
&[&self.clouds[write], &self.cloud_dist[write]],
|
||||
&self.cloud_pipeline,
|
||||
&groups[read],
|
||||
stamp,
|
||||
@@ -525,8 +576,13 @@ impl Hd {
|
||||
depth: &wgpu::TextureView,
|
||||
) {
|
||||
if self.clouds_active {
|
||||
self.volumes
|
||||
.prepare_clouds(device, &self.frame, &self.sky.lut, &self.smoke.atlas);
|
||||
self.volumes.prepare_clouds(
|
||||
device,
|
||||
&self.frame,
|
||||
&self.sky.lut,
|
||||
&self.smoke.atlas,
|
||||
depth,
|
||||
);
|
||||
self.volumes
|
||||
.draw_cloud_shadow(encoder, stamp(&mut self.timing, "hd cloud shadows"));
|
||||
self.volumes
|
||||
@@ -570,3 +626,48 @@ pub fn shader_sources() -> Vec<(&'static str, String, &'static [wgpu::BindingTyp
|
||||
("fog", fog_body(), &FOG_BINDINGS),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every world-anchored noise lookup divides the world coordinate (the camera's x, z wrapped to
|
||||
/// 65536) by a scale that divides 65536, or the pattern jumps where the wrap happens (x or z = 0).
|
||||
#[test]
|
||||
fn noise_scales_divide_the_camera_wrap() {
|
||||
let mut found = 0;
|
||||
for (name, src) in [("cloud_common", CLOUD_COMMON), ("fog", FOG)] {
|
||||
for key in ["w / ", "p3 / "] {
|
||||
for part in src.split(key).skip(1) {
|
||||
let num: String = part
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_digit() || *c == '.')
|
||||
.collect();
|
||||
let Ok(scale) = num.parse::<f64>() else {
|
||||
continue;
|
||||
};
|
||||
found += 1;
|
||||
assert!(
|
||||
scale >= 1.0 && scale.fract() == 0.0 && 65536 % (scale as u64) == 0,
|
||||
"{name}: the scale {scale} does not divide 65536"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(found >= 8, "found only {found} noise lookups");
|
||||
}
|
||||
|
||||
/// The cloud noise's height is measured from the layer's base, not from the camera: the same
|
||||
/// world point gives the same noise coordinate at any camera height.
|
||||
#[test]
|
||||
fn cloud_noise_height_is_anchored_to_the_layer() {
|
||||
assert!(CLOUD_COMMON.contains("pos.y - frame.clouds.x"));
|
||||
// frame.clouds.x = CLOUD_BASE - camera height: y_world - CLOUD_BASE however high the camera.
|
||||
for cam in [64.0f32, 210.0, 1000.0] {
|
||||
let world_y = 230.0f32;
|
||||
let rel = world_y - cam;
|
||||
let base_rel = CLOUD_BASE - cam;
|
||||
assert!((rel - base_rel - (world_y - CLOUD_BASE)).abs() < 1e-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user