Browse Source

Merge pull request #80939 from RandomShaper/fix_onion

Revive onion skinning
Rémi Verschelde 1 year ago
parent
commit
5680d729f2

+ 135 - 110
editor/plugins/animation_player_editor_plugin.cpp

@@ -112,6 +112,7 @@ void AnimationPlayerEditor::_notification(int p_what) {
 			}
 
 			last_active = player->is_playing();
+
 			updating = false;
 		} break;
 
@@ -942,11 +943,6 @@ void AnimationPlayerEditor::_update_player() {
 	onion_toggle->set_disabled(no_anims_found);
 	onion_skinning->set_disabled(no_anims_found);
 
-	if (hack_disable_onion_skinning) {
-		onion_toggle->set_disabled(true);
-		onion_skinning->set_disabled(true);
-	}
-
 	_update_animation_list_icons();
 
 	updating = false;
@@ -1150,33 +1146,33 @@ void AnimationPlayerEditor::forward_force_draw_over_viewport(Control *p_overlay)
 
 	float alpha_step = 1.0 / (onion.steps + 1);
 
-	int cidx = 0;
+	uint32_t capture_idx = 0;
 	if (onion.past) {
-		float alpha = 0;
+		float alpha = 0.0f;
 		do {
 			alpha += alpha_step;
 
-			if (onion.captures_valid[cidx]) {
+			if (onion.captures_valid[capture_idx]) {
 				RS::get_singleton()->canvas_item_add_texture_rect_region(
-						ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[cidx]), src_rect, Color(1, 1, 1, alpha));
+						ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha));
 			}
 
-			cidx++;
-		} while (cidx < onion.steps);
+			capture_idx++;
+		} while (capture_idx < onion.steps);
 	}
 	if (onion.future) {
-		float alpha = 1;
-		int base_cidx = cidx;
+		float alpha = 1.0f;
+		uint32_t base_cidx = capture_idx;
 		do {
 			alpha -= alpha_step;
 
-			if (onion.captures_valid[cidx]) {
+			if (onion.captures_valid[capture_idx]) {
 				RS::get_singleton()->canvas_item_add_texture_rect_region(
-						ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[cidx]), src_rect, Color(1, 1, 1, alpha));
+						ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha));
 			}
 
-			cidx++;
-		} while (cidx < base_cidx + onion.steps); // In case there's the present capture at the end, skip it.
+			capture_idx++;
+		} while (capture_idx < base_cidx + onion.steps); // In case there's the present capture at the end, skip it.
 	}
 }
 
