Skip to content

Commit

Permalink
Add physics shape caching to prevent duplicate shape generation
Browse files Browse the repository at this point in the history
- Deduplicate all physics shapes by asset hash generated on the fly during loading the object
- Accumulate statistics about what we didn't recreate
  • Loading branch information
RevoluPowered committed Mar 26, 2024
1 parent 80a8d03 commit a5f03c1
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 27 deletions.
79 changes: 56 additions & 23 deletions mirror-godot-app/gameplay/space_object/scaled_model.gd
Original file line number Diff line number Diff line change
Expand Up @@ -257,21 +257,48 @@ func _setup_new_physics_colliders(desired_shape_type: String) -> void:
# Note: Trigger or not doesn't matter because the layer is NO_COLLIDE.
model_body.body_mode = JBody3D.BodyMode.STATIC
model_body.set_layer_name(&"NO_COLLIDE")
var asset_hash = Net.file_client._file_cache.get_hash_for_asset(_space_object.asset_data.file_url)
var cache_key = asset_hash + "-" + desired_shape_type

if desired_shape_type == "Model Shapes":
_generate_model_shape_collision()
var promise = await _generate_model_shape_collision(cache_key)
await promise.wait_till_fulfilled()
_space_object.shape = promise.get_result()
_model_provided_shapes_require_static = not _space_object.shape.is_convex()
elif desired_shape_type == "Capsule":
_generate_capsule_shape_collision()
else: # Concave or Convex mesh shape.
_generate_mesh_collision(desired_shape_type == "Concave")
var promise = await _generate_mesh_collision(cache_key, desired_shape_type == "Concave")
await promise.wait_till_fulfilled()
_space_object.shape = promise.get_result()

await get_tree().create_timer(2.0).timeout
_space_object.set_ignore_state_sync(false)


func _generate_model_shape_collision() -> void:
func _generate_capsule_shape_collision() -> void:
var aabb: AABB = TMNodeUtil.get_local_aabb_of_descendants(self)
var capsule = JCapsuleShape3D.new()
capsule.height = max(aabb.size.y, aabb.size.x/2.0)
capsule.radius = aabb.size.x/2.0
var body_transform := Transform3D(Basis.IDENTITY, aabb.get_center())
var compound_shape := JCompoundShape3D.new()
compound_shape.shapes = [capsule]
compound_shape.transforms = [body_transform]
_space_object.shape = compound_shape
_model_provided_shapes_require_static = false


func _generate_model_shape_collision(cache_key: String) -> Promise:
if Zone.hash_requests.has(cache_key):
Zone.hash_requests[cache_key] += 1
return Zone.physics_hash_promises[cache_key]
else:
Zone.hash_requests[cache_key] = 1
Zone.physics_hash_promises[cache_key] = Promise.new()

var shapes: Array[JShape3D] = []
var transforms: Array[Transform3D] = []

for model_body in _model_provided_bodies:
if not is_instance_valid(model_body):
printerr("A model body was invalid! Model nodes should never be freed while the SpaceObject still exists.")
Expand All @@ -281,31 +308,26 @@ func _generate_model_shape_collision() -> void:
if shape:
shapes.push_back(shape)
transforms.push_back(body_transform)

var compound_shape := JCompoundShape3D.new()
compound_shape.shapes = shapes
compound_shape.transforms = transforms
_space_object.shape = compound_shape

_model_provided_shapes_require_static = not compound_shape.is_convex()
var promise = Zone.physics_hash_promises[cache_key]
promise.set_result(compound_shape)
return promise


func _generate_capsule_shape_collision() -> void:
var aabb: AABB = TMNodeUtil.get_local_aabb_of_descendants(self)
var capsule = JCapsuleShape3D.new()
capsule.height = max(aabb.size.y, aabb.size.x/2.0)
capsule.radius = aabb.size.x/2.0
var body_transform := Transform3D(Basis.IDENTITY, aabb.get_center())
var compound_shape := JCompoundShape3D.new()
compound_shape.shapes = [capsule]
compound_shape.transforms = [body_transform]
_space_object.shape = compound_shape
_model_provided_shapes_require_static = false

static var cumulative_collision_mesh_assignment_time = 0


func _generate_mesh_collision(is_concave: bool) -> void:
var mi: Array[Node] = TMNodeUtil.recursive_find_nodes_by_type(self, MeshInstance3D)
func _generate_mesh_collision(cache_key: String, is_concave: bool) -> Promise:
if Zone.hash_requests.has(cache_key):
Zone.hash_requests[cache_key] += 1
return Zone.physics_hash_promises[cache_key]
else:
Zone.hash_requests[cache_key] = 1
Zone.physics_hash_promises[cache_key] = Promise.new()
var start_time = Time.get_unix_time_from_system()
var mi := Util.recursive_find_nodes_of_type(self, MeshInstance3D)
var mesh_instances: Array[MeshInstance3D] = []
mesh_instances.resize(mi.size())
for i in range(mi.size()):
Expand All @@ -315,16 +337,27 @@ func _generate_mesh_collision(is_concave: bool) -> void:

var async_collider_construction = true
var shape: JShape3D = null

if not async_collider_construction:
shape = Zone.shapes_generator.generate_shape_for_meshes(body, mesh_instances, is_concave)[0]
else:
var promise = Zone.shapes_generator.async_generate_shape_for_meshes(body, mesh_instances, is_concave)
assert(promise != null)
await promise.wait_till_fulfilled()
shape = promise.get_result()

assert(shape != null)
body.shape = shape
# to ensure the pointers always match we must set it to the same reference
# this de-duplicates the shapes
# this is faster because one pointer for 50+ shapes, and 0 generation time for them.
var promise = Zone.physics_hash_promises[cache_key]
promise.set_result(body.shape)
var shape_time = Time.get_unix_time_from_system() - start_time
print("took ", shape_time, "to generate collision shape")
cumulative_collision_mesh_assignment_time += shape_time
print("cumulatively we have taken: ", cumulative_collision_mesh_assignment_time, " seconds")
return promise


## Ensure concave shapes are static, since physics engines do not support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ var _mesh_rid_to_convex_shape_map = {
var _meshes_to_generate_queue = [
#MeshPromisePair, (...)
]
var _generate_thread: Thread = Thread.new()

# var _generate_thread: Thread = Thread.new()
var cumulative_collision_shape_generation_time = 0.0
func async_generate_shape_for_meshes(body: JBody3D, in_meshes: Array[MeshInstance3D], is_concave: bool) -> Promise:
var promise = Promise.new()

Expand Down Expand Up @@ -43,6 +43,7 @@ func _thread_generate_collision(mesh_promise_pair: MeshPromisePair):


func generate_shape_for_meshes(body: JBody3D, in_meshes: Array[MeshInstance3D], is_concave: bool) -> Array:
var start_time = Time.get_unix_time_from_system()
var generated_shapes: Array[JShape3D]
var generated_shapes_rid: Array[RID]
var shapes: Array[JShape3D]
Expand Down Expand Up @@ -105,6 +106,10 @@ func generate_shape_for_meshes(body: JBody3D, in_meshes: Array[MeshInstance3D],

# The shape is 100% expected at this point.
assert(result_shape)
var cumulative_time = Time.get_unix_time_from_system() - start_time
print("Import time for shape: ", cumulative_time)
cumulative_collision_shape_generation_time += cumulative_time
print("[", Zone.get_instance_type(), "] Cumulative import time ", cumulative_collision_shape_generation_time)
return [result_shape, generated_shapes, generated_shapes_rid]


Expand All @@ -121,4 +126,4 @@ class MeshPromisePair:
var meshes: Array[MeshInstance3D]
var promise: Promise
var is_concave: bool
var rid_map
var rid_map
3 changes: 3 additions & 0 deletions mirror-godot-app/scripts/autoload/zone/zone.gd
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ enum DENIED_REASON {
@onready var _ws_debug_prints = ProjectSettings.get_setting("debug_flags/show_web_socket_debug", false)
@onready var _space_scene: PackedScene = preload("res://scenes/space_scene.tscn")

var hash_requests: Dictionary = {}
var physics_hash_promises: Dictionary = {}

var current_mode = ZONE_MODE.EDIT
var space: Dictionary = {}
var space_objects: Array = []
Expand Down
28 changes: 27 additions & 1 deletion mirror-godot-app/scripts/net/file_cache.gd
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ signal threaded_model_loaded(cache_key: String, node: Node)

const _STORAGE_CACHE_FILENAME: String = "cache.json"

var _storage_cache: Dictionary = {}
var _storage_cache: Dictionary = {} # url, filename
var _file_hashes: Dictionary = {} # url, hash
var _duplicate_hash_counter: Dictionary = {} # hash, times occoured

var _model_load_queue: Array[KeyPromisePair] = []

Expand Down Expand Up @@ -117,6 +119,30 @@ func get_file_path(file_name: String) -> String:
return storage_path_format % file_name


func get_hash_for_asset(cache_key: String) -> Variant:
cache_key = cache_key.uri_decode()
if not _storage_cache.has(cache_key):
return ""
var file_name: String = _storage_cache[cache_key]
var file_path: String = get_file_path(file_name)
if not FileAccess.file_exists(file_path):
return ""
if not _file_hashes.has(cache_key):
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA1)
var file = FileAccess.open(file_path, FileAccess.READ)
while not file.eof_reached():
ctx.update(file.get_buffer(1024))
var res = ctx.finish()
var hash_string: String = res.hex_encode()
_file_hashes[cache_key] = hash_string
_duplicate_hash_counter[hash_string] = 0
return hash_string
else:
_duplicate_hash_counter[_file_hashes[cache_key]] += 1
return _file_hashes[cache_key]


## Tries to load a file into memory from disk cache and returns the payload or null.
func try_load_cached_file(cache_key: String) -> Variant:
cache_key = cache_key.uri_decode()
Expand Down

0 comments on commit a5f03c1

Please sign in to comment.