Files
fps_project_1/addons/dreadpon.spatial_gardener/arborist/arborist.gd

614 lines
21 KiB
GDScript

@tool
extends Node3D
#-------------------------------------------------------------------------------
# Handles managing OctreeManager objects and changes applied when painting
# Instigates updates to OctreeManager MutliMeshInstance (MMI) objects
# To show the correct LOD variant when moving closer/further to plants
#-------------------------------------------------------------------------------
# It's worth considering to split this object into mutliple:
# Octree management
# Updating positions of individual plants (through painting)
# Threaded updates to LODs (if possible)
# However, these functions are very closely related, so maybe I'm overthinking this
const Logger = preload("../utility/logger.gd")
const Globals = preload("../utility/globals.gd")
const FunLib = preload("../utility/fun_lib.gd")
const Greenhouse_Plant = preload("../greenhouse/greenhouse_plant.gd")
const Toolshed_Brush = preload("../toolshed/toolshed_brush.gd")
const PaintingChanges = preload("painting_changes.gd")
const MMIOctreeManager = preload("mmi_octree/mmi_octree_manager.gd")
const UndoRedoInterface = preload("../utility/undo_redo_interface.gd")
const StrokeHandler = preload("stroke_handler/stroke_handler.gd")
const SH_Paint = preload("stroke_handler/sh_paint.gd")
const SH_Erase = preload("stroke_handler/sh_erase.gd")
const SH_Single = preload("stroke_handler/sh_single.gd")
const SH_Reapply = preload("stroke_handler/sh_reapply.gd")
const SH_Manual = preload("stroke_handler/sh_manual.gd")
var MMI_container:Node3D = null
var octree_managers:Array
var gardening_collision_mask:int = 0
# A manual override fot the camera (mainly used in Editor)
var active_camera_override:Camera3D = null
var active_stroke_handler:StrokeHandler = null
var active_painting_changes:PaintingChanges = null
# Threading LOD updates is not working for some reason. Gives error "Condition "!multimesh" is true." when closing a scene
# This might be related to https://github.com/godotengine/godot/pull/54650
# Possibly, there are some leftover references after closing a scene and idk how I'm supposed to clean them up
#var mutex_placement:Mutex = null
#var thread_instance_placement:Thread
#var semaphore_instance_placement:Semaphore
#var exit_instance_placement:bool
#var done_instance_placement:bool
var _undo_redo = null
var debug_redraw_requested_managers:Array = []
var logger = null
signal req_debug_redraw(octree_managers, requested_indexes)
signal member_count_updated(octree_index, new_count)
#-------------------------------------------------------------------------------
# Lifecycle and initialization
#-------------------------------------------------------------------------------
func _init():
set_meta("class", "Arborist")
func _ready():
# Workaround below fixes the problem of instanced nodes "sharing" exported arrays (and resources inside them)
# When instanced in the editor
# See https://github.com/godotengine/godot/issues/16478
# This fix is needed, so we can have multiple instances of same terrain with same plant placement
# But have LOD switch independently for each of these terrains
if octree_managers == null:
octree_managers = []
else:
var octree_managers_copy = octree_managers.duplicate()
octree_managers = []
for octree_manager in octree_managers_copy:
octree_managers.append(octree_manager.duplicate_tree())
logger = Logger.get_for(self, name)
owner = get_tree().get_edited_scene_root()
MMI_container = get_node_or_null("MMI_container")
if MMI_container && !is_instance_of(MMI_container, Node3D):
remove_child(MMI_container)
MMI_container.queue_free()
MMI_container = null
if !MMI_container:
FunLib.free_children(self)
MMI_container = Node3D.new()
MMI_container.name = "MMI_container"
add_child(MMI_container)
MMI_container.owner = owner
for octree_manager in octree_managers:
octree_manager.restore_after_load(MMI_container)
func _enter_tree():
pass
# thread_instance_placement = Thread.new()
# mutex_placement = Mutex.new()
# semaphore_instance_placement = Semaphore.new()
#
# exit_instance_placement = false
# done_instance_placement = true
#
# thread_instance_placement.start(Callable(self,"thread_update_LODs"))
func _notification(what):
match what:
NOTIFICATION_PREDELETE:
for octree_manager in octree_managers:
octree_manager.free_refs()
func _exit_tree():
pass
# This is... weird
# Apparently I need to free any Resources that are left after closing a scene
# I'm not exactly sure why
# And it *might* be destructive to do so in editor
#if Engine.is_editor_hint(): return
#for octree_manager in octree_managers:
#octree_manager.destroy()
# mutex_placement.lock()
# exit_instance_placement = true
# done_instance_placement = false
# mutex_placement.unlock()
#
# semaphore_instance_placement.post()
# thread_instance_placement.wait_to_finish()
#
# thread_instance_placement = null
# mutex_placement = null
# semaphore_instance_placement = null
# Expected to be called inside or after a parent's _ready()
func setup(plant_states):
verify_all_plants(plant_states)
# Restore all OctreeManager objects after load
# Create missing ones
func verify_all_plants(plant_states_to_verify:Array):
if !is_inside_tree(): return
debug_print_lifecycle("verifying for plant_states: " + str(plant_states_to_verify))
for plant_index in range(0, plant_states_to_verify.size()):
if octree_managers.size() - 1 >= plant_index:
octree_managers[plant_index].restore_after_load(MMI_container)
connect_octree_manager(octree_managers[plant_index])
else:
add_plant_octree_manager(plant_states_to_verify[plant_index], plant_index)
#-------------------------------------------------------------------------------
# Management of plant OctreeManager objects
#-------------------------------------------------------------------------------
# Instigate the OctreeManager adding process in response to an external signal
func on_plant_added(plant_state, plant_index:int):
debug_print_lifecycle("plant: %s added at plant_index %d" % [str(plant_state), plant_index])
add_plant_octree_manager(plant_state, plant_index)
request_debug_redraw_from_index(plant_index)
call_deferred("emit_member_count", plant_index)
# Instigate the OctreeManager removal process in response to an external signal
func on_plant_removed(plant_state, plant_index:int):
debug_print_lifecycle("plant: %s removed at plant_index %d" % [str(plant_state), plant_index])
remove_plant_octree_manager(plant_state, plant_index)
request_debug_redraw_from_index(plant_index)
# Up-to-date LOD variants of an OctreeManager
func on_LOD_variant_added(plant_index:int, mesh_index:int, LOD_variant):
debug_print_lifecycle("LOD Variant: %s added at plant_index %d and mesh_index %d" % [str(LOD_variant), plant_index, mesh_index])
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.insert_LOD_variant(LOD_variant, mesh_index)
octree_manager.set_LODs_to_active_index()
# Up-to-date LOD variants of an OctreeManager
func on_LOD_variant_removed(plant_index:int, mesh_index:int):
debug_print_lifecycle("LOD Variant: removed at plant_index %d and mesh_index %d" % [plant_index, mesh_index])
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.remove_LOD_variant(mesh_index)
octree_manager.set_LODs_to_active_index()
# Up-to-date LOD variants of an OctreeManager
func on_LOD_variant_set(plant_index:int, mesh_index:int, LOD_variant):
debug_print_lifecycle("LOD Variant: %s set at plant_index %d and mesh_index %d" % [str(LOD_variant), plant_index, mesh_index])
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.set_LOD_variant(LOD_variant, mesh_index)
octree_manager.set_LODs_to_active_index()
# Up-to-date LOD variants of an OctreeManager
func on_LOD_variant_prop_changed_spawned_spatial(plant_index:int, mesh_index:int, LOD_variant):
debug_print_lifecycle("LOD Variant: %s spawned spatial changed at plant_index %d and mesh_index %d" % [str(LOD_variant), plant_index, mesh_index])
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.set_LOD_variant_spawned_spatial(LOD_variant, mesh_index)
octree_manager.reset_member_spatials()
# Make sure LODs in OctreeNodes correspond to their active_LOD_index
# This is the preffered way to 'refresh' MMIs inside OctreeNodes
func set_LODs_to_active_index(plant_index:int):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.set_LODs_to_active_index()
# Initialize an OctreeManager for a given plant
func add_plant_octree_manager(plant_state, plant_index:int):
var octree_manager:MMIOctreeManager = MMIOctreeManager.new()
octree_manager.init_octree(
plant_state.plant.mesh_LOD_max_capacity, plant_state.plant.mesh_LOD_min_size,
Vector3.ZERO, MMI_container, plant_state.plant.mesh_LOD_min_size)
octree_manager.LOD_max_distance = plant_state.plant.mesh_LOD_max_distance
octree_manager.LOD_kill_distance = plant_state.plant.mesh_LOD_kill_distance
octree_managers.insert(plant_index, octree_manager)
for mesh_index in range (0, plant_state.plant.mesh_LOD_variants.size()):
var LOD_variant = plant_state.plant.mesh_LOD_variants[mesh_index]
octree_manager.insert_LOD_variant(LOD_variant, mesh_index)
connect_octree_manager(octree_manager)
# Remove an OctreeManager for a given plant
func remove_plant_octree_manager(plant_state, plant_index:int):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
disconnect_octree_manager(octree_manager)
octree_manager.prepare_for_removal()
octree_managers.remove_at(plant_index)
# A request to reconfigure an octree
func reconfigure_octree(plant_state, plant_index:int):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.rebuild_octree(plant_state.plant.mesh_LOD_max_capacity, plant_state.plant.mesh_LOD_min_size)
# A request to recenter an octree
func recenter_octree(plant_state, plant_index:int):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.recenter_octree()
# Connect all OctreeManager signals
func connect_octree_manager(octree_manager:MMIOctreeManager):
if !octree_manager.req_debug_redraw.is_connected(on_req_debug_redraw):
octree_manager.req_debug_redraw.connect(on_req_debug_redraw.bind(octree_manager))
# Disconnect all OctreeManager signals
func disconnect_octree_manager(octree_manager:MMIOctreeManager):
if octree_manager.req_debug_redraw.is_connected(on_req_debug_redraw):
octree_manager.req_debug_redraw.disconnect(on_req_debug_redraw)
#-------------------------------------------------------------------------------
# Setting/updating variables to outside signals
#-------------------------------------------------------------------------------
# To be called by a signal from Greenhouse_PlantState -> Gardener -> Arborist
func update_plant_LOD_max_distance(plant_index, val):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.LOD_max_distance = val
# To be called by a signal from Greenhouse_PlantState -> Gardener -> Arborist
func update_plant_LOD_kill_distance(plant_index, val):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.LOD_kill_distance = val
# To be called by a signal from Gardener -> Arborist
func set_gardening_collision_mask(_gardening_collision_mask):
gardening_collision_mask = _gardening_collision_mask
#-------------------------------------------------------------------------------
# Application of brushes and transform generation
#-------------------------------------------------------------------------------
# Create PaintingChanges and a StrokeHandler for this specific brush stroke
func on_stroke_started(brush:Toolshed_Brush, plant_states:Array):
var space_state := get_world_3d().direct_space_state
var camera = get_camera_3d()
active_painting_changes = PaintingChanges.new()
match brush.behavior_brush_type:
brush.BrushType.PAINT:
active_stroke_handler = SH_Paint.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
brush.BrushType.ERASE:
active_stroke_handler = SH_Erase.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
brush.BrushType.SINGLE:
active_stroke_handler = SH_Single.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
brush.BrushType.REAPPLY:
active_stroke_handler = SH_Reapply.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
_:
active_stroke_handler = StrokeHandler.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
debug_print_lifecycle("Stroke %s started" % [active_stroke_handler.get_meta("class")])
# Draw instances at the new brush position
# And collect them all into one PaintingChanges object
func on_stroke_updated(brush_data:Dictionary):
assert(active_stroke_handler)
assert(active_painting_changes)
debug_print_lifecycle("Stroke %s updating..." % [active_stroke_handler.get_meta("class")])
var msec_start = FunLib.get_msec()
# mutex_placement.lock()
var changes = active_stroke_handler.get_stroke_update_changes(brush_data, global_transform)
apply_stroke_update_changes(changes)
# mutex_placement.unlock()
active_painting_changes.append_changes(changes)
var msec_end = FunLib.get_msec()
debug_print_lifecycle("Total stroke %s update took: %s" % [active_stroke_handler.get_meta("class"), FunLib.msec_to_time(msec_end - msec_start)])
# Use collected PaintingChanges to add UndoRedo actions
func on_stroke_finished():
assert(active_stroke_handler)
assert(active_painting_changes)
UndoRedoInterface.create_action(_undo_redo, "Apply Arborist MMI changes", 0, false, self)
UndoRedoInterface.add_do_method(_undo_redo, _action_apply_changes.bind(active_painting_changes))
UndoRedoInterface.add_undo_method(_undo_redo, _action_apply_changes.bind(active_painting_changes.pop_opposite()))
# We toggle this flag to avoid reapplying already commited changes all over again
UndoRedoInterface.commit_action(_undo_redo, false)
debug_print_lifecycle("Stroke %s finished, total changes made: %d" % [active_stroke_handler.get_meta("class"), active_painting_changes.changes.size()])
active_stroke_handler = null
active_painting_changes = null
# A wrapper for applying changes to avoid reaplying UndoRedo actions on commit_action()
func _action_apply_changes(changes):
# mutex_placement.lock()
apply_stroke_update_changes(changes)
# mutex_placement.unlock()
#-------------------------------------------------------------------------------
# Updating OctreeManager objects
#-------------------------------------------------------------------------------
# Replace LOD_Variants inside of a shared array owned by this OctreeManager
func refresh_octree_shared_LOD_variants(plant_index:int, LOD_variants:Array):
if octree_managers.size() > plant_index:
octree_managers[plant_index].set_LOD_variants(LOD_variants)
# Add changes to corresponding OctreeManager queues
# Then process them all at once
func apply_stroke_update_changes(changes:PaintingChanges):
debug_print_lifecycle(" Applying %d stroke changes" % [changes.changes.size()])
var msec_start = FunLib.get_msec()
var affected_octree_managers := []
for change in changes.changes:
var octree_manager:MMIOctreeManager = octree_managers[change.at_index]
match change.change_type:
0:
octree_manager.queue_placeforms_add(change.new_val)
1:
octree_manager.queue_placeforms_remove(change.new_val)
2:
octree_manager.queue_placeforms_set(change.new_val)
if !affected_octree_managers.has(change.at_index):
affected_octree_managers.append(change.at_index)
for index in affected_octree_managers:
var octree_manager = octree_managers[index]
octree_manager.process_queues()
emit_member_count(index)
var msec_end = FunLib.get_msec()
debug_print_lifecycle(" Applying stroke changes took: %s" % [FunLib.msec_to_time(msec_end - msec_start)])
func emit_member_count(octree_index:int):
member_count_updated.emit(octree_index, octree_managers[octree_index].root_octree_node.get_nested_member_count())
func _process(delta):
# try_update_LODs()
if visible:
update_LODs()
request_debug_redraw()
# Trigger a threaded LOD update
#func try_update_LODs():
# var should_post = false
#
# mutex_placement.lock()
# if done_instance_placement:
# done_instance_placement = false
# should_post = true
# mutex_placement.unlock()
#
# if should_post:
# semaphore_instance_placement.post()
# A function that carries out threaded LOD updates
#func thread_update_LODs(arg = null):
# while true:
# semaphore_instance_placement.wait()
#
# var should_exit = false
# mutex_placement.lock()
# if exit_instance_placement:
# should_exit = true
# mutex_placement.unlock()
# if should_exit: break
#
# mutex_placement.lock()
# update_LODs()
# done_instance_placement = true
# mutex_placement.unlock()
# Instigate LOD updates in OctreeManager objects
func update_LODs():
var camera_to_use:Camera3D = get_camera_3d()
if camera_to_use:
var camera_pos := camera_to_use.global_transform.origin
for octree_manager in octree_managers:
octree_manager.update_LODs(camera_pos, global_transform)
# This exists to properly render instances in editor even if there is no forwarded_input()
else:
for octree_manager in octree_managers:
octree_manager.update_LODs_no_camera()
# Add instances as a batch (mostly, as a result of importing Greenhouse data)
func batch_add_instances(placeforms: Array, plant_idx: int):
active_painting_changes = PaintingChanges.new()
active_stroke_handler = SH_Manual.new()
for placeform in placeforms:
active_stroke_handler.add_instance_placeform(placeform, plant_idx, active_painting_changes)
apply_stroke_update_changes(active_painting_changes)
on_stroke_finished()
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
func _unhandled_input(event):
if is_instance_of(event, InputEventKey) && !event.pressed:
if event.keycode == debug_get_dump_tree_key():
for octree_manager in octree_managers:
logger.info(octree_manager.root_octree_node.debug_dump_tree())
#-------------------------------------------------------------------------------
# Utility
#-------------------------------------------------------------------------------
# A hack to get editor camera
# active_camera_override should be set by a Gardener
# In-game just gets an active viewport's camera
func get_camera_3d():
if is_instance_valid(active_camera_override):
return active_camera_override
else:
active_camera_override = null
return get_viewport().get_camera_3d()
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
func _get(property):
match property:
"octree_managers":
return octree_managers
return null
func _set(property, val):
var return_val = true
match property:
"octree_managers":
octree_managers = val
_:
return_val = false
return return_val
func _get_property_list():
var props := [
{
"name": "octree_managers",
"type": TYPE_ARRAY,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE
},
]
return props
func _get_configuration_warnings():
var MMI_container_check = get_node("MMI_container")
if MMI_container_check && is_instance_of(MMI_container_check, Node3D):
return ""
else:
return "Arborist is missing a valid MMI_container child\nSince it should be created automatically, try reloading a scene or recreating a Gardener"
func add_child(node:Node, legible_unique_name:bool = false, internal:InternalMode=0) -> void:
super.add_child(node, legible_unique_name)
update_configuration_warnings()
#-------------------------------------------------------------------------------
# Debug
#-------------------------------------------------------------------------------
# A wrapper to request debug redraw for a specific OctreeManager
func request_debug_redraw_from_index(plant_index):
for index in range(plant_index, octree_managers.size()):
on_req_debug_redraw(octree_managers[index])
# Add an OctreeManager to the debug redraw waiting list
func on_req_debug_redraw(octree_manager:MMIOctreeManager):
if debug_redraw_requested_managers.has(octree_manager): return
debug_redraw_requested_managers.append(octree_manager)
# Request a debug redraw for all OctreeManager objects in a waiting list using a signal
# We manually get all indexes here instead of when an OctreeManager is added to the waiting list
# Because we expect the order of managers might change and indexes will become inaccurate
# Typically called from _process()
func request_debug_redraw():
if debug_redraw_requested_managers.is_empty(): return
var requested_indexes := []
for octree_manager in debug_redraw_requested_managers:
requested_indexes.append(octree_managers.find(octree_manager))
if !requested_indexes.is_empty():
req_debug_redraw.emit(octree_managers)
debug_redraw_requested_managers = []
func debug_get_dump_tree_key():
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/dump_all_octrees_key", 0)
return Globals.index_to_enum(key, Globals.KeyboardKey)
func debug_print_lifecycle(string:String):
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/arborist_log_lifecycle", false): return
logger.info(string)