diff --git a/src/d_main.c b/src/d_main.c
index 2f564c6c8f7fd183bb315dd03c5493dca28cb16d..963439d663398b33ad2f5984e549fc545439feb5 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -477,6 +477,7 @@ static void D_Display(void)
 
 			if (!automapactive && !dedicated && cv_renderview.value)
 			{
+				R_ApplyLevelInterpolators(cv_frameinterpolation.value == 1 ? rendertimefrac : FRACUNIT);
 				PS_START_TIMING(ps_rendercalltime);
 				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
 				{
@@ -525,6 +526,7 @@ static void D_Display(void)
 						V_DoPostProcessor(1, postimgtype2, postimgparam2);
 				}
 				PS_STOP_TIMING(ps_rendercalltime);
+				R_RestoreLevelInterpolators();
 			}
 
 			if (lastdraw)
diff --git a/src/g_game.c b/src/g_game.c
index 8eb731b1ff3675c523b46cc203fc2d55086ada27..6d263e3020c8b1a1c10aadeb06acf32a8ef19104 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -1830,6 +1830,7 @@ void G_DoLoadLevel(boolean resetplayer)
 	}
 
 	// Setup the level.
+	R_InitializeLevelInterpolators();
 	if (!P_LoadLevel(false, false)) // this never returns false?
 	{
 		// fail so reset game stuff
diff --git a/src/p_ceilng.c b/src/p_ceilng.c
index 50344ee0ccf9df0daf12f45321521cf6af0ed6e3..f1cca0825d885ebb91bd814733d38d5f6e6d9284 100644
--- a/src/p_ceilng.c
+++ b/src/p_ceilng.c
@@ -13,6 +13,7 @@
 
 #include "doomdef.h"
 #include "p_local.h"
+#include "r_fps.h"
 #include "r_main.h"
 #include "s_sound.h"
 #include "z_zone.h"
@@ -602,6 +603,9 @@ INT32 EV_DoCeiling(line_t *line, ceiling_e type)
 		ceiling->tag = tag;
 		ceiling->type = type;
 		firstone = 0;
+
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&ceiling->thinker, sec, true);
 	}
 	return rtn;
 }
@@ -679,6 +683,10 @@ INT32 EV_DoCrush(line_t *line, ceiling_e type)
 
 		ceiling->tag = tag;
 		ceiling->type = type;
+
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&ceiling->thinker, sec, false);
+		R_CreateInterpolator_SectorPlane(&ceiling->thinker, sec, true);
 	}
 	return rtn;
 }
diff --git a/src/p_floor.c b/src/p_floor.c
index 5536ee913acf2c958694e3c1d6bb451a50bcbfb8..5fcefd2035185aca6e617ffcf68372660d605f2b 100644
--- a/src/p_floor.c
+++ b/src/p_floor.c
@@ -16,6 +16,7 @@
 #include "m_random.h"
 #include "p_local.h"
 #include "p_slopes.h"
+#include "r_fps.h"
 #include "r_state.h"
 #include "s_sound.h"
 #include "z_zone.h"
@@ -573,6 +574,8 @@ void T_ContinuousFalling(continuousfall_t *faller)
 	{
 		faller->sector->ceilingheight = faller->ceilingstartheight;
 		faller->sector->floorheight = faller->floorstartheight;
+
+		R_ClearLevelInterpolatorState(&faller->thinker);
 	}
 
 	P_CheckSector(faller->sector, false); // you might think this is irrelevant. you would be wrong
@@ -2010,6 +2013,9 @@ void EV_DoFloor(line_t *line, floor_e floortype)
 		}
 
 		firstone = 0;
+
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&dofloor->thinker, sec, false);
 	}
 }
 
@@ -2140,6 +2146,10 @@ void EV_DoElevator(line_t *line, elevator_e elevtype, boolean customspeed)
 			default:
 				break;
 		}
+
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&elevator->thinker, sec, false);
+		R_CreateInterpolator_SectorPlane(&elevator->thinker, sec, true);
 	}
 }
 
@@ -2318,6 +2328,10 @@ void EV_DoContinuousFall(sector_t *sec, sector_t *backsector, fixed_t spd, boole
 
 	faller->destheight = backwards ? backsector->ceilingheight : backsector->floorheight;
 	faller->direction = backwards ? 1 : -1;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&faller->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&faller->thinker, sec, true);
 }
 
 // Some other 3dfloor special things Tails 03-11-2002 (Search p_mobj.c for description)
diff --git a/src/p_spec.c b/src/p_spec.c
index 3fe77d7a2e8fa378fabc9615b89d826229c923e0..f6a0ce60643037e7eb5ab113d714555103cf11c2 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -20,6 +20,7 @@
 #include "p_local.h"
 #include "p_setup.h" // levelflats for flat animation
 #include "r_data.h"
