Converts parallax to pixel offsets, saves a bunch of cpu time, makes things nicer on clients too (#83395)

## About The Pull Request

Right now parallax is like a quarter of SSinput, which is BAD. It's so
high mostly because of the animates we need to do, but also due to the
cost of setting screen_loc.


![image](https://github.com/tgstation/tgstation/assets/58055496/8e4ec4b7-2101-4dca-91b8-4db9e79de7c4)

This sucks. The default step is to reduce the poll rate of the effect,
but I don't want to do that because it SUCKS. Sooooo how can we
optimize.

Well, if we stop thinking in terms of screen_loc, which is a string
(tree shit) and also unanimatable, and start working in pixel offsets,
this'd be a way cheaper.

We can make that happen by sticking all our parallax layers on one rock
screen object. Then they have relative positions and can be pixel offset
(I have stolen this concept wholesale from Ter)

This works unreasonably well, roughly a 65% cost reduction. S good shit.


![image](https://github.com/tgstation/tgstation/assets/58055496/1e6c4455-a13b-44c3-bf59-71ef26cac9fd)

While I'm here...

[uses KEEP_TOGETHER to reduce clientside load, makes the flying
animation
better.](https://github.com/tgstation/tgstation/commit/52610398e2dc221c80d1f629e9c9f8fb59498977)

We were individually rendering all like fucking 24 480x480 overlays on
all 5 parallax layers, which means we had to apply our transform to EACH
ONE. This has GOTTA suck shit for clients, so let's... not? Should help.

The existing flying animation makes me depressed. it has some very
visible stutter, and jumps around a lot.

We can deal with the starting stutter by avoiding starting a new
animation on the layer until the old one is finished. This is what was
SUPPOSED to be happening, but because we fired one timer for all the
layers, they'd desync and jump in ugly ways.

This means we need to use one timer per layer, which does induce more
cost then I'd like. IDK how I feel about this to be honest.

I try and reduce ending weirdness by unscaling time at the end, so
different aspects don't slow down at different rates.

Speed on the parallax animation was weird, it'd spike up, then dip down
in flight.
This was because the percieved rate of change from the quad easing was
closer to 2x the existing.
I've handled this by halving the animation time in the loop

Oh also there's no sense calling the update animation proc if we are
coming to a stop, and thus have no follow up animation.


## Changelog

🆑 LemonInTheDark
refactor: I have reworked how parallax and its animations (space travel)
work. Please report any bugs lads!
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>
This commit is contained in:
LemonInTheDark
2024-05-27 10:43:25 -06:00
committed by GitHub
co-authored by san7890
parent 91f30899b0
commit 07882b23cc
3 changed files with 87 additions and 81 deletions
+81 -79
View File
@@ -11,6 +11,10 @@
for(var/atom/movable/screen/plane_master/parallax as anything in get_true_plane_masters(PLANE_SPACE_PARALLAX))
parallax.unhide_plane(screenmob)
if(isnull(C.parallax_rock))
C.parallax_rock = new(null, src)
C.screen |= C.parallax_rock
if(!length(C.parallax_layers_cached))
C.parallax_layers_cached = list()
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_1(null, src)
@@ -25,7 +29,7 @@
if (length(C.parallax_layers) > C.parallax_layers_max)
C.parallax_layers.len = C.parallax_layers_max
C.screen |= (C.parallax_layers)
C.parallax_rock.vis_contents = C.parallax_layers
// We could do not do parallax for anything except the main plane group
// This could be changed, but it would require refactoring this whole thing
// And adding non client particular hooks for all the inputs, and I do not have the time I'm sorry :(
@@ -44,7 +48,7 @@
/datum/hud/proc/remove_parallax(mob/viewmob)
var/mob/screenmob = viewmob || mymob
var/client/C = screenmob.client
C.screen -= (C.parallax_layers_cached)
C.screen -= (C.parallax_rock)
for(var/atom/movable/screen/plane_master/plane_master as anything in screenmob.hud_used.get_true_plane_masters(PLANE_SPACE))
if(screenmob != mymob)
C.screen -= locate(/atom/movable/screen/plane_master/parallax_white) in C.screen
@@ -104,83 +108,65 @@
update_parallax(screen_mob)
// This sets which way the current shuttle is moving (returns true if the shuttle has stopped moving so the caller can append their animation)
/datum/hud/proc/set_parallax_movedir(new_parallax_movedir = 0, skip_windups, mob/viewmob)
/datum/hud/proc/set_parallax_movedir(new_parallax_movedir = NONE, skip_windups, mob/viewmob)
. = FALSE
var/mob/screenmob = viewmob || mymob
var/client/C = screenmob.client
if(new_parallax_movedir == C.parallax_movedir)
return
var/animatedir = new_parallax_movedir
if(new_parallax_movedir == FALSE)
var/animate_time = 0
for(var/thing in C.parallax_layers)
var/atom/movable/screen/parallax_layer/L = thing
L.icon_state = initial(L.icon_state)
L.update_o(C.view)
var/T = PARALLAX_LOOP_TIME / L.speed
if (T > animate_time)
animate_time = T
C.dont_animate_parallax = world.time + min(animate_time, PARALLAX_LOOP_TIME)
animatedir = C.parallax_movedir
var/matrix/newtransform
switch(animatedir)
var/animation_dir = new_parallax_movedir || C.parallax_movedir
var/matrix/new_transform
switch(animation_dir)
if(NORTH)
newtransform = matrix(1, 0, 0, 0, 1, 480)
new_transform = matrix(1, 0, 0, 0, 1, 480)
if(SOUTH)
newtransform = matrix(1, 0, 0, 0, 1,-480)
new_transform = matrix(1, 0, 0, 0, 1,-480)
if(EAST)
newtransform = matrix(1, 0, 480, 0, 1, 0)
new_transform = matrix(1, 0, 480, 0, 1, 0)
if(WEST)
newtransform = matrix(1, 0,-480, 0, 1, 0)
new_transform = matrix(1, 0,-480, 0, 1, 0)
var/shortesttimer
if(!skip_windups)
for(var/thing in C.parallax_layers)
var/atom/movable/screen/parallax_layer/L = thing
var/longest_timer = 0
for(var/key in C.parallax_animate_timers)
deltimer(C.parallax_animate_timers[key])
C.parallax_animate_timers = list()
for(var/atom/movable/screen/parallax_layer/layer as anything in C.parallax_layers)
var/scaled_time = PARALLAX_LOOP_TIME / layer.speed
if(new_parallax_movedir == NONE) // If we're stopping, we need to stop on the same dime, yeah?
scaled_time = PARALLAX_LOOP_TIME
longest_timer = max(longest_timer, scaled_time)
var/T = PARALLAX_LOOP_TIME / L.speed
if (isnull(shortesttimer))
shortesttimer = T
if (T < shortesttimer)
shortesttimer = T
L.transform = newtransform
animate(L, transform = matrix(), time = T, easing = QUAD_EASING | (new_parallax_movedir ? EASE_IN : EASE_OUT), flags = ANIMATION_END_NOW)
if (new_parallax_movedir)
L.transform = newtransform
animate(transform = matrix(), time = T) //queue up another animate so lag doesn't create a shutter
C.parallax_movedir = new_parallax_movedir
if (C.parallax_animate_timer)
deltimer(C.parallax_animate_timer)
var/datum/callback/CB = CALLBACK(src, PROC_REF(update_parallax_motionblur), C, animatedir, new_parallax_movedir, newtransform)
if(skip_windups)
CB.Invoke()
else
C.parallax_animate_timer = addtimer(CB, min(shortesttimer, PARALLAX_LOOP_TIME), TIMER_CLIENT_TIME|TIMER_STOPPABLE)
/datum/hud/proc/update_parallax_motionblur(client/C, animatedir, new_parallax_movedir, matrix/newtransform)
if(!C)
return
C.parallax_animate_timer = FALSE
for(var/thing in C.parallax_layers)
var/atom/movable/screen/parallax_layer/L = thing
if (!new_parallax_movedir)
animate(L)
if(skip_windups)
update_parallax_motionblur(C, layer, new_parallax_movedir, new_transform)
continue
var/newstate = initial(L.icon_state)
var/T = PARALLAX_LOOP_TIME / L.speed
layer.transform = new_transform
animate(layer, transform = matrix(), time = scaled_time, easing = QUAD_EASING | (new_parallax_movedir ? EASE_IN : EASE_OUT))
if (new_parallax_movedir == NONE)
continue
//queue up another animate so lag doesn't create a shutter
animate(transform = new_transform, time = 0)
animate(transform = matrix(), time = scaled_time / 2)
C.parallax_animate_timers[layer] = addtimer(CALLBACK(src, PROC_REF(update_parallax_motionblur), C, layer, new_parallax_movedir, new_transform), scaled_time, TIMER_CLIENT_TIME|TIMER_STOPPABLE)
if (newstate in icon_states(L.icon))
L.icon_state = newstate
L.update_o(C.view)
C.dont_animate_parallax = world.time + min(longest_timer, PARALLAX_LOOP_TIME)
C.parallax_movedir = new_parallax_movedir
L.transform = newtransform
/datum/hud/proc/update_parallax_motionblur(client/C, atom/movable/screen/parallax_layer/layer, new_parallax_movedir, matrix/new_transform)
if(!C)
return
C.parallax_animate_timers -= layer
animate(L, transform = L.transform, time = 0, loop = -1, flags = ANIMATION_END_NOW)
animate(transform = matrix(), time = T)
// If we are moving in a direction, we used the QUAD_EASING function with EASE_IN
// This means our position function is x^2. This is always LESS then the linear we're using here
// But if we just used the same time delay, our rate of change would mismatch. f'(1) = 2x for quad easing, rather then the 1 we get for linear
// (This is because of how derivatives work right?)
// Because of this, while our actual rate of change from before was PARALLAX_LOOP_TIME, our perceived rate of change was PARALLAX_LOOP_TIME / 2 (lower == faster).
// Let's account for that here
var/scaled_time = (PARALLAX_LOOP_TIME / layer.speed) / 2
animate(layer, transform = new_transform, time = 0, loop = -1, flags = ANIMATION_END_NOW)
animate(transform = matrix(), time = scaled_time)
/datum/hud/proc/update_parallax(mob/viewmob)
var/mob/screenmob = viewmob || mymob
@@ -217,36 +203,41 @@
var/our_speed = parallax_layer.speed
var/change_x
var/change_y
var/old_x = parallax_layer.offset_x
var/old_y = parallax_layer.offset_y
if(parallax_layer.absolute)
// We use change here so the typically large absolute objects (just lavaland for now) don't jitter so much
change_x = (posobj.x - SSparallax.planet_x_offset) * our_speed + parallax_layer.offset_x
change_y = (posobj.y - SSparallax.planet_y_offset) * our_speed + parallax_layer.offset_y
change_x = (posobj.x - SSparallax.planet_x_offset) * our_speed + old_x
change_y = (posobj.y - SSparallax.planet_y_offset) * our_speed + old_y
else
change_x = offset_x * our_speed
change_y = offset_y * our_speed
// This is how we tile parralax sprites
// It doesn't use change because we really don't want to animate this
if(parallax_layer.offset_x - change_x > 240)
if(old_x - change_x > 240)
parallax_layer.offset_x -= 480
else if(parallax_layer.offset_x - change_x < -240)
parallax_layer.pixel_w = parallax_layer.offset_x
else if(old_x - change_x < -240)
parallax_layer.offset_x += 480
if(parallax_layer.offset_y - change_y > 240)
parallax_layer.pixel_w = parallax_layer.offset_x
if(old_y - change_y > 240)
parallax_layer.offset_y -= 480
else if(parallax_layer.offset_y - change_y < -240)
parallax_layer.pixel_z = parallax_layer.offset_y
else if(old_y - change_y < -240)
parallax_layer.offset_y += 480
parallax_layer.pixel_z = parallax_layer.offset_y
// Now that we have our offsets, let's do our positioning
parallax_layer.offset_x -= change_x
parallax_layer.offset_y -= change_y
parallax_layer.screen_loc = "CENTER-7:[round(parallax_layer.offset_x, 1)],CENTER-7:[round(parallax_layer.offset_y, 1)]"
// We're going to use a transform to "glide" that last movement out, so it looks nicer
// Now that we have our offsets, let's do our positioning
// We're going to use an animate to "glide" that last movement out, so it looks nicer
// Don't do any animates if we're not actually moving enough distance yeah? thanks lad
if(run_parralax && (largest_change * our_speed > 1))
parallax_layer.transform = matrix(1,0,change_x, 0,1,change_y)
animate(parallax_layer, transform=matrix(), time = glide_rate)
animate(parallax_layer, pixel_w = round(parallax_layer.offset_x, 1), pixel_z = round(parallax_layer.offset_y, 1), time = glide_rate)
else
parallax_layer.pixel_w = round(parallax_layer.offset_x, 1)
parallax_layer.pixel_z = round(parallax_layer.offset_y, 1)
/atom/movable/proc/update_parallax_contents()
for(var/mob/client_mob as anything in client_mobs_in_contents)
@@ -258,6 +249,15 @@
var/area/areaobj = get_area(client.eye)
hud_used.set_parallax_movedir(areaobj.parallax_movedir, TRUE)
// Root object for parallax, all parallax layers are drawn onto this
INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_home)
/atom/movable/screen/parallax_home
icon = null
blend_mode = BLEND_ADD
plane = PLANE_SPACE_PARALLAX
screen_loc = "CENTER-7,CENTER-7"
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
// We need parallax to always pass its args down into initialize, so we immediate init it
INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer)
/atom/movable/screen/parallax_layer
@@ -266,9 +266,9 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer)
var/offset_x = 0
var/offset_y = 0
var/absolute = FALSE
appearance_flags = APPEARANCE_UI | KEEP_TOGETHER
blend_mode = BLEND_ADD
plane = PLANE_SPACE_PARALLAX
screen_loc = "CENTER-7,CENTER-7"
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
/atom/movable/screen/parallax_layer/Initialize(mapload, datum/hud/hud_owner, template = FALSE)
@@ -302,15 +302,17 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer)
// Turn the view size into a grid of correctly scaled overlays
var/list/viewscales = getviewsize(view)
var/countx = CEILING((viewscales[1] / 2) * parallax_scaler, 1) + 1
var/county = CEILING((viewscales[2] / 2) * parallax_scaler, 1) + 1
// This could be half the size but we need to provide space for parallax movement on mob movement, and movement on scroll from shuttles, so like this instead
var/countx = (CEILING((viewscales[1] / 2) * parallax_scaler, 1) + 1)
var/county = (CEILING((viewscales[2] / 2) * parallax_scaler, 1) + 1)
var/list/new_overlays = new
for(var/x in -countx to countx)
for(var/y in -county to county)
if(x == 0 && y == 0)
continue
var/mutable_appearance/texture_overlay = mutable_appearance(icon, icon_state)
texture_overlay.transform = matrix(1, 0, x*480, 0, 1, y*480)
texture_overlay.pixel_w += 480 * x
texture_overlay.pixel_z += 480 * y
new_overlays += texture_overlay
cut_overlays()
add_overlay(new_overlays)
+3 -2
View File
@@ -197,6 +197,7 @@
var/list/parallax_layers
var/list/parallax_layers_cached
var/atom/movable/screen/parallax_home/parallax_rock
///this is the last recorded client eye by SSparallax/fire()
var/atom/movable/movingmob
var/turf/previous_turf
@@ -206,8 +207,8 @@
var/parallax_movedir = 0
/// How many parallax layers to show our client
var/parallax_layers_max = 4
/// Timer for the area directional animation
var/parallax_animate_timer
/// Timers for the area directional animation, one for each layer
var/list/parallax_animate_timers
/// Do we want to do parallax animations at all?
/// Exists to prevent laptop fires
var/do_parallax_animations = TRUE
+3
View File
@@ -601,6 +601,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
QDEL_NULL(void)
QDEL_NULL(tooltips)
QDEL_NULL(loot_panel)
QDEL_NULL(parallax_rock)
QDEL_LIST(parallax_layers_cached)
parallax_layers = null
seen_messages = null
Master.UpdateTickRate()
..() //Even though we're going to be hard deleted there are still some things that want to know the destroy is happening