|
| 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