diff --git a/3d/physics_interpolation/.editorconfig b/3d/physics_interpolation/.editorconfig
new file mode 100644
index 00000000..f28239ba
--- /dev/null
+++ b/3d/physics_interpolation/.editorconfig
@@ -0,0 +1,4 @@
+root = true
+
+[*]
+charset = utf-8
diff --git a/3d/physics_interpolation/README.md b/3d/physics_interpolation/README.md
new file mode 100644
index 00000000..7cc78adc
--- /dev/null
+++ b/3d/physics_interpolation/README.md
@@ -0,0 +1,25 @@
+# Physics Interpolation
+
+This demo showcases [physics interpolation](https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/index.html)
+in 3D with varying camera modes (first person, third person, fixed perspective).
+This is also known as *fixed timestep interpolation*.
+
+Physics interpolation makes motion appear smooth regardless of the rendered
+framerate and physics ticks per second configured in the Project Settings.
+There are some caveats related to its usage though, such as increased latency
+and potential issues with teleporting objects. It's recommended to go
+through the
+[documentation](https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/physics_interpolation_introduction.html)
+when enabling physics interpolation in a project.
+
+Physics interpolation is enabled by default in this project. Press T
+to toggle it while the demo is running. This allows you to see the impact on
+smoothness of physics interpolation.
+
+Language: GDScript
+
+Renderer: Compatibility
+
+## Screenshots
+
+
diff --git a/3d/physics_interpolation/box.tscn b/3d/physics_interpolation/box.tscn
new file mode 100644
index 00000000..1e6c6a4b
--- /dev/null
+++ b/3d/physics_interpolation/box.tscn
@@ -0,0 +1,18 @@
+[gd_scene load_steps=4 format=3 uid="uid://cvgxw8nq67xxm"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_pq8q7"]
+
+[sub_resource type="BoxMesh" id="BoxMesh_pyidc"]
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_oq5cr"]
+albedo_color = Color(0.5544661, 0.39379695, 0.15444939, 1)
+
+[node name="Box" type="RigidBody3D"]
+mass = 10.0
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+shape = SubResource("BoxShape3D_pq8q7")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+mesh = SubResource("BoxMesh_pyidc")
+surface_material_override/0 = SubResource("StandardMaterial3D_oq5cr")
diff --git a/3d/physics_interpolation/bullet.gd b/3d/physics_interpolation/bullet.gd
new file mode 100644
index 00000000..d6f718fd
--- /dev/null
+++ b/3d/physics_interpolation/bullet.gd
@@ -0,0 +1,27 @@
+extends RigidBody3D
+
+@export var scale_curve: Curve
+
+var _enabled: bool = false
+
+
+func _ready() -> void:
+ $CollisionShape3D.disabled = true
+
+
+func _physics_process(_delta: float) -> void:
+ # Start with physics collision disabled until the first tick.
+ # This prevents bullets from colliding with the player when first shot, while still
+ # allowing them to be moved by the player when they're laying on the ground.
+ if !_enabled:
+ $CollisionShape3D.disabled = false
+ _enabled = true
+
+ # Apply the appearance scaling according to the scale curve.
+ var time_left_ratio: float = 1.0 - $Timer.time_left / $Timer.wait_time
+ var scale_sampled := scale_curve.sample_baked(time_left_ratio)
+ $Scaler.scale = Vector3.ONE * scale_sampled
+
+
+func _on_timer_timeout() -> void:
+ queue_free()
diff --git a/3d/physics_interpolation/bullet.gd.uid b/3d/physics_interpolation/bullet.gd.uid
new file mode 100644
index 00000000..b222102c
--- /dev/null
+++ b/3d/physics_interpolation/bullet.gd.uid
@@ -0,0 +1 @@
+uid://dohdjr8ilotkr
diff --git a/3d/physics_interpolation/bullet.tscn b/3d/physics_interpolation/bullet.tscn
new file mode 100644
index 00000000..3bd4c014
--- /dev/null
+++ b/3d/physics_interpolation/bullet.tscn
@@ -0,0 +1,86 @@
+[gd_scene load_steps=15 format=3 uid="uid://2r5wc1n0ybju"]
+
+[ext_resource type="Script" uid="uid://dohdjr8ilotkr" path="res://bullet.gd" id="1_v7oki"]
+[ext_resource type="Texture2D" uid="uid://b70s43gn8c3kx" path="res://spark_particle.png" id="2_v8qja"]
+
+[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_v8qja"]
+bounce = 0.61
+
+[sub_resource type="Curve" id="Curve_t4vbm"]
+_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.02, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
+point_count = 3
+
+[sub_resource type="SphereShape3D" id="SphereShape3D_rtl8c"]
+radius = 0.2
+
+[sub_resource type="Gradient" id="Gradient_v8qja"]
+colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0)
+
+[sub_resource type="GradientTexture1D" id="GradientTexture1D_t4vbm"]
+gradient = SubResource("Gradient_v8qja")
+
+[sub_resource type="Curve" id="Curve_v8qja"]
+_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
+point_count = 2
+
+[sub_resource type="CurveTexture" id="CurveTexture_t4vbm"]
+curve = SubResource("Curve_v8qja")
+
+[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_v7oki"]
+direction = Vector3(0, 1, 0)
+initial_velocity_max = 1.0
+gravity = Vector3(0, 1, 0)
+scale_min = 0.2
+scale_curve = SubResource("CurveTexture_t4vbm")
+color_ramp = SubResource("GradientTexture1D_t4vbm")
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_t4vbm"]
+transparency = 1
+blend_mode = 1
+shading_mode = 0
+vertex_color_use_as_albedo = true
+albedo_color = Color(2.33575, 3.29442, 3.29442, 1)
+albedo_texture = ExtResource("2_v8qja")
+texture_filter = 5
+
+[sub_resource type="QuadMesh" id="QuadMesh_rtl8c"]
+material = SubResource("StandardMaterial3D_t4vbm")
+size = Vector2(0.2, 0.2)
+
+[sub_resource type="SphereMesh" id="SphereMesh_v7oki"]
+radial_segments = 24
+rings = 12
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_v7oki"]
+albedo_color = Color(0.431649, 0.57319, 0.501392, 1)
+metallic = 1.0
+
+[node name="Bullet" type="RigidBody3D"]
+collision_layer = 2
+physics_material_override = SubResource("PhysicsMaterial_v8qja")
+script = ExtResource("1_v7oki")
+scale_curve = SubResource("Curve_t4vbm")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+shape = SubResource("SphereShape3D_rtl8c")
+
+[node name="GPUParticles3D" type="GPUParticles3D" parent="."]
+amount = 64
+transform_align = 1
+process_material = SubResource("ParticleProcessMaterial_v7oki")
+draw_pass_1 = SubResource("QuadMesh_rtl8c")
+
+[node name="Scaler" type="Node3D" parent="."]
+transform = Transform3D(0.001, 0, 0, 0, 0.001, 0, 0, 0, 0.001, 0, 0, 0)
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="Scaler"]
+transform = Transform3D(0.4, 0, 0, 0, 0.4, 0, 0, 0, 0.4, 0, 0, 0)
+mesh = SubResource("SphereMesh_v7oki")
+skeleton = NodePath("../..")
+surface_material_override/0 = SubResource("StandardMaterial3D_v7oki")
+
+[node name="Timer" type="Timer" parent="."]
+wait_time = 5.0
+autostart = true
+
+[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
diff --git a/3d/physics_interpolation/icon.png.import b/3d/physics_interpolation/icon.png.import
new file mode 100644
index 00000000..51c1edf0
--- /dev/null
+++ b/3d/physics_interpolation/icon.png.import
@@ -0,0 +1,41 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5avn6uhdr88"
+path.s3tc="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://icon.png"
+dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.s3tc.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0
diff --git a/3d/physics_interpolation/icon.svg b/3d/physics_interpolation/icon.svg
new file mode 100644
index 00000000..da56e13e
--- /dev/null
+++ b/3d/physics_interpolation/icon.svg
@@ -0,0 +1 @@
+
diff --git a/3d/physics_interpolation/icon.svg.import b/3d/physics_interpolation/icon.svg.import
new file mode 100644
index 00000000..245a2f42
--- /dev/null
+++ b/3d/physics_interpolation/icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cb05v27t8vx5t"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/3d/physics_interpolation/player.gd b/3d/physics_interpolation/player.gd
new file mode 100644
index 00000000..51816e71
--- /dev/null
+++ b/3d/physics_interpolation/player.gd
@@ -0,0 +1,202 @@
+extends CharacterBody3D
+
+const MOUSE_SENSITIVITY = 2.5
+const CAMERA_SMOOTH_SPEED = 10.0
+const MOVE_SPEED = 3.0
+const FRICTION = 10.0
+const JUMP_VELOCITY = 8.0
+const BULLET_SPEED = 9.0
+
+const Bullet = preload("res://bullet.tscn")
+
+# Define our FPS and TPS player views using Euler angles.
+var _yaw: float = 0.0
+var _pitch: float = 0.0
+
+# XZ direction the player is looking at.
+var _dir := Vector3(sin(_yaw), 0, cos(_yaw))
+
+# TPS camera.
+var _tps_camera_proximity: float = 3.0
+var _tps_camera_look_from := Vector3()
+
+enum CameraType {
+ CAM_FIXED, ## Fixed camera perspective.
+ CAM_FPS, ## First-person perspective.
+ CAM_TPS, ## Third-person perspective.
+}
+
+# Current camera type.
+# (Note that we toggle this in `_ready()`, so it actually starts with FPS camera.)
+var _cam_type := CameraType.CAM_FIXED
+
+var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
+
+
+func _ready() -> void:
+ # Capture the mouse (stops the mouse cursor from showing and ensures it stays within the window).
+ Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
+
+ # We define the TPS camera in global space by setting it
+ # as `top_level` so it ignores the parent transform.
+ $Rig/Camera_TPS.top_level = true
+
+ # Perform the logic to create FPS view to start with.
+ cycle_camera_type()
+
+
+func _input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ _yaw -= event.screen_relative.x * MOUSE_SENSITIVITY * 0.001
+ _pitch += event.screen_relative.y * MOUSE_SENSITIVITY * 0.002
+ _pitch = clamp(_pitch, -PI, PI)
+ $Rig.rotation = Vector3(0, _yaw, 0)
+
+
+func _update_camera(delta: float) -> void:
+ # Keep the player direction up-to-date based on the yaw.
+ _dir.x = sin(_yaw)
+ _dir.z = cos(_yaw)
+
+ # Rotate the head (and FPS camera and firing origin) with the
+ # pitch from the mouse.
+ $Rig/Head.rotation = Vector3(_pitch * -0.5, 0, 0)
+
+ match _cam_type:
+ CameraType.CAM_TPS:
+ # We will focus the TPS camera on the head of the player.
+ var target: Vector3 = $Rig/Head.get_global_transform_interpolated().origin
+
+ # Calculate a position to look at the player from.
+ var pos := target
+
+ # The camera should be behind the player, so offset the camera relative to direction.
+ pos.x += _dir.x * _tps_camera_proximity
+ pos.z += _dir.z * _tps_camera_proximity
+
+ # Move the TPS camera up and down depending on the pitch.
+ # There's no special formula here, just something that looks okay.
+ pos.y += 2.0 + _pitch * _tps_camera_proximity * 0.2
+
+ # Offset from the old `_tps_camera_look_from` to the new position
+ # we want the TPS camera to move to.
+ var offset: Vector3 = pos - _tps_camera_look_from
+ var l: float = offset.length()
+
+ # We cap how far we allow the TPS camera to move on each update,
+ # so we get a smooth movement rather than snapping.
+ var tps_cam_speed: float = CAMERA_SMOOTH_SPEED * delta
+
+ # If we are trying to move further than the maximum allowed,
+ # we resize the offset to `tps_cam_speed`.
+ if l > tps_cam_speed:
+ offset *= tps_cam_speed / l
+
+ # Move the TPS camera.
+ _tps_camera_look_from += offset
+
+ # `look_at_from_position()` does all the magic for us.
+ $Rig/Camera_TPS.look_at_from_position(_tps_camera_look_from, target, Vector3(0, 1, 0))
+
+ # For a real TPS camera, some other things to try:
+ # - Ray cast from the player towards the camera to prevent it looking through walls.
+ # The SpringArm3D node can be useful here.
+ # - Try smoothing the camera by yaw/pitch from the player rather than offset.
+
+
+func cycle_camera_type() -> void:
+ match _cam_type:
+ CameraType.CAM_FIXED:
+ _cam_type = CameraType.CAM_FPS
+ $Rig/Head/Camera_FPS.make_current()
+ CameraType.CAM_FPS:
+ _cam_type = CameraType.CAM_TPS
+ $Rig/Camera_TPS.make_current()
+ CameraType.CAM_TPS:
+ _cam_type = CameraType.CAM_FIXED
+ get_node("../Camera_Fixed").make_current()
+
+ # Hide body in FPS view (but keep shadow casting to improve spatial awareness).
+ if _cam_type == CameraType.CAM_FPS:
+ $Rig/Mesh_Body.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
+ else:
+ $Rig/Mesh_Body.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
+
+
+func _process(delta: float) -> void:
+ if Input.is_action_just_pressed(&"cycle_camera_type"):
+ cycle_camera_type()
+
+ if Input.is_action_just_pressed(&"toggle_physics_interpolation"):
+ get_tree().physics_interpolation = not get_tree().physics_interpolation
+
+ if Input.is_action_just_pressed(&"fire"):
+ var bullet: RigidBody3D = Bullet.instantiate()
+
+ # Figure out where we want the bullet to spawn.
+ # We use a dummy Node offset from the head, but you may want to use e.g.
+ # a BoneAttachment3D, or dummy node on a weapon.
+ var transform_3d: Transform3D = $Rig/Head/Fire_Origin.get_global_transform_interpolated()
+ bullet.position = transform_3d.origin
+
+ # We can calculate the direction the bullet should travel from the basis (rotation)
+ # of the dummy Node.
+ var bul_dir: Vector3 = transform_3d.basis[2].normalized()
+
+ # Give our physics bullet some starting velocity.
+ bullet.linear_velocity = bul_dir * -BULLET_SPEED
+ get_parent().add_child(bullet)
+
+ # A moving start for a bullet using physics interpolation can be done
+ # by resetting, *then* offsetting the position in the direction of travel.
+ # This means that on the first tick the bullet will be moving rather than
+ # standing still, as standing still on the first tick can look unnatural.
+ bullet.reset_physics_interpolation()
+ bullet.position -= bul_dir * (1.0 - Engine.get_physics_interpolation_fraction())
+
+
+ # If we pressed reset, or went too far from the origin, move back to the origin.
+ if Input.is_action_just_pressed(&"reset_position") or position.length() > 10.0:
+ position = Vector3(0, 1, 0)
+ velocity = Vector3()
+ reset_physics_interpolation()
+ _yaw = 0.0
+ _pitch = 0.0
+ $Rig.rotation = Vector3(0, _yaw, 0)
+
+ if Input.is_action_just_pressed(&"jump") and is_on_floor():
+ velocity.y += JUMP_VELOCITY
+
+ # We update our camera every frame.
+ # Our camera is not physics interpolated, as we want fast response from the mouse.
+ # However in the case of first-person and third-person views, the position is indirectly
+ # inherited from physics-interpolated player, so we get nice smooth motion while still
+ # having quick mouse response.
+ _update_camera(delta)
+
+
+# When physics interpolation is active on the node,
+# you should move it on the physics tick (physics_process)
+# rather than on the frame (process).
+func _physics_process(delta: float) -> void:
+ var move := Vector3()
+
+ # Calculate movement relative to the player's coordinate system.
+ var input: Vector2 = Input.get_vector(&"move_left", &"move_right", &"move_forward", &"move_backward") * MOVE_SPEED
+ move.x = input.x
+ move.z = input.y
+
+ # Apply gravity.
+ move.y -= gravity * delta
+
+ # Apply mouse rotation to the move, so that it is now in global space.
+ move = move.rotated(Vector3(0, 1, 0), _yaw)
+
+ # Apply the global space move to the physics.
+ velocity += move
+
+ move_and_slide()
+
+ # Apply friction to horizontal motion in a tick rate-independent manner.
+ var friction_delta := exp(-FRICTION * delta)
+ velocity = Vector3(velocity.x * friction_delta, velocity.y, velocity.z * friction_delta)
diff --git a/3d/physics_interpolation/player.gd.uid b/3d/physics_interpolation/player.gd.uid
new file mode 100644
index 00000000..1f19f7aa
--- /dev/null
+++ b/3d/physics_interpolation/player.gd.uid
@@ -0,0 +1 @@
+uid://cusp3a7wm67ss
diff --git a/3d/physics_interpolation/project.godot b/3d/physics_interpolation/project.godot
new file mode 100644
index 00000000..0f7a4a22
--- /dev/null
+++ b/3d/physics_interpolation/project.godot
@@ -0,0 +1,133 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Physics Interpolation"
+config/description="This demo showcases physics interpolation in 3D with varying camera modes
+(first person, third person, fixed perspective). This is also known as fixed timestep interpolation."
+run/main_scene="uid://bj5w87j8wwnsl"
+config/features=PackedStringArray("4.5", "GL Compatibility")
+config/icon="res://icon.svg"
+
+[debug]
+
+gdscript/warnings/untyped_declaration=1
+
+[display]
+
+window/stretch/mode="canvas_items"
+window/stretch/aspect="expand"
+
+[input]
+
+ui_accept={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+ui_left={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
+]
+}
+ui_right={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
+]
+}
+ui_up={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
+, null, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
+]
+}
+ui_down={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
+]
+}
+jump={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
+, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
+]
+}
+fire={
+"deadzone": 0.2,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(56, 19),"global_position":Vector2(65, 67),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
+move_forward={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":122,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+move_backward={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+move_left={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+move_right={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+reset_position={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":82,"physical_keycode":0,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null)
+]
+}
+cycle_camera_type={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+toggle_physics_interpolation={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":84,"physical_keycode":0,"key_label":0,"unicode":116,"location":0,"echo":false,"script":null)
+]
+}
+
+[physics]
+
+common/physics_ticks_per_second=20
+3d/physics_engine="Jolt Physics"
+3d/default_gravity=20.0
+common/physics_interpolation=true
+
+[rendering]
+
+renderer/rendering_method="gl_compatibility"
+renderer/rendering_method.mobile="gl_compatibility"
+lights_and_shadows/directional_shadow/soft_shadow_filter_quality=4
+textures/default_filters/anisotropic_filtering_level=4
+anti_aliasing/quality/msaa_3d=2
diff --git a/3d/physics_interpolation/root.tscn b/3d/physics_interpolation/root.tscn
new file mode 100644
index 00000000..a3dff6e8
--- /dev/null
+++ b/3d/physics_interpolation/root.tscn
@@ -0,0 +1,141 @@
+[gd_scene load_steps=15 format=3 uid="uid://bj5w87j8wwnsl"]
+
+[ext_resource type="Script" uid="uid://cusp3a7wm67ss" path="res://player.gd" id="1_pq8q7"]
+[ext_resource type="PackedScene" uid="uid://cvgxw8nq67xxm" path="res://box.tscn" id="2_pyidc"]
+[ext_resource type="PackedScene" uid="uid://dtuq8cdwg46ti" path="res://wall.tscn" id="3_vvh5c"]
+
+[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_vvh5c"]
+
+[sub_resource type="Sky" id="Sky_28aoi"]
+sky_material = SubResource("ProceduralSkyMaterial_vvh5c")
+
+[sub_resource type="Environment" id="Environment_vvh5c"]
+background_mode = 2
+sky = SubResource("Sky_28aoi")
+ambient_light_source = 2
+ambient_light_color = Color(1, 1, 1, 1)
+ambient_light_energy = 0.3
+tonemap_mode = 4
+glow_enabled = true
+glow_hdr_threshold = 0.7
+
+[sub_resource type="CylinderShape3D" id="CylinderShape3D_vvh5c"]
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_vvh5c"]
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pq8q7"]
+albedo_color = Color(1, 0, 0, 1)
+
+[sub_resource type="PrismMesh" id="PrismMesh_pq8q7"]
+size = Vector3(1, 1, 0.01)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pyidc"]
+transparency = 1
+no_depth_test = true
+albedo_color = Color(1, 1, 0, 0.501961)
+
+[sub_resource type="BoxMesh" id="BoxMesh_vho56"]
+size = Vector3(10, 0.2, 10)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vvh5c"]
+albedo_color = Color(0.178458, 0.199519, 0.338574, 1)
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_vvh5c"]
+size = Vector3(10, 1, 10)
+
+[node name="Root" type="Node3D"]
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_vvh5c")
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(0.258819, 0.616341, -0.743732, 0, 0.769968, 0.638083, 0.965926, -0.165148, 0.199282, 0, 6.24965, 0)
+shadow_enabled = true
+shadow_normal_bias = 2.7
+directional_shadow_mode = 0
+directional_shadow_fade_start = 1.0
+directional_shadow_max_distance = 15.0
+
+[node name="Player" type="CharacterBody3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.24778, 0)
+script = ExtResource("1_pq8q7")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
+shape = SubResource("CylinderShape3D_vvh5c")
+
+[node name="Rig" type="Node3D" parent="Player"]
+physics_interpolation_mode = 2
+
+[node name="Mesh_Body" type="MeshInstance3D" parent="Player/Rig"]
+mesh = SubResource("CylinderMesh_vvh5c")
+skeleton = NodePath("../..")
+surface_material_override/0 = SubResource("StandardMaterial3D_pq8q7")
+
+[node name="Camera_TPS" type="Camera3D" parent="Player/Rig"]
+transform = Transform3D(1, 0, 0, 0, 0.819152, 0.573576, 0, -0.573576, 0.819152, 0, 1.72429, 2.14884)
+
+[node name="Head" type="Node3D" parent="Player/Rig"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.12412, 0)
+
+[node name="Camera_FPS" type="Camera3D" parent="Player/Rig/Head"]
+current = true
+
+[node name="Mesh_Head" type="MeshInstance3D" parent="Player/Rig/Head"]
+transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0, -0.5)
+mesh = SubResource("PrismMesh_pq8q7")
+skeleton = NodePath("../..")
+surface_material_override/0 = SubResource("StandardMaterial3D_pyidc")
+
+[node name="Fire_Origin" type="Node3D" parent="Player/Rig/Head"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1.60726)
+
+[node name="Camera_Fixed" type="Camera3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 0.788161, 0.615469, 0, -0.615469, 0.788161, 0, 4.94837, 5.75259)
+
+[node name="Background" type="Node3D" parent="."]
+
+[node name="Floor" type="MeshInstance3D" parent="Background"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0)
+mesh = SubResource("BoxMesh_vho56")
+skeleton = NodePath("../..")
+surface_material_override/0 = SubResource("StandardMaterial3D_vvh5c")
+
+[node name="StaticBody3D" type="StaticBody3D" parent="Background/Floor"]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Background/Floor/StaticBody3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.4, 0)
+shape = SubResource("BoxShape3D_vvh5c")
+
+[node name="Box" parent="Background" instance=ExtResource("2_pyidc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.596531, 2.19581)
+can_sleep = false
+
+[node name="Wall" parent="Background" instance=ExtResource("3_vvh5c")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5)
+
+[node name="Wall2" parent="Background" instance=ExtResource("3_vvh5c")]
+transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, -5)
+
+[node name="Wall3" parent="Background" instance=ExtResource("3_vvh5c")]
+transform = Transform3D(-4.37114e-08, 0, 1, 0, 1, 0, -1, 0, -4.37114e-08, 5, 0, 0)
+
+[node name="Wall4" parent="Background" instance=ExtResource("3_vvh5c")]
+transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, -5, 0, 0)
+
+[node name="Label" type="Label" parent="."]
+self_modulate = Color(1, 1, 1, 0.627451)
+anchors_preset = 2
+anchor_top = 1.0
+anchor_bottom = 1.0
+offset_left = 24.0
+offset_top = -151.0
+offset_right = 181.0
+offset_bottom = -24.0
+grow_vertical = 0
+theme_override_constants/outline_size = 5
+text = "WASD - Move
+Space - Jump
+Left Mouse - Fire
+Tab - Toggle Camera
+R - Reset Player
+T - Toggle Physics Interpolation"
diff --git a/3d/physics_interpolation/screenshots/.gdignore b/3d/physics_interpolation/screenshots/.gdignore
new file mode 100644
index 00000000..e69de29b
diff --git a/3d/physics_interpolation/screenshots/physics_interpolation.webp b/3d/physics_interpolation/screenshots/physics_interpolation.webp
new file mode 100644
index 00000000..88c31730
Binary files /dev/null and b/3d/physics_interpolation/screenshots/physics_interpolation.webp differ
diff --git a/3d/physics_interpolation/spark_particle.png b/3d/physics_interpolation/spark_particle.png
new file mode 100644
index 00000000..b11cb691
Binary files /dev/null and b/3d/physics_interpolation/spark_particle.png differ
diff --git a/3d/physics_interpolation/spark_particle.png.import b/3d/physics_interpolation/spark_particle.png.import
new file mode 100644
index 00000000..1936444a
--- /dev/null
+++ b/3d/physics_interpolation/spark_particle.png.import
@@ -0,0 +1,40 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b70s43gn8c3kx"
+path="res://.godot/imported/spark_particle.png-c360d6960fdef6ea0e055d81cca0d9a7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://spark_particle.png"
+dest_files=["res://.godot/imported/spark_particle.png-c360d6960fdef6ea0e055d81cca0d9a7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0
diff --git a/3d/physics_interpolation/wall.tscn b/3d/physics_interpolation/wall.tscn
new file mode 100644
index 00000000..dbaef11f
--- /dev/null
+++ b/3d/physics_interpolation/wall.tscn
@@ -0,0 +1,21 @@
+[gd_scene load_steps=4 format=3 uid="uid://dtuq8cdwg46ti"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_pq8q7"]
+size = Vector3(10.2, 1, 0.2)
+
+[sub_resource type="BoxMesh" id="BoxMesh_pyidc"]
+size = Vector3(10.2, 1, 0.2)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_oh6kn"]
+albedo_color = Color(0.3204, 0.72, 0.072, 1)
+
+[node name="Wall" type="StaticBody3D"]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
+shape = SubResource("BoxShape3D_pq8q7")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
+mesh = SubResource("BoxMesh_pyidc")
+surface_material_override/0 = SubResource("StandardMaterial3D_oh6kn")