Merge pull request #2972 from kacpherek/fix/player-nausea-eyeblossom

Show Nausea on the player: spin the view and draw the swirl
This commit is contained in:
rdbt
2026-10-28 00:43:30 +00:00
committed by GitHub
9 changed files with 107 additions and 8 deletions
+21
View File
@@ -772,6 +772,27 @@ pub fn bob_matrix(state: &BobState, damage_tilt_strength: f64, bob_view_option:
pose
}
/// The spinning effect `GameRenderer.renderLevel` multiplies into the level's
/// projection after the bob: a skew about a tilted axis, from a portal's or
/// Nausea's intensity (already scaled by Distortion Effects) and the angle in
/// degrees. The first-person hand keeps its own projection without it.
pub fn spinning_effect_matrix(intensity: f32, angle_degrees: f32) -> glam::Mat4 {
if intensity <= 0.0 {
return glam::Mat4::IDENTITY;
}
let mut skew = 5.0 / (intensity * intensity + 5.0) - intensity * 0.04;
skew *= skew;
let axis = glam::Vec3::new(
0.0,
std::f32::consts::SQRT_2 / 2.0,
std::f32::consts::SQRT_2 / 2.0,
);
let angle = angle_degrees * DEG_TO_RAD;
glam::Mat4::from_axis_angle(axis, angle)
* glam::Mat4::from_scale(glam::Vec3::new(1.0 / skew, 1.0, 1.0))
* glam::Mat4::from_axis_angle(axis, -angle)
}
#[cfg(test)]
mod tests {
#[test]
+4
View File
@@ -29,6 +29,10 @@ impl EffectInstance {
fn infinite(&self) -> bool {
self.duration == -1
}
/// `MobEffectInstance.endsWithin`.
pub fn ends_within(&self, ticks: i32) -> bool {
!self.infinite() && self.duration <= ticks
}
fn shorter_than(&self, other: &Self) -> bool {
!self.infinite() && (self.duration < other.duration || other.infinite())
}
+8 -1
View File
@@ -11684,6 +11684,11 @@ impl Game<'_> {
} else {
self.bob_matrix(camera_alpha as f32)
};
let spin = if title {
glam::Mat4::IDENTITY
} else {
self.spinning_matrix(camera_alpha as f32)
};
let terrain_distance = self.terrain_distance();
let distance = (terrain_distance * 16) as f32;
let depth_far =
@@ -11691,7 +11696,7 @@ impl Game<'_> {
self.view_frustum = self
.renderer
.as_ref()
.map(|r| r.camera_frustum(fov_degrees, yaw, pitch, bob, depth_far));
.map(|r| r.camera_frustum(fov_degrees, yaw, pitch, bob * spin, depth_far));
// Keep the buffer local while builders need mutable access to the game.
let mut entities = std::mem::take(&mut self.entity_vertices);
entities.clear();
@@ -11774,6 +11779,7 @@ impl Game<'_> {
pitch,
fov_degrees,
bob,
spin,
sky,
distance,
detail_distance: (self.streaming.radius * 16) as f32,
@@ -14688,6 +14694,7 @@ fn loading_frame(hud: &HudBatch) -> Frame<'_> {
pitch: 10.0,
fov_degrees: 85.0,
bob: glam::Mat4::IDENTITY,
spin: glam::Mat4::IDENTITY,
sky: horizon_client::environment::Sky::default(),
distance: 12.0,
detail_distance: 12.0,
+25 -1
View File
@@ -1,11 +1,13 @@
//! Field of view (`Camera.tickFov`, `calculateFov`,
//! `AbstractClientPlayer.getFieldOfViewModifier`), view and hurt bobbing
//! (`GameRenderer.bobHurt`, `bobView`) and the spyglass (`SpyglassItem`,
//! (`GameRenderer.bobHurt`, `bobView`), the portal and Nausea spin
//! (`GameRenderer.renderLevel`) and the spyglass (`SpyglassItem`,
//! `Hud.extractSpyglassOverlay`). The arithmetic is `horizon_client::camera`.
use super::Game;
use horizon_client::{
camera::{
BobState, CameraFluid, FovPlayer, HurtBob, UseItem, bob_matrix, field_of_view_modifier,
spinning_effect_matrix,
},
fog::{FogType, fog_type},
hud::HudBatch,
@@ -26,6 +28,28 @@ impl Game<'_> {
pub(super) fn fov_effect_scale(&self) -> f32 {
self.settings.file.double("fovEffectScale") as f32
}
/// `options.screenEffectScale` (Distortion Effects).
pub(super) fn screen_effect_scale(&self) -> f32 {
self.settings.file.double("screenEffectScale") as f32
}
/// `LocalPlayer.getEffectBlendFactor(NAUSEA, partial)`: 0 without the effect.
pub(super) fn nausea_blend(&self, partial: f32) -> f32 {
if self.potions.effects.get("nausea").is_none() {
return 0.0;
}
let fx = &self.screen_fx;
fx.o_nausea + partial * (fx.nausea - fx.o_nausea)
}
/// The spin `GameRenderer.renderLevel` multiplies into the level's
/// projection: the larger of the portal and Nausea intensities, scaled by
/// Distortion Effects squared, turning by `LocalPlayer.getSpinningEffectAngle`.
pub(super) fn spinning_matrix(&self, partial: f32) -> glam::Mat4 {
let fx = &self.screen_fx;
let portal = fx.o_portal + partial * (fx.portal - fx.o_portal);
let scale = self.screen_effect_scale();
let intensity = portal.max(self.nausea_blend(partial)) * (scale * scale);
spinning_effect_matrix(intensity, fx.spinning_time + partial * fx.spinning_speed)
}
/// What `AbstractClientPlayer.getFieldOfViewModifier` reads.
fn fov_player(&self) -> Option<FovPlayer> {
let p = self.player.as_ref()?;
@@ -214,8 +214,8 @@ impl Game<'_> {
percent_frozen: p.percent_frozen(),
partial,
portal: [fx.o_portal, fx.portal],
nausea: 0.0,
screen_effect_scale: 1.0,
nausea: self.nausea_blend(partial),
screen_effect_scale: self.screen_effect_scale(),
};
let mut scope = self.scope_scale;
let gui_overlays =
@@ -252,6 +252,12 @@ impl Game<'_> {
let brightness = self.eye_brightness(e);
let inside_portal = self.client_portal_effect();
self.screen_fx.tick(brightness, inside_portal);
let nausea = self
.potions
.effects
.get("nausea")
.map(|e| !e.ends_within(60));
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;
+4 -1
View File
@@ -219,6 +219,9 @@ pub struct Frame<'a> {
/// The `GameRenderer.bobHurt`/`bobView` pose multiplied into the
/// projection (`crate::camera::bob_matrix`).
pub bob: Mat4,
/// The spinning effect multiplied into the level's projection only
/// (`crate::camera::spinning_effect_matrix`), not the hand's.
pub spin: Mat4,
pub sky: Sky,
/// Render distance in blocks (fog end).
pub distance: f32,
@@ -3417,7 +3420,7 @@ impl Renderer {
frame.fov_degrees,
frame.yaw,
frame.pitch,
frame.bob,
frame.bob * frame.spin,
frame.depth_far,
)
}
+3 -2
View File
@@ -361,8 +361,9 @@ impl NativePost {
pub fn frame_inputs(frame: &Frame, size: [u32; 2], seconds: f32, index: u32) -> FrameInputs {
let aspect = size[0] as f32 / size[1].max(1) as f32;
let view = crate::camera::view_matrix(frame.yaw, frame.pitch);
let projection =
super::world_projection(frame.fov_degrees, aspect, frame.depth_far) * frame.bob;
let projection = super::world_projection(frame.fov_degrees, aspect, frame.depth_far)
* frame.bob
* frame.spin;
// A celestial body's direction at `angle` degrees (SkyRenderer: Y -90°, then X by the
// angle, of straight up): overhead at 0, rising in the east (+x).
let body = |angle: f32| {
@@ -287,7 +287,8 @@ impl FrameUniforms {
aspect,
0.05,
frame.depth_far,
) * frame.bob;
) * frame.bob
* frame.spin;
let camera = frame.camera.as_vec3();
let (previous_camera, previous_view, previous_projection) =
self.previous
@@ -69,6 +69,12 @@ pub struct ScreenEffects {
pub vignette_brightness: f32,
pub portal: f32,
pub o_portal: f32,
/// Nausea's `MobEffectInstance.BlendState` factor and the previous tick's.
pub nausea: f32,
pub o_nausea: f32,
/// `LocalPlayer.spinningEffectTime` and `spinningEffectSpeed` (degrees).
pub spinning_time: f32,
pub spinning_speed: f32,
}
impl Default for ScreenEffects {
fn default() -> Self {
@@ -76,6 +82,10 @@ impl Default for ScreenEffects {
vignette_brightness: 1.0,
portal: 0.0,
o_portal: 0.0,
nausea: 0.0,
o_nausea: 0.0,
spinning_time: 0.0,
spinning_speed: 0.0,
}
}
}
@@ -105,6 +115,28 @@ impl ScreenEffects {
};
self.portal = (self.portal + step).clamp(0.0, 1.0);
}
/// `LocalPlayer.tickSpinningEffect` and Nausea's blend. `nausea` is `None`
/// without the effect, else whether it is past its blend-out point
/// (`!endsWithin(60)`, `MobEffects.NAUSEA`).
pub fn tick_spinning(&mut self, nausea: Option<bool>) {
self.o_nausea = self.nausea;
match nausea {
// BlendState.tick: fades in over 150 ticks and out over 20.
Some(active) => {
let (target, duration) = if active { (1.0, 150.0) } else { (0.0, 20.0) };
self.nausea += (target - self.nausea).clamp(-1.0 / duration, 1.0 / duration);
}
// The blend state belongs to the instance, so a new one starts at 0.
None => self.nausea = 0.0,
}
if self.portal > 0.0 || self.nausea > 0.0 {
self.spinning_speed =
(self.portal * 20.0 + self.nausea * 7.0) / (self.portal + self.nausea);
self.spinning_time += self.spinning_speed;
} else {
self.spinning_speed = 0.0;
}
}
pub fn build(&self, input: &ScreenInputs) -> Vec<ScreenQuad> {
let mut out = Vec::new();
let sprite = |name: &str| input.sprites.get(name).map(|r| [r.u0, r.v0, r.u1, r.v1]);