+#include "r_fps.h"
 #include "r_textures.h"
 #include "m_random.h"
 #include "p_mobj.h"
@@ -5695,6 +5696,9 @@ static void P_AddPlaneDisplaceThinker(INT32 type, fixed_t speed, INT32 control,
 	displace->speed = speed;
 	displace->type = type;
 	displace->reverse = reverse;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&displace->thinker, &sectors[affectee], false);
 }
 
 /** Adds a Mario block thinker, which changes the block's texture between blank
@@ -5754,6 +5758,10 @@ static void P_AddRaiseThinker(sector_t *sec, INT16 tag, fixed_t speed, fixed_t c
 		raise->flags |= RF_REVERSE;
 	if (spindash)
 		raise->flags |= RF_SPINDASH;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&raise->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&raise->thinker, sec, true);
 }
 
 static void P_AddAirbob(sector_t *sec, INT16 tag, fixed_t dist, boolean raise, boolean spindash, boolean dynamic)
@@ -5779,6 +5787,10 @@ static void P_AddAirbob(sector_t *sec, INT16 tag, fixed_t dist, boolean raise, b
 		airbob->flags |= RF_SPINDASH;
 	if (dynamic)
 		airbob->flags |= RF_DYNAMIC;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&airbob->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&airbob->thinker, sec, true);
 }
 
 /** Adds a thwomp thinker.
@@ -5819,6 +5831,10 @@ static inline void P_AddThwompThinker(sector_t *sec, line_t *sourceline, fixed_t
 	sec->ceilingdata = thwomp;
 	// Start with 'resting' texture
 	sides[sourceline->sidenum[0]].midtexture = sides[sourceline->sidenum[0]].bottomtexture;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&thwomp->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&thwomp->thinker, sec, true);
 }
 
 /** Adds a thinker which checks if any MF_ENEMY objects with health are in the defined area.
diff --git a/src/p_tick.c b/src/p_tick.c
index ca70fb92662bdf2d898c7dce7a46b6acea40abc2..c2f2b0579f39e7e73a69514a8b61f8f81e80d238 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -764,6 +764,11 @@ void P_Ticker(boolean run)
 		LUA_HOOK(PostThinkFrame);
 	}
 
+	if (run)
+	{
+		R_UpdateLevelInterpolators();
+	}
+
 	P_MapEnd();
 
 //	Z_CheckMemCleanup();
diff --git a/src/r_fps.c b/src/r_fps.c
index 9bbae067621cb2e94a56f17c3ff2abd411f7ae3a..b69531574c64ac14a6237fa3d5809b0d3fe06b4d 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -20,6 +20,7 @@
 #include "r_plane.h"
 #include "p_spec.h"
 #include "r_state.h"
+#include "z_zone.h"
 #ifdef HWRENDER
 #include "hardware/hw_main.h" // for cv_glshearing
 #endif
@@ -40,6 +41,11 @@ viewvars_t *newview = &p1view_new;
 
 enum viewcontext_e viewcontext = VIEWCONTEXT_PLAYER1;
 
+static levelinterpolator_t **levelinterpolators;
+static size_t levelinterpolators_len;
+static size_t levelinterpolators_size;
+
+
 static fixed_t R_LerpFixed(fixed_t from, fixed_t to, fixed_t frac)
 {
 	return from + FixedMul(frac, to - from);
@@ -194,3 +200,155 @@ void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjst
 	out->z = R_LerpFixed(mobj->old_z, mobj->z, frac);
 	out->angle = mobj->angle;
 }
+
+static void AddInterpolator(levelinterpolator_t* interpolator)
+{
+	if (levelinterpolators_len >= levelinterpolators_size)
+	{
+		if (levelinterpolators_size == 0)
+		{
+			levelinterpolators_size = 128;
+		}
+		else
+		{
+			levelinterpolators_size *= 2;
+		}
+		
+		levelinterpolators = Z_ReallocAlign(
+			(void*) levelinterpolators,
+			sizeof(levelinterpolator_t*) * levelinterpolators_size,
+			PU_LEVEL,
+			NULL,
+			sizeof(levelinterpolator_t*) * 8
+		);
+	}
+
+	levelinterpolators[levelinterpolators_len] = interpolator;
+	levelinterpolators_len += 1;
+}
+
+static levelinterpolator_t *CreateInterpolator(levelinterpolator_type_e type, thinker_t *thinker)
+{
+	levelinterpolator_t *ret = (levelinterpolator_t*) Z_CallocAlign(
+		sizeof(levelinterpolator_t),
+		PU_LEVEL,
+		NULL,
+		sizeof(levelinterpolator_t) * 8
+	);
+
+	ret->type = type;
+	ret->thinker = thinker;
+
+	AddInterpolator(ret);
+
+	return ret;
+}
+
+void R_CreateInterpolator_SectorPlane(thinker_t *thinker, sector_t *sector, boolean ceiling)
+{
+	levelinterpolator_t *interp = CreateInterpolator(LVLINTERP_SectorPlane, thinker);
+	interp->sectorplane.sector = sector;
+	interp->sectorplane.ceiling = ceiling;
+	if (ceiling)
+	{
+		interp->sectorplane.oldheight = interp->sectorplane.bakheight = sector->ceilingheight;
+	}
+	else
+	{
+		interp->sectorplane.oldheight = interp->sectorplane.bakheight = sector->floorheight;
+	}
+}
+
+void R_InitializeLevelInterpolators(void)
+{
+	levelinterpolators_len = 0;
+	levelinterpolators_size = 0;
+	levelinterpolators = NULL;
+}
+
+static void UpdateLevelInterpolatorState(levelinterpolator_t *interp)
+{
+	switch (interp->type)
+	{
+	case LVLINTERP_SectorPlane:
+		interp->sectorplane.oldheight = interp->sectorplane.bakheight;
+		interp->sectorplane.bakheight = interp->sectorplane.ceiling ? interp->sectorplane.sector->ceilingheight : interp->sectorplane.sector->floorheight;
+		break;
+	}
+}
+
+void R_UpdateLevelInterpolators(void)
+{
+	size_t i;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+		
+		UpdateLevelInterpolatorState(interp);
+	}
+}
+
+void R_ClearLevelInterpolatorState(thinker_t *thinker)
+{
+	
+	size_t i;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+		
+		if (interp->thinker == thinker)
+		{
+			// Do it twice to make the old state match the new
+			UpdateLevelInterpolatorState(interp);
+			UpdateLevelInterpolatorState(interp);
+		}
+	}
+}
+
+void R_ApplyLevelInterpolators(fixed_t frac)
+{
+	size_t i;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+
+		switch (interp->type)
+		{
+		case LVLINTERP_SectorPlane:
+			if (interp->sectorplane.ceiling)
+			{
+				interp->sectorplane.sector->ceilingheight = R_LerpFixed(interp->sectorplane.oldheight, interp->sectorplane.bakheight, frac);
+			}
+			else
+			{
+				interp->sectorplane.sector->floorheight = R_LerpFixed(interp->sectorplane.oldheight, interp->sectorplane.bakheight, frac);
+			}
+		}
+	}
+}
+
+void R_RestoreLevelInterpolators(void)
+{
+	size_t i;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+		
+		switch (interp->type)
+		{
+		case LVLINTERP_SectorPlane:
+			if (interp->sectorplane.ceiling)
+			{
+				interp->sectorplane.sector->ceilingheight = interp->sectorplane.bakheight;
+			}
+			else
+			{
+				interp->sectorplane.sector->floorheight = interp->sectorplane.bakheight;
+			}
+		}
+	}
+}
diff --git a/src/r_fps.h b/src/r_fps.h
index 04f44de6e274369757789b871c196022fe815234..df506fb568b5058e002050789bc4deedf81be263 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -51,6 +51,27 @@ typedef struct {
 	angle_t angle;
 } interpmobjstate_t;
 
+// Level interpolators
+
+// The union tag for levelinterpolator_t
+typedef enum {
+	LVLINTERP_SectorPlane,
+} levelinterpolator_type_e;
+
+// Tagged union of a level interpolator
+typedef struct levelinterpolator_s {
+	levelinterpolator_type_e type;
+	thinker_t *thinker;
+	union {
+		struct {
+			sector_t *sector;
+			fixed_t oldheight;
+			fixed_t bakheight;
+			boolean ceiling;
+		} sectorplane;
+	};
+} levelinterpolator_t;
+
 // Interpolates the current view variables (r_state.h) against the selected view context in R_SetViewContext
 void R_InterpolateView(fixed_t frac);
 // Buffer the current new views into the old views. Call once after each real tic.
@@ -64,4 +85,17 @@ void R_InterpolateMobjState(mobj_t *mobj, fixed_t frac, interpmobjstate_t *out);
 // Evaluate the interpolated mobj state for the given precipmobj
 void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjstate_t *out);
 
+void R_CreateInterpolator_SectorPlane(thinker_t *thinker, sector_t *sector, boolean ceiling);
+
+// Initialize level interpolators after a level change
+void R_InitializeLevelInterpolators(void);
+// Update level interpolators, storing the previous and current states.
+void R_UpdateLevelInterpolators(void);
+// Clear states for all level interpolators for the thinker
+void R_ClearLevelInterpolatorState(thinker_t *thinker);
+// Apply level interpolators to the actual game state
+void R_ApplyLevelInterpolators(fixed_t frac);
+// Restore level interpolators to the real game state
+void R_RestoreLevelInterpolators(void);
+
 #endif