@@ -1266,7 +1262,7 @@ void AnimationPlayerEditor::_seek_value_changed(float p_value, bool p_set, bool
 
 	if (!p_timeline_only) {
 		if (player->is_valid() && !p_set) {
-			double delta = pos - player->get_current_animation_position();
+			double delta = player->get_current_animation_position();
 			player->seek(pos, true, true);
 			player->seek(pos + delta, true, true);
 		} else {
@@ -1394,7 +1390,10 @@ void AnimationPlayerEditor::_onion_skinning_menu(int p_option) {
 			onion.enabled = !onion.enabled;
 
 			if (onion.enabled) {
-				_start_onion_skinning();
+				if (get_player() && !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) {
+					EditorNode::get_singleton()->show_warning(TTR("Onion skinning requires a RESET animation."));
+				}
+				_start_onion_skinning(); // It will check for RESET animation anyway.
 			} else {
 				_stop_onion_skinning();
 			}
@@ -1416,7 +1415,7 @@ void AnimationPlayerEditor::_onion_skinning_menu(int p_option) {
 			onion.steps = (p_option - ONION_SKINNING_1_STEP) + 1;
 			int one_frame_idx = menu->get_item_index(ONION_SKINNING_1_STEP);
 			for (int i = 0; i <= ONION_SKINNING_LAST_STEPS_OPTION - ONION_SKINNING_1_STEP; i++) {
-				menu->set_item_checked(one_frame_idx + i, onion.steps == i + 1);
+				menu->set_item_checked(one_frame_idx + i, (int)onion.steps == i + 1);
 			}
 		} break;
 		case ONION_SKINNING_DIFFERENCES_ONLY: {
@@ -1475,15 +1474,15 @@ void AnimationPlayerEditor::_editor_visibility_changed() {
 bool AnimationPlayerEditor::_are_onion_layers_valid() {
 	ERR_FAIL_COND_V(!onion.past && !onion.future, false);
 
-	Point2 capture_size = get_tree()->get_root()->get_size();
-	return onion.captures.size() == onion.get_needed_capture_count() && onion.capture_size == capture_size;
+	Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID);
+	return onion.captures.size() == onion.get_capture_count() && onion.capture_size == capture_size;
 }
 
 void AnimationPlayerEditor::_allocate_onion_layers() {
 	_free_onion_layers();
 
-	int captures = onion.get_needed_capture_count();
-	Point2 capture_size = get_tree()->get_root()->get_size();
+	int captures = onion.get_capture_count();
+	Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID);
 
 	onion.captures.resize(captures);
 	onion.captures_valid.resize(captures);
@@ -1492,7 +1491,7 @@ void AnimationPlayerEditor::_allocate_onion_layers() {
 		bool is_present = onion.differences_only && i == captures - 1;
 
 		// Each capture is a viewport with a canvas item attached that renders a full-size rect with the contents of the main viewport.
-		onion.captures.write[i] = RS::get_singleton()->viewport_create();
+		onion.captures[i] = RS::get_singleton()->viewport_create();
 
 		RS::get_singleton()->viewport_set_size(onion.captures[i], capture_size.width, capture_size.height);
 		RS::get_singleton()->viewport_set_update_mode(onion.captures[i], RS::VIEWPORT_UPDATE_ALWAYS);
@@ -1502,13 +1501,13 @@ void AnimationPlayerEditor::_allocate_onion_layers() {
 
 	// Reset the capture canvas item to the current root viewport texture (defensive).
 	RS::get_singleton()->canvas_item_clear(onion.capture.canvas_item);
-	RS::get_singleton()->canvas_item_add_texture_rect(onion.capture.canvas_item, Rect2(Point2(), capture_size), get_tree()->get_root()->get_texture()->get_rid());
+	RS::get_singleton()->canvas_item_add_texture_rect(onion.capture.canvas_item, Rect2(Point2(), Point2(capture_size.x, -capture_size.y)), get_tree()->get_root()->get_texture()->get_rid());
 
 	onion.capture_size = capture_size;
 }
 
 void AnimationPlayerEditor::_free_onion_layers() {
-	for (int i = 0; i < onion.captures.size(); i++) {
+	for (uint32_t i = 0; i < onion.captures.size(); i++) {
 		if (onion.captures[i].is_valid()) {
 			RS::get_singleton()->free(onion.captures[i]);
 		}
@@ -1524,7 +1523,7 @@ void AnimationPlayerEditor::_prepare_onion_layers_1() {
 		return;
 	}
 
-	if (!onion.enabled || !is_processing() || !is_visible() || !get_player()) {
+	if (!onion.enabled || !is_visible() || !get_player() || !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) {
 		_stop_onion_skinning();
 		return;
 	}
@@ -1540,14 +1539,10 @@ void AnimationPlayerEditor::_prepare_onion_layers_1() {
 	}
 
 	// And go to next step afterwards.
-	call_deferred(SNAME("_prepare_onion_layers_2"));
+	callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_prolog).call_deferred();
 }
 
-void AnimationPlayerEditor::_prepare_onion_layers_1_deferred() {
-	call_deferred(SNAME("_prepare_onion_layers_1"));
-}
-
-void AnimationPlayerEditor::_prepare_onion_layers_2() {
+void AnimationPlayerEditor::_prepare_onion_layers_2_prolog() {
 	Ref<Animation> anim = player->get_animation(player->get_assigned_animation());
 	if (!anim.is_valid()) {
 		return;
@@ -1558,21 +1553,20 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() {
 	}
 
 	// Hide superfluous elements that would make the overlay unnecessary cluttered.
-	Dictionary canvas_edit_state;
-	Dictionary spatial_edit_state;
 	if (Node3DEditor::get_singleton()->is_visible()) {
 		// 3D
-		spatial_edit_state = Node3DEditor::get_singleton()->get_state();
-		Dictionary new_state = spatial_edit_state.duplicate();
+		onion.temp.spatial_edit_state = Node3DEditor::get_singleton()->get_state();
+		Dictionary new_state = onion.temp.spatial_edit_state.duplicate();
 		new_state["show_grid"] = false;
 		new_state["show_origin"] = false;
-		Array orig_vp = spatial_edit_state["viewports"];
+		Array orig_vp = onion.temp.spatial_edit_state["viewports"];
 		Array vp;
 		vp.resize(4);
 		for (int i = 0; i < vp.size(); i++) {
 			Dictionary d = ((Dictionary)orig_vp[i]).duplicate();
 			d["use_environment"] = false;
 			d["doppler"] = false;
+			d["listener"] = false;
 			d["gizmos"] = onion.include_gizmos ? d["gizmos"] : Variant(false);
 			d["information"] = false;
 			vp[i] = d;
@@ -1580,23 +1574,27 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() {
 		new_state["viewports"] = vp;
 		// TODO: Save/restore only affected entries.
 		Node3DEditor::get_singleton()->set_state(new_state);
-	} else { // CanvasItemEditor
-		// 2D
-		canvas_edit_state = CanvasItemEditor::get_singleton()->get_state();
-		Dictionary new_state = canvas_edit_state.duplicate();
+	} else {
+		// CanvasItemEditor.
+		onion.temp.canvas_edit_state = CanvasItemEditor::get_singleton()->get_state();
+		Dictionary new_state = onion.temp.canvas_edit_state.duplicate();
+		new_state["show_origin"] = false;
 		new_state["show_grid"] = false;
 		new_state["show_rulers"] = false;
 		new_state["show_guides"] = false;
 		new_state["show_helpers"] = false;
 		new_state["show_zoom_control"] = false;
+		new_state["show_edit_locks"] = false;
+		new_state["grid_visibility"] = 2; // TODO: Expose CanvasItemEditor::GRID_VISIBILITY_HIDE somehow and use it.
+		new_state["show_transformation_gizmos"] = onion.include_gizmos ? new_state["gizmos"] : Variant(false);
 		// TODO: Save/restore only affected entries.
 		CanvasItemEditor::get_singleton()->set_state(new_state);
 	}
 
 	// Tweak the root viewport to ensure it's rendered before our target.
 	RID root_vp = get_tree()->get_root()->get_viewport_rid();
-	Rect2 root_vp_screen_rect = Rect2(Vector2(), get_tree()->get_root()->get_size());
-	RS::get_singleton()->viewport_attach_to_screen(root_vp, Rect2());
+	onion.temp.screen_rect = Rect2(Vector2(), DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID));
+	RS::get_singleton()->viewport_attach_to_screen(root_vp, Rect2(), DisplayServer::INVALID_WINDOW_ID);
 	RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_ALWAYS);
 
 	RID present_rid;
@@ -1611,8 +1609,8 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() {
 	}
 
 	// Backup current animation state.
-	Ref<AnimatedValuesBackup> backup_current = player->make_backup();
-	float cpos = player->get_current_animation_position();
+	onion.temp.anim_values_backup = player->make_backup();
+	onion.temp.anim_player_position = player->get_current_animation_position();
 
 	// Render every past/future step with the capture shader.
 
@@ -1620,55 +1618,94 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() {
 	onion.capture.material->set_shader_parameter("bkg_color", GLOBAL_GET("rendering/environment/defaults/default_clear_color"));
 	onion.capture.material->set_shader_parameter("differences_only", onion.differences_only);
 	onion.capture.material->set_shader_parameter("present", onion.differences_only ? RS::get_singleton()->viewport_get_texture(present_rid) : RID());
-
-	int step_off_a = onion.past ? -onion.steps : 0;
-	int step_off_b = onion.future ? onion.steps : 0;
-	int cidx = 0;
 	onion.capture.material->set_shader_parameter("dir_color", onion.force_white_modulate ? Color(1, 1, 1) : Color(EDITOR_GET("editors/animation/onion_layers_past_color")));
-	for (int step_off = step_off_a; step_off <= step_off_b; step_off++) {
-		if (step_off == 0) {
-			// Skip present step and switch to the color of future.
-			if (!onion.force_white_modulate) {
-				onion.capture.material->set_shader_parameter("dir_color", EDITOR_GET("editors/animation/onion_layers_future_color"));
-			}
-			continue;
-		}
 
-		float pos = cpos + step_off * anim->get_step();
+	uint32_t p_capture_idx = 0;
+	int first_step_offset = onion.past ? -(int)onion.steps : 0;
+	_prepare_onion_layers_2_step_prepare(first_step_offset, p_capture_idx);
+}
+
+void AnimationPlayerEditor::_prepare_onion_layers_2_step_prepare(int p_step_offset, uint32_t p_capture_idx) {
+	uint32_t next_capture_idx = p_capture_idx;
+	if (p_step_offset == 0) {
+		// Skip present step and switch to the color of future.
+		if (!onion.force_white_modulate) {
+			onion.capture.material->set_shader_parameter("dir_color", EDITOR_GET("editors/animation/onion_layers_future_color"));
+		}
+	} else {
+		Ref<Animation> anim = player->get_animation(player->get_assigned_animation());
+		double pos = onion.temp.anim_player_position + p_step_offset * anim->get_step();
 
 		bool valid = anim->get_loop_mode() != Animation::LOOP_NONE || (pos >= 0 && pos <= anim->get_length());
-		onion.captures_valid.write[cidx] = valid;
+		onion.captures_valid[p_capture_idx] = valid;
 		if (valid) {
 			player->seek(pos, true);
-			get_tree()->flush_transform_notifications(); // Needed for transforms of Node3Ds.
-
-			RS::get_singleton()->viewport_set_active(onion.captures[cidx], true);
-			RS::get_singleton()->viewport_set_parent_viewport(root_vp, onion.captures[cidx]);
-			RS::get_singleton()->draw(false);
-			RS::get_singleton()->viewport_set_active(onion.captures[cidx], false);
+			OS::get_singleton()->get_main_loop()->process(0);
+			// This is the key: process the frame and let all callbacks/updates/notifications happen
+			// so everything (transforms, skeletons, etc.) is up-to-date visually.
+			callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_step_capture).bind(p_step_offset, p_capture_idx).call_deferred();
+			return;
+		} else {
+			next_capture_idx++;
 		}
+	}
+
+	int last_step_offset = onion.future ? onion.steps : 0;
+	if (p_step_offset < last_step_offset) {
+		_prepare_onion_layers_2_step_prepare(p_step_offset + 1, next_capture_idx);
+	} else {
+		_prepare_onion_layers_2_epilog();
+	}
+}
+
+void AnimationPlayerEditor::_prepare_onion_layers_2_step_capture(int p_step_offset, uint32_t p_capture_idx) {
+	DEV_ASSERT(p_step_offset != 0);
+	DEV_ASSERT(onion.captures_valid[p_capture_idx]);
 
-		cidx++;
+	RID root_vp = get_tree()->get_root()->get_viewport_rid();
+	RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], true);
+	RS::get_singleton()->viewport_set_parent_viewport(root_vp, onion.captures[p_capture_idx]);
+	RS::get_singleton()->draw(false);
+	RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], false);
+
+	int last_step_offset = onion.future ? onion.steps : 0;
+	if (p_step_offset < last_step_offset) {
+		_prepare_onion_layers_2_step_prepare(p_step_offset + 1, p_capture_idx + 1);
+	} else {
+		_prepare_onion_layers_2_epilog();
 	}
+}
 
+void AnimationPlayerEditor::_prepare_onion_layers_2_epilog() {
 	// Restore root viewport.
+	RID root_vp = get_tree()->get_root()->get_viewport_rid();
 	RS::get_singleton()->viewport_set_parent_viewport(root_vp, RID());
-	RS::get_singleton()->viewport_attach_to_screen(root_vp, root_vp_screen_rect);
+	RS::get_singleton()->viewport_attach_to_screen(root_vp, onion.temp.screen_rect, DisplayServer::MAIN_WINDOW_ID);
 	RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_WHEN_VISIBLE);
 
-	// Restore animation state
-	// (Seeking with update=true wouldn't do the trick because the current value of the properties
-	// may not match their value for the current point in the animation).
-	player->seek(cpos, false);
-	player->restore(backup_current);
+	// Restore animation state.
+	// Here we're combine the power of seeking back to the original position and
+	// restoring the values backup. In most cases they will bring the same value back,
+	// but there are cases handled by one that the other can't.
+	// Namely:
+	// - Seeking won't restore any values that may have been modified by the user
+	//   in the node after the last time the AnimationPlayer updated it.
+	// - Restoring the backup won't account for values that are not directly involved
+	//   in the animation but a consequence of them (e.g., SkeletonModification2DLookAt).
+	// FIXME: Since backup of values is based on the reset animation, only values
+	//        backed by a proper reset animation will work correctly with onion
+	//        skinning and the possibility to restore the values mentioned in the
+	//        first point above is gone. Still good enough.
+	player->seek(onion.temp.anim_player_position, true, true);
+	player->restore(onion.temp.anim_values_backup);
 
 	// Restore state of main editors.
 	if (Node3DEditor::get_singleton()->is_visible()) {
 		// 3D
-		Node3DEditor::get_singleton()->set_state(spatial_edit_state);
+		Node3DEditor::get_singleton()->set_state(onion.temp.spatial_edit_state);
 	} else { // CanvasItemEditor
 		// 2D
-		CanvasItemEditor::get_singleton()->set_state(canvas_edit_state);
+		CanvasItemEditor::get_singleton()->set_state(onion.temp.canvas_edit_state);
 	}
 
 	// Update viewports with skin layers overlaid for the actual engine loop render.
@@ -1677,21 +1714,26 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() {
 }
 
 void AnimationPlayerEditor::_start_onion_skinning() {
-	// FIXME: Using "process_frame" makes onion layers update one frame behind the current.
-	if (!get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred))) {
-		get_tree()->connect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred));
+	if (get_player() && !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) {
+		onion.enabled = false;
+		onion_toggle->set_pressed_no_signal(false);
+		return;
+	}
+	if (!get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) {
+		get_tree()->connect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1));
 	}
 }
 
 void AnimationPlayerEditor::_stop_onion_skinning() {
-	if (get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred))) {
-		get_tree()->disconnect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred));
+	if (get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) {
+		get_tree()->disconnect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1));
 
 		_free_onion_layers();
 
