Skip to content

Commit 01d7f10

Browse files
committed
Add 3D physics interpolation demo
Demonstrate first person shooter and third person shooter cameras.
1 parent 0d6b772 commit 01d7f10

15 files changed

+646
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8

3d/physics_interpolation_3d/box.tscn

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[gd_scene load_steps=4 format=3 uid="uid://cvgxw8nq67xxm"]
2+
3+
[sub_resource type="BoxShape3D" id="BoxShape3D_pq8q7"]
4+
5+
[sub_resource type="BoxMesh" id="BoxMesh_pyidc"]
6+
7+
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_oq5cr"]
8+
albedo_color = Color(0.5544661, 0.39379695, 0.15444939, 1)
9+
10+
[node name="Box" type="RigidBody3D"]
11+
mass = 10.0
12+
13+
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
14+
shape = SubResource("BoxShape3D_pq8q7")
15+
16+
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
17+
mesh = SubResource("BoxMesh_pyidc")
18+
surface_material_override/0 = SubResource("StandardMaterial3D_oq5cr")

3d/physics_interpolation_3d/bullet.gd

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
extends RigidBody3D
2+
3+
const _appearance_life = 1
4+
const _max_life = 100
5+
var _life = 0
6+
var _enabled = false
7+
8+
func _ready() -> void:
9+
$CollisionShape3D.disabled = true
10+
11+
func _physics_process(_delta: float) -> void:
12+
if !_enabled:
13+
$CollisionShape3D.disabled = false
14+
_enabled = true
15+
16+
17+
_life += 1
18+
19+
var life_left = _max_life - _life
20+
21+
var appearance_fract = min(float (_life) / float (_appearance_life), 1.0)
22+
var fract = float (life_left) / float (_max_life)
23+
fract *= appearance_fract
24+
25+
fract = max(fract, 0.0001)
26+
27+
$Scaler.scale = Vector3(fract, fract, fract)
28+
29+
if _life >= _max_life:
30+
queue_free()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://dohdjr8ilotkr
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
[gd_scene load_steps=12 format=3 uid="uid://2r5wc1n0ybju"]
2+
3+
[ext_resource type="Script" uid="uid://dohdjr8ilotkr" path="res://bullet.gd" id="1_v7oki"]
4+
[ext_resource type="Texture2D" uid="uid://b70s43gn8c3kx" path="res://spark_particle2.png" id="2_v8qja"]
5+
6+
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_v8qja"]
7+
bounce = 0.61
8+
9+
[sub_resource type="SphereShape3D" id="SphereShape3D_rtl8c"]
10+
radius = 0.2
11+
12+
[sub_resource type="Gradient" id="Gradient_v8qja"]
13+
14+
[sub_resource type="GradientTexture1D" id="GradientTexture1D_t4vbm"]
15+
gradient = SubResource("Gradient_v8qja")
16+
17+
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_v7oki"]
18+
direction = Vector3(0, 1, 0)
19+
initial_velocity_max = 1.0
20+
gravity = Vector3(0, 1, 0)
21+
scale_min = 0.19999999
22+
color_ramp = SubResource("GradientTexture1D_t4vbm")
23+
24+
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_t4vbm"]
25+
transparency = 1
26+
albedo_texture = ExtResource("2_v8qja")
27+
28+
[sub_resource type="QuadMesh" id="QuadMesh_rtl8c"]
29+
material = SubResource("StandardMaterial3D_t4vbm")
30+
size = Vector2(0.2, 0.2)
31+
32+
[sub_resource type="SphereMesh" id="SphereMesh_v7oki"]
33+
34+
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_v7oki"]
35+
albedo_color = Color(0.43164876, 0.5731902, 0.5013925, 1)
36+
metallic = 1.0
37+
38+
[node name="Bullet" type="RigidBody3D"]
39+
physics_material_override = SubResource("PhysicsMaterial_v8qja")
40+
script = ExtResource("1_v7oki")
41+
42+
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
43+
shape = SubResource("SphereShape3D_rtl8c")
44+
45+
[node name="GPUParticles3D" type="GPUParticles3D" parent="."]
46+
transform_align = 1
47+
process_material = SubResource("ParticleProcessMaterial_v7oki")
48+
draw_pass_1 = SubResource("QuadMesh_rtl8c")
49+
50+
[node name="Scaler" type="Node3D" parent="."]
51+
transform = Transform3D(0.001, 0, 0, 0, 0.001, 0, 0, 0, 0.001, 0, 0, 0)
52+
53+
[node name="MeshInstance3D" type="MeshInstance3D" parent="Scaler"]
54+
transform = Transform3D(0.4, 0, 0, 0, 0.4, 0, 0, 0, 0.4, 0, 0, 0)
55+
mesh = SubResource("SphereMesh_v7oki")
56+
skeleton = NodePath("../..")
57+
surface_material_override/0 = SubResource("StandardMaterial3D_v7oki")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="CompressedTexture2D"
5+
uid="uid://c5avn6uhdr88"
6+
path.s3tc="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.s3tc.ctex"
7+
metadata={
8+
"imported_formats": ["s3tc_bptc"],
9+
"vram_texture": true
10+
}
11+
12+
[deps]
13+
14+
source_file="res://icon.png"
15+
dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.s3tc.ctex"]
16+
17+
[params]
18+
19+
compress/mode=2
20+
compress/high_quality=false
21+
compress/lossy_quality=0.7
22+
compress/uastc_level=0
23+
compress/rdo_quality_loss=0.0
24+
compress/hdr_compression=1
25+
compress/normal_map=0
26+
compress/channel_pack=0
27+
mipmaps/generate=true
28+
mipmaps/limit=-1
29+
roughness/mode=0
30+
roughness/src_normal=""
31+
process/channel_remap/red=0
32+
process/channel_remap/green=1
33+
process/channel_remap/blue=2
34+
process/channel_remap/alpha=3
35+
process/fix_alpha_border=true
36+
process/premult_alpha=false
37+
process/normal_map_invert_y=false
38+
process/hdr_as_srgb=false
39+
process/hdr_clamp_exposure=false
40+
process/size_limit=0
41+
detect_3d/compress_to=0

