Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OMI_physics_gravity implementation and test files #5

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ This repository is used by the [Open Metaverse Interoperability Group](https://o

Extensions implemented in this repository:

| Extension name | Import | Export | Godot version | Link |
| ----------------------- | ------ | ------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **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) |
| **OMI_seat** | Yes | Yes | 4.0+ | [OMI_seat extension spec](https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_seat) |
| **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) |
| Extension name | Import | Export | Godot version | Link |
| ------------------------------ | ------ | ------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **OMI_physics_gravity** | Yes | Yes | 4.4+ | [OMI_physics_gravity extension spec](https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_gravity) |
| **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) |
| **OMI_seat** | Yes | Yes | 4.0+ | [OMI_seat extension spec](https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_seat) |
| **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) |

Extensions implemented upstream in Godot Engine:

Expand Down
2 changes: 2 additions & 0 deletions addons/omi_extensions/omi_extensions_plugin.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ func _enter_tree() -> void:
GLTFDocument.register_gltf_document_extension(ext, true)
ext = GLTFDocumentExtensionOMISpawnPoint.new()
GLTFDocument.register_gltf_document_extension(ext)
ext = GLTFDocumentExtensionOMIPhysicsGravity.new()
GLTFDocument.register_gltf_document_extension(ext, true)
ext = GLTFDocumentExtensionOMIPhysicsJoint.new()
GLTFDocument.register_gltf_document_extension(ext)
add_node_3d_gizmo_plugin(seat_gizmo_plugin)
Expand Down
312 changes: 312 additions & 0 deletions addons/omi_extensions/physics_gravity/custom_gravity_area.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
@tool
class_name CustomGravityArea3D
extends Area3D


enum CustomGravityType {
DIRECTIONAL, ## Gravity in a direction in local space.
POINT, ## Gravity towards the local origin point.
DISC, ## Gravity towards a filled circle on the local XZ plane.
TORUS, ## Gravity towards a hollow circle on the local XZ plane.
LINE, ## Gravity towards a line defined by points in local space.
SHAPED, ## Gravity towards a shape in local space.
}

@export var custom_gravity_type: CustomGravityType:
set(value):
custom_gravity_type = value
notify_property_list_changed()

var direction := Vector3.DOWN
var radius: float = 1.0
var line_points: PackedVector3Array
var shape: Shape3D


func _ready() -> void:
if gravity_space_override == SPACE_OVERRIDE_DISABLED:
push_warning("CustomGravityArea3D has its Area3D gravity override disabled, this node will not have gravity.")
if gravity_type != GRAVITY_TYPE_TARGET:
push_warning("CustomGravityArea3D has its Area3D gravity type not set to target. The CustomGravityArea3D gravity logic will not be used.")


func _calculate_gravity_target(local_position: Vector3) -> Vector3:
match custom_gravity_type:
CustomGravityType.DIRECTIONAL:
return local_position + direction
CustomGravityType.POINT:
return Vector3.ZERO
CustomGravityType.DISC:
var flat_position = Vector3(local_position.x, 0.0, local_position.z)
return flat_position.limit_length(radius)
CustomGravityType.TORUS:
var flat_position = Vector3(local_position.x, 0.0, local_position.z)
return flat_position.normalized() * radius
CustomGravityType.LINE:
var closest_point := Vector3.ZERO
var closest_distance_sq: float = INF
for i in range(line_points.size() - 1):
var a: Vector3 = line_points[i]
var b: Vector3 = line_points[i + 1]
var closest: Vector3 = Geometry3D.get_closest_point_to_segment(local_position, a, b)
var distance_sq: float = local_position.distance_squared_to(closest)
if distance_sq < closest_distance_sq:
closest_point = closest
closest_distance_sq = distance_sq
return closest_point
CustomGravityType.SHAPED:
return _get_closest_point_on_shape(shape, local_position)
return Vector3()