-		// Clean up the overlay.
+		// Clean up.
 		onion.can_overlay = false;
 		plugin->update_overlays();
+		onion.temp = {};
 	}
 }
 
@@ -1773,8 +1815,6 @@ void AnimationPlayerEditor::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_list_changed"), &AnimationPlayerEditor::_list_changed);
 	ClassDB::bind_method(D_METHOD("_animation_duplicate"), &AnimationPlayerEditor::_animation_duplicate);
 
-	ClassDB::bind_method(D_METHOD("_prepare_onion_layers_1"), &AnimationPlayerEditor::_prepare_onion_layers_1);
-	ClassDB::bind_method(D_METHOD("_prepare_onion_layers_2"), &AnimationPlayerEditor::_prepare_onion_layers_2);
 	ClassDB::bind_method(D_METHOD("_start_onion_skinning"), &AnimationPlayerEditor::_start_onion_skinning);
 	ClassDB::bind_method(D_METHOD("_stop_onion_skinning"), &AnimationPlayerEditor::_stop_onion_skinning);
 
@@ -1914,16 +1954,6 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug
 	onion_skinning->get_popup()->add_check_item(TTR("Include Gizmos (3D)"), ONION_SKINNING_INCLUDE_GIZMOS);
 	hb->add_child(onion_skinning);
 