3d/physics_interpolation_3d/icon.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="CompressedTexture2D"
5+
uid="uid://cb05v27t8vx5t"
6+
path.s3tc="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.s3tc.ctex"
7+
metadata={
8+
"imported_formats": ["s3tc_bptc"],
9+
"vram_texture": true
10+
}
11+
12+
[deps]
13+
14+
source_file="res://icon.svg"
15+
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.s3tc.ctex"]
16+
17+
[params]
18+
19+
compress/mode=2
20+
compress/high_quality=false
21+
compress/lossy_quality=0.7
22+
compress/uastc_level=0
23+
compress/rdo_quality_loss=0.0
24+
compress/hdr_compression=1
25+
compress/normal_map=0
26+
compress/channel_pack=0
27+
mipmaps/generate=true
28+
mipmaps/limit=-1
29+
roughness/mode=0
30+
roughness/src_normal=""
31+
process/channel_remap/red=0
32+
process/channel_remap/green=1
33+
process/channel_remap/blue=2
34+
process/channel_remap/alpha=3
35+
process/fix_alpha_border=true
36+
process/premult_alpha=false
37+
process/normal_map_invert_y=false
38+
process/hdr_as_srgb=false
39+
process/hdr_clamp_exposure=false
40+
process/size_limit=0
41+
detect_3d/compress_to=0
42+
svg/scale=1.0
43+
editor/scale_with_editor_scale=false
44+
editor/convert_colors_with_editor_theme=false

