Browse Source

Add collision pairs test to 2D/3D physics tests

Functional test used for checking/debugging different collision cases for all possible pairs of shape types.
PouleyKetchoupp 4 years ago
parent
commit
9ad473c633

+ 38 - 21
2d/physics_tests/test.gd

@@ -9,13 +9,16 @@ var _timer_started = false
 
 var _wait_physics_ticks_counter = 0
 
-
-class Line:
-	var pos_start
-	var pos_end
+class Circle2D:
+	extends Node2D
+	var center
+	var radius
 	var color
 
-var _lines = []
+	func _draw():
+		draw_circle(center, radius, color)
+
+var _drawn_nodes = []
 
 
 func _physics_process(_delta):
@@ -25,23 +28,37 @@ func _physics_process(_delta):
 			emit_signal("wait_done")
 
 
-func _draw():
-	for line in _lines:
-		draw_line(line.pos_start, line.pos_end, line.color, 1.5)
-
-
 func add_line(pos_start, pos_end, color):
-	var line = Line.new()
-	line.pos_start = pos_start
-	line.pos_end = pos_end
-	line.color = color
-	_lines.push_back(line)
-	update()
-
-
-func clear_lines():
-	_lines.clear()
-	update()
+	var line = Line2D.new()
+	line.points = [pos_start, pos_end]
+	line.width = 1.5
+	line.default_color = color
+	_drawn_nodes.push_back(line)
+	add_child(line)
+
+
+func add_circle(pos, radius, color):
+	var circle = Circle2D.new()
+	circle.center = pos
+	circle.radius = radius
+	circle.color = color
+	_drawn_nodes.push_back(circle)
+	add_child(circle)
+
+
+func add_shape(shape, transform, color):
+	var collision = CollisionShape2D.new()
+	collision.shape = shape
+	collision.transform = transform
+	collision.modulate = color
+	_drawn_nodes.push_back(collision)
+	add_child(collision)
+
+
+func clear_drawn_nodes():
+	for node in _drawn_nodes:
+		node.queue_free()
+	_drawn_nodes.clear()
 
 
 func create_rigidbody_box(size):

+ 4 - 0
2d/physics_tests/tests.gd

@@ -14,6 +14,10 @@ var _tests = [
 		"id": "Functional Tests/Box Pyramid",
 		"path": "res://tests/functional/test_pyramid.tscn",
 	},