-	// FIXME: Onion skinning disabled for now as it's broken and triggers fast
-	// flickering red/blue modulation (GH-53870).
-	if (hack_disable_onion_skinning) {
-		onion_toggle->set_disabled(true);
-		onion_toggle->set_tooltip_text(TTR("Onion Skinning temporarily disabled due to rendering bug."));
-
-		onion_skinning->set_disabled(true);
-		onion_skinning->set_tooltip_text(TTR("Onion Skinning temporarily disabled due to rendering bug."));
-	}
-
 	hb->add_child(memnew(VSeparator));
 
 	pin = memnew(Button);
@@ -2013,24 +2043,13 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug
 
 	track_editor->connect(SNAME("visibility_changed"), callable_mp(this, &AnimationPlayerEditor::_editor_visibility_changed));
 
-	onion.enabled = false;
-	onion.past = true;
-	onion.future = false;
-	onion.steps = 1;
-	onion.differences_only = false;
-	onion.force_white_modulate = false;
-	onion.include_gizmos = false;
-
-	onion.last_frame = 0;
-	onion.can_overlay = false;
-	onion.capture_size = Size2();
 	onion.capture.canvas = RS::get_singleton()->canvas_create();
 	onion.capture.canvas_item = RS::get_singleton()->canvas_item_create();
 	RS::get_singleton()->canvas_item_set_parent(onion.capture.canvas_item, onion.capture.canvas);
 