3d/physics_interpolation_3d/player.gd

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
extends CharacterBody3D
2+
3+
# Define our FPS and TPS player views using Euler angles.
4+
var _yaw : float = 0
5+
var _pitch = 0
6+
7+
# XZ direction the player is looking at.
8+
var _direction = Vector3(-sin(_yaw), 0, -cos(_yaw))
9+
10+
# TPS Camera.
11+
var _tps_camera_proximity = 3
12+
var _tps_camera_look_from = Vector3()
13+
14+
# Fixed camera, first person shooter, third person shooter.
15+
enum CameraType {CAM_FIXED, CAM_FPS, CAM_TPS}
16+
17+
# Current camera type.
18+
# (Note we toggle this in _ready() so it actually starts with FPS camera.)
19+
var _cam_type = CameraType.CAM_FIXED
20+
21+
var _bullet_scene = load("res://bullet.tscn")
22+
23+
func _ready():
24+
# Capture the mouse, stop the cursor showing.
25+
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
26+
27+
# We define the TPS camera in global space by setting it
28+
# as top_level so it ignores the parent transform.
29+
$Rig/Camera_TPS.set_as_top_level(true)
30+
31+
# Perform the logic to create FPS view to start with.
32+
toggle_camera_type()
33+
34+
# Input can come in freely at any point during the frame or tick.
35+
# We use this to transform the player rig immediately (so no lag),
36+
# but this means we should turn off physics interpolation for the rig
37+
# (we use the physics_interpolation_mode in the inspector, but the mode
38+
# can also be changed via script).
39+
func _input(event: InputEvent) -> void:
40+
if event is InputEventMouseMotion:
41+
_yaw -= event.relative.x * 0.005 # How much we react to left right mouse.
42+
_pitch += event.relative.y * 0.01 # How much we react to up down mouse.
43+
_pitch = clamp(_pitch, -PI, PI) # Don't look higher or lower than 90 degrees.
44+
45+
# Apply only the yaw to the rig (we don't want the body mesh to change pitch).
46+
# Pitch is handled separately.
47+
$Rig.rotation = Vector3(0, _yaw, 0)
48+
49+
func _update_camera(_delta: float):
50+
# Keep the player direction up to date based on the yaw.
51+
_direction.x = -sin(_yaw)
52+
_direction.z = -cos(_yaw)
53+
54+
# Rotate the head (and FPS camera and firing origin) with the
55+
# pitch from the mouse.
56+
$Rig/Head.rotation = Vector3(_pitch * -0.5, 0, 0)
57+
58+
if _cam_type == CameraType.CAM_TPS:
59+
# We will focus the TPS camera on the head of the player.
60+
var target = $Rig/Head.get_global_transform_interpolated().origin
61+
62+
# Calculate a position to look at the player from.
63+
var pos = target
64+
65+
# The camera should be behind the player, so reverse the polarity
66+
# of direction.
67+
pos.x -= _direction.x * _tps_camera_proximity
68+
pos.z -= _direction.z * _tps_camera_proximity
69+
70+
# Move the TPS camera up and down depending on the pitch.
71+
# There's no special formula here, just something that looks okay.
72+
pos.y += 2.0 + (_pitch * (0.2 * _tps_camera_proximity))
73+
74+
# Offset from the old _tps_camera_look_from to the new position
75+
# we want the TPS camera to move to.
76+
var offset = pos - _tps_camera_look_from
77+
var l = offset.length()
78+
79+
# We cap how far we allow the TPS camera to move on each update,
80+
# so we get a smooth movement, rather than snapping.
81+
var tps_cam_speed = _delta * 8.0
82+
83+
# If we are trying to move further than the maximum allowed,
84+
# we resize the offset to tps_cam_speed.
85+
if (l > tps_cam_speed):
86+
offset *= tps_cam_speed / l
87+
88+
# Move the TPS camera.
89+
_tps_camera_look_from += offset
90+
91+
# The look_at_from_position does all the magic for us.
92+
$Rig/Camera_TPS.look_at_from_position(_tps_camera_look_from, target, Vector3(0, 1, 0))
93+
94+
# For a real TPS camera some other things to try:
95+
# * Ray cast from the player towards the camera to prevent it looking through walls.
96+
# * Try smoothing the camera by yaw / pitch from the player rather than offset.
97+
98+
func toggle_camera_type():
99+
match _cam_type:
100+
CameraType.CAM_FIXED:
101+
_cam_type = CameraType.CAM_FPS
102+
$Rig/Head/Camera_FPS.make_current()
103+
CameraType.CAM_FPS:
104+
_cam_type = CameraType.CAM_TPS
105+
$Rig/Camera_TPS.make_current()
106+
CameraType.CAM_TPS:
107+
_cam_type = CameraType.CAM_FIXED
108+
get_node("../Camera_Fixed").make_current()
109+
110+
# Hide body in FPS view, show in all other views.
111+
$Rig/Mesh_Body.visible = _cam_type != CameraType.CAM_FPS
112+
113+
func _process(_delta: float) -> void:
114+
# Allow selecting different cameras.
115+
if (Input.is_action_just_pressed("ui_focus_next")):
116+
toggle_camera_type()
117+
118+
if (Input.is_action_just_pressed("fire")):
119+
# Create a bullet...
120+
var bul = _bullet_scene.instantiate()
121+
122+
# Figure out where we want the bullet to spawn.
123+
# We use a dummy Node offset from the head, but you may want to use e.g.
124+
# a bone attachment, or dummy node on a weapon.
125+
var tr : Transform3D = $Rig/Head/Fire_Origin.get_global_transform_interpolated()
126+
bul.position = tr.origin
127+
128+
# We can calculate the direction the bullet should travel from the basis (rotation)
129+
# of the dummy Node.
130+
var bul_dir = tr.basis[2].normalized()
131+
132+
# Let's give our physics bullet some velocity.
133+
bul.linear_velocity = bul_dir * -9
134+
get_node("..").add_child(bul)
135+
136+
# A moving start for a bullet using physics interpolation can be done
137+
# by resetting, THEN offsetting the position in the direction of travel.
138+
# This means that on the first tick the bullet will be moving rather than
139+
# standing still, which can look unnatural.
140+
bul.reset_physics_interpolation()
141+
bul.position -= bul_dir * (1.0 - Engine.get_physics_interpolation_fraction())
142+
143+
144+
# If we pressed reset, or too far from the origin... move the player back to origin.
145+
if (Input.is_action_just_pressed("ui_accept") or position.length() > 10):
146+
position = Vector3(0, 1, 0)
147+
velocity = Vector3()
148+
reset_physics_interpolation()
149+
_yaw = 0
150+
_pitch = 0
151+
152+
if (Input.is_action_just_pressed("jump")) and is_on_floor():
153+
velocity += Vector3(0, 12, 0)
154+
155+
# We update our camera every frame.
156+
# Our camera is not physics interpolated, as we want fast response from the mouse.
157+
# However in the case of FPS and TPS, the position is indirectly inherited from
158+
# the physics interpolated player, so we get nice smooth motion, but quick mouse
159+
# response.
160+
_update_camera(_delta)
161+
162+
# When physics interpolation is active on the node,
163+
# you should move it on the physics tick (physics_process)
164+
# rather than on the frame (process).
165+
func _physics_process(_delta: float) -> void:
166+
167+
var move : Vector3 = Vector3()
168+
169+
# Let us calculate a move relative to the player coordinate system.
170+
if Input.is_action_pressed("ui_up"):
171+
move.z -= 1
172+
if Input.is_action_pressed("ui_down"):
173+
move.z += 1
174+
if Input.is_action_pressed("ui_left"):
175+
move.x -= 1
176+
if Input.is_action_pressed("ui_right"):
177+
move.x += 1
178+
179+
# Apply some gravity.
180+
move.y -= 0.9
181+
182+
# Apply mouse rotation to the move, so that it is now in global space.
183+
move = move.rotated(Vector3(0, 1, 0), _yaw)
184+
185+
# Apply the global space move to the physics.
186+
velocity += move
187+
188+
move_and_slide()
189+
190+
# Apply some friction.
191+
velocity *= 0.9
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://cusp3a7wm67ss

0 commit comments

Comments
 (0)