+	{
+		"id": "Functional Tests/Collision Pairs",
+		"path": "res://tests/functional/test_collision_pairs.tscn",
+	},
 	{
 		"id": "Functional Tests/Raycasting",
 		"path": "res://tests/functional/test_raycasting.tscn",

+ 206 - 0
2d/physics_tests/tests/functional/test_collision_pairs.gd

@@ -0,0 +1,206 @@
+extends Test
+
+
+const OPTION_TYPE_RECTANGLE = "Collision type/Rectangle (1)"
+const OPTION_TYPE_SPHERE = "Collision type/Sphere (2)"
+const OPTION_TYPE_CAPSULE = "Collision type/Capsule (3)"
+const OPTION_TYPE_CONVEX_POLYGON = "Collision type/Convex Polygon (4)"
+const OPTION_TYPE_CONCAVE_SEGMENTS = "Collision type/Concave Segments (5)"
+
+const OPTION_SHAPE_RECTANGLE = "Shape type/Rectangle"
+const OPTION_SHAPE_SPHERE = "Shape type/Sphere"
+const OPTION_SHAPE_CAPSULE = "Shape type/Capsule"
+const OPTION_SHAPE_CONVEX_POLYGON = "Shape type/Convex Polygon"
+const OPTION_SHAPE_CONCAVE_POLYGON = "Shape type/Concave Polygon"
+const OPTION_SHAPE_CONCAVE_SEGMENTS = "Shape type/Concave Segments"
+
+const OFFSET_RANGE = 120.0
+
+export(Vector2) var offset = Vector2.ZERO
+
+var _update_collision = false
+var _collision_test_index = 0
+var _current_offset = Vector2.ZERO
+var _collision_shapes = []
+
+
+func _ready():
+	_initialize_collision_shapes()
+
+	$Options.add_menu_item(OPTION_TYPE_RECTANGLE)
+	$Options.add_menu_item(OPTION_TYPE_SPHERE)
+	$Options.add_menu_item(OPTION_TYPE_CAPSULE)
+	$Options.add_menu_item(OPTION_TYPE_CONVEX_POLYGON)
+	$Options.add_menu_item(OPTION_TYPE_CONCAVE_SEGMENTS)
+
+	$Options.add_menu_item(OPTION_SHAPE_RECTANGLE, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_SPHERE, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_CAPSULE, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_CONVEX_POLYGON, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_CONCAVE_POLYGON, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_CONCAVE_SEGMENTS, true, true)
+
+	$Options.connect("option_selected", self, "_on_option_selected")
+	$Options.connect("option_changed", self, "_on_option_changed")
+
+	yield(start_timer(0.5), "timeout")
+	if is_timer_canceled():
+		return
+
+	_update_collision = true
+
+
+func _input(event):
+	var key_event = event as InputEventKey
+	if (key_event and not key_event.pressed):
+		if (key_event.scancode == KEY_1):
+			_on_option_selected(OPTION_TYPE_RECTANGLE)
+		elif (key_event.scancode == KEY_2):
+			_on_option_selected(OPTION_TYPE_SPHERE)
+		elif (key_event.scancode == KEY_3):
+			_on_option_selected(OPTION_TYPE_CAPSULE)
+		elif (key_event.scancode == KEY_4):
+			_on_option_selected(OPTION_TYPE_CONVEX_POLYGON)
+		elif (key_event.scancode == KEY_5):
+			_on_option_selected(OPTION_TYPE_CONCAVE_SEGMENTS)
+
+
+func _physics_process(_delta):
+	if not _update_collision:
+		return
+
+	_update_collision = false
+
+	_do_collision_test()
+
+
+func set_h_offset(value):
+	offset.x = value * OFFSET_RANGE
+	_update_collision = true
+
+
+func set_v_offset(value):
+	offset.y = -value * OFFSET_RANGE
+	_update_collision = true
+
+
+func _initialize_collision_shapes():
+	_collision_shapes.clear()
+
+	for node in $Shapes.get_children():
+		var body = node as PhysicsBody2D
+		var shape = body.shape_owner_get_shape(0, 0)
+		shape.resource_name = node.name.substr("RigidBody".length())
+
+		_collision_shapes.push_back(shape)
+
+
+func _do_collision_test():
+	clear_drawn_nodes()
+
+	var shape = _collision_shapes[_collision_test_index]
+
+	Log.print_log("* Start %s collision tests..." % shape.resource_name)
+
+	var shape_query = Physics2DShapeQueryParameters.new()
+	shape_query.set_shape(shape)
+	var shape_scale = Vector2(0.5, 0.5)
+	shape_query.transform = Transform2D.IDENTITY.scaled(shape_scale)
+
+	for node in $Shapes.get_children():
+		if not node.visible:
+			continue
+
+		var body = node as PhysicsBody2D
+		var space_state = body.get_world_2d().direct_space_state
+
+		Log.print_log("* Testing: %s" % body.name)
+
+		var center = body.position
+
+		# Collision at the center inside.
+		var res = _add_collision(space_state, center, shape, shape_query)
+		Log.print_log("Collision center inside: %s" % ("NO HIT" if res.empty() else "HIT"))
+
+	Log.print_log("* Done.")
+
+
+func _add_collision(space_state, pos, shape, shape_query):
+	shape_query.transform.origin = pos + offset
+	var results = space_state.collide_shape(shape_query)
+
+	var color
+	if results.empty():
+		color = Color.white.darkened(0.5)
+	else:
+		color = Color.green
+
+	# Draw collision query shape.
+	add_shape(shape, shape_query.transform, color)
+
+	# Draw contact positions.
+	for contact_pos in results:
+		add_circle(contact_pos, 1.0, Color.red)
+
+	return results
+
+
+func _on_option_selected(option):
+	match option:
+		OPTION_TYPE_RECTANGLE:
+			_collision_test_index = _find_type_index("Rectangle")
+			_update_collision = true
+		OPTION_TYPE_SPHERE:
+			_collision_test_index = _find_type_index("Sphere")
+			_update_collision = true
+		OPTION_TYPE_CAPSULE:
+			_collision_test_index = _find_type_index("Capsule")
+			_update_collision = true
+		OPTION_TYPE_CONVEX_POLYGON:
+			_collision_test_index = _find_type_index("ConvexPolygon")
+			_update_collision = true
+		OPTION_TYPE_CONCAVE_SEGMENTS:
+			_collision_test_index = _find_type_index("ConcaveSegments")
+			_update_collision = true
+
+
+func _find_type_index(type_name):
+	for type_index in _collision_shapes.size():
+		var type_shape = _collision_shapes[type_index]
+		if type_shape.resource_name.find(type_name) > -1:
+			return type_index
+
+	Log.print_error("Invalid collision type: " + type_name)
+	return -1
+
+
+func _on_option_changed(option, checked):
+	var node
+
+	match option:
+		OPTION_SHAPE_RECTANGLE:
+			node = _find_shape_node("Rectangle")
+		OPTION_SHAPE_SPHERE:
+			node = _find_shape_node("Sphere")
+		OPTION_SHAPE_CAPSULE:
+			node = _find_shape_node("Capsule")
+		OPTION_SHAPE_CONVEX_POLYGON:
+			node = _find_shape_node("ConvexPolygon")
+		OPTION_SHAPE_CONCAVE_POLYGON:
+			node = _find_shape_node("ConcavePolygon")
+		OPTION_SHAPE_CONCAVE_SEGMENTS:
+			node = _find_shape_node("ConcaveSegments")
+
+	if node:
+		node.visible = checked
+		node.get_child(0).disabled = not checked
+		_update_collision = true
+
+
+func _find_shape_node(type_name):
+	var node = $Shapes.find_node("RigidBody%s" % type_name)
+
+	if not node:
+		Log.print_error("Invalid shape type: " + type_name)
+
+	return node

+ 149 - 0
2d/physics_tests/tests/functional/test_collision_pairs.tscn

@@ -0,0 +1,149 @@
+[gd_scene load_steps=8 format=2]
+
+[ext_resource path="res://tests/functional/test_collision_pairs.gd" type="Script" id=1]
+[ext_resource path="res://assets/godot-head.png" type="Texture" id=2]
+[ext_resource path="res://tests/test_options.tscn" type="PackedScene" id=3]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 40, 60 )
+
+[sub_resource type="CircleShape2D" id=2]
+radius = 60.0
+
+[sub_resource type="CapsuleShape2D" id=3]
+radius = 30.0
+height = 50.0
+
+[sub_resource type="ConcavePolygonShape2D" id=4]
+segments = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 6.44476, -42.9695, 11.127, -54.3941, 11.127, -54.3941, 26.9528, -49.4309, 26.9528, -49.4309, 26.2037, -36.508, 26.2037, -36.508, 37.5346, -28.1737, 37.5346, -28.1737, 47.6282, -34.3806, 47.6282, -34.3806, 58.0427, -20.9631, 58.0427, -20.9631, 51.113, -10.2876, 51.113, -10.2876, 50.9869, 35.2694, 50.9869, 35.2694, 38.8, 47.5, 38.8, 47.5, 15.9852, 54.3613, 15.9852, 54.3613, -14.9507, 54.1845, -14.9507, 54.1845, -36.5, 48.1, -36.5, 48.1, -50.4828, 36.33, -50.4828, 36.33, -51.3668, -9.98545, -51.3668, -9.98545, -57.8889, -20.5885, -57.8889, -20.5885, -46.9473, -34.7342, -46.9473, -34.7342, -37.4014, -28.547, -37.4014, -28.547, -26.0876, -37.0323, -26.0876, -37.0323, -26.9862, -49.15, -26.9862, -49.15, -11.4152, -54.5332, -11.4152, -54.5332, -5.93512, -43.2195 )
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 1 )
+
+[node name="Options" parent="." instance=ExtResource( 3 )]
+
+[node name="Shapes" type="Node2D" parent="."]
+z_index = -1
+z_as_relative = false
+
+[node name="RigidBodyRectangle" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 114.877, 248.76 )
+mode = 1
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodyRectangle"]
+rotation = -1.19206
+scale = Vector2( 1.2, 1.2 )
+shape = SubResource( 1 )
+
+[node name="RigidBodySphere" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 314.894, 257.658 )
+mode = 1
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodySphere"]
+shape = SubResource( 2 )
+
+[node name="RigidBodyCapsule" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 465.629, 261.204 )
+mode = 1
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodyCapsule"]
+rotation = -0.202458
+scale = Vector2( 1.2, 1.2 )
+shape = SubResource( 3 )
+
+[node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 613.385, 252.771 )
+mode = 1
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Shapes/RigidBodyConvexPolygon"]
+polygon = PoolVector2Array( 10.7, -54.5, 28.3596, -49.4067, 47.6282, -34.3806, 57.9717, -20.9447, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -58.0115, -20.515, -46.9473, -34.7342, -26.0876, -50.1138, -11.4152, -54.5332 )
+
+[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConvexPolygon"]
+modulate = Color( 1, 1, 1, 0.392157 )
+texture = ExtResource( 2 )
+
+[node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 771.159, 252.771 )
+mode = 1
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Shapes/RigidBodyConcavePolygon"]
+polygon = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 11.127, -54.3941, 26.9528, -49.4309, 26.2037, -36.508, 37.5346, -28.1737, 47.6282, -34.3806, 58.0427, -20.9631, 51.113, -10.2876, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -51.3668, -9.98545, -57.8889, -20.5885, -46.9473, -34.7342, -37.4014, -28.547, -26.0876, -37.0323, -26.9862, -49.15, -11.4152, -54.5332 )
+
+[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConcavePolygon"]
+modulate = Color( 1, 1, 1, 0.392157 )
+texture = ExtResource( 2 )
+
+[node name="RigidBodyConcaveSegments" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 930.097, 252.771 )
+mode = 1
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodyConcaveSegments"]
+shape = SubResource( 4 )
+
+[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConcaveSegments"]
+modulate = Color( 1, 1, 1, 0.392157 )
+texture = ExtResource( 2 )
+
+[node name="Controls" type="VBoxContainer" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 25.3619
+margin_top = 416.765
+margin_right = 218.362
+margin_bottom = 458.765
+custom_constants/separation = 10
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="OffsetH" type="HBoxContainer" parent="Controls"]
+margin_right = 193.0
+margin_bottom = 16.0
+custom_constants/separation = 20
+alignment = 2
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="Controls/OffsetH"]
+margin_top = 1.0
+margin_right = 53.0
+margin_bottom = 15.0
+text = "Offset H"
+
+[node name="HSlider" type="HSlider" parent="Controls/OffsetH"]
+margin_left = 73.0
+margin_right = 193.0
+margin_bottom = 16.0
+rect_min_size = Vector2( 120, 0 )
+min_value = -1.0
+max_value = 1.0
+step = 0.01
+
+[node name="OffsetV" type="HBoxContainer" parent="Controls"]
+margin_top = 26.0
+margin_right = 193.0
+margin_bottom = 42.0
+custom_constants/separation = 20
+alignment = 2
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="Controls/OffsetV"]
+margin_left = 2.0
+margin_top = 1.0
+margin_right = 53.0
+margin_bottom = 15.0
+text = "Offset V"
+
+[node name="HSlider" type="HSlider" parent="Controls/OffsetV"]
+margin_left = 73.0
+margin_right = 193.0
+margin_bottom = 16.0
+rect_min_size = Vector2( 120, 0 )
+min_value = -1.0
+max_value = 1.0
+step = 0.01
+[connection signal="value_changed" from="Controls/OffsetH/HSlider" to="." method="set_h_offset"]
+[connection signal="value_changed" from="Controls/OffsetV/HSlider" to="." method="set_v_offset"]

