camera fluid: lava fog starts when a corner of the near plane dips under the lava

This commit is contained in:
iLostName committed 2026-11-01 20:12:01 +01:00
1 parent e5a4324ca3
commit 30715a97d9
6 files changed
+105 -27

No files matched your search

@@ -419,6 +419,31 @@ pub enum CameraFluid {
PowderSnow,
}
/// `Camera.NearPlane`: the near plane's centre and its half extents toward
/// the camera's left and up, relative to the camera.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NearPlane {
pub forward: [f64; 3],
pub left: [f64; 3],
pub up: [f64; 3],
}
impl NearPlane {
/// `forward`, `getTopLeft`, `getTopRight`, `getBottomLeft`, `getBottomRight`:
/// the points `Camera.getFluidInCamera` looks for lava and powder snow at.
pub fn points(&self) -> [[f64; 3]; 5] {
let corner = |up: f64, left: f64| -> [f64; 3] {
std::array::from_fn(|i| self.forward[i] + self.up[i] * up + self.left[i] * left)
};
[
self.forward,
corner(1.0, 1.0),
corner(1.0, -1.0),
corner(-1.0, 1.0),
corner(-1.0, -1.0),
]
}
}
/// `Camera`: the eye-height and field-of-view easing and the view placement.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Camera {
@@ -481,6 +506,19 @@ impl Camera {
self.up = self.rotation.transform([0.0, 1.0, 0.0]);
self.left = self.rotation.transform([-1.0, 0.0, 0.0]);
}
/// `Camera.getNearPlane(fov)` for a `width` x `height` window, at the
/// projection's `zNear` of 0.05.
pub fn near_plane(&self, fov: f32, width: f32, height: f32) -> NearPlane {
let z_near = f64::from(0.05f32);
let aspect = f64::from(width) / f64::from(height);
let plane_height = horizon_core::jmath::tan(f64::from(fov * DEG_TO_RAD) / 2.0) * z_near;
let scale = |v: [f32; 3], by: f64| v.map(|c| f64::from(c) * by);
NearPlane {
forward: scale(self.forwards, z_near),
left: scale(self.left, plane_height * aspect),
up: scale(self.up, plane_height),
}
}
/// `Camera.move(forwards, up, right)`.
pub fn move_by(&mut self, forwards: f32, up: f32, right: f32) {
let offset = self.rotation.transform([right, up, -forwards]);
+42 -18
View File
@@ -5,6 +5,7 @@
use crate::{
biome::Biomes,
blocks::{BlockFacts, Fluid},
camera::NearPlane,
environment::Sky,
world::World,
};
@@ -135,9 +136,24 @@ fn camera_fluid_height(world: &World, facts: &BlockFacts, b: [i32; 3]) -> Option
};
Some((s.fluid, height))
}
/// `Camera.getFluidInCamera` (near-plane points reduced to the camera and
/// the point just in front of it).
pub fn fog_type(world: &World, facts: &BlockFacts, camera: [f64; 3], forward: [f64; 3]) -> FogType {
/// `Camera.getFluidInCamera`: water at the camera, then lava or powder snow
/// at the near plane's centre and corners.
pub fn fog_type(world: &World, facts: &BlockFacts, camera: [f64; 3], near: &NearPlane) -> FogType {
camera_fog_type(
camera,
near,
|b| camera_fluid_height(world, facts, b),
|b| &*facts.get(world.block(b[0], b[1], b[2])).name == "minecraft:powder_snow",
)
}
/// `fog_type` over a block lookup: the fluid and its camera height, and
/// whether the block is powder snow.
fn camera_fog_type(
camera: [f64; 3],
near: &NearPlane,
fluid: impl Fn([i32; 3]) -> Option<(Fluid, f64)>,
powder_snow: impl Fn([i32; 3]) -> bool,
) -> FogType {
let block = |p: [f64; 3]| {
[
p[0].floor() as i32,
@@ -146,28 +162,19 @@ pub fn fog_type(world: &World, facts: &BlockFacts, camera: [f64; 3], forward: [f
]
};
let b = block(camera);
if let Some((Fluid::Water, h)) = camera_fluid_height(world, facts, b)
if let Some((Fluid::Water, h)) = fluid(b)
&& camera[1] < f64::from(b[1]) + h
{
return FogType::Water;
}
for p in [
[
camera[0] + forward[0] * 0.05,
camera[1] + forward[1] * 0.05,
camera[2] + forward[2] * 0.05,
],
camera,
] {
for point in near.points() {
let p: [f64; 3] = std::array::from_fn(|i| camera[i] + point[i]);
let b = block(p);
match camera_fluid_height(world, facts, b) {
match fluid(b) {
Some((Fluid::Lava, h)) if p[1] <= f64::from(b[1]) + h => return FogType::Lava,
Some((Fluid::Lava, _)) => {}
_ => {
if &*facts.get(world.block(b[0], b[1], b[2])).name == "minecraft:powder_snow" {
return FogType::PowderSnow;
}
}
_ if powder_snow(b) => return FogType::PowderSnow,
_ => {}
}
}
FogType::Atmospheric
@@ -448,6 +455,23 @@ mod tests {
}
}
#[test]
fn lava_fog_starts_when_a_near_plane_corner_dips_under_the_surface() {
// Flowing lava (amount 4) on y 64: the camera sees its surface at 64 + 4/9.
let surface = 64.0 + 4.0 / 9.0;
let fluid = |b: [i32; 3]| (b[1] == 64).then_some((Fluid::Lava, 4.0 / 9.0));
let fog = |y: f64, pitch: f32| {
let mut camera = crate::camera::Camera::default();
camera.set_rotation(0.0, pitch);
let near = camera.near_plane(70.0, 1600.0, 900.0);
camera_fog_type([0.5, y, 0.5], &near, fluid, |_| false)
};
// Looking level, the near plane reaches tan(35 degrees) * 0.05 = 0.035 below the eye.
assert_eq!(fog(surface + 0.02, 0.0), FogType::Lava);
assert_eq!(fog(surface + 0.05, 0.0), FogType::Atmospheric);
// The eye itself is not tested: looking up, the whole plane is above it.
assert_eq!(fog(surface - 0.01, -90.0), FogType::Atmospheric);
}
#[test]
fn boss_world_fog_closes_in() {
// AtmosphericFogEnvironment.setupFog under a createWorldFog boss bar.
let sky = Sky {
+1 -1
View File
@@ -15290,7 +15290,7 @@ impl Game<'_> {
&self.world,
self.res.facts(),
[camera.x, camera.y, camera.z],
self.forward(),
&self.near_plane(),
),
horizon_client::fog::FogType::Lava | horizon_client::fog::FogType::PowderSnow
),
+1 -1
View File
@@ -107,7 +107,7 @@ impl Game<'_> {
}
/// The fluid at the camera, as `Camera.getFluidInCamera` reports it.
pub(super) fn camera_fluid(&self, camera: [f64; 3]) -> CameraFluid {
match fog_type(&self.world, self.res.facts(), camera, self.forward()) {
match fog_type(&self.world, self.res.facts(), camera, &self.near_plane()) {
FogType::Water => CameraFluid::Water,
FogType::Lava => CameraFluid::Lava,
FogType::PowderSnow => CameraFluid::PowderSnow,
+17 -4
View File
@@ -2,6 +2,7 @@
//! campfire smoke and rain splashes (`ClientLevel.tickWeatherEffects`).
use super::{Dim, Game};
use horizon_client::assets::Properties;
use horizon_client::camera::NearPlane;
use horizon_client::fog::{FogData, FogInputs, FogType, fog_type, update_rain_fog};
use horizon_client::{
animate_tick::{AnimateContext, animate_tick, campfire_particles, marker_particle_target},
@@ -85,6 +86,18 @@ impl Game<'_> {
yaw.cos() * pitch.cos(),
]
}
/// `Camera.getNearPlane` at the FOV option, as `getFluidInCamera` asks for it.
pub(super) fn near_plane(&self) -> NearPlane {
let [width, height] = self
.renderer
.as_ref()
.map_or([1, 1], |r| [r.config.width, r.config.height]);
self.view.near_plane(
self.settings.fov as i32 as f32,
width as f32,
height.max(1) as f32,
)
}
/// `FogRenderer.setupFog` for the frame's camera, and Horizon's Fog option.
pub(super) fn frame_fog(
&self,
@@ -95,7 +108,7 @@ impl Game<'_> {
) -> FogData {
let forward = self.forward();
let at = [camera.x, camera.y, camera.z];
let fog_type = fog_type(&self.world, self.res.facts(), at, forward);
let fog_type = fog_type(&self.world, self.res.facts(), at, &self.near_plane());
let (rain, thunder) = if self.dim == Dim::Overworld {
self.world.weather.levels(self.clock.fraction() as f32)
} else {
@@ -239,7 +252,7 @@ impl Game<'_> {
&self.world,
self.res.facts(),
[camera.x, camera.y, camera.z],
self.forward(),
&self.near_plane(),
) != FogType::Water
}
/// `ScreenEffectRenderer` + `Hud` camera overlays for this frame.
@@ -312,7 +325,7 @@ impl Game<'_> {
}
}
let in_water =
fog_type(&self.world, self.res.facts(), eye, self.forward()) == FogType::Water;
fog_type(&self.world, self.res.facts(), eye, &self.near_plane()) == FogType::Water;
let water =
(in_water && !spectator).then(|| (self.eye_brightness(eye), self.yaw, self.pitch));
// Hud.extractCameraOverlays; the spyglass scope (and its easing) is
@@ -383,7 +396,7 @@ impl Game<'_> {
self.screen_fx.tick_spinning(nausea);
// LocalPlayer water vision and AtmosphericFogEnvironment rain fog.
let in_water =
fog_type(&self.world, self.res.facts(), e, self.forward()) == FogType::Water;
fog_type(&self.world, self.res.facts(), e, &self.near_plane()) == FogType::Water;
self.fog.tick(in_water);
let b = [
e[0].floor() as i32,
+6 -3
View File
@@ -1251,9 +1251,12 @@ impl Game<'_> {
if self.potions.effects.get("conduit_power").is_some()
&& let Some(eye) = self.eye_position()
{
let in_water =
horizon_client::fog::fog_type(&self.world, self.res.facts(), eye, self.forward())
== FogType::Water;
let in_water = horizon_client::fog::fog_type(
&self.world,
self.res.facts(),
eye,
&self.near_plane(),
) == FogType::Water;
return self.fog.water_vision(in_water);
}
0.0