-	onion.capture.material = Ref<ShaderMaterial>(memnew(ShaderMaterial));
+	onion.capture.material.instantiate();
 
-	onion.capture.shader = Ref<Shader>(memnew(Shader));
+	onion.capture.shader.instantiate();
 	onion.capture.shader->set_code(R"(
 // Animation editor onion skinning shader.
 
@@ -2047,10 +2066,15 @@ float zero_if_equal(vec4 a, vec4 b) {
 
 void fragment() {
 	vec4 capture_samp = texture(TEXTURE, UV);
-	vec4 present_samp = texture(present, UV);
 	float bkg_mask = zero_if_equal(capture_samp, bkg_color);
-	float diff_mask = 1.0 - zero_if_equal(present_samp, bkg_color);
-	diff_mask = min(1.0, diff_mask + float(!differences_only));
+	float diff_mask = 1.0;
+	if (differences_only) {
+		// FIXME: If Y-flips across render target, canvas item, etc. was handled correctly,
+		//        this would not be as convoluted in the shader.
+		vec4 capture_samp2 = texture(TEXTURE, vec2(UV.x, 1.0 - UV.y));
+		vec4 present_samp = texture(present, vec2(UV.x, 1.0 - UV.y));
+		diff_mask = 1.0 - zero_if_equal(present_samp, bkg_color);
+	}
 	COLOR = vec4(capture_samp.rgb * dir_color.rgb, bkg_mask * diff_mask);
 }
 )");
@@ -2061,6 +2085,7 @@ AnimationPlayerEditor::~AnimationPlayerEditor() {
 	_free_onion_layers();
 	RS::get_singleton()->free(onion.capture.canvas);
 	RS::get_singleton()->free(onion.capture.canvas_item);
+	onion.capture = {};
 }
 
 void AnimationPlayerEditorPlugin::_notification(int p_what) {

+ 18 - 9
editor/plugins/animation_player_editor_plugin.h

@@ -136,20 +136,18 @@ class AnimationPlayerEditor : public VBoxContainer {
 	AnimationTrackEditor *track_editor = nullptr;
 	static AnimationPlayerEditor *singleton;
 
-	bool hack_disable_onion_skinning = true; // Temporary hack for GH-53870.
-
 	// Onion skinning.
 	struct {
 		// Settings.
 		bool enabled = false;
-		bool past = false;
+		bool past = true;
 		bool future = false;
-		int steps = 0;
+		uint32_t steps = 1;
 		bool differences_only = false;
 		bool force_white_modulate = false;
 		bool include_gizmos = false;
 
-		int get_needed_capture_count() const {
+		uint32_t get_capture_count() const {
 			// 'Differences only' needs a capture of the present.
 			return (past && future ? 2 * steps : steps) + (differences_only ? 1 : 0);
 		}
@@ -158,14 +156,23 @@ class AnimationPlayerEditor : public VBoxContainer {
 		int64_t last_frame = 0;
 		int can_overlay = 0;
 		Size2 capture_size;
-		Vector<RID> captures;
-		Vector<bool> captures_valid;
+		LocalVector<RID> captures;
+		LocalVector<bool> captures_valid;
 		struct {
 			RID canvas;
 			RID canvas_item;
 			Ref<ShaderMaterial> material;
 			Ref<Shader> shader;
 		} capture;
+
+		// Cross-call state.
+		struct {
+			double anim_player_position = 0.0;
+			Ref<AnimatedValuesBackup> anim_values_backup;
+			Rect2 screen_rect;
+			Dictionary canvas_edit_state;
+			Dictionary spatial_edit_state;
+		} temp;
 	} onion;
 
 	void _select_anim_by_name(const String &p_anim);
@@ -215,8 +222,10 @@ class AnimationPlayerEditor : public VBoxContainer {
 	void _allocate_onion_layers();
 	void _free_onion_layers();
 	void _prepare_onion_layers_1();
-	void _prepare_onion_layers_1_deferred();
-	void _prepare_onion_layers_2();
+	void _prepare_onion_layers_2_prolog();
+	void _prepare_onion_layers_2_step_prepare(int p_step_offset, uint32_t p_capture_idx);
+	void _prepare_onion_layers_2_step_capture(int p_step_offset, uint32_t p_capture_idx);
+	void _prepare_onion_layers_2_epilog();
 	void _start_onion_skinning();
 	void _stop_onion_skinning();
 

+ 3 - 2
servers/rendering/renderer_rd/renderer_compositor_rd.cpp

@@ -103,8 +103,9 @@ void RendererCompositorRD::begin_frame(double frame_step) {
 }
 
 void RendererCompositorRD::end_frame(bool p_swap_buffers) {
-	// TODO: Likely pass a bool to swap buffers to avoid display?
-	RD::get_singleton()->swap_buffers();
+	if (p_swap_buffers) {
+		RD::get_singleton()->swap_buffers();
+	}
 }
 
 void RendererCompositorRD::initialize() {

+ 8 - 5
servers/rendering/renderer_viewport.cpp

@@ -616,7 +616,7 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) {
 	}
 }
 
-void RendererViewport::draw_viewports() {
+void RendererViewport::draw_viewports(bool p_swap_buffers) {
 	timestamp_vp_map.clear();
 
 	// get our xr interface in case we need it
@@ -799,11 +799,14 @@ void RendererViewport::draw_viewports() {
 	total_draw_calls_used = draw_calls_used;
 
 	RENDER_TIMESTAMP("< Render Viewports");
-	//this needs to be called to make screen swapping more efficient
-	RSG::rasterizer->prepare_for_blitting_render_targets();
 
-	for (const KeyValue<int, Vector<BlitToScreen>> &E : blit_to_screen_list) {
-		RSG::rasterizer->blit_render_targets_to_screen(E.key, E.value.ptr(), E.value.size());
+	if (p_swap_buffers) {
+		//this needs to be called to make screen swapping more efficient
+		RSG::rasterizer->prepare_for_blitting_render_targets();
+
+		for (const KeyValue<int, Vector<BlitToScreen>> &E : blit_to_screen_list) {
+			RSG::rasterizer->blit_render_targets_to_screen(E.key, E.value.ptr(), E.value.size());
+		}
 	}
 }
 

+ 1 - 1
servers/rendering/renderer_viewport.h

@@ -299,7 +299,7 @@ public:
 	void handle_timestamp(String p_timestamp, uint64_t p_cpu_time, uint64_t p_gpu_time);
 
 	void set_default_clear_color(const Color &p_color);
-	void draw_viewports();
+	void draw_viewports(bool p_swap_buffers);
 
 	bool free(RID p_rid);
 

+ 1 - 1
servers/rendering/rendering_server_default.cpp

@@ -88,7 +88,7 @@ void RenderingServerDefault::_draw(bool p_swap_buffers, double frame_step) {
 
 	RSG::scene->render_probes();
 
-	RSG::viewport->draw_viewports();
+	RSG::viewport->draw_viewports(p_swap_buffers);
 	RSG::canvas_render->update();
 
 	if (OS::get_singleton()->get_current_rendering_driver_name() != "opengl3" && OS::get_singleton()->get_current_rendering_driver_name() != "opengl3_angle") {