+ 7 - 6
2d/physics_tests/tests/functional/test_raycasting.gd

@@ -13,22 +13,23 @@ func _ready():
 
 
 func _physics_process(_delta):
-	if !_do_raycasts:
+	if not _do_raycasts:
 		return
 
 	_do_raycasts = false
 
 	Log.print_log("* Start Raycasting...")
 
-	clear_lines()
+	clear_drawn_nodes()
 
-	for shape in $Shapes.get_children():
-		var body = shape as PhysicsBody2D
+	for node in $Shapes.get_children():
+		var body = node as PhysicsBody2D
 		var space_state = body.get_world_2d().direct_space_state
+		var body_name = body.name.substr("RigidBody".length())
 
-		Log.print_log("* Testing: %s" % body.name)
+		Log.print_log("* Testing: %s" % body_name)
 
-		var center = body.global_transform.origin
+		var center = body.position
 
 		# Raycast entering from the top.
 		var res = _add_raycast(space_state, center - Vector2(0, 100), center)

+ 38 - 24
2d/physics_tests/tests/functional/test_raycasting.tscn

@@ -1,4 +1,4 @@
-[gd_scene load_steps=6 format=2]
+[gd_scene load_steps=7 format=2]
 
 [ext_resource path="res://assets/godot-head.png" type="Texture" id=1]
 [ext_resource path="res://tests/functional/test_raycasting.gd" type="Script" id=2]
@@ -6,12 +6,15 @@
 [sub_resource type="RectangleShape2D" id=1]
 extents = Vector2( 40, 60 )
 
+[sub_resource type="CircleShape2D" id=3]
+radius = 60.0
+
 [sub_resource type="CapsuleShape2D" id=2]
 radius = 30.0
 height = 50.0
 
-[sub_resource type="CircleShape2D" id=3]
-radius = 60.0
+[sub_resource type="ConcavePolygonShape2D" id=4]
+segments = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 6.44476, -42.9695, 11.127, -54.3941, 11.127, -54.3941, 26.9528, -49.4309, 26.9528, -49.4309, 26.2037, -36.508, 26.2037, -36.508, 37.5346, -28.1737, 37.5346, -28.1737, 47.6282, -34.3806, 47.6282, -34.3806, 58.0427, -20.9631, 58.0427, -20.9631, 51.113, -10.2876, 51.113, -10.2876, 50.9869, 35.2694, 50.9869, 35.2694, 38.8, 47.5, 38.8, 47.5, 15.9852, 54.3613, 15.9852, 54.3613, -14.9507, 54.1845, -14.9507, 54.1845, -36.5, 48.1, -36.5, 48.1, -50.4828, 36.33, -50.4828, 36.33, -51.3668, -9.98545, -51.3668, -9.98545, -57.8889, -20.5885, -57.8889, -20.5885, -46.9473, -34.7342, -46.9473, -34.7342, -37.4014, -28.547, -37.4014, -28.547, -26.0876, -37.0323, -26.0876, -37.0323, -26.9862, -49.15, -26.9862, -49.15, -11.4152, -54.5332, -11.4152, -54.5332, -5.93512, -43.2195 )
 
 [node name="Test" type="Node2D"]
 script = ExtResource( 2 )
@@ -22,47 +25,58 @@ z_as_relative = false
 
 [node name="RigidBodyRectangle" type="RigidBody2D" parent="Shapes"]
 position = Vector2( 114.877, 248.76 )
-mode = 3
+mode = 1
 
 [node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodyRectangle"]
 rotation = -1.19206
-scale = Vector2( 1.5, 1.5 )
+scale = Vector2( 1.2, 1.2 )
 shape = SubResource( 1 )
 
+[node name="RigidBodySphere" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 314.894, 257.658 )
+mode = 1
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodySphere"]
+shape = SubResource( 3 )
+
 [node name="RigidBodyCapsule" type="RigidBody2D" parent="Shapes"]
-position = Vector2( 313.583, 261.204 )
-mode = 3
+position = Vector2( 465.629, 261.204 )
+mode = 1
 
 [node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodyCapsule"]
 rotation = -0.202458
-scale = Vector2( 1.5, 1.5 )
+scale = Vector2( 1.2, 1.2 )
 shape = SubResource( 2 )
 
-[node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="Shapes"]
-position = Vector2( 514.899, 252.771 )
-mode = 3
+[node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 613.385, 252.771 )
+mode = 1
 
-[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConcavePolygon"]
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Shapes/RigidBodyConvexPolygon"]
+polygon = PoolVector2Array( 10.7, -54.5, 28.3596, -49.4067, 47.6282, -34.3806, 57.9717, -20.9447, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -58.0115, -20.515, -46.9473, -34.7342, -26.0876, -50.1138, -11.4152, -54.5332 )
+
+[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConvexPolygon"]
 modulate = Color( 1, 1, 1, 0.392157 )
 texture = ExtResource( 1 )
 
+[node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 771.159, 252.771 )
+mode = 1
+
 [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Shapes/RigidBodyConcavePolygon"]
 polygon = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 11.127, -54.3941, 26.9528, -49.4309, 26.2037, -36.508, 37.5346, -28.1737, 47.6282, -34.3806, 58.0427, -20.9631, 51.113, -10.2876, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -51.3668, -9.98545, -57.8889, -20.5885, -46.9473, -34.7342, -37.4014, -28.547, -26.0876, -37.0323, -26.9862, -49.15, -11.4152, -54.5332 )
 
-[node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="Shapes"]
-position = Vector2( 738.975, 252.771 )
-mode = 3
-
-[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConvexPolygon"]
+[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConcavePolygon"]
 modulate = Color( 1, 1, 1, 0.392157 )
 texture = ExtResource( 1 )
 
-[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Shapes/RigidBodyConvexPolygon"]
-polygon = PoolVector2Array( 10.7, -54.5, 28.3596, -49.4067, 47.6282, -34.3806, 57.9717, -20.9447, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -58.0115, -20.515, -46.9473, -34.7342, -26.0876, -50.1138, -11.4152, -54.5332 )
+[node name="RigidBodyConcaveSegments" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 930.097, 252.771 )
+mode = 1
 
-[node name="RigidBodySphere" type="RigidBody2D" parent="Shapes"]
-position = Vector2( 917.136, 270.868 )
-mode = 3
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodyConcaveSegments"]
+shape = SubResource( 4 )
 
-[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodySphere"]
-shape = SubResource( 3 )
+[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConcaveSegments"]
+modulate = Color( 1, 1, 1, 0.392157 )
+texture = ExtResource( 1 )

+ 10 - 8
2d/physics_tests/tests/functional/test_shapes.tscn

@@ -36,25 +36,27 @@ shape = SubResource( 2 )
 [node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="DynamicShapes"]
 position = Vector2( 683.614, 132.749 )
 
-[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConcavePolygon"]
-scale = Vector2( 0.5, 0.5 )
-texture = ExtResource( 1 )
-
 [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="DynamicShapes/RigidBodyConcavePolygon"]
 scale = Vector2( 0.5, 0.5 )
 polygon = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 11.127, -54.3941, 26.9528, -49.4309, 26.2037, -36.508, 37.5346, -28.1737, 47.6282, -34.3806, 58.0427, -20.9631, 51.113, -10.2876, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -51.3668, -9.98545, -57.8889, -20.5885, -46.9473, -34.7342, -37.4014, -28.547, -26.0876, -37.0323, -26.9862, -49.15, -11.4152, -54.5332 )
 
-[node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="DynamicShapes"]
-position = Vector2( 473.536, 134.336 )
-
-[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConvexPolygon"]
+[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConcavePolygon"]
+self_modulate = Color( 1, 1, 1, 0.392157 )
 scale = Vector2( 0.5, 0.5 )
 texture = ExtResource( 1 )
 
+[node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 473.536, 134.336 )
+
 [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="DynamicShapes/RigidBodyConvexPolygon"]
 scale = Vector2( 0.5, 0.5 )
 polygon = PoolVector2Array( 10.7, -54.5, 28.3596, -49.4067, 47.6282, -34.3806, 57.9717, -20.9447, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -58.0115, -20.515, -46.9473, -34.7342, -26.0876, -50.1138, -11.4152, -54.5332 )
 
+[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConvexPolygon"]
+self_modulate = Color( 1, 1, 1, 0.392157 )
+scale = Vector2( 0.5, 0.5 )
+texture = ExtResource( 1 )
+
 [node name="RigidBodySphere" type="RigidBody2D" parent="DynamicShapes"]
 position = Vector2( 919.968, 115.129 )
 

+ 10 - 8
2d/physics_tests/tests/performance/test_perf_contacts.tscn

@@ -50,21 +50,23 @@ shape = SubResource( 3 )
 [node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="DynamicShapes"]
 position = Vector2( 300, 1024 )
 
-[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConvexPolygon"]
-scale = Vector2( 0.5, 0.5 )
-texture = ExtResource( 3 )
-
 [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="DynamicShapes/RigidBodyConvexPolygon"]
 scale = Vector2( 0.5, 0.5 )
 polygon = PoolVector2Array( 10.7, -54.5, 28.3596, -49.4067, 47.6282, -34.3806, 57.9717, -20.9447, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -58.0115, -20.515, -46.9473, -34.7342, -26.0876, -50.1138, -11.4152, -54.5332 )
 
-[node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="DynamicShapes"]
-position = Vector2( 400, 1024 )
-
-[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConcavePolygon"]
+[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConvexPolygon"]
+self_modulate = Color( 1, 1, 1, 0.392157 )
 scale = Vector2( 0.5, 0.5 )
 texture = ExtResource( 3 )
 
+[node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 400, 1024 )
+
 [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="DynamicShapes/RigidBodyConcavePolygon"]
 scale = Vector2( 0.5, 0.5 )
 polygon = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 11.127, -54.3941, 26.9528, -49.4309, 26.2037, -36.508, 37.5346, -28.1737, 47.6282, -34.3806, 58.0427, -20.9631, 51.113, -10.2876, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -51.3668, -9.98545, -57.8889, -20.5885, -46.9473, -34.7342, -37.4014, -28.547, -26.0876, -37.0323, -26.9862, -49.15, -11.4152, -54.5332 )
+
+[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConcavePolygon"]
+self_modulate = Color( 1, 1, 1, 0.392157 )
+scale = Vector2( 0.5, 0.5 )
+texture = ExtResource( 3 )

+ 16 - 3
2d/physics_tests/utils/option_menu.gd

@@ -3,9 +3,10 @@ extends MenuButton
 
 
 signal option_selected(item_path)
+signal option_changed(item_path, checked)
 
 
-func add_menu_item(item_path):
+func add_menu_item(item_path, checkbox = false, checked = false):
 	var path_elements = item_path.split("/", false)
 	var path_element_count = path_elements.size()
 	assert(path_element_count > 0)
@@ -17,7 +18,12 @@ func add_menu_item(item_path):
 		path += popup_label + "/"
 		popup = _add_popup(popup, path, popup_label)
 
-	_add_item(popup, path_elements[path_element_count - 1])
+	var label = path_elements[path_element_count - 1]
+	if checkbox:
+		popup.add_check_item(label)
+		popup.set_item_checked(popup.get_item_count() - 1, checked)
+	else:
+		popup.add_item(label)
 
 
 func _add_item(parent_popup, label):
@@ -33,6 +39,7 @@ func _add_popup(parent_popup, path, label):
 
 	var popup_menu = PopupMenu.new()
 	popup_menu.name = label
+	popup_menu.hide_on_checkable_item_selection = false
 
 	parent_popup.add_child(popup_menu)
 	parent_popup.add_submenu_item(label, label)
@@ -44,4 +51,10 @@ func _add_popup(parent_popup, path, label):
 
 func _on_item_pressed(item_index, popup_menu, path):
 	var item_path = path + popup_menu.get_item_text(item_index)
-	emit_signal("option_selected", item_path)
+
+	if popup_menu.is_item_checkable(item_index):
+		var checked = not popup_menu.is_item_checked(item_index)
+		popup_menu.set_item_checked(item_index, checked)
+		emit_signal("option_changed", item_path, checked)
+	else:
+		emit_signal("option_selected", item_path)

+ 44 - 0
3d/physics_tests/test.gd

@@ -1,6 +1,7 @@
 class_name Test
 extends Node
 
+
 signal wait_done()
 
 var _timer
@@ -8,6 +9,8 @@ var _timer_started = false
 
 var _wait_physics_ticks_counter = 0
 
+var _drawn_nodes = []
+
 
 func _physics_process(_delta):
 	if (_wait_physics_ticks_counter > 0):
@@ -16,6 +19,47 @@ func _physics_process(_delta):
 			emit_signal("wait_done")
 
 
+func add_sphere(pos, radius, color):
+	var sphere = MeshInstance.new()
+
+	var sphere_mesh = SphereMesh.new()
+	sphere_mesh.radius = radius
+	sphere_mesh.height = radius * 2.0
+	sphere.mesh = sphere_mesh
+
+	var material = SpatialMaterial.new()
+	material.flags_unshaded = true
+	material.albedo_color = color
+	sphere.material_override = material
+
+	_drawn_nodes.push_back(sphere)
+	add_child(sphere)
+
+	sphere.global_transform.origin = pos
+
+
+func add_shape(shape, transform, color):
+	var collision = CollisionShape.new()
+	collision.shape = shape
+
+	_drawn_nodes.push_back(collision)
+	add_child(collision)
+
+	var mesh_instance = collision.get_child(0)
+	var material = SpatialMaterial.new()
+	material.flags_unshaded = true
+	material.albedo_color = color
+	mesh_instance.material_override = material
+
+	collision.global_transform = transform
+
+
+func clear_drawn_nodes():
+	for node in _drawn_nodes:
+		node.queue_free()
+	_drawn_nodes.clear()
+
+
 func create_rigidbody_box(size):
 	var template_shape = BoxShape.new()
 	template_shape.extents = 0.5 * size

+ 4 - 0
3d/physics_tests/tests.gd

@@ -22,6 +22,10 @@ var _tests = [
 		"id": "Functional Tests/Box Pyramid",
 		"path": "res://tests/functional/test_pyramid.tscn",
 	},
+	{
+		"id": "Functional Tests/Collision Pairs",
+		"path": "res://tests/functional/test_collision_pairs.tscn",
+	},
 	{
 		"id": "Functional Tests/Raycasting",
 		"path": "res://tests/functional/test_raycasting.tscn",

+ 211 - 0
3d/physics_tests/tests/functional/test_collision_pairs.gd

@@ -0,0 +1,211 @@
+extends Test
+
+
+const OPTION_TYPE_BOX = "Collision type/Box (1)"
+const OPTION_TYPE_SPHERE = "Collision type/Sphere (2)"
+const OPTION_TYPE_CAPSULE = "Collision type/Capsule (3)"
+const OPTION_TYPE_CYLINDER = "Collision type/Cylinder (4)"
+const OPTION_TYPE_CONVEX_POLYGON = "Collision type/Convex Polygon (5)"
+
+const OPTION_SHAPE_BOX = "Shape type/Box"
+const OPTION_SHAPE_SPHERE = "Shape type/Sphere"
+const OPTION_SHAPE_CAPSULE = "Shape type/Capsule"
+const OPTION_SHAPE_CYLINDER = "Shape type/Cylinder"
+const OPTION_SHAPE_CONVEX_POLYGON = "Shape type/Convex Polygon"
+const OPTION_SHAPE_CONCAVE_POLYGON = "Shape type/Concave Polygon"
+
+const OFFSET_RANGE = 3.0
+
+export(Vector3) var offset = Vector3.ZERO
+
+var _update_collision = false
+var _collision_test_index = 0
+var _current_offset = Vector3.ZERO
+var _collision_shapes = []
+
+
+func _ready():
+	_initialize_collision_shapes()
+
+	$Options.add_menu_item(OPTION_TYPE_BOX)
+	$Options.add_menu_item(OPTION_TYPE_SPHERE)
+	$Options.add_menu_item(OPTION_TYPE_CAPSULE)
+	$Options.add_menu_item(OPTION_TYPE_CYLINDER)
+	$Options.add_menu_item(OPTION_TYPE_CONVEX_POLYGON)
+
+	$Options.add_menu_item(OPTION_SHAPE_BOX, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_SPHERE, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_CAPSULE, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_CYLINDER, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_CONVEX_POLYGON, true, true)
+	$Options.add_menu_item(OPTION_SHAPE_CONCAVE_POLYGON, true, true)
+
+	$Options.connect("option_selected", self, "_on_option_selected")
+	$Options.connect("option_changed", self, "_on_option_changed")
+
+	yield(start_timer(0.5), "timeout")
+	if is_timer_canceled():
+		return
+
+	_update_collision = true
+
+
+func _input(event):
+	var key_event = event as InputEventKey
+	if (key_event and not key_event.pressed):
+		if (key_event.scancode == KEY_1):
+			_on_option_selected(OPTION_TYPE_BOX)
+		elif (key_event.scancode == KEY_2):
+			_on_option_selected(OPTION_TYPE_SPHERE)
+		elif (key_event.scancode == KEY_3):
+			_on_option_selected(OPTION_TYPE_CAPSULE)
+		elif (key_event.scancode == KEY_4):
+			_on_option_selected(OPTION_TYPE_CYLINDER)
+		elif (key_event.scancode == KEY_5):
+			_on_option_selected(OPTION_TYPE_CONVEX_POLYGON)
+
+
+func _physics_process(_delta):
+	if not _update_collision:
+		return
+
+	_update_collision = false
+
+	_do_collision_test()
+
+
+func set_x_offset(value):
+	offset.x = value * OFFSET_RANGE
+	_update_collision = true
+
+
+func set_y_offset(value):
+	offset.y = value * OFFSET_RANGE
+	_update_collision = true
+
+
+func set_z_offset(value):
+	offset.z = value * OFFSET_RANGE
+	_update_collision = true
+
+
+func _initialize_collision_shapes():
+	_collision_shapes.clear()
+
+	for node in $Shapes.get_children():
+		var body = node as PhysicsBody
+		var shape = body.shape_owner_get_shape(0, 0)
+		shape.resource_name = node.name.substr("RigidBody".length())
+
+		_collision_shapes.push_back(shape)
+
+
+func _do_collision_test():
+	clear_drawn_nodes()
+
+	var shape = _collision_shapes[_collision_test_index]
+
+	Log.print_log("* Start %s collision tests..." % shape.resource_name)
+
+	var shape_query = PhysicsShapeQueryParameters.new()
+	shape_query.set_shape(shape)
+	var shape_scale = Vector3(0.5, 0.5, 0.5)
+	shape_query.transform = Transform.IDENTITY.scaled(shape_scale)
+
+	for node in $Shapes.get_children():
+		if not node.visible:
+			continue
+
+		var body = node as PhysicsBody
+		var space_state = body.get_world().direct_space_state
+
+		Log.print_log("* Testing: %s" % body.name)
+
+		var center = body.global_transform.origin
+
+		# Collision at the center inside.
+		var res = _add_collision(space_state, center, shape, shape_query)
+		Log.print_log("Collision center inside: %s" % ("NO HIT" if res.empty() else "HIT"))
+
+	Log.print_log("* Done.")
+
+
+func _add_collision(space_state, pos, shape, shape_query):
+	shape_query.transform.origin = pos + offset
+	var results = space_state.collide_shape(shape_query)
+
+	var color
+	if results.empty():
+		color = Color.white.darkened(0.5)
+	else:
+		color = Color.green
+
+	# Draw collision query shape.
+	add_shape(shape, shape_query.transform, color)
+
+	# Draw contact positions.
+	for contact_pos in results:
+		add_sphere(contact_pos, 0.05, Color.red)
+
+	return results
+
+
+func _on_option_selected(option):
+	match option:
+		OPTION_TYPE_BOX:
+			_collision_test_index = _find_type_index("Box")
+			_update_collision = true
+		OPTION_TYPE_SPHERE:
+			_collision_test_index = _find_type_index("Sphere")
+			_update_collision = true
+		OPTION_TYPE_CAPSULE:
+			_collision_test_index = _find_type_index("Capsule")
+			_update_collision = true
+		OPTION_TYPE_CYLINDER:
+			_collision_test_index = _find_type_index("Cylinder")
+			_update_collision = true
+		OPTION_TYPE_CONVEX_POLYGON:
+			_collision_test_index = _find_type_index("ConvexPolygon")
+			_update_collision = true
+
+
+func _find_type_index(type_name):
+	for type_index in _collision_shapes.size():
+		var type_shape = _collision_shapes[type_index]
+		if type_shape.resource_name.find(type_name) > -1:
+			return type_index
+
+	Log.print_error("Invalid collision type: " + type_name)
+	return -1
+
+
+func _on_option_changed(option, checked):
+	var node
+
+	match option:
+		OPTION_SHAPE_BOX:
+			node = _find_shape_node("Box")
+		OPTION_SHAPE_SPHERE:
+			node = _find_shape_node("Sphere")
+		OPTION_SHAPE_CAPSULE:
+			node = _find_shape_node("Capsule")
+		OPTION_SHAPE_CYLINDER:
+			node = _find_shape_node("Cylinder")
+		OPTION_SHAPE_CONVEX_POLYGON:
+			node = _find_shape_node("ConvexPolygon")
+		OPTION_SHAPE_CONCAVE_POLYGON:
+			node = _find_shape_node("ConcavePolygon")
+
+	if node:
+		node.visible = checked
+		node.get_child(0).disabled = not checked
+		_update_collision = true
+
+
+func _find_shape_node(type_name):
+	var node = $Shapes.find_node("RigidBody%s" % type_name)
+
+	if not node:
+		Log.print_error("Invalid shape type: " + type_name)
+
+	return node

+ 169 - 0
3d/physics_tests/tests/functional/test_collision_pairs.tscn

@@ -0,0 +1,169 @@
+[gd_scene load_steps=11 format=2]
+
+[ext_resource path="res://assets/robot_head/godot3_robot_head_collision.tres" type="Shape" id=1]
+[ext_resource path="res://tests/functional/test_collision_pairs.gd" type="Script" id=2]
+[ext_resource path="res://utils/exception_cylinder.gd" type="Script" id=3]
+[ext_resource path="res://utils/camera_orbit.gd" type="Script" id=4]
+[ext_resource path="res://tests/test_options.tscn" type="PackedScene" id=5]
+
+[sub_resource type="BoxShape" id=1]
+
+[sub_resource type="SphereShape" id=5]
+
+[sub_resource type="CapsuleShape" id=2]
+
+[sub_resource type="CylinderShape" id=3]
+
+[sub_resource type="ConvexPolygonShape" id=4]
+points = PoolVector3Array( -0.7, 0, -0.7, -0.3, 0, 0.8, 0.8, 0, -0.3, 0, -1, 0 )
+
+[node name="Test" type="Spatial"]
+script = ExtResource( 2 )
+
+[node name="Options" parent="." instance=ExtResource( 5 )]
+
+[node name="Controls" type="VBoxContainer" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 25.0
+margin_top = 417.0
+margin_right = -806.0
+margin_bottom = -141.0
+custom_constants/separation = 10
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="OffsetX" type="HBoxContainer" parent="Controls"]
+margin_right = 193.0
+margin_bottom = 16.0
+custom_constants/separation = 20
+alignment = 2
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="Controls/OffsetX"]
+margin_left = 2.0
+margin_top = 1.0
+margin_right = 53.0
+margin_bottom = 15.0
+text = "Offset X"
+
+[node name="HSlider" type="HSlider" parent="Controls/OffsetX"]
+margin_left = 73.0
+margin_right = 193.0
+margin_bottom = 16.0
+rect_min_size = Vector2( 120, 0 )
+min_value = -1.0
+max_value = 1.0
+step = 0.01
+
+[node name="OffsetY" type="HBoxContainer" parent="Controls"]
+margin_top = 26.0
+margin_right = 193.0
+margin_bottom = 42.0
+custom_constants/separation = 20
+alignment = 2
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="Controls/OffsetY"]
+margin_left = 3.0
+margin_top = 1.0
+margin_right = 53.0
+margin_bottom = 15.0
+text = "Offset Y"
+
+[node name="HSlider" type="HSlider" parent="Controls/OffsetY"]
+margin_left = 73.0
+margin_right = 193.0
+margin_bottom = 16.0
+rect_min_size = Vector2( 120, 0 )
+min_value = -1.0
+max_value = 1.0
+step = 0.01
+
+[node name="OffsetZ" type="HBoxContainer" parent="Controls"]
+margin_top = 52.0
+margin_right = 193.0
+margin_bottom = 68.0
+custom_constants/separation = 20
+alignment = 2
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="Controls/OffsetZ"]
+margin_left = 2.0
+margin_top = 1.0
+margin_right = 53.0
+margin_bottom = 15.0
+text = "Offset Z"
+
+[node name="HSlider" type="HSlider" parent="Controls/OffsetZ"]
+margin_left = 73.0
+margin_right = 193.0
+margin_bottom = 16.0
+rect_min_size = Vector2( 120, 0 )
+min_value = -1.0
+max_value = 1.0
+step = 0.01
+
+[node name="Shapes" type="Spatial" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.35591, 0 )
+
+[node name="RigidBodyBox" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, 0 )
+mode = 3
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyBox"]
+transform = Transform( 0.579556, 0.0885213, 0.145926, 0, 0.939693, -0.205212, -0.155291, 0.330366, 0.544604, 0, 0, 0 )
+shape = SubResource( 1 )
+
+[node name="RigidBodySphere" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 0 )
+mode = 3
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodySphere"]
+transform = Transform( 1.2, 0, 0, 0, 1.2, 0, 0, 0, 1.2, 0, 0, 0 )
+shape = SubResource( 5 )
+
+[node name="RigidBodyCapsule" type="RigidBody" parent="Shapes"]
+mode = 3
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyCapsule"]
+transform = Transform( 0.8, 0, 0, 0, -1.30337e-07, -0.8, 0, 0.8, -1.30337e-07, 0, 0, 0 )
+shape = SubResource( 2 )
+
+[node name="RigidBodyCylinder" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 0 )
+mode = 3
+script = ExtResource( 3 )
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyCylinder"]
+transform = Transform( 0.772741, -0.258819, 2.59821e-08, 0.2, 0.933013, -0.207055, 0.0535898, 0.25, 0.772741, 0, 0, 0 )
+shape = SubResource( 3 )
+
+[node name="RigidBodyConvexPolygon" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 6, -0.211, 0 )
+mode = 3
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyConvexPolygon"]
+transform = Transform( 2, 0, 0, 0, 2.89766, -0.517939, 0, 0.776908, 1.93177, 0, 0.3533, 0 )
+shape = SubResource( 4 )
+
+[node name="RigidBodyConcavePolygon" type="StaticBody" parent="Shapes"]
+transform = Transform( 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, -6, 3.93357 )
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyConcavePolygon"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0 )
+shape = ExtResource( 1 )
+
+[node name="Camera" type="Camera" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.8667, 11.8164 )
+script = ExtResource( 4 )
+[connection signal="value_changed" from="Controls/OffsetX/HSlider" to="." method="set_x_offset"]
+[connection signal="value_changed" from="Controls/OffsetY/HSlider" to="." method="set_y_offset"]
+[connection signal="value_changed" from="Controls/OffsetZ/HSlider" to="." method="set_z_offset"]

+ 1 - 1
3d/physics_tests/tests/functional/test_raycasting.gd

@@ -23,7 +23,7 @@ func _ready():
 
 
 func _physics_process(_delta):
-	if !_do_raycasts:
+	if not _do_raycasts:
 		return
 
 	_do_raycasts = false

+ 14 - 14
3d/physics_tests/tests/functional/test_raycasting.tscn

@@ -7,6 +7,8 @@
 
 [sub_resource type="BoxShape" id=1]
 
+[sub_resource type="SphereShape" id=5]
+
 [sub_resource type="CapsuleShape" id=2]
 
 [sub_resource type="CylinderShape" id=3]
@@ -14,8 +16,6 @@
 [sub_resource type="ConvexPolygonShape" id=4]
 points = PoolVector3Array( -0.7, 0, -0.7, -0.3, 0, 0.8, 0.8, 0, -0.3, 0, -1, 0 )
 
-[sub_resource type="SphereShape" id=5]
-
 [node name="Test" type="Spatial"]
 script = ExtResource( 2 )
 
@@ -30,15 +30,23 @@ mode = 3
 transform = Transform( 0.579556, 0.0885213, 0.145926, 0, 0.939693, -0.205212, -0.155291, 0.330366, 0.544604, 0, 0, 0 )
 shape = SubResource( 1 )
 
-[node name="RigidBodyCapsule" type="RigidBody" parent="Shapes"]
+[node name="RigidBodySphere" type="RigidBody" parent="Shapes"]
 transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 0 )
 mode = 3
 
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodySphere"]
+transform = Transform( 1.2, 0, 0, 0, 1.2, 0, 0, 0, 1.2, 0, 0, 0 )
+shape = SubResource( 5 )
+
+[node name="RigidBodyCapsule" type="RigidBody" parent="Shapes"]
+mode = 3
+
 [node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyCapsule"]
 transform = Transform( 0.8, 0, 0, 0, -1.30337e-07, -0.8, 0, 0.8, -1.30337e-07, 0, 0, 0 )
 shape = SubResource( 2 )
 
 [node name="RigidBodyCylinder" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 0 )
 mode = 3
 script = ExtResource( 3 )
 
@@ -46,22 +54,14 @@ script = ExtResource( 3 )
 transform = Transform( 0.772741, -0.258819, 2.59821e-08, 0.2, 0.933013, -0.207055, 0.0535898, 0.25, 0.772741, 0, 0, 0 )
 shape = SubResource( 3 )
 
-[node name="RigidBodyConvex" type="RigidBody" parent="Shapes"]
-transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 3, -0.210678, 0 )
+[node name="RigidBodyConvexPolygon" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 6, -0.211, 0 )
 mode = 3
 
-[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyConvex"]
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyConvexPolygon"]
 transform = Transform( 2, 0, 0, 0, 2.89766, -0.517939, 0, 0.776908, 1.93177, 0, 0.3533, 0 )
 shape = SubResource( 4 )
 
-[node name="RigidBodySphere" type="RigidBody" parent="Shapes"]
-transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, 0 )
-mode = 3
-
-[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodySphere"]
-transform = Transform( 1.2, 0, 0, 0, 1.2, 0, 0, 0, 1.2, 0, 0, 0 )
-shape = SubResource( 5 )
-
 [node name="StaticBodyHead" type="StaticBody" parent="Shapes"]
 transform = Transform( 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, -6, 3.93357 )
 

+ 5 - 5
3d/physics_tests/tests/performance/test_perf_contacts.gd

@@ -3,12 +3,12 @@ extends Test
 
 const OPTION_TYPE_ALL = "Shape type/All"
 const OPTION_TYPE_BOX = "Shape type/Box"
+const OPTION_TYPE_SPHERE = "Shape type/Sphere"
 const OPTION_TYPE_CAPSULE = "Shape type/Capsule"
 const OPTION_TYPE_CYLINDER = "Shape type/Cylinder"
 const OPTION_TYPE_CONVEX = "Shape type/Convex"
-const OPTION_TYPE_SPHERE = "Shape type/Sphere"
-export(Array) var spawns = Array()
 
+export(Array) var spawns = Array()
 export(int) var spawn_count = 100
 export(int, 1, 10) var spawn_multiplier = 5
 
@@ -27,10 +27,10 @@ func _ready():
 
 	$Options.add_menu_item(OPTION_TYPE_ALL)
 	$Options.add_menu_item(OPTION_TYPE_BOX)
+	$Options.add_menu_item(OPTION_TYPE_SPHERE)
 	$Options.add_menu_item(OPTION_TYPE_CAPSULE)
 	$Options.add_menu_item(OPTION_TYPE_CYLINDER)
 	$Options.add_menu_item(OPTION_TYPE_CONVEX)
-	$Options.add_menu_item(OPTION_TYPE_SPHERE)
 	$Options.connect("option_selected", self, "_on_option_selected")
 
 	_start_all_types()
@@ -51,14 +51,14 @@ func _on_option_selected(option):
 			_start_all_types()
 		OPTION_TYPE_BOX:
 			_start_type(_find_type_index("Box"))
+		OPTION_TYPE_SPHERE:
+			_start_type(_find_type_index("Sphere"))
 		OPTION_TYPE_CAPSULE:
 			_start_type(_find_type_index("Capsule"))
 		OPTION_TYPE_CYLINDER:
 			_start_type(_find_type_index("Cylinder"))
 		OPTION_TYPE_CONVEX:
 			_start_type(_find_type_index("Convex"))
-		OPTION_TYPE_SPHERE:
-			_start_type(_find_type_index("Sphere"))
 
 
 func _find_type_index(type_name):

+ 16 - 3
3d/physics_tests/utils/option_menu.gd

@@ -3,9 +3,10 @@ extends MenuButton
 
 
 signal option_selected(item_path)
+signal option_changed(item_path, checked)
 
 
-func add_menu_item(item_path):
+func add_menu_item(item_path, checkbox = false, checked = false):
 	var path_elements = item_path.split("/", false)
 	var path_element_count = path_elements.size()
 	assert(path_element_count > 0)
@@ -17,7 +18,12 @@ func add_menu_item(item_path):
 		path += popup_label + "/"
 		popup = _add_popup(popup, path, popup_label)
 
-	_add_item(popup, path_elements[path_element_count - 1])
+	var label = path_elements[path_element_count - 1]
+	if checkbox:
+		popup.add_check_item(label)
+		popup.set_item_checked(popup.get_item_count() - 1, checked)
+	else:
+		popup.add_item(label)
 
 
 func _add_item(parent_popup, label):
@@ -33,6 +39,7 @@ func _add_popup(parent_popup, path, label):
 
 	var popup_menu = PopupMenu.new()
 	popup_menu.name = label
+	popup_menu.hide_on_checkable_item_selection = false
 
 	parent_popup.add_child(popup_menu)
 	parent_popup.add_submenu_item(label, label)
@@ -44,4 +51,10 @@ func _add_popup(parent_popup, path, label):
 
 func _on_item_pressed(item_index, popup_menu, path):
 	var item_path = path + popup_menu.get_item_text(item_index)
-	emit_signal("option_selected", item_path)
+
+	if popup_menu.is_item_checkable(item_index):
+		var checked = not popup_menu.is_item_checked(item_index)
+		popup_menu.set_item_checked(item_index, checked)
+		emit_signal("option_changed", item_path, checked)
+	else:
+		emit_signal("option_selected", item_path)