static func _project_point_onto_triangle(point: Vector3, a: Vector3, b: Vector3, c: Vector3) -> Vector3:
var plane: Plane = Plane(a, b, c)
var projected: Vector3 = plane.project(point)
var bary: Vector3 = Geometry3D.get_triangle_barycentric_coords(projected, a, b, c)
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:
return projected # If all barycentric coordinates are between 0 and 1, this is on the triangle.
# Else, find which two barycentric coordinates are the greatest, and project onto that line segment.
if bary.x < bary.y and bary.x < bary.z:
return Geometry3D.get_closest_point_to_segment(projected, b, c)
if bary.y < bary.x and bary.y < bary.z:
return Geometry3D.get_closest_point_to_segment(projected, a, c)
return Geometry3D.get_closest_point_to_segment(projected, a, b)


static func _get_closest_point_on_shape(shape: Shape3D, point: Vector3) -> Vector3:
if shape is BoxShape3D:
var extents = shape.size * 0.5
return point.clamp(-extents, extents)
if shape is SphereShape3D:
return point.limit_length(shape.radius)
if shape is CapsuleShape3D:
var mid_extent: float = (shape.height - shape.radius * 2.0) * 0.5
var projected: Vector3 = Geometry3D.get_closest_point_to_segment(point, Vector3(0.0, -mid_extent, 0.0), Vector3(0.0, mid_extent, 0.0))
var difference: Vector3 = (point - projected).limit_length(shape.radius)
return projected + difference
if shape is CylinderShape3D:
var extent: float = shape.height * 0.5
var projected: Vector3 = Geometry3D.get_closest_point_to_segment(point, Vector3(0.0, -extent, 0.0), Vector3(0.0, extent, 0.0))
var flat_location = Vector3(point.x, 0.0, point.z)
return projected + flat_location.limit_length(shape.radius)
if shape is ConcavePolygonShape3D:
var closest_point := Vector3.ZERO
var closest_distance_sq: float = INF
var faces: PackedVector3Array = shape.get_faces()
for i in range(0, faces.size(), 3):
var on_triangle: Vector3 = _project_point_onto_triangle(point, faces[i], faces[i + 1], faces[i + 2])
var distance_sq: float = point.distance_squared_to(on_triangle)
if distance_sq < closest_distance_sq:
closest_point = on_triangle
closest_distance_sq = distance_sq
return closest_point
printerr("Unsupported shape: ", shape)
return point


func _get_property_list() -> Array[Dictionary]:
var properties: Array[Dictionary] = []
match custom_gravity_type:
CustomGravityType.DIRECTIONAL:
properties.append({
"name": "direction",
"type": TYPE_VECTOR3,
"usage": PROPERTY_USAGE_DEFAULT,
})
CustomGravityType.DISC, CustomGravityType.TORUS:
properties.append({
"name": "radius",
"type": TYPE_FLOAT,
"usage": PROPERTY_USAGE_DEFAULT,
})
CustomGravityType.LINE:
properties.append({
"name": "line_points",
"type": TYPE_PACKED_VECTOR3_ARRAY,
"usage": PROPERTY_USAGE_DEFAULT,
})
CustomGravityType.SHAPED:
properties.append({
"name": "shape",
"type": TYPE_OBJECT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_RESOURCE_TYPE,
"hint_string": "Shape3D"
})
return properties


# Everything below this point is for GLTF serialization.
func _get_or_create_state_shapes_in_state(gltf_state: GLTFState) -> Array:
var state_extensions: Dictionary = gltf_state.json.get_or_add("extensions", {})
if not state_extensions.has("OMI_physics_shape"):
state_extensions["OMI_physics_shape"] = {}
gltf_state.add_used_extension("OMI_physics_shape", false)
var omi_physics_shape_ext: Dictionary = state_extensions["OMI_physics_shape"]
var state_shapes: Array = omi_physics_shape_ext.get_or_add("shapes", [])
return state_shapes


func to_dictionary(gltf_state: GLTFState) -> Dictionary:
var ret: Dictionary = area_gravity_to_dictionary(self)
if gravity_type != Area3D.GravityType.GRAVITY_TYPE_TARGET:
return ret
var type_string: String = _gravity_type_enum_to_string(custom_gravity_type)
ret["type"] = type_string
var sub_dict: Dictionary = {}
if custom_gravity_type == CustomGravityType.DIRECTIONAL:
if not direction.is_equal_approx(Vector3.DOWN):
sub_dict = { "direction": [direction.x, direction.y, direction.z] }
else:
if gravity_point_unit_distance != 0.0:
sub_dict = { "unitDistance": gravity_point_unit_distance }
match custom_gravity_type:
CustomGravityType.DISC, CustomGravityType.TORUS:
if radius != 1.0:
sub_dict["radius"] = radius
CustomGravityType.LINE:
var point_numbers: Array = []
for line_point in line_points:
point_numbers.append(line_point.x)
point_numbers.append(line_point.y)
point_numbers.append(line_point.z)
sub_dict["points"] = point_numbers
CustomGravityType.SHAPED:
var state_shapes: Array = _get_or_create_state_shapes_in_state(gltf_state)
var gltf_shape := GLTFPhysicsShape.from_resource(shape)
sub_dict["shape"] = state_shapes.size()
state_shapes.append(gltf_shape.to_dictionary())
if not sub_dict.is_empty():
ret[type_string] = sub_dict
return ret


## Functionality common to all Godot Area3D nodes including non-CustomGravityArea3D nodes.
static func area_gravity_to_dictionary(area: Area3D) -> Dictionary:
var ret: Dictionary = {}
var space_override: Area3D.SpaceOverride = area.gravity_space_override
if space_override == Area3D.SpaceOverride.SPACE_OVERRIDE_DISABLED:
return ret
ret["gravity"] = area.gravity
if area.priority != 0:
ret["priority"] = area.priority
if space_override == Area3D.SpaceOverride.SPACE_OVERRIDE_REPLACE:
ret["replace"] = true
ret["stop"] = true
elif space_override == Area3D.SpaceOverride.SPACE_OVERRIDE_COMBINE_REPLACE:
ret["stop"] = true
elif space_override == Area3D.SpaceOverride.SPACE_OVERRIDE_REPLACE_COMBINE:
ret["replace"] = true
if area.gravity_type == Area3D.GravityType.GRAVITY_TYPE_DIRECTIONAL:
var dir: Vector3 = area.gravity_direction * area.global_basis.orthonormalized()
if not dir.is_equal_approx(Vector3.DOWN):
ret["directional"] = { "direction": [dir.x, dir.y, dir.z] }
ret["type"] = "directional"
elif area.gravity_type == Area3D.GravityType.GRAVITY_TYPE_POINT:
var unit_dist: float = area.gravity_point_unit_distance
if unit_dist != 0.0:
ret["point"] = { "unitDistance": unit_dist }
ret["type"] = "point"
return ret


static func from_dictionary(dict: Dictionary, gltf_state: GLTFState) -> CustomGravityArea3D:
if "type" not in dict:
printerr('GLTF gravity import: Missing required field "type", expected "directional", "point", "disc", "torus", "line", or "shaped".')
return null
if "gravity" not in dict:
printerr('GLTF gravity import: Missing required field "gravity", expected a number in meters per second squared.')
return null
var type_string = dict.get("type")
if type_string not in ["directional", "point", "disc", "torus", "line", "shaped"]:
printerr("GLTF gravity import: Invalid gravity type, found: ", dict.get("type"), ' but expected "directional", "point", "disc", "torus", "line", or "shaped".')
return null
var gravity_amount = dict.get("gravity")
if not gravity_amount is float: # All JSON numbers are floats.
printerr("GLTF gravity import: Invalid gravity, found: ", dict.get("gravity"), ' but expected a number.')
return null
var ret: CustomGravityArea3D = CustomGravityArea3D.new()
ret.gravity_type = Area3D.GRAVITY_TYPE_TARGET
ret.custom_gravity_type = _gravity_type_string_to_enum(type_string)
ret.gravity = gravity_amount
var priority = dict.get("priority")
if priority is float: # All JSON numbers are floats.
ret.priority = priority
var replace: bool = dict.get("replace", false)
var stop: bool = dict.get("stop", false)
if replace and stop:
ret.gravity_space_override = Area3D.SpaceOverride.SPACE_OVERRIDE_REPLACE
elif stop:
ret.gravity_space_override = Area3D.SpaceOverride.SPACE_OVERRIDE_COMBINE_REPLACE
elif replace:
ret.gravity_space_override = Area3D.SpaceOverride.SPACE_OVERRIDE_REPLACE_COMBINE
else:
ret.gravity_space_override = Area3D.SpaceOverride.SPACE_OVERRIDE_COMBINE
var sub_dict = dict.get(type_string)
if not sub_dict is Dictionary:
return ret
var direction = sub_dict.get("direction")
if direction is Array:
ret.direction = Vector3(direction[0], direction[1], direction[2])
var unit_distance = sub_dict.get("unitDistance")
if unit_distance is float:
ret.gravity_point_unit_distance = unit_distance
var radius = sub_dict.get("radius")
if radius is float:
ret.radius = radius
var points = sub_dict.get("points")
if points is Array:
var packed_points := PackedVector3Array()
for i in range(0, points.size(), 3):
packed_points.append(Vector3(points[i], points[i + 1], points[i + 2]))
ret.line_points = packed_points
var shape = sub_dict.get("shape")
if shape is float: # Integer but all JSON numbers are floats.
var shape_index: int = shape
if shape_index < 0:
printerr("GLTF gravity import: Invalid shape index, found: ", shape, " but expected a non-negative integer.")
return ret
var state_shapes: Array = gltf_state.get_additional_data(&"GLTFPhysicsShapes")
if shape_index >= state_shapes.size():
printerr("GLTF gravity import: Shape index ", shape_index, " is out of bounds (size=", state_shapes.size(), ").")
return ret
var gltf_shape: GLTFPhysicsShape = state_shapes[shape_index]
ret.shape = gltf_shape.to_resource(true)
return ret


static func _gravity_type_enum_to_string(type: CustomGravityType) -> String:
# The type value may be set to `"directional"`, `"point"`, `"disc"`, `"torus"`, `"line"`, or `"shaped"`.
match type:
CustomGravityType.DIRECTIONAL:
return "directional"
CustomGravityType.POINT:
return "point"
CustomGravityType.DISC:
return "disc"
CustomGravityType.TORUS:
return "torus"
CustomGravityType.LINE:
return "line"
CustomGravityType.SHAPED:
return "shaped"
assert(false, "GLTF gravity export: Invalid gravity type.")
return ""


static func _gravity_type_string_to_enum(type: String) -> CustomGravityType:
match type:
"directional":
return CustomGravityType.DIRECTIONAL
"point":
return CustomGravityType.POINT
"disc":
return CustomGravityType.DISC
"torus":
return CustomGravityType.TORUS
"line":
return CustomGravityType.LINE
"shaped":
return CustomGravityType.SHAPED
printerr("GLTF gravity import: Unknown gravity type: ", type)
return CustomGravityType.DIRECTIONAL
13 changes: 13 additions & 0 deletions addons/omi_extensions/physics_gravity/global_gravity_setter.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class_name GlobalGravitySetter
extends Node


@export var gravity: float = 9.80665
@export var direction: Vector3 = Vector3.DOWN


func _ready() -> void:
var world_space_rid: RID = get_viewport().find_world_3d().space
PhysicsServer3D.area_set_param(world_space_rid, PhysicsServer3D.AREA_PARAM_GRAVITY, gravity)
PhysicsServer3D.area_set_param(world_space_rid, PhysicsServer3D.AREA_PARAM_GRAVITY_VECTOR, direction)
queue_free()
Loading