built more assets and started playing with foliage painting
This commit is contained in:
613
addons/dreadpon.spatial_gardener/arborist/arborist.gd
Normal file
613
addons/dreadpon.spatial_gardener/arborist/arborist.gd
Normal file
@@ -0,0 +1,613 @@
|
||||
@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)
|
||||
@@ -0,0 +1,405 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# A helper class that creates a placement area (grid)
|
||||
# And makes sure all instances of a given plant are evenly spaced
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# It uses a 2D grid-based algorithm to maintain both:
|
||||
# close-to-desired instance density
|
||||
# an even instance distribution
|
||||
# This article was used as a main reference (grid uses a simple logic and gives good-enough results)
|
||||
# https://www.gamedeveloper.com/disciplines/random-scattering-creating-realistic-landscapes
|
||||
# However, I was not able to derive a solution that creates a grid that evenly projects on a surface of any angle
|
||||
# When you approach 45 degrees you start seeing bigger spacing, which becomes unusable by 90 degrees
|
||||
# So I thought of trying to project a 3D grid, but that raised a lot of questions on it's own
|
||||
# This is still an open question really
|
||||
# So I dug through some code online and decided on a system I discuss in BrushPlacementArea section
|
||||
|
||||
# Algorithm:
|
||||
# Use the brush radius, position and the surface normal under it's center to describe a flat circle in 3D space
|
||||
# This circle provides a plane and bounds for us to generate the transforms. Most other operations operate in 2D space aligned to said plane
|
||||
# Generate a virtual 2D grid with distance between points representing the plant density (less density == bigger distance)
|
||||
# Find all "overlapping" plants of the same type in the sphere's radius
|
||||
# We only check if the placements (origin positions) of plants are within the sphere. No actual boundary tests
|
||||
# Add a max instance check (how many instances total can fit in a circle) and pad the max distance from center slightly OUTSIDE the circle
|
||||
# Last one is needed to prevent spawning at half-distance near the circle's edge
|
||||
# Project the overlaps to our plane and snap them to the closest point on the grid
|
||||
# These grid points become occupied and cannot spawn a new instance
|
||||
# All the points that remain will spawn an instance
|
||||
# Add a random jitter to each placement (it should be smaller than 0.5 to prevent occasional overlap)
|
||||
# Get the raycast start and end positions
|
||||
# They are aligned to the plane's normal and limit_length at the sphere's bounds
|
||||
# Last one prevents out raycast from going outside the sphere
|
||||
# Back in the StrokeHandler, use these positions to raycast to surface and determine actual placement positions
|
||||
|
||||
# The problem with this method is that the grid CAN and WILL be generated at uneven intervals as the brush moves
|
||||
# An example, where "( )" - last grid, "[ ]" - current grid, "P" - point
|
||||
# (P)(P)
|
||||
# (P)(P) <- align
|
||||
# [P][P] <- align
|
||||
# [P][P]
|
||||
# /\
|
||||
# do not align
|
||||
# That leads to some points spawning closer than they should
|
||||
# While not ideal, this is somewhat mitigated with max instances check
|
||||
# And snapping brush to the virtual grid
|
||||
# Ideally I would like the grid to be independent from the brush position
|
||||
# But I don't know how to accurately project 3D grid points on an angled surface while keeping them EVENLY SPACED no matter the angle
|
||||
# 2D grids give me a close enough result
|
||||
|
||||
# TODO Write an article discussing this method that is actually readable by a normal human being
|
||||
|
||||
|
||||
const Globals = preload("../utility/globals.gd")
|
||||
const FunLib = preload("../utility/fun_lib.gd")
|
||||
const Logger = preload("../utility/logger.gd")
|
||||
const MMIOctreeManager = preload("mmi_octree/mmi_octree_manager.gd")
|
||||
const MMIOctreeNode = preload("mmi_octree/mmi_octree_node.gd")
|
||||
|
||||
|
||||
var sphere_pos:Vector3 = Vector3.ZERO
|
||||
var sphere_radius:float = 0.0
|
||||
var sphere_diameter:float = 0.0
|
||||
|
||||
var plane_axis_vectors:Array = []
|
||||
var point_distance:float = 0.0
|
||||
var grid_linear_size:int = 0
|
||||
var grid_offset:float = 0.0
|
||||
var placement_grid:Array = []
|
||||
var diagonal_dilation:float = 1.0
|
||||
|
||||
var placement_overlaps:Array = []
|
||||
var overlapped_member_data:Array = []
|
||||
var overdense_member_data:Array = []
|
||||
|
||||
var raycast_positions:Array = []
|
||||
var max_placements_allowed:int = 0
|
||||
|
||||
var jitter_fraction:float = 0.0
|
||||
|
||||
var logger = null
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Initialization
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _init(__sphere_pos:Vector3,__sphere_radius:float,__plane_normal:Vector3,__jitter_fraction:float = 0.6):
|
||||
logger = Logger.get_for(self)
|
||||
sphere_pos = __sphere_pos
|
||||
sphere_radius = __sphere_radius
|
||||
sphere_diameter = sphere_radius * 2.0
|
||||
jitter_fraction = __jitter_fraction * 0.5
|
||||
init_axis_vectors(__plane_normal)
|
||||
|
||||
|
||||
# Find two perpedicular vectors, so all 3 describe a plane/flat circle
|
||||
func init_axis_vectors(source:Vector3):
|
||||
var nx: float = abs(source.x)
|
||||
var ny: float = abs(source.y)
|
||||
var nz: float = abs(source.z)
|
||||
var axis1:Vector3
|
||||
var axis2:Vector3
|
||||
|
||||
if nz > nx && nz > ny:
|
||||
axis1 = Vector3(1,0,0)
|
||||
else:
|
||||
axis1 = Vector3(0,0,1)
|
||||
|
||||
axis1 = (axis1 - source * axis1.dot(source)).normalized()
|
||||
axis2 = axis1.cross(source).normalized()
|
||||
plane_axis_vectors = [source, axis1, axis2]
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Grid data
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Initialize vital grid data
|
||||
# Grid is used to maintain plant spacing defined by density
|
||||
# Grid data helps to work with that spacing inside a given brush sphere
|
||||
func init_grid_data(plant_density:float, brush_strength:float):
|
||||
point_distance = get_point_distance(plant_density, brush_strength)
|
||||
|
||||
# Find how many linear segments fit within a diameter
|
||||
if point_distance > 0.0:
|
||||
grid_linear_size = floor(sphere_diameter / point_distance)
|
||||
|
||||
# Then offset grid_linear_size by half a segment if it's an odd number
|
||||
if grid_linear_size % 2 == 0:
|
||||
grid_linear_size += 1
|
||||
point_distance *= (sphere_diameter / point_distance * 1) / (sphere_diameter / point_distance)
|
||||
|
||||
# Subtract 1 because number of segments on a line is always 1 less than the number of points
|
||||
# Get max length all points occupy and take half of it
|
||||
grid_offset = (grid_linear_size - 1) * point_distance * -0.5
|
||||
else:
|
||||
grid_linear_size = 0
|
||||
grid_offset = 0.0
|
||||
|
||||
init_placement_grid()
|
||||
debug_visualize_placement_grid("Grid initialized:")
|
||||
debug_log_grid("point_distance: %f, grid_linear_size: %d, grid_offset: %f" % [point_distance, grid_linear_size, grid_offset])
|
||||
|
||||
|
||||
# Initialize the grid itself excluding points outside the sphere/plane circle
|
||||
func init_placement_grid():
|
||||
placement_grid = []
|
||||
var radius_squared = pow(sphere_radius, 2)
|
||||
|
||||
for x in range(0, grid_linear_size):
|
||||
placement_grid.append([])
|
||||
placement_grid[x].resize(grid_linear_size)
|
||||
for y in range(0, grid_linear_size):
|
||||
var local_pos = grid_coord_to_local_pos(Vector2(x, y))
|
||||
var dist = local_pos.length_squared()
|
||||
|
||||
if dist <= radius_squared:
|
||||
placement_grid[x][y] = true
|
||||
max_placements_allowed += 1
|
||||
else:
|
||||
placement_grid[x][y] = false
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Overlaps
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Initialize placements overlapping with the given sphere
|
||||
# Resulting placements will be in range (-sphere_radius, sphere_radius)
|
||||
# Edge extension is used to grad one more/one less loop of overlaps
|
||||
# Typically used to grab members that were displaced with jitter_fraction
|
||||
# And happen to be outside our sphere_radius, but still belong to overlapped grid cells
|
||||
func init_placement_overlaps(octree_manager:MMIOctreeManager, edge_extension:int = 0):
|
||||
placement_overlaps = []
|
||||
overlapped_member_data = []
|
||||
|
||||
var max_dist = sphere_radius + point_distance * edge_extension
|
||||
get_overlap_member_data(octree_manager.root_octree_node, max_dist)
|
||||
|
||||
|
||||
# Recursively calculate placement overlaps in an octree
|
||||
func get_overlap_member_data(octree_node:MMIOctreeNode, max_dist:float):
|
||||
var max_bounds_to_center_dist = octree_node.max_bounds_to_center_dist
|
||||
var dist_node: float = clamp((octree_node.center_pos - sphere_pos).length() - max_bounds_to_center_dist - sphere_radius, 0.0, INF)
|
||||
if dist_node >= max_dist: return
|
||||
|
||||
if !octree_node.is_leaf:
|
||||
for child_node in octree_node.child_nodes:
|
||||
get_overlap_member_data(child_node, max_dist)
|
||||
else:
|
||||
var max_dist_squared = pow(max_dist, 2.0)
|
||||
var node_address = octree_node.get_address()
|
||||
for member_idx in range(0, octree_node.member_count()):
|
||||
var placeform = octree_node.get_placeform(member_idx)
|
||||
var placement = placeform[0] - sphere_pos
|
||||
var dist_squared = placement.length_squared()
|
||||
if dist_squared <= max_dist_squared:
|
||||
placement_overlaps.append(placement)
|
||||
overlapped_member_data.append({"node_address": node_address, "member_idx": member_idx})
|
||||
|
||||
|
||||
# Get all overlaps that don't fit into the density grid
|
||||
func get_placeforms_for_deletion():
|
||||
if overdense_member_data.size() <= 0: return []
|
||||
|
||||
var placeforms_for_deletion := []
|
||||
# Don't delete more than is exessive or actually overdense
|
||||
var deletion_count: int = min(overlapped_member_data.size() - max_placements_allowed, overdense_member_data.size())
|
||||
var deletion_increment := float(deletion_count) / float(overdense_member_data.size())
|
||||
var deletion_progress := 0.0
|
||||
|
||||
if deletion_increment <= 0: return []
|
||||
|
||||
# # This part picks every [N]th member for deletion
|
||||
# # [N] is defined by deletion_increment and can be fractional
|
||||
# # E.g. we can delete an approximation of every 0.3th, 0.75th, etc. element
|
||||
for index in range(0, overdense_member_data.size()):
|
||||
deletion_progress += deletion_increment
|
||||
if deletion_progress >= 1.0:
|
||||
deletion_progress -= 1.0
|
||||
placeforms_for_deletion.append(overdense_member_data[index])
|
||||
|
||||
return placeforms_for_deletion
|
||||
|
||||
|
||||
# Mark grid coordinates as invalid if they are already occupied
|
||||
# (if there is a plant origin near a grid point)
|
||||
func invalidate_occupied_points():
|
||||
for placement_index in range(0, placement_overlaps.size()):
|
||||
var placement_overlap = placement_overlaps[placement_index]
|
||||
# Project a local-space overlap to our plane
|
||||
var projected_overlap = Vector2(plane_axis_vectors[1].dot(placement_overlap), plane_axis_vectors[2].dot(placement_overlap))
|
||||
var grid_coord = local_pos_to_grid_coord(projected_overlap)
|
||||
grid_coord = Vector2(clamp(grid_coord.x, 0.0, grid_linear_size - 1), clamp(grid_coord.y, 0.0, grid_linear_size - 1))
|
||||
|
||||
if placement_grid.size() > 0 && placement_grid[grid_coord.x][grid_coord.y]:
|
||||
invalidate_self_or_neighbor(grid_coord)
|
||||
else:
|
||||
overdense_member_data.append(overlapped_member_data[placement_index])
|
||||
|
||||
|
||||
|
||||
func invalidate_self_or_neighbor(grid_coord:Vector2):
|
||||
if placement_grid[grid_coord.x][grid_coord.y]:
|
||||
placement_grid[grid_coord.x][grid_coord.y] = false
|
||||
|
||||
# Because our grid depends on brush position, sometimes we have cells that appear unoccupied
|
||||
# (Due to placements being *slightly* outside the cell)
|
||||
# So we nudge our overlaps one cell in whatever direction
|
||||
|
||||
# CURRENT VERSION DOESN'T SOLVE THE ISSUE
|
||||
# THIS NEEDS TO APPROXIMATE THE BEST FITTING CELL BY COMPARING DISTANCES
|
||||
# AND AT THIS POINT IT'S TOO MANY CALCULATIONS
|
||||
# SO WE SUSPEND THIS FOR NOW
|
||||
|
||||
# else:
|
||||
# var new_grid_coord
|
||||
# var offset_lookup = [
|
||||
# Vector2(-1,-1), Vector2(0,-1), Vector2(1,-1),
|
||||
# Vector2(-1,0), Vector2(1,0),
|
||||
# Vector2(-1,1), Vector2(0,1), Vector2(1,1)]
|
||||
# for offset in offset_lookup:
|
||||
# new_grid_coord = grid_coord + offset
|
||||
# if placement_grid[new_grid_coord.x][new_grid_coord.y]:
|
||||
# placement_grid[new_grid_coord.x][new_grid_coord.y] = false
|
||||
# break
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Raycast setup
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Get raycast start and end positions that go through the whole sphere
|
||||
# Rays will start and end at the opposite sphere borders
|
||||
# And will have the same direction as the brush normal
|
||||
func get_valid_raycast_positions() -> Array:
|
||||
invalidate_occupied_points()
|
||||
debug_visualize_placement_grid("Grid invalidated occupied points:")
|
||||
generate_raycast_positions()
|
||||
return raycast_positions
|
||||
|
||||
|
||||
# Generate a randomized placement for each point on the grid
|
||||
# And deproject it onto our brush sphere using the surface normal
|
||||
func generate_raycast_positions():
|
||||
raycast_positions = []
|
||||
for x in range(0, grid_linear_size):
|
||||
for y in range(0, grid_linear_size):
|
||||
if !placement_grid[x][y]: continue
|
||||
var grid_coord := Vector2(x, y)
|
||||
var UV_jitter := Vector2(randf_range(-jitter_fraction, jitter_fraction), randf_range(-jitter_fraction, jitter_fraction))
|
||||
grid_coord += UV_jitter
|
||||
var centered_UV := grid_coord_to_centered_UV(grid_coord)
|
||||
|
||||
# Compensating a floating point error by padding the value a bit
|
||||
if centered_UV.length_squared() > 0.999:
|
||||
centered_UV = centered_UV.limit_length(0.999)
|
||||
|
||||
var UV_distance_to_surface:Vector3 = sqrt(1.0 - (pow(centered_UV.x, 2) + pow(centered_UV.y, 2))) * plane_axis_vectors[0]
|
||||
var UV_point_on_plane:Vector3 = centered_UV.x * plane_axis_vectors[1] + centered_UV.y * plane_axis_vectors[2]
|
||||
var raycast_start:Vector3 = sphere_pos + sphere_radius * (UV_point_on_plane + UV_distance_to_surface)
|
||||
var raycast_end:Vector3 = sphere_pos + sphere_radius * (UV_point_on_plane - UV_distance_to_surface)
|
||||
raycast_positions.append([raycast_start, raycast_end])
|
||||
|
||||
# This was made to make sure we don't go over a max instance limit
|
||||
# I refactored placement logic to snap brush_pos to a density grid
|
||||
# Yet it doesn't 100% work on angled surfaces
|
||||
# We still might go over max placements, hence the limit check below
|
||||
# The percieved visual density should be unaffected though, especially at high (>= 0.5) jitter
|
||||
while raycast_positions.size() + placement_overlaps.size() > max_placements_allowed && !raycast_positions.is_empty():
|
||||
raycast_positions.remove_at(randi() % raycast_positions.size())
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Utility
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Get a linear distance between two points
|
||||
# Separated into a function because we need this in StrokeHandler as well
|
||||
static func get_point_distance(plant_density, brush_strength) -> float:
|
||||
# Convert square density to linear density
|
||||
# Then density per PLANT_DENSITY_UNITS to density per 1 unit
|
||||
# That is a distance between two points
|
||||
if brush_strength <= 0.0:
|
||||
return 0.0
|
||||
return (Globals.PLANT_DENSITY_UNITS / sqrt(plant_density * brush_strength))
|
||||
|
||||
|
||||
# Convert grid coordinates to local position around the sphere center
|
||||
# Resulting vector will be in range (-sphere_radius, sphere_radius)
|
||||
func grid_coord_to_local_pos(grid_coord:Vector2) -> Vector2:
|
||||
return Vector2(
|
||||
grid_coord.x * point_distance + grid_offset,
|
||||
grid_coord.y * point_distance + grid_offset
|
||||
)
|
||||
|
||||
|
||||
# Convert local position to grid coordinates
|
||||
# Resulting vector will try to fit in range (0, grid_linear_size)
|
||||
# (indexes might be outside the grid if positions lie outside, so the result usually should be limit_length or rejected manually)
|
||||
func local_pos_to_grid_coord(local_pos:Vector2) -> Vector2:
|
||||
if point_distance <= 0.0:
|
||||
return Vector2.ZERO
|
||||
|
||||
return Vector2(
|
||||
round((local_pos.x - grid_offset) / point_distance),
|
||||
round((local_pos.y - grid_offset) / point_distance)
|
||||
)
|
||||
|
||||
|
||||
# Convert grid coordinates to UV space in range (-1.0, 1.0)
|
||||
func grid_coord_to_centered_UV(grid_coord:Vector2) -> Vector2:
|
||||
var local_pos = grid_coord_to_local_pos(grid_coord)
|
||||
return local_pos / sphere_radius
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Debug
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Print a nicely formatted placement grid to console
|
||||
func debug_visualize_placement_grid(prefix:String = ""):
|
||||
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/brush_placement_area_log_grid", false): return
|
||||
|
||||
if prefix != "":
|
||||
logger.info(prefix)
|
||||
if placement_grid.size() > 0:
|
||||
for y in range(0, placement_grid[0].size()):
|
||||
var string = "|"
|
||||
for x in range(0, placement_grid.size()):
|
||||
if placement_grid[x][y]:
|
||||
string += " "
|
||||
else:
|
||||
string += "X"
|
||||
if x < placement_grid[x].size() - 1:
|
||||
string += "|"
|
||||
string += "|"
|
||||
logger.info(string)
|
||||
print("\n")
|
||||
|
||||
|
||||
func debug_log_grid(string:String):
|
||||
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/brush_placement_area_log_grid", false): return
|
||||
logger.info(string)
|
||||
@@ -0,0 +1,369 @@
|
||||
@tool
|
||||
extends Resource
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Handles higher-level management of OctreeNode objects
|
||||
# Creation of new trees (octree roots), some of the growing/collapsing functionality
|
||||
# Exposes lifecycle management to outside programs
|
||||
# And passes changes made to members/plants to its OctreeNodes
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
const MMIOctreeNode = preload("mmi_octree_node.gd")
|
||||
const FunLib = preload("../../utility/fun_lib.gd")
|
||||
const DponDebugDraw = preload("../../utility/debug_draw.gd")
|
||||
const GreenhouseLODVariant = preload("../../greenhouse/greenhouse_LOD_variant.gd")
|
||||
|
||||
|
||||
@export var root_octree_node: Resource = null
|
||||
@export var LOD_variants : Array[GreenhouseLODVariant] : set = set_LOD_variants
|
||||
@export var LOD_max_distance:float
|
||||
@export var LOD_kill_distance:float
|
||||
|
||||
var add_placeforms_queue:Array
|
||||
var remove_placeforms_queue:Array
|
||||
var set_placeforms_queue:Array
|
||||
|
||||
|
||||
signal req_debug_redraw
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _init():
|
||||
resource_local_to_scene = true
|
||||
set_meta("class", "MMIOctreeManager")
|
||||
resource_name = "MMIOctreeManager"
|
||||
|
||||
if LOD_variants == null || LOD_variants.is_empty():
|
||||
LOD_variants = []
|
||||
add_placeforms_queue = []
|
||||
remove_placeforms_queue = []
|
||||
set_placeforms_queue = []
|
||||
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
if is_instance_valid(root_octree_node):
|
||||
# Avoid memory leaks when OctreeNode leaks MMI nodes and spawned spatials
|
||||
root_octree_node.prepare_for_removal()
|
||||
|
||||
|
||||
# Duplictes the octree structure
|
||||
func duplicate_tree():
|
||||
var copy = duplicate(false)
|
||||
copy.root_octree_node = copy.root_octree_node.duplicate_tree()
|
||||
copy.connect_node(copy.root_octree_node)
|
||||
LOD_variants = LOD_variants.duplicate()
|
||||
return copy
|
||||
|
||||
|
||||
func deep_copy():
|
||||
var copy = duplicate(false)
|
||||
copy.root_octree_node = copy.root_octree_node.deep_copy()
|
||||
copy.connect_node(copy.root_octree_node)
|
||||
LOD_variants = LOD_variants.duplicate()
|
||||
return copy
|
||||
|
||||
|
||||
# Restore any states that might be broken after loading OctreeNode objects
|
||||
func restore_after_load(__MMI_container:Node3D):
|
||||
if is_instance_valid(root_octree_node):
|
||||
root_octree_node.restore_after_load(__MMI_container, LOD_variants)
|
||||
connect_node(root_octree_node)
|
||||
request_debug_redraw()
|
||||
|
||||
|
||||
func init_octree(members_per_node:int, root_extent:float, center:Vector3 = Vector3.ZERO, MMI_container:Node3D = null, min_leaf_extent:float = 0.0):
|
||||
root_octree_node = MMIOctreeNode.new(null, members_per_node, root_extent, center, -1, min_leaf_extent, MMI_container, LOD_variants)
|
||||
connect_node(root_octree_node)
|
||||
request_debug_redraw()
|
||||
|
||||
|
||||
func connect_node(octree_node:MMIOctreeNode):
|
||||
assert(octree_node)
|
||||
FunLib.ensure_signal(octree_node.placeforms_rejected, grow_to_members)
|
||||
FunLib.ensure_signal(octree_node.collapse_self_possible, collapse_root)
|
||||
FunLib.ensure_signal(octree_node.req_debug_redraw, request_debug_redraw)
|
||||
|
||||
|
||||
func disconnect_node(octree_node:MMIOctreeNode):
|
||||
assert(octree_node)
|
||||
octree_node.placeforms_rejected.disconnect(grow_to_members)
|
||||
octree_node.collapse_self_possible.disconnect(collapse_root)
|
||||
octree_node.req_debug_redraw.disconnect(request_debug_redraw)
|
||||
|
||||
|
||||
func prepare_for_removal():
|
||||
if root_octree_node:
|
||||
root_octree_node.prepare_for_removal()
|
||||
|
||||
|
||||
# Free anything that might incur a circular reference or a memory leak
|
||||
# Anything that is @export'ed is NOT touched here
|
||||
# We count on Godot's own systems to handle that in whatever way works best
|
||||
# TODO: this is very similar to prepare_for_removal(), need to determine how best to combine the two
|
||||
# will need to happen around v2.0.0, since it's a very risky change
|
||||
func free_refs():
|
||||
if !root_octree_node: return
|
||||
root_octree_node.free_refs()
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Restructuring
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Rebuild the tree with new extent and member limitations
|
||||
# The resulting octree node layout depends on the order of members in which they are added
|
||||
# Hence the layout may difer if the members are the same, but belong to different nodes each time
|
||||
# I.e. it can't be predicted with members_per_node and min_leaf_extent alone, for now it is (as far as it matters) non-deterministic
|
||||
func rebuild_octree(members_per_node:int, min_leaf_extent:float):
|
||||
assert(root_octree_node)
|
||||
var all_placeforms:Array = []
|
||||
root_octree_node.get_nested_placeforms(all_placeforms)
|
||||
root_octree_node.prepare_for_removal()
|
||||
|
||||
init_octree(members_per_node, min_leaf_extent, Vector3.ZERO,
|
||||
root_octree_node.MMI_container, min_leaf_extent)
|
||||
|
||||
if !all_placeforms.is_empty():
|
||||
queue_placeforms_add_bulk(all_placeforms)
|
||||
process_queues()
|
||||
request_debug_redraw()
|
||||
|
||||
debug_manual_root_logger("rebuilt root")
|
||||
|
||||
|
||||
# Recenter a tree and shrink to fit it's current members
|
||||
func recenter_octree():
|
||||
assert(root_octree_node)
|
||||
|
||||
var last_root:MMIOctreeNode = root_octree_node
|
||||
var all_placeforms:Array = []
|
||||
last_root.get_nested_placeforms(all_placeforms)
|
||||
last_root.prepare_for_removal()
|
||||
|
||||
var new_center:Vector3 = Vector3.ZERO
|
||||
var new_extent:float = last_root.min_leaf_extent
|
||||
|
||||
if all_placeforms.size() > 0:
|
||||
for placeform in all_placeforms:
|
||||
new_center += placeform[0]
|
||||
new_center /= all_placeforms.size()
|
||||
|
||||
for placeform in all_placeforms:
|
||||
var delta_pos = (placeform[0] - new_center).abs()
|
||||
new_extent = max(new_extent, max(delta_pos.x, max(delta_pos.y, delta_pos.z)))
|
||||
|
||||
init_octree(last_root.max_members, new_extent, new_center,
|
||||
root_octree_node.MMI_container, last_root.min_leaf_extent)
|
||||
|
||||
if !all_placeforms.is_empty():
|
||||
queue_placeforms_add_bulk(all_placeforms)
|
||||
process_queues()
|
||||
request_debug_redraw()
|
||||
|
||||
debug_manual_root_logger("recentered root")
|
||||
|
||||
|
||||
# Grow the tree to fit any members outside it's current bounds (by creating a whole new layer on top)
|
||||
func grow_to_members(placeforms:Array):
|
||||
assert(root_octree_node) # 'root_octree_node' is not initialized
|
||||
assert(placeforms.size() > 0) # 'placeforms' is empty
|
||||
|
||||
var target_point = placeforms[0][0]
|
||||
|
||||
var last_root:MMIOctreeNode = root_octree_node
|
||||
disconnect_node(last_root)
|
||||
var last_octant = last_root._map_point_to_opposite_octant(target_point)
|
||||
var new_center = last_root.center_pos - last_root._get_octant_center_offset(last_octant)
|
||||
|
||||
init_octree(last_root.max_members, last_root.extent * 2.0, new_center, last_root.MMI_container, last_root.min_leaf_extent)
|
||||
debug_manual_root_logger("grew to members")
|
||||
root_octree_node.adopt_child(last_root, last_octant)
|
||||
var root_copy = root_octree_node
|
||||
|
||||
add_placeforms(placeforms)
|
||||
root_copy.try_collapse_children(0)
|
||||
|
||||
|
||||
# Make one of the root's children the new root
|
||||
func collapse_root(new_root_octant):
|
||||
assert(root_octree_node) # 'root_octree_node' is not initialized
|
||||
|
||||
var last_root:MMIOctreeNode = root_octree_node
|
||||
disconnect_node(last_root)
|
||||
|
||||
root_octree_node = last_root.child_nodes[new_root_octant]
|
||||
last_root.collapse_self(new_root_octant)
|
||||
|
||||
connect_node(root_octree_node)
|
||||
root_octree_node.safe_init_root()
|
||||
root_octree_node.try_collapse_self(0)
|
||||
|
||||
|
||||
func get_all_placeforms(target_array: Array = []):
|
||||
if root_octree_node:
|
||||
return root_octree_node.get_nested_placeforms(target_array)
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Processing members
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Queue changes for bulk processing
|
||||
func queue_placeforms_add(placeform):
|
||||
add_placeforms_queue.append(placeform)
|
||||
|
||||
|
||||
func queue_placeforms_add_bulk(placeforms: Array):
|
||||
add_placeforms_queue.append_array(placeforms)
|
||||
|
||||
|
||||
# Queue changes for bulk processing
|
||||
func queue_placeforms_remove(placeform):
|
||||
remove_placeforms_queue.append(placeform)
|
||||
|
||||
|
||||
# Queue changes for bulk processing
|
||||
func queue_placeforms_set(change):
|
||||
set_placeforms_queue.append(change)
|
||||
|
||||
|
||||
# Bulk process the queues
|
||||
func process_queues():
|
||||
assert(root_octree_node) # 'root_octree_node' is not initialized
|
||||
var affected_addressed := []
|
||||
|
||||
if !add_placeforms_queue.is_empty():
|
||||
add_placeforms(add_placeforms_queue)
|
||||
if !remove_placeforms_queue.is_empty():
|
||||
remove_placeforms(remove_placeforms_queue)
|
||||
if !set_placeforms_queue.is_empty():
|
||||
set_placeforms(set_placeforms_queue)
|
||||
|
||||
add_placeforms_queue = []
|
||||
remove_placeforms_queue = []
|
||||
set_placeforms_queue = []
|
||||
|
||||
# Make sure we update LODs even for nodes at max LOD index
|
||||
# Since we changed their children most likely
|
||||
set_LODs_to_active_index()
|
||||
|
||||
|
||||
func add_placeforms(placeforms:Array):
|
||||
assert(root_octree_node) # 'root_octree_node' is not initialized
|
||||
assert(placeforms.size() > 0) # 'placeforms' is empty
|
||||
|
||||
root_octree_node.add_members(placeforms)
|
||||
root_octree_node.MMI_refresh_instance_placements_recursive()
|
||||
request_debug_redraw()
|
||||
|
||||
|
||||
func remove_placeforms(placeforms:Array):
|
||||
assert(root_octree_node) # 'root_octree_node' is not initialized
|
||||
assert(placeforms.size() > 0) # 'placeforms' is empty
|
||||
|
||||
root_octree_node.remove_members(placeforms)
|
||||
root_octree_node.process_collapse_children()
|
||||
root_octree_node.process_collapse_self()
|
||||
root_octree_node.MMI_refresh_instance_placements_recursive()
|
||||
request_debug_redraw()
|
||||
|
||||
# if root_octree_node.child_nodes.size() <= 0 && root_octree_node.members.size() <= 0:
|
||||
# reset_root_size()
|
||||
|
||||
|
||||
func set_placeforms(changes:Array):
|
||||
assert(root_octree_node) # 'root_octree_node' is not initialized
|
||||
assert(changes.size() > 0) # 'changes' is empty
|
||||
|
||||
root_octree_node.set_members(changes)
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# LOD management
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
func set_LOD_variants(val):
|
||||
LOD_variants.resize(0)
|
||||
for LOD_variant in val:
|
||||
LOD_variants.append(LOD_variant)
|
||||
|
||||
|
||||
# Up-to-date LOD variants of an OctreeNode
|
||||
func insert_LOD_variant(variant, index:int):
|
||||
LOD_variants.insert(index, variant)
|
||||
|
||||
|
||||
# Up-to-date LOD variants of an OctreeNode
|
||||
func remove_LOD_variant(index:int):
|
||||
LOD_variants.remove_at(index)
|
||||
|
||||
|
||||
# Up-to-date LOD variants of an OctreeNode
|
||||
func set_LOD_variant(variant, index:int):
|
||||
LOD_variants[index] = variant
|
||||
|
||||
|
||||
# Up-to-date LOD variants of an OctreeNode
|
||||
func set_LOD_variant_spawned_spatial(variant, index:int):
|
||||
# No need to manually set spawned_spatial, it will be inherited from parent resource
|
||||
|
||||
# /\ I don't quite remember what this comment meant, but since LOD_Variants are shared
|
||||
# It seems to imply that the line below in not neccessary
|
||||
# So I commented it out for now
|
||||
# LOD_variants[index].spawned_spatial = variant
|
||||
pass
|
||||
|
||||
|
||||
func reset_member_spatials():
|
||||
root_octree_node.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():
|
||||
root_octree_node.set_LODs_to_active_index()
|
||||
|
||||
|
||||
# Update LODs in OctreeNodes depending on their distance to camera
|
||||
func update_LODs(camera_pos:Vector3, container_transform:Transform3D):
|
||||
camera_pos = container_transform.affine_inverse() * camera_pos
|
||||
root_octree_node.update_LODs(camera_pos, LOD_max_distance, LOD_kill_distance)
|
||||
|
||||
|
||||
func update_LODs_no_camera():
|
||||
root_octree_node.update_LODs(Vector3.ZERO, -1.0, -1.0)
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Debug
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# A callback to request a debug redraw
|
||||
func request_debug_redraw():
|
||||
req_debug_redraw.emit()
|
||||
|
||||
|
||||
# Manually trigger a Logger message when an OctreeNode doesn't know an important action happened
|
||||
func debug_manual_root_logger(message:String):
|
||||
root_octree_node.print_address(message)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# A storage object for changes to the octree members
|
||||
# To be passed to UndoRedo or executed on the spot
|
||||
# Can also generate opposite actions (provided it's set up correctly)
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
enum ChangeType {APPEND, ERASE, SET}
|
||||
|
||||
var changes:Array = []
|
||||
var _opposite_changes:Array = []
|
||||
|
||||
|
||||
|
||||
|
||||
func _init(__changes:Array = []):
|
||||
changes = __changes
|
||||
|
||||
|
||||
# Add both the current and opposite changes
|
||||
func add_change(change_type:int, at_index:int, new_val, old_val):
|
||||
var change:Change = Change.new(change_type, at_index, new_val, old_val)
|
||||
changes.append(change)
|
||||
_opposite_changes.append(get_opposite_change(change))
|
||||
|
||||
|
||||
# Append second PaintingChanges non-destructively to the second object
|
||||
func append_changes(painting_changes):
|
||||
for change in painting_changes.changes:
|
||||
add_change(change.change_type, change.at_index, change.new_val, change.old_val)
|
||||
|
||||
|
||||
# Generate an opposite action
|
||||
# For now it really just swaps new_val and old_val
|
||||
# But I'm keeping it as-is just in case I need something more complex
|
||||
func get_opposite_change(change):
|
||||
var opposite_change:Change = null
|
||||
match change.change_type:
|
||||
ChangeType.APPEND:
|
||||
opposite_change = Change.new(ChangeType.ERASE, change.at_index, change.old_val, change.new_val)
|
||||
ChangeType.ERASE:
|
||||
opposite_change = Change.new(ChangeType.APPEND, change.at_index, change.old_val, change.new_val)
|
||||
ChangeType.SET:
|
||||
opposite_change = Change.new(ChangeType.SET, change.at_index, change.old_val, change.new_val)
|
||||
|
||||
return opposite_change
|
||||
|
||||
|
||||
# Get all opposite changes as a new PaintingChanges object and remove them from the current one
|
||||
func pop_opposite():
|
||||
var opposite = get_script().new(_opposite_changes)
|
||||
_opposite_changes = []
|
||||
return opposite
|
||||
|
||||
|
||||
func _to_string():
|
||||
var string = "["
|
||||
for change in changes:
|
||||
string += str(change) + ","
|
||||
string.trim_suffix(",")
|
||||
string += "]"
|
||||
|
||||
return string
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# A storage object for a specific octree member change
|
||||
# To be generated and stored by PaintingChanges
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Change extends RefCounted:
|
||||
|
||||
var change_type:int = -1
|
||||
var at_index:int = -1
|
||||
var new_val = null
|
||||
var old_val = null
|
||||
|
||||
|
||||
func _init(_change_type:int = -1, _at_index:int = -1, _new_val = null, _old_val = null):
|
||||
change_type = _change_type
|
||||
at_index = _at_index
|
||||
new_val = _new_val
|
||||
old_val = _old_val
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "[%d, %d, %s, %s]" % [change_type, at_index, str(new_val), str(old_val)]
|
||||
51
addons/dreadpon.spatial_gardener/arborist/placeform.gd
Normal file
51
addons/dreadpon.spatial_gardener/arborist/placeform.gd
Normal file
@@ -0,0 +1,51 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# PLACEment transFORM
|
||||
# A pseudo-struct meant to store a placement (initial position), surface normal
|
||||
# Final Transform3D and an occupied octree octant (what part of the 2x2x2 cube it's in)
|
||||
#
|
||||
# Originally was a resource, but after some quick tests, the overhead of having
|
||||
# Thousands of Resources as simple containers became apparent
|
||||
# It was decided to use an Array as a fastest and most compact built-in container
|
||||
#
|
||||
# This script provides an function library to more easily construct such arrays
|
||||
# And provide access to methods that were formerly part of this Resource
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# [0] - placement,
|
||||
# [1] - surface_normal
|
||||
# [2] - transform
|
||||
# [3] - octree_octant
|
||||
|
||||
|
||||
static func mk(placement:Vector3 = Vector3(), surface_normal:Vector3 = Vector3(), transform:Transform3D = Transform3D(), octree_octant:int = 0) -> Array:
|
||||
return [
|
||||
# A designated position for an instance
|
||||
placement,
|
||||
# A normal of the surface
|
||||
surface_normal,
|
||||
# An actual transform derived from placement including random offsets, rotations, scaling, etc.
|
||||
transform,
|
||||
# Occupied octant is mostly used to quick access the child node of an octree node
|
||||
# E.g. when aplying PaintingChanges
|
||||
octree_octant
|
||||
]
|
||||
|
||||
|
||||
static func to_str(placeform: Array) -> String:
|
||||
return '[%s, %s, %s, %s, %d]' % [str(placeform[0]), str(placeform[1]), str(placeform[2].basis), str(placeform[2].origin), placeform[3]]
|
||||
|
||||
|
||||
static func get_origin_offset(placeform: Array) -> float:
|
||||
var difference = placeform[2].origin - placeform[0]
|
||||
var offset = placeform[1].dot(difference.normalized()) * difference.length()
|
||||
return offset
|
||||
|
||||
|
||||
static func set_placement_from_origin_offset(placeform: Array, offset: float):
|
||||
placeform[0] = placeform[2].origin - placeform[1] * offset
|
||||
return placeform
|
||||
@@ -0,0 +1,49 @@
|
||||
@tool
|
||||
extends Resource
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# A resource that stores a placement (initial position), surface normal
|
||||
# Final Transform3D and an occupied octree octant (what part of the 2x2x2 cube it's in)
|
||||
# Ideally this should be a struct to avoid the overhead of saving/loading thousands of small resources
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# TODO find out if there *is* any overhead to this and if yes - make this a C++ struct or at least a GDScript dictionary
|
||||
|
||||
|
||||
# A designated position for an instance
|
||||
@export var placement:Vector3 = Vector3()
|
||||
# A normal of the surface
|
||||
@export var surface_normal:Vector3 = Vector3()
|
||||
# An actual transform derived from placement including random offsets, rotations, scaling, etc.
|
||||
@export var transform:Transform3D = Transform3D()
|
||||
# Occupied octant is mostly used to quick access the child node of an octree node
|
||||
# E.g. when aplying PaintingChanges
|
||||
@export var octree_octant:int = 0
|
||||
|
||||
|
||||
|
||||
|
||||
func _init(_placement:Vector3 = Vector3(), _surface_normal:Vector3 = Vector3(), _transform:Transform3D = Transform3D(), _octree_octant:int = 0):
|
||||
set_meta("class", "PlacementTransform")
|
||||
resource_name = "PlacementTransform"
|
||||
|
||||
placement = _placement
|
||||
surface_normal = _surface_normal
|
||||
transform = _transform
|
||||
octree_octant = _octree_octant
|
||||
|
||||
|
||||
func _to_string():
|
||||
return '[%s, %s, %s, %s, %d]' % [str(placement), str(surface_normal), str(transform.origin), str(transform.basis), octree_octant]
|
||||
|
||||
|
||||
func get_origin_offset() -> float:
|
||||
var difference = transform.origin - placement
|
||||
var offset = surface_normal.dot(difference.normalized()) * difference.length()
|
||||
return offset
|
||||
|
||||
|
||||
func set_placement_from_origin_offset(offset: float) -> Resource:
|
||||
placement = transform.origin - surface_normal * offset
|
||||
return self
|
||||
@@ -0,0 +1,40 @@
|
||||
@tool
|
||||
extends "stroke_handler.gd"
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Handle a regular erasing brush stroke
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Remove members from an octree according to the target density
|
||||
|
||||
|
||||
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
|
||||
super(_brush, _plant_states, _octree_managers, _space_state, _camera, _collision_mask)
|
||||
set_meta("class", "SH_Erase")
|
||||
|
||||
|
||||
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
|
||||
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
|
||||
|
||||
# We create a grid and detect overlaps
|
||||
brush_placement_area.init_grid_data(plant.density_per_units, 1.0 - brush.behavior_strength)
|
||||
# brush_placement_area.max_placements_allowed *= 1.0 - brush.behavior_strength
|
||||
brush_placement_area.init_placement_overlaps(octree_manager)
|
||||
|
||||
# We get overdense members - those that can't fit in a grid since their cells are already occupied
|
||||
# Then erase all of them
|
||||
brush_placement_area.invalidate_occupied_points()
|
||||
var placeforms_data_for_deletion = brush_placement_area.get_placeforms_for_deletion()
|
||||
for placeform_data_for_deletion in placeforms_data_for_deletion:
|
||||
var octree_node = octree_manager.root_octree_node.find_child_by_address(placeform_data_for_deletion.node_address)
|
||||
var placeform = octree_node.get_placeform(placeform_data_for_deletion.member_idx)
|
||||
|
||||
painting_changes.add_change(PaintingChanges.ChangeType.ERASE, plant_index, placeform, placeform)
|
||||
|
||||
|
||||
# No brush strength - no member filtering needed
|
||||
# Just make changes with ALL overlapped points
|
||||
func proj_get_stroke_update_changes(placeforms_data_in_brush: Array, plant:Greenhouse_Plant, plant_index: int, octree_manager:MMIOctreeManager, painting_changes:PaintingChanges):
|
||||
for placeform_data in placeforms_data_in_brush:
|
||||
painting_changes.add_change(PaintingChanges.ChangeType.ERASE, plant_index, placeform_data.placeform, placeform_data.placeform)
|
||||
@@ -0,0 +1,28 @@
|
||||
@tool
|
||||
extends "stroke_handler.gd"
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Make painting changes by manually supplying data
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Meant to be used from code to add or remove instances
|
||||
|
||||
|
||||
func _init():
|
||||
super(null, [], [], null, null, -1)
|
||||
|
||||
set_meta("class", "SH_Manual")
|
||||
|
||||
|
||||
func add_instance(placement: Vector3, surface_normal: Vector3, transform: Transform3D, plant_index: int, painting_changes: PaintingChanges):
|
||||
var placeform:Array = Placeform.mk(placement, surface_normal, transform)
|
||||
painting_changes.add_change(PaintingChanges.ChangeType.APPEND, plant_index, placeform, placeform)
|
||||
|
||||
|
||||
func add_instance_placeform(placeform: Array, plant_index: int, painting_changes: PaintingChanges):
|
||||
painting_changes.add_change(PaintingChanges.ChangeType.APPEND, plant_index, placeform, placeform)
|
||||
|
||||
|
||||
func remove_instance(placeform: Array, plant_index: int, painting_changes: PaintingChanges):
|
||||
painting_changes.add_change(PaintingChanges.ChangeType.ERASE, plant_index, placeform, placeform)
|
||||
@@ -0,0 +1,48 @@
|
||||
@tool
|
||||
extends "stroke_handler.gd"
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Handle a regular painting brush stroke
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Add members to an octree according to the target density
|
||||
|
||||
|
||||
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
|
||||
super(_brush, _plant_states, _octree_managers, _space_state, _camera, _collision_mask)
|
||||
|
||||
set_meta("class", "SH_Paint")
|
||||
|
||||
|
||||
func should_abort_early(brush_data:Dictionary):
|
||||
if brush.behavior_overlap_mode == Toolshed_Brush.OverlapMode.PROJECTION: return true
|
||||
if brush.behavior_strength <= 0.0: return true
|
||||
return false
|
||||
|
||||
|
||||
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
|
||||
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
|
||||
|
||||
# We create a grid, detect overlaps and get a list of raycast positions that aren't occupied
|
||||
brush_placement_area.init_grid_data(plant.density_per_units, brush.behavior_strength)
|
||||
# Previously we expanded the search area by 1 unit to eliminate placing instances right outside our area as it moves
|
||||
# (since these would seem onoccupied to the placement logic)
|
||||
# But I turned on placement amount limiter and it looks surprisingly fine
|
||||
# And as a result doesn't cause a bug where small brushes with small density place plants *too* rarely (because of that search expansion)
|
||||
brush_placement_area.init_placement_overlaps(octree_manager, 0)#1)
|
||||
var raycast_positions = brush_placement_area.get_valid_raycast_positions()
|
||||
for raycast_position in raycast_positions:
|
||||
# We raycast along the surface normal using brush sphere as our bounds
|
||||
raycast_position[0] = container_transform * raycast_position[0]
|
||||
raycast_position[1] = container_transform * raycast_position[1]
|
||||
var params = PhysicsRayQueryParameters3D.create(raycast_position[0], raycast_position[1])
|
||||
var ray_result = space_state.intersect_ray(params)
|
||||
|
||||
if !ray_result.is_empty() && ray_result.collider.collision_layer & collision_mask:
|
||||
if !TransformGenerator.is_plant_slope_allowed(ray_result.normal, plant): continue
|
||||
# Generate transforms and add them to the array
|
||||
var member_pos = container_transform.affine_inverse() * ray_result.position
|
||||
var plant_transform:Transform3D = TransformGenerator.generate_plant_transform(member_pos, ray_result.normal, plant, randomizer)
|
||||
var placeform: Array = Placeform.mk(member_pos, ray_result.normal, plant_transform)
|
||||
painting_changes.add_change(PaintingChanges.ChangeType.APPEND, plant_index, placeform, placeform)
|
||||
@@ -0,0 +1,57 @@
|
||||
@tool
|
||||
extends "stroke_handler.gd"
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Handle a reapply transforms brush stroke
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Get overlapping placements and generate a new Transform3D for each of them
|
||||
|
||||
|
||||
# We keep references to placements we already reapplied as to not continously regenerate them
|
||||
var reapplied_octree_members:Array
|
||||
|
||||
|
||||
|
||||
|
||||
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
|
||||
super(_brush, _plant_states, _octree_managers, _space_state, _camera, _collision_mask)
|
||||
|
||||
set_meta("class", "SH_Reapply")
|
||||
reapplied_octree_members = []
|
||||
|
||||
|
||||
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
|
||||
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
|
||||
|
||||
# We detect overlaps first
|
||||
brush_placement_area.init_placement_overlaps(octree_manager)
|
||||
# For each overlap we generate a new Transform3D and add it to the PaintingChange
|
||||
create_painting_changes(brush_placement_area.overlapped_member_data, plant, plant_index, octree_manager, painting_changes)
|
||||
|
||||
|
||||
func proj_get_stroke_update_changes(placeform_data_array: Array, plant:Greenhouse_Plant, plant_index: int, octree_manager:MMIOctreeManager, painting_changes:PaintingChanges):
|
||||
create_painting_changes(placeform_data_array, plant, plant_index, octree_manager, painting_changes)
|
||||
|
||||
|
||||
func create_painting_changes(placeform_data_array: Array, plant:Greenhouse_Plant, plant_index: int, octree_manager:MMIOctreeManager, painting_changes:PaintingChanges):
|
||||
for placeform_data in placeform_data_array:
|
||||
var octree_node = octree_manager.root_octree_node.find_child_by_address(placeform_data.node_address)
|
||||
var placeform = octree_node.get_placeform(placeform_data.member_idx)
|
||||
|
||||
# We use Vector3.to_string() to generate our reference keys
|
||||
# I assume it's fully deterministic at least in the scope of an individual OS
|
||||
var octree_member_key = str(placeform[0])
|
||||
if reapplied_octree_members.has(octree_member_key): continue
|
||||
reapplied_octree_members.append(octree_member_key)
|
||||
|
||||
var plant_transform := TransformGenerator.generate_plant_transform(placeform[0], placeform[1], plant, randomizer)
|
||||
var new_placeform := Placeform.mk(placeform[0], placeform[1], plant_transform, placeform[3])
|
||||
|
||||
# Painting changes here are non-standart: they actually have an octree node address and member index bundled
|
||||
# We can't reliably use an address when adding/removing members since the octree might grow/collapse
|
||||
# But here it's fine since we don't change the amount of members
|
||||
painting_changes.add_change(PaintingChanges.ChangeType.SET, plant_index,
|
||||
{"placeform": new_placeform, "index": placeform_data.member_idx, "address": placeform_data.node_address},
|
||||
{"placeform": placeform, "index": placeform_data.member_idx, "address": placeform_data.node_address})
|
||||
@@ -0,0 +1,32 @@
|
||||
@tool
|
||||
extends "stroke_handler.gd"
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Handle a single placement brush stroke
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Meant to be used with single mouse clicks
|
||||
# Dragging while pressing will place as many instances as the framerate allows
|
||||
|
||||
|
||||
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
|
||||
super(_brush, _plant_states, _octree_managers, _space_state, _camera, _collision_mask)
|
||||
|
||||
set_meta("class", "SH_Single")
|
||||
|
||||
|
||||
func should_abort_early(brush_data:Dictionary):
|
||||
if brush.behavior_overlap_mode == Toolshed_Brush.OverlapMode.PROJECTION: return true
|
||||
return false
|
||||
|
||||
|
||||
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
|
||||
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
|
||||
|
||||
var member_pos = brush_data.brush_pos
|
||||
|
||||
var plant_transform: Transform3D = TransformGenerator.generate_plant_transform(member_pos, brush_data.brush_normal, plant, randomizer)
|
||||
var placeform: Array = Placeform.mk(member_pos, brush_data.brush_normal, plant_transform)
|
||||
painting_changes.add_change(PaintingChanges.ChangeType.APPEND, plant_index, placeform, placeform)
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# A base object that gathers plant positions/overlaps
|
||||
# And generates neccessary PaintingChange depending on the stroke type it handles
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
const Logger = preload("../../utility/logger.gd")
|
||||
const FunLib = preload("../../utility/fun_lib.gd")
|
||||
const DponDebugDraw = preload("../../utility/debug_draw.gd")
|
||||
const Greenhouse_Plant = preload("../../greenhouse/greenhouse_plant.gd")
|
||||
const Placeform = preload("../placeform.gd")
|
||||
const Toolshed_Brush = preload("../../toolshed/toolshed_brush.gd")
|
||||
const BrushPlacementArea = preload("../brush_placement_area.gd")
|
||||
const TransformGenerator = preload("../transform_generator.gd")
|
||||
const PaintingChanges = preload("../painting_changes.gd")
|
||||
const MMIOctreeManager = preload("../mmi_octree/mmi_octree_manager.gd")
|
||||
const MMIOctreeNode = preload("../mmi_octree/mmi_octree_node.gd")
|
||||
|
||||
|
||||
var randomizer:RandomNumberGenerator
|
||||
var transform_generator:TransformGenerator
|
||||
|
||||
var brush:Toolshed_Brush
|
||||
var plant_states:Array
|
||||
var octree_managers:Array
|
||||
var space_state:PhysicsDirectSpaceState3D
|
||||
var camera: Camera3D
|
||||
var collision_mask:int
|
||||
|
||||
var debug_draw_enabled: bool = false
|
||||
var simplify_projection_frustum: bool = false
|
||||
|
||||
# Mouse position in screen space cached for one update cycle
|
||||
var _cached_mouse_pos: Vector2 = Vector2()
|
||||
|
||||
var logger = null
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Initialization
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
|
||||
set_meta("class", "StrokeHandler")
|
||||
|
||||
brush = _brush
|
||||
plant_states = _plant_states
|
||||
octree_managers = _octree_managers
|
||||
space_state = _space_state
|
||||
camera = _camera
|
||||
collision_mask = _collision_mask
|
||||
|
||||
randomizer = RandomNumberGenerator.new()
|
||||
randomizer.seed = Time.get_ticks_msec()
|
||||
logger = Logger.get_for(self)
|
||||
|
||||
debug_draw_enabled = FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/stroke_handler_debug_draw", true)
|
||||
simplify_projection_frustum = FunLib.get_setting_safe("dreadpons_spatial_gardener/painting/simplify_projection_frustum", true)
|
||||
|
||||
if debug_draw_enabled:
|
||||
debug_mk_debug_draw()
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# PaintingChange management
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# A method for building PaintingChanges
|
||||
# If we have 'brush' in _init(), we do we need 'brush_data' you might ask
|
||||
# That's because 'brush' gives us brush settings (size, strength, etc.)
|
||||
# While 'brush_data' gives up-to-date transformations and surface normal of a brush in world-space
|
||||
func get_stroke_update_changes(brush_data:Dictionary, container_transform:Transform3D) -> PaintingChanges:
|
||||
var painting_changes = PaintingChanges.new()
|
||||
if should_abort_early(brush_data): return painting_changes
|
||||
|
||||
var msec_start = FunLib.get_msec()
|
||||
|
||||
brush_data = modify_brush_data_to_container(brush_data, container_transform)
|
||||
|
||||
for plant_index in range(0, plant_states.size()):
|
||||
if !plant_states[plant_index].plant_brush_active: continue
|
||||
var plant = plant_states[plant_index].plant
|
||||
|
||||
handle_plant_stroke(brush_data, container_transform, plant, plant_index, painting_changes)
|
||||
|
||||
var msec_end = FunLib.get_msec()
|
||||
debug_print_lifecycle(" Stroke %s changes update took: %s" % [get_meta("class"), FunLib.msec_to_time(msec_end - msec_start)])
|
||||
return painting_changes
|
||||
|
||||
|
||||
# Are there any conditions that force us to abort before anything starts?
|
||||
func should_abort_early(brush_data:Dictionary):
|
||||
return false
|
||||
|
||||
|
||||
func modify_brush_data_to_container(brush_data:Dictionary, container_transform:Transform3D) -> Dictionary:
|
||||
match brush.behavior_overlap_mode:
|
||||
Toolshed_Brush.OverlapMode.VOLUME:
|
||||
return volume_modify_brush_data_to_container(brush_data, container_transform)
|
||||
return brush_data
|
||||
|
||||
|
||||
func handle_plant_stroke(brush_data:Dictionary, container_transform:Transform3D, plant:Greenhouse_Plant, plant_index:int, painting_changes:PaintingChanges):
|
||||
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
|
||||
|
||||
match brush.behavior_overlap_mode:
|
||||
|
||||
Toolshed_Brush.OverlapMode.VOLUME:
|
||||
var plant_brush_data = volume_modify_brush_data_to_plant(brush_data, plant)
|
||||
# BrushPlacementArea is the "brains" of my placement logic, so we initialize it here
|
||||
var brush_placement_area := BrushPlacementArea.new(plant_brush_data.brush_pos, brush.shape_volume_size * 0.5, plant_brush_data.brush_normal, plant.offset_jitter_fraction)
|
||||
volume_get_stroke_update_changes(plant_brush_data, plant, plant_index, octree_manager, brush_placement_area, container_transform, painting_changes)
|
||||
|
||||
Toolshed_Brush.OverlapMode.PROJECTION:
|
||||
# This will store dictionaries so it shares the same data structure as brush_placement_area.overlapped_member_data
|
||||
# But with member itself added into the mix. It'll look like this:
|
||||
# [{"node_address": node_address, "member_idx": member_idx, "placeform": placeform}, ...]
|
||||
var placeforms_in_brush := []
|
||||
var frustum_planes := []
|
||||
_cached_mouse_pos = camera.get_viewport().get_mouse_position()
|
||||
|
||||
proj_define_frustum(brush_data, frustum_planes)
|
||||
proj_get_placeforms_data_in_frustum(frustum_planes, placeforms_in_brush, octree_manager.root_octree_node, container_transform)
|
||||
proj_filter_placeforms_to_brush_circle(placeforms_in_brush, container_transform)
|
||||
if !brush.behavior_passthrough:
|
||||
proj_filter_obstructed_placeforms(placeforms_in_brush, container_transform)
|
||||
proj_get_stroke_update_changes(placeforms_in_brush, plant, plant_index, octree_manager, painting_changes)
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Volume brush handling
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
func volume_modify_brush_data_to_container(brush_data:Dictionary, container_transform:Transform3D):
|
||||
brush_data = brush_data.duplicate()
|
||||
brush_data.brush_pos = container_transform.affine_inverse() * brush_data.brush_pos
|
||||
return brush_data
|
||||
|
||||
|
||||
# Modify the brush data according to the plant
|
||||
func volume_modify_brush_data_to_plant(brush_data:Dictionary, plant) -> Dictionary:
|
||||
# Here used to be code to snap the brush_pos to the virtual density grid of a plant
|
||||
return brush_data.duplicate()
|
||||
|
||||
|
||||
# Called when the Painter brush stroke is updated (moved)
|
||||
# To be overridden
|
||||
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
|
||||
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
|
||||
return null
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Projection brush handling
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
func proj_filter_placeforms_to_brush_circle(placeforms_data_in_frustum: Array, container_transform:Transform3D):
|
||||
var brush_radius_squared: float = pow(brush.shape_projection_size * 0.5, 2.0)
|
||||
var viewport_size: Vector2i = camera.get_viewport().size
|
||||
|
||||
for i in range(placeforms_data_in_frustum.size() -1, -1, -1):
|
||||
var placeform_data = placeforms_data_in_frustum[i]
|
||||
var placement = container_transform * placeform_data.placeform[0]
|
||||
var screen_space_pos := camera.unproject_position(placement)
|
||||
var dist_squared = (screen_space_pos - _cached_mouse_pos).length_squared()
|
||||
|
||||
# Remove those outside brush radius
|
||||
if dist_squared > brush_radius_squared:
|
||||
placeforms_data_in_frustum.remove_at(i)
|
||||
# Remove those outside viewport
|
||||
elif screen_space_pos.x < 0 || screen_space_pos.y < 0 || screen_space_pos.x > viewport_size.x || screen_space_pos.y > viewport_size.y:
|
||||
placeforms_data_in_frustum.remove_at(i)
|
||||
|
||||
|
||||
func proj_define_frustum(brush_data:Dictionary, frustum_planes: Array) -> Array:
|
||||
var brush_half_size = brush.shape_projection_size * 0.5
|
||||
var brush_rect := Rect2(-brush_half_size, -brush_half_size, brush.shape_projection_size, brush.shape_projection_size)
|
||||
var frustum_points := []
|
||||
frustum_points.resize(8)
|
||||
frustum_planes.resize(6)
|
||||
|
||||
project_frustum_points(frustum_points, brush_rect)
|
||||
define_frustum_plane_array(frustum_planes, frustum_points)
|
||||
|
||||
debug_draw_point_array(frustum_points)
|
||||
debug_draw_plane_array(frustum_planes, [frustum_points[0], frustum_points[2], frustum_points[4], frustum_points[6], frustum_points[4], frustum_points[7]])
|
||||
|
||||
return frustum_planes
|
||||
|
||||
|
||||
func proj_filter_obstructed_placeforms(placeforms_data_in_frustum: Array, container_transform:Transform3D):
|
||||
# This is a margin to offset our cast due to miniscule errors in placement
|
||||
# And built-in collision shape margin
|
||||
# Not off-setting an endpoint will mark some visible instances as obstructed
|
||||
var raycast_margin = FunLib.get_setting_safe("dreadpons_spatial_gardener/painting/projection_raycast_margin", 0.1)
|
||||
for i in range(placeforms_data_in_frustum.size() -1, -1, -1):
|
||||
var placeform_data = placeforms_data_in_frustum[i]
|
||||
var ray_start: Vector3 = camera.global_transform.origin
|
||||
var ray_vector = container_transform * placeform_data.placeform[0] - ray_start
|
||||
var ray_end: Vector3 = ray_start + ray_vector.normalized() * (ray_vector.length() - raycast_margin)
|
||||
|
||||
var params = PhysicsRayQueryParameters3D.create(ray_start, ray_end, collision_mask)
|
||||
var ray_result = space_state.intersect_ray(params)
|
||||
|
||||
if !ray_result.is_empty():# && ray_result.collider.collision_layer & collision_mask:
|
||||
placeforms_data_in_frustum.remove_at(i)
|
||||
|
||||
|
||||
# Called when the Painter brush stroke is updated (moved)
|
||||
# To be overridden
|
||||
func proj_get_stroke_update_changes(placeforms_in_brush: Array, plant:Greenhouse_Plant, plant_index: int, octree_manager:MMIOctreeManager, painting_changes:PaintingChanges):
|
||||
return null
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Projection, intersection and other Projection-brush geometry
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Recursively iterate over octree nodes to find nodes and members within brush frustum
|
||||
func proj_get_placeforms_data_in_frustum(frustum_planes: Array, placeforms_data_in_frustum: Array, octree_node: MMIOctreeNode, container_transform:Transform3D):
|
||||
var octree_node_transform := Transform3D(container_transform.basis, container_transform * octree_node.center_pos)
|
||||
var octree_node_extents := Vector3(octree_node.extent, octree_node.extent, octree_node.extent)
|
||||
debug_draw_cube(octree_node_transform.origin, octree_node_extents * 2.0, octree_node_transform.basis.get_rotation_quaternion(), octree_node_transform.basis)
|
||||
|
||||
if is_box_intersecting_frustum(frustum_planes, octree_node_transform, octree_node_extents):
|
||||
if octree_node.is_leaf:
|
||||
var node_address = octree_node.get_address()
|
||||
for member_idx in range(0, octree_node.member_count()):
|
||||
var placeform = octree_node.get_placeform(member_idx)
|
||||
placeforms_data_in_frustum.append({"node_address": node_address, "member_idx": member_idx, "placeform": placeform})
|
||||
else:
|
||||
for child_node in octree_node.child_nodes:
|
||||
proj_get_placeforms_data_in_frustum(frustum_planes, placeforms_data_in_frustum, child_node, container_transform)
|
||||
|
||||
|
||||
# This is an approximation (but THIS frustum check IS MEANT to be an approximation, so it's fine)
|
||||
# This WILL fail on cube's screen-projected 'corners'
|
||||
# Since technically it will intersect some planes of our frustum
|
||||
# Which is fine because we perform distance checks to individual members later on
|
||||
func is_box_intersecting_frustum(frustum_planes: Array, box_transform: Transform3D, box_extents: Vector3):
|
||||
# Extents == half-size
|
||||
var oriented_box_extents = [box_extents.x * box_transform.basis.x, box_extents.y * box_transform.basis.y, box_extents.z * box_transform.basis.z]
|
||||
var box_points := [
|
||||
box_transform.origin + oriented_box_extents[0] + oriented_box_extents[1] + oriented_box_extents[2],
|
||||
box_transform.origin + oriented_box_extents[0] + oriented_box_extents[1] - oriented_box_extents[2],
|
||||
box_transform.origin + oriented_box_extents[0] - oriented_box_extents[1] + oriented_box_extents[2],
|
||||
box_transform.origin + oriented_box_extents[0] - oriented_box_extents[1] - oriented_box_extents[2],
|
||||
box_transform.origin - oriented_box_extents[0] + oriented_box_extents[1] + oriented_box_extents[2],
|
||||
box_transform.origin - oriented_box_extents[0] - oriented_box_extents[1] + oriented_box_extents[2],
|
||||
box_transform.origin - oriented_box_extents[0] + oriented_box_extents[1] - oriented_box_extents[2],
|
||||
box_transform.origin - oriented_box_extents[0] - oriented_box_extents[1] - oriented_box_extents[2],
|
||||
]
|
||||
debug_draw_point_array(box_points, Color.YELLOW)
|
||||
|
||||
for plane in frustum_planes:
|
||||
var points_inside := 0
|
||||
for point in box_points:
|
||||
points_inside += 1 if plane.is_point_over(point) else 0
|
||||
|
||||
if points_inside == 0:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
|
||||
# Project frustum points from screen-space to world-space
|
||||
func project_frustum_points(frustum_points: Array, brush_rect: Rect2):
|
||||
# A simple version for frustum projection which assumes four corners of the screen in world-space
|
||||
# To be roughly at camera's origin (camera.near is the default 0.05 and thus negligible)
|
||||
if simplify_projection_frustum:
|
||||
var origin_point = project_point(0.0)
|
||||
# Upper left, near + far
|
||||
frustum_points[0] = origin_point
|
||||
frustum_points[1] = project_point(camera.far - 0.1, Vector2(brush_rect.position.x, brush_rect.position.y))
|
||||
# Lower left, near + far
|
||||
frustum_points[2] = origin_point
|
||||
frustum_points[3] = project_point(camera.far - 0.1, Vector2(brush_rect.position.x, brush_rect.end.y))
|
||||
# Lower right, near + far
|
||||
frustum_points[4] = origin_point
|
||||
frustum_points[5] = project_point(camera.far - 0.1, Vector2(brush_rect.end.x, brush_rect.end.y))
|
||||
# Upper right, near + far
|
||||
frustum_points[6] = origin_point
|
||||
frustum_points[7] = project_point(camera.far - 0.1, Vector2(brush_rect.end.x, brush_rect.position.y))
|
||||
|
||||
# A complex version for frustum projection which uses camera.near for bigger accuracy
|
||||
# Relevant when camera.near is greater than default of 0.05 (like 1.0 or 2.0)
|
||||
else:
|
||||
# Upper left, near + far
|
||||
project_frustum_point_pair(frustum_points, 0, 1, Vector2(brush_rect.position.x, brush_rect.position.y))
|
||||
# Lower left, near + far
|
||||
project_frustum_point_pair(frustum_points, 2, 3, Vector2(brush_rect.position.x, brush_rect.end.y))
|
||||
# Lower right, near + far
|
||||
project_frustum_point_pair(frustum_points, 4, 5, Vector2(brush_rect.end.x, brush_rect.end.y))
|
||||
# Upper right, near + far
|
||||
project_frustum_point_pair(frustum_points, 6, 7, Vector2(brush_rect.end.x, brush_rect.position.y))
|
||||
|
||||
|
||||
func project_frustum_point_pair(frustum_points: Array, idx_0: int, idx_1: int, offset: Vector2):
|
||||
frustum_points[idx_0] = project_point(camera.near, offset)
|
||||
frustum_points[idx_1] = project_point(camera.far - 0.1, offset)
|
||||
|
||||
|
||||
# Define an array of 6 frustum planes
|
||||
func define_frustum_plane_array(frustum_planes: Array, frustum_points: Array):
|
||||
frustum_planes[0] = define_frustum_plane(frustum_points[0], frustum_points[3], frustum_points[1])
|
||||
frustum_planes[1] = define_frustum_plane(frustum_points[2], frustum_points[5], frustum_points[3])
|
||||
frustum_planes[2] = define_frustum_plane(frustum_points[4], frustum_points[7], frustum_points[5])
|
||||
frustum_planes[3] = define_frustum_plane(frustum_points[6], frustum_points[1], frustum_points[7])
|
||||
if simplify_projection_frustum:
|
||||
# Since all points involved here will be the same point due to simplification (see 'project_frustum_points()')
|
||||
# Approximate the points forming a plane by using camera's basis vectors
|
||||
frustum_planes[4] = define_frustum_plane(frustum_points[4], frustum_points[2] - camera.global_transform.basis.x, frustum_points[0] + camera.global_transform.basis.y)
|
||||
else:
|
||||
# Here all our points are different, so use them as-is
|
||||
frustum_planes[4] = define_frustum_plane(frustum_points[4], frustum_points[2], frustum_points[0])
|
||||
frustum_planes[5] = define_frustum_plane(frustum_points[7], frustum_points[1], frustum_points[3])
|
||||
|
||||
|
||||
# Plane equation is
|
||||
# ax + by + xz + d = 0
|
||||
# Or this with a common point on a plane
|
||||
# a(x - x0) + b(y - y0) + c(z - z0) = 0
|
||||
# Where (x0, y0, z0) is any point on the plane (lets use common_point)
|
||||
#
|
||||
# So plane equation becomes
|
||||
# normal.x * ux - normal.x * common_point.x +
|
||||
# normal.y * uy - normal.y * common_point.y +
|
||||
# normal.z * uz - normal.z * common_point.z
|
||||
# = 0
|
||||
# Where ux, uy, uz are unkown variables that are substituted when solving a plane equations
|
||||
#
|
||||
# Now we combine our scalar values and move them to the right side
|
||||
# normal.x * ux + normal.y * uy + normal.z * uz
|
||||
# = normal.x * common_point.x + normal.y * common_point.y + normal.z * common_point.z
|
||||
#
|
||||
# That should be it, distance to origin is
|
||||
# d = normal.x * common_point.x + normal.y * common_point.y + normal.z * common_point.z
|
||||
# Which is esentially a dot product :)
|
||||
func define_frustum_plane(common_point: Vector3, point_0: Vector3, point_1: Vector3):
|
||||
var normal := (point_0 - common_point).cross(point_1 - common_point).normalized()
|
||||
var dist_to_origin = normal.dot(common_point)
|
||||
var plane = Plane(normal, dist_to_origin)
|
||||
|
||||
return plane
|
||||
|
||||
|
||||
func project_point(distance: float, offset: Vector2 = Vector2.ZERO) -> Vector3:
|
||||
return camera.project_position(_cached_mouse_pos + offset, distance)
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Debug
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
func debug_print_lifecycle(string:String):
|
||||
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/arborist_log_lifecycle", false): return
|
||||
logger.info(string)
|
||||
|
||||
|
||||
func debug_mk_debug_draw():
|
||||
var context = camera.get_tree().edited_scene_root#.find_child('Gardener').get_parent()
|
||||
if !context.has_node('DponDebugDraw'):
|
||||
var debug_draw := DponDebugDraw.new()
|
||||
debug_draw.name = 'DponDebugDraw'
|
||||
context.add_child(debug_draw)
|
||||
|
||||
|
||||
func debug_draw_plane_array(planes: Array, origin_points: Array, color: Color = Color.RED):
|
||||
if !debug_draw_enabled: return
|
||||
for idx in range(0, planes.size()):
|
||||
debug_draw_plane(origin_points[idx], planes[idx], color)
|
||||
|
||||
|
||||
func debug_draw_point_array(points: Array, color: Color = Color.GREEN):
|
||||
if !debug_draw_enabled: return
|
||||
for point in points:
|
||||
debug_draw_point(point, color)
|
||||
|
||||
|
||||
func debug_draw_plane(draw_origin: Vector3, plane: Plane, color: Color = Color.RED):
|
||||
if !debug_draw_enabled: return
|
||||
var context = camera.get_tree().edited_scene_root.find_child('Gardener').get_parent()
|
||||
context.get_node('DponDebugDraw').draw_plane(draw_origin, camera.far * 0.5, plane.normal, color, context, 2.0, camera.global_transform.basis.y, 10.0)
|
||||
|
||||
|
||||
func debug_draw_point(draw_origin: Vector3, color: Color = Color.GREEN):
|
||||
if !debug_draw_enabled: return
|
||||
var context = camera.get_tree().edited_scene_root.find_child('Gardener').get_parent()
|
||||
context.get_node('DponDebugDraw').draw_cube(draw_origin, Vector3.ONE, Quaternion(), color, context, 10.0)
|
||||
|
||||
|
||||
func debug_draw_cube(draw_origin: Vector3, size: Vector3, rotation: Quaternion, basis: Basis = Basis(), color: Color = Color.BLUE):
|
||||
if !debug_draw_enabled: return
|
||||
var context = camera.get_tree().edited_scene_root.find_child('Gardener').get_parent()
|
||||
size = Vector3(size.x * basis.x.length(), size.y * basis.y.length(), size.z * basis.z.length())
|
||||
context.get_node('DponDebugDraw').draw_cube(draw_origin, size, rotation, color, context, 10.0)
|
||||
116
addons/dreadpon.spatial_gardener/arborist/transform_generator.gd
Normal file
116
addons/dreadpon.spatial_gardener/arborist/transform_generator.gd
Normal file
@@ -0,0 +1,116 @@
|
||||
@tool
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# A function library for generating individual plant tranforms
|
||||
# Based on the given placement position and Greenhouse_Plant settings
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
const FunLib = preload("../utility/fun_lib.gd")
|
||||
const Greenhouse_Plant = preload("../greenhouse/greenhouse_plant.gd")
|
||||
|
||||
|
||||
|
||||
|
||||
# Randomize an instance transform according to its Greenhouse_Plant settings
|
||||
static func generate_plant_transform(placement, normal, plant, randomizer) -> Transform3D:
|
||||
var up_vector_primary:Vector3 = get_dir_vector(plant.up_vector_primary_type, plant.up_vector_primary, normal)
|
||||
var up_vector_secondary:Vector3 = get_dir_vector(plant.up_vector_secondary_type, plant.up_vector_secondary, normal)
|
||||
var plant_up_vector:Vector3 = lerp(up_vector_primary, up_vector_secondary, plant.up_vector_blending).normalized()
|
||||
|
||||
var fwd_vector_primary:Vector3 = get_dir_vector(plant.fwd_vector_primary_type, plant.fwd_vector_primary, normal)
|
||||
var fwd_vector_secondary:Vector3 = get_dir_vector(plant.fwd_vector_secondary_type, plant.fwd_vector_secondary, normal)
|
||||
var plant_fwd_vector:Vector3 = lerp(fwd_vector_primary, fwd_vector_secondary, plant.fwd_vector_blending).normalized()
|
||||
|
||||
var plant_scale:Vector3 = FunLib.vector_tri_lerp(
|
||||
plant.scale_range[0],
|
||||
plant.scale_range[1],
|
||||
get_scaling_randomized_weight(plant.scale_scaling_type, randomizer)
|
||||
)
|
||||
|
||||
var plant_y_offset = lerp(plant.offset_y_range[0], plant.offset_y_range[1], randomizer.randf_range(0.0, 1.0)) * plant_scale
|
||||
|
||||
var plant_rotation = Vector3(
|
||||
deg_to_rad(lerp(-plant.rotation_random_x, plant.rotation_random_x, randomizer.randf_range(0.0, 1.0))),
|
||||
deg_to_rad(lerp(-plant.rotation_random_y, plant.rotation_random_y, randomizer.randf_range(0.0, 1.0))),
|
||||
deg_to_rad(lerp(-plant.rotation_random_z, plant.rotation_random_z, randomizer.randf_range(0.0, 1.0)))
|
||||
)
|
||||
|
||||
var plant_basis:Basis = Basis()
|
||||
plant_basis.y = plant_up_vector
|
||||
|
||||
# If one of the forward vectors is unused and contributes to a blend
|
||||
if ((plant.fwd_vector_primary_type == Greenhouse_Plant.DirectionVectorType.UNUSED && plant.fwd_vector_blending != 1.0)
|
||||
|| (plant.fwd_vector_secondary_type == Greenhouse_Plant.DirectionVectorType.UNUSED && plant.fwd_vector_blending != 0.0)):
|
||||
# Use automatic forward vector
|
||||
plant_basis.z = Vector3.FORWARD.rotated(plant_up_vector, plant_rotation.y)
|
||||
else:
|
||||
plant_basis.z = plant_fwd_vector.rotated(plant_up_vector, plant_rotation.y)
|
||||
|
||||
plant_basis.x = plant_basis.y.cross(plant_basis.z)
|
||||
plant_basis.z = plant_basis.x.cross(plant_basis.y)
|
||||
plant_basis = plant_basis.orthonormalized()
|
||||
plant_basis = plant_basis.rotated(plant_basis.x, plant_rotation.x)
|
||||
plant_basis = plant_basis.rotated(plant_basis.z, plant_rotation.z)
|
||||
|
||||
plant_basis.x *= plant_scale.x
|
||||
plant_basis.y *= plant_scale.y
|
||||
plant_basis.z *= plant_scale.z
|
||||
|
||||
var plant_origin = placement + plant_y_offset * plant_basis.y.normalized()
|
||||
var plant_transform = Transform3D(plant_basis, plant_origin)
|
||||
return plant_transform
|
||||
|
||||
|
||||
# See slope_allowedRange in Greenhouse_Plant
|
||||
static func is_plant_slope_allowed(normal, plant) -> bool:
|
||||
var up_vector_primary:Vector3 = get_dir_vector(plant.up_vector_primary_type, plant.up_vector_primary, normal)
|
||||
var slope_angle = abs(rad_to_deg(up_vector_primary.angle_to(normal)))
|
||||
return slope_angle >= plant.slope_allowed_range[0] && slope_angle <= plant.slope_allowed_range[1]
|
||||
|
||||
|
||||
# Choose the appropriate direction vector
|
||||
static func get_dir_vector(dir_vector_type, custom_vector:Vector3, normal:Vector3) -> Vector3:
|
||||
match dir_vector_type:
|
||||
Greenhouse_Plant.DirectionVectorType.WORLD_X:
|
||||
return Vector3.RIGHT
|
||||
Greenhouse_Plant.DirectionVectorType.WORLD_Y:
|
||||
return Vector3.UP
|
||||
Greenhouse_Plant.DirectionVectorType.WORLD_Z:
|
||||
return Vector3.FORWARD
|
||||
Greenhouse_Plant.DirectionVectorType.NORMAL:
|
||||
return normal
|
||||
Greenhouse_Plant.DirectionVectorType.CUSTOM:
|
||||
return custom_vector.normalized()
|
||||
return Vector3.UP
|
||||
|
||||
|
||||
# Enforce the scaling plane lock if present
|
||||
# The scaling itself is already enforced by Greenhouse_Plant
|
||||
# But we need to enforce the randomization as well
|
||||
static func get_scaling_randomized_weight(scaling_type, randomizer) -> Vector3:
|
||||
var scale_weight = Vector3()
|
||||
match scaling_type:
|
||||
Greenhouse_Plant.ScalingType.UNIFORM:
|
||||
scale_weight.x = randomizer.randf_range(0.0, 1.0)
|
||||
scale_weight.y = scale_weight.x
|
||||
scale_weight.z = scale_weight.x
|
||||
Greenhouse_Plant.ScalingType.FREE:
|
||||
scale_weight.x = randomizer.randf_range(0.0, 1.0)
|
||||
scale_weight.y = randomizer.randf_range(0.0, 1.0)
|
||||
scale_weight.z = randomizer.randf_range(0.0, 1.0)
|
||||
Greenhouse_Plant.ScalingType.LOCK_XY:
|
||||
scale_weight.x = randomizer.randf_range(0.0, 1.0)
|
||||
scale_weight.y = scale_weight.x
|
||||
scale_weight.z = randomizer.randf_range(0.0, 1.0)
|
||||
Greenhouse_Plant.ScalingType.LOCK_ZY:
|
||||
scale_weight.x = randomizer.randf_range(0.0, 1.0)
|
||||
scale_weight.y = randomizer.randf_range(0.0, 1.0)
|
||||
scale_weight.z = scale_weight.y
|
||||
Greenhouse_Plant.ScalingType.LOCK_XZ:
|
||||
scale_weight.x = randomizer.randf_range(0.0, 1.0)
|
||||
scale_weight.y = randomizer.randf_range(0.0, 1.0)
|
||||
scale_weight.z = scale_weight.x
|
||||
|
||||
return scale_weight
|
||||
Reference in New Issue
Block a user