Skip to content

Commit db56dc1

Browse files
committed
Add OMI_physics_gravity implementation and test files
1 parent f72e4f5 commit db56dc1

File tree

52 files changed

+3166
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3166
-6
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Extensions implemented in this repository:
88

99
| Extension name | Import | Export | Godot version | Link |
1010
| ------------------------------ | ------ | ------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
11+
| **OMI_physics_gravity** | Yes | Yes | 4.5+ | [OMI_physics_gravity extension spec](https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_gravity) |
1112
| **OMI_physics_joint** | Yes | Yes | 4.1+ | [OMI_physics_joint extension spec](https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_joint) |
1213
| **OMI_seat** | Yes | Yes | 4.0+ | [OMI_seat extension spec](https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_seat) |
1314
| **OMI_spawn_point** | Yes | No | 4.0+ | [OMI_spawn_point extension spec](https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_spawn_point) |

addons/omi_extensions/omi_extensions_plugin.gd

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ func _enter_tree() -> void:
1313
GLTFDocument.register_gltf_document_extension(ext, true)
1414
ext = GLTFDocumentExtensionOMISpawnPoint.new()
1515
GLTFDocument.register_gltf_document_extension(ext)
16+
ext = GLTFDocumentExtensionOMIPhysicsGravity.new()
17+
GLTFDocument.register_gltf_document_extension(ext, true)
1618
ext = GLTFDocumentExtensionOMIPhysicsJoint.new()
1719
GLTFDocument.register_gltf_document_extension(ext)
1820
ext = GLTFDocumentExtensionOMIVehicle.new()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
@tool
2+
class_name CustomGravityArea3D
3+
extends Area3D
4+
5+
6+
enum CustomGravityType {
7+
DIRECTIONAL, ## Gravity in a direction in local space.
8+
POINT, ## Gravity towards the local origin point.
9+
DISC, ## Gravity towards a filled circle on the local XZ plane.
10+
TORUS, ## Gravity towards a hollow circle on the local XZ plane.
11+
LINE, ## Gravity towards a line defined by points in local space.
12+
SHAPED, ## Gravity towards a shape in local space.
13+
}
14+
15+
@export var custom_gravity_type: CustomGravityType:
16+
set(value):
17+
custom_gravity_type = value
18+
notify_property_list_changed()
19+
20+
var direction := Vector3.DOWN
21+
var radius: float = 1.0
22+
var line_points: PackedVector3Array
23+
var shape: Shape3D
24+
25+
26+
func _ready() -> void:
27+
if gravity_space_override == SPACE_OVERRIDE_DISABLED:
28+
push_warning("CustomGravityArea3D has its Area3D gravity override disabled, this node will not have gravity.")
29+
if gravity_type != GRAVITY_TYPE_TARGET:
30+
push_warning("CustomGravityArea3D has its Area3D gravity type not set to target. The CustomGravityArea3D gravity logic will not be used.")
31+
32+
33+
func _calculate_gravity_target(local_position: Vector3) -> Vector3:
34+
match custom_gravity_type:
35+
CustomGravityType.DIRECTIONAL:
36+
return local_position + direction
37+
CustomGravityType.POINT:
38+
return Vector3.ZERO
39+
CustomGravityType.DISC:
40+
var flat_position = Vector3(local_position.x, 0.0, local_position.z)
41+
return flat_position.limit_length(radius)
42+
CustomGravityType.TORUS:
43+
var flat_position = Vector3(local_position.x, 0.0, local_position.z)
44+
return flat_position.normalized() * radius
45+
CustomGravityType.LINE:
46+
var closest_point := Vector3.ZERO
47+
var closest_distance_sq: float = INF
48+
for i in range(line_points.size() - 1):
49+
var a: Vector3 = line_points[i]
50+
var b: Vector3 = line_points[i + 1]
51+
var closest: Vector3 = Geometry3D.get_closest_point_to_segment(local_position, a, b)
52+
var distance_sq: float = local_position.distance_squared_to(closest)
53+
if distance_sq < closest_distance_sq:
54+
closest_point = closest
55+
closest_distance_sq = distance_sq
56+
return closest_point
57+
CustomGravityType.SHAPED:
58+
return _get_closest_point_on_shape(shape, local_position)
59+
return Vector3()
60+
61+
62+
static func _project_point_onto_triangle(point: Vector3, a: Vector3, b: Vector3, c: Vector3) -> Vector3:
63+
var plane: Plane = Plane(a, b, c)
64+
var projected: Vector3 = plane.project(point)
65+
var bary: Vector3 = Geometry3D.get_triangle_barycentric_coords(projected, a, b, c)
66+
if 0.0 < bary.x and bary.x < 1.0 and 0.0 < bary.y and bary.y < 1.0 and 0.0 < bary.z and bary.z < 1.0:
67+
return projected # If all barycentric coordinates are between 0 and 1, this is on the triangle.
68+
# Else, find which two barycentric coordinates are the greatest, and project onto that line segment.
69+
if bary.x < bary.y and bary.x < bary.z:
70+
return Geometry3D.get_closest_point_to_segment(projected, b, c)
71+
if bary.y < bary.x and bary.y < bary.z:
72+
return Geometry3D.get_closest_point_to_segment(projected, a, c)
73+
return Geometry3D.get_closest_point_to_segment(projected, a, b)
74+
75+
76+
static func _get_closest_point_on_shape(shape: Shape3D, point: Vector3) -> Vector3:
77+
if shape is BoxShape3D:
78+
var extents = shape.size * 0.5
79+
return point.clamp(-extents, extents)
80+
if shape is SphereShape3D:
81+
return point.limit_length(shape.radius)
82+
if shape is CapsuleShape3D:
83+
var mid_extent: float = (shape.height - shape.radius * 2.0) * 0.5
84+
var projected: Vector3 = Geometry3D.get_closest_point_to_segment(point, Vector3(0.0, -mid_extent, 0.0), Vector3(0.0, mid_extent, 0.0))
85+
var difference: Vector3 = (point - projected).limit_length(shape.radius)
86+
return projected + difference
87+
if shape is CylinderShape3D:
88+
var extent: float = shape.height * 0.5
89+
var projected: Vector3 = Geometry3D.get_closest_point_to_segment(point, Vector3(0.0, -extent, 0.0), Vector3(0.0, extent, 0.0))
90+
var flat_location = Vector3(point.x, 0.0, point.z)
91+
return projected + flat_location.limit_length(shape.radius)
92+
if shape is ConcavePolygonShape3D:
93+
var closest_point := Vector3.ZERO
94+
var closest_distance_sq: float = INF
95+
var faces: PackedVector3Array = shape.get_faces()
96+
for i in range(0, faces.size(), 3):
97+
var on_triangle: Vector3 = _project_point_onto_triangle(point, faces[i], faces[i + 1], faces[i + 2])
98+
var distance_sq: float = point.distance_squared_to(on_triangle)
99+
if distance_sq < closest_distance_sq:
100+
closest_point = on_triangle
101+
closest_distance_sq = distance_sq
102+
return closest_point
103+
printerr("Unsupported shape: ", shape)
104+
return point
105+
106+
107+
func _get_property_list() -> Array[Dictionary]:
108+
var properties: Array[Dictionary] = []
109+
match custom_gravity_type:
110+
CustomGravityType.DIRECTIONAL:
111+
properties.append({
112+
"name": "direction",
113+
"type": TYPE_VECTOR3,
114+
"usage": PROPERTY_USAGE_DEFAULT,
115+
})
116+
CustomGravityType.DISC, CustomGravityType.TORUS:
117+
properties.append({
118+
"name": "radius",
119+
"type": TYPE_FLOAT,
120+
"usage": PROPERTY_USAGE_DEFAULT,
121+
})
122+
CustomGravityType.LINE:
123+
properties.append({
124+
"name": "line_points",
125+
"type": TYPE_PACKED_VECTOR3_ARRAY,
126+
"usage": PROPERTY_USAGE_DEFAULT,
127+
})
128+
CustomGravityType.SHAPED:
129+
properties.append({
130+
"name": "shape",
131+
"type": TYPE_OBJECT,
132+
"usage": PROPERTY_USAGE_DEFAULT,
133+
"hint": PROPERTY_HINT_RESOURCE_TYPE,
134+
"hint_string": "Shape3D"
135+
})
136+
return properties
137+
138+
139+
# Everything below this point is for GLTF serialization.
140+
func _get_or_create_state_shapes_in_state(gltf_state: GLTFState) -> Array:
141+
var state_extensions: Dictionary = gltf_state.json.get_or_add("extensions", {})
142+
if not state_extensions.has("OMI_physics_shape"):
143+
state_extensions["OMI_physics_shape"] = {}
144+
gltf_state.add_used_extension("OMI_physics_shape", false)
145+
var omi_physics_shape_ext: Dictionary = state_extensions["OMI_physics_shape"]
146+
var state_shapes: Array = omi_physics_shape_ext.get_or_add("shapes", [])
147+
return state_shapes
148+
149+
150+
func to_dictionary(gltf_state: GLTFState) -> Dictionary:
151+
var ret: Dictionary = area_gravity_to_dictionary(self)
152+
if gravity_type != Area3D.GravityType.GRAVITY_TYPE_TARGET:
153+
return ret
154+
var type_string: String = _gravity_type_enum_to_string(custom_gravity_type)
155+
ret["type"] = type_string
156+
var sub_dict: Dictionary = {}
157+
if custom_gravity_type == CustomGravityType.DIRECTIONAL:
158+
if not direction.is_equal_approx(Vector3.DOWN):
159+
sub_dict = { "direction": [direction.x, direction.y, direction.z] }
160+
else:
161+
if gravity_point_unit_distance != 0.0:
162+
sub_dict = { "unitDistance": gravity_point_unit_distance }
163+
match custom_gravity_type:
164+
CustomGravityType.DISC, CustomGravityType.TORUS:
165+
if radius != 1.0:
166+
sub_dict["radius"] = radius
167+
CustomGravityType.LINE:
168+
var point_numbers: Array = []
169+
for line_point in line_points:
170+
point_numbers.append(line_point.x)
171+
point_numbers.append(line_point.y)
172+
point_numbers.append(line_point.z)
173+
sub_dict["points"] = point_numbers
174+
CustomGravityType.SHAPED:
175+
var state_shapes: Array = _get_or_create_state_shapes_in_state(gltf_state)
176+
var gltf_shape := GLTFPhysicsShape.from_resource(shape)
177+
sub_dict["shape"] = state_shapes.size()
178+
state_shapes.append(gltf_shape.to_dictionary())
179+
if not sub_dict.is_empty():
180+
ret[type_string] = sub_dict
181+
return ret
182+
183+
184+
## Functionality common to all Godot Area3D nodes including non-CustomGravityArea3D nodes.
185+
static func area_gravity_to_dictionary(area: Area3D) -> Dictionary:
186+
var ret: Dictionary = {}
187+
var space_override: Area3D.SpaceOverride = area.gravity_space_override
188+
if space_override == Area3D.SpaceOverride.SPACE_OVERRIDE_DISABLED:
189+
return ret
190+
ret["gravity"] = area.gravity
191+
if area.priority != 0:
192+
ret["priority"] = area.priority
193+
if space_override == Area3D.SpaceOverride.SPACE_OVERRIDE_REPLACE:
194+
ret["replace"] = true
195+
ret["stop"] = true
196+
elif space_override == Area3D.SpaceOverride.SPACE_OVERRIDE_COMBINE_REPLACE:
197+
ret["stop"] = true
198+
elif space_override == Area3D.SpaceOverride.SPACE_OVERRIDE_REPLACE_COMBINE:
199+
ret["replace"] = true
200+
if area.gravity_type == Area3D.GravityType.GRAVITY_TYPE_DIRECTIONAL:
201+
var dir: Vector3 = area.gravity_direction * area.global_basis.orthonormalized()
202+
if not dir.is_equal_approx(Vector3.DOWN):
203+
ret["directional"] = { "direction": [dir.x, dir.y, dir.z] }
204+
ret["type"] = "directional"
205+
elif area.gravity_type == Area3D.GravityType.GRAVITY_TYPE_POINT:
206+
var unit_dist: float = area.gravity_point_unit_distance
207+
if unit_dist != 0.0:
208+
ret["point"] = { "unitDistance": unit_dist }
209+
ret["type"] = "point"
210+
return ret
211+
212+
213+
static func from_dictionary(dict: Dictionary, gltf_state: GLTFState) -> CustomGravityArea3D:
214+
if "type" not in dict:
215+
printerr('GLTF gravity import: Missing required field "type", expected "directional", "point", "disc", "torus", "line", or "shaped".')
216+
return null
217+
if "gravity" not in dict:
218+
printerr('GLTF gravity import: Missing required field "gravity", expected a number in meters per second squared.')
219+
return null
220+
var type_string = dict.get("type")
221+
if type_string not in ["directional", "point", "disc", "torus", "line", "shaped"]:
222+
printerr("GLTF gravity import: Invalid gravity type, found: ", dict.get("type"), ' but expected "directional", "point", "disc", "torus", "line", or "shaped".')
223+
return null
224+
var gravity_amount = dict.get("gravity")
225+
if not gravity_amount is float: # All JSON numbers are floats.
226+
printerr("GLTF gravity import: Invalid gravity, found: ", dict.get("gravity"), ' but expected a number.')
227+
return null
228+
var ret: CustomGravityArea3D = CustomGravityArea3D.new()
229+
ret.gravity_type = Area3D.GRAVITY_TYPE_TARGET
230+
ret.custom_gravity_type = _gravity_type_string_to_enum(type_string)
231+
ret.gravity = gravity_amount
232+
var priority = dict.get("priority")
233+
if priority is float: # All JSON numbers are floats.
234+
ret.priority = priority
235+
var replace: bool = dict.get("replace", false)
236+
var stop: bool = dict.get("stop", false)
237+
if replace and stop:
238+
ret.gravity_space_override = Area3D.SpaceOverride.SPACE_OVERRIDE_REPLACE
239+
elif stop:
240+
ret.gravity_space_override = Area3D.SpaceOverride.SPACE_OVERRIDE_COMBINE_REPLACE
241+
elif replace:
242+
ret.gravity_space_override = Area3D.SpaceOverride.SPACE_OVERRIDE_REPLACE_COMBINE
243+
else:
244+
ret.gravity_space_override = Area3D.SpaceOverride.SPACE_OVERRIDE_COMBINE
245+
var sub_dict = dict.get(type_string)
246+
if not sub_dict is Dictionary:
247+
return ret
248+
var direction = sub_dict.get("direction")
249+
if direction is Array:
250+
ret.direction = Vector3(direction[0], direction[1], direction[2])
251+
var unit_distance = sub_dict.get("unitDistance")
252+
if unit_distance is float:
253+
ret.gravity_point_unit_distance = unit_distance
254+
var radius = sub_dict.get("radius")
255+
if radius is float:
256+
ret.radius = radius
257+
var points = sub_dict.get("points")
258+
if points is Array:
259+
var packed_points := PackedVector3Array()
260+
for i in range(0, points.size(), 3):
261+
packed_points.append(Vector3(points[i], points[i + 1], points[i + 2]))
262+
ret.line_points = packed_points
263+
var shape = sub_dict.get("shape")
264+
if shape is float: # Integer but all JSON numbers are floats.
265+
var shape_index: int = shape
266+
if shape_index < 0:
267+
printerr("GLTF gravity import: Invalid shape index, found: ", shape, " but expected a non-negative integer.")
268+
return ret
269+
var state_shapes: Array = gltf_state.get_additional_data(&"GLTFPhysicsShapes")
270+
if shape_index >= state_shapes.size():
271+
printerr("GLTF gravity import: Shape index ", shape_index, " is out of bounds (size=", state_shapes.size(), ").")
272+
return ret
273+
var gltf_shape: GLTFPhysicsShape = state_shapes[shape_index]
274+
ret.shape = gltf_shape.to_resource(true)
275+
return ret
276+
277+
278+
static func _gravity_type_enum_to_string(type: CustomGravityType) -> String:
279+
# The type value may be set to `"directional"`, `"point"`, `"disc"`, `"torus"`, `"line"`, or `"shaped"`.
280+
match type:
281+
CustomGravityType.DIRECTIONAL:
282+
return "directional"
283+
CustomGravityType.POINT:
284+
return "point"
285+
CustomGravityType.DISC:
286+
return "disc"
287+
CustomGravityType.TORUS:
288+
return "torus"
289+
CustomGravityType.LINE:
290+
return "line"
291+
CustomGravityType.SHAPED:
292+
return "shaped"
293+
assert(false, "GLTF gravity export: Invalid gravity type.")
294+
return ""
295+
296+
297+
static func _gravity_type_string_to_enum(type: String) -> CustomGravityType:
298+
match type:
299+
"directional":
300+
return CustomGravityType.DIRECTIONAL
301+
"point":
302+
return CustomGravityType.POINT
303+
"disc":
304+
return CustomGravityType.DISC
305+
"torus":
306+
return CustomGravityType.TORUS
307+
"line":
308+
return CustomGravityType.LINE
309+
"shaped":
310+
return CustomGravityType.SHAPED
311+
printerr("GLTF gravity import: Unknown gravity type: ", type)
312+
return CustomGravityType.DIRECTIONAL

0 commit comments

Comments
 (0)