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:
@@ -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]
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user