## The editor node that allows you to shatter a MeshInstance3D. @tool @icon("res://addons/voronoishatter/tools/voronoishatter.svg") extends Node3D class_name VoronoiShatter @export_tool_button("Generate Fracture Meshes", "MeshInstance3D") var execute_action = execute @export_category("Materials") ## Randomly assign color to each resulting fracture mesh (ignoring any other materials). Good for previewing the fractures. @export var random_color: bool = true: set(value): random_color = value notify_property_list_changed() var _inherit_outer_material := false ## Inherit the target mesh's surface materials @export var inherit_outer_material: bool: get: return _inherit_outer_material set(value): _inherit_outer_material = value notify_property_list_changed() ## This material is applied to all clipped/original surfaces @export var outer_material: StandardMaterial3D ## This material applies to the inner surfaces within the volume @export var inner_material: StandardMaterial3D @export_category("Structure") ## The number of samples used to determine the fracture points. Lower samples = bigger pieces (and faster generation). However, a < ~6 sample-size may lead to missing pieces. @export_range(1, 1024, 1, "hide_slider") var samples: int = 32: set(value): samples = value refresh_view() ## The random seed used to generate the samples. @export var seed: int = 0: set(value): seed = value refresh_view() ## Cell scale - change the size of the shape that is actually clipped @export var cell_scale: float = 1.0 ## An optional 3D texture that influences where the samples points are generated. @export var sample_texture: Texture3D: set(value): if sample_texture: sample_texture.changed.disconnect(refresh_view) sample_texture = value if sample_texture: value.changed.connect(refresh_view) refresh_view() ## Randomize the random seed. @export_tool_button("Randomize seed", "RandomNumberGenerator") var randomize_seed_action = randomize_seed @export_category("Original Mesh") ## Hide the target mesh after generating the fracture mesh. @export var hide_original: bool = true ## Delete any fracture mesh children of this node before generation. @export var delete_existing_fractures: bool = true const NO_MESH_CHILD = "VoronoiShatter must have a MeshInstance3D as a child." func randomize_seed(): seed = randi() var editable_owner: Node var voronoi_generator: VoronoiGenerator func _enter_tree(): # Plugin initialization if Engine.is_editor_hint(): EditorInterface.get_selection().connect("selection_changed", refresh_view) editable_owner = get_tree().get_edited_scene_root() voronoi_generator = Engine.get_singleton("EditorVoronoiGenerator") as VoronoiGenerator func _exit_tree() -> void: if Engine.is_editor_hint(): EditorInterface.get_selection().disconnect("selection_changed", refresh_view) func get_target_mesh() -> MeshInstance3D: for child in get_children(): if is_instance_of(child, MeshInstance3D): return child return null func execute(): started = Time.get_ticks_usec() await get_tree().process_frame if delete_existing_fractures: for child in get_children(): if is_instance_of(child, VoronoiCollection): child.queue_free() generate_fracture_meshes(get_config()) var current_collection var started = null func generate_fracture_meshes(config: VoronoiGeneratorConfig): var target = get_target_mesh() if not target: VoronoiLog.err(NO_MESH_CHILD) return VoronoiLog.log("Creating Voronoi geometry for %s..." % target.name) # Create the parent node collection current_collection = VoronoiCollection.new() current_collection.set_block_signals(true) current_collection.name = "Fractured_" + target.name + "_" + str(Time.get_ticks_msec()) add_child(current_collection) current_collection.set_owner(owner) if hide_original: target.visible = false var results := voronoi_generator.create_from_mesh(target, config) for result in results: create_from_voronoi_mesh(result) VoronoiLog.log("Completed in " + str((Time.get_ticks_usec() - started) / 10e5) + " seconds") current_collection.set_block_signals(false) func create_from_voronoi_mesh(voronoi_mesh: VoronoiMesh): if voronoi_mesh == null: VoronoiLog.err("Skipping creation of null mesh") var mesh_instance = MeshInstance3D.new() mesh_instance.set_block_signals(true) var target = get_target_mesh() var mesh = voronoi_mesh.mesh mesh_instance.scale = target.scale mesh_instance.name = "FracturedPiece_" + str(mesh.get_rid()) mesh_instance.mesh = mesh mesh_instance.position -= voronoi_mesh.position * target.scale mesh_instance.scale = Vector3.ONE * cell_scale var has_outside_faces = mesh_instance.mesh.get_surface_count() > 1 if random_color: var random_color: Color = Color(randf(), randf(), randf()) for surface in range(mesh_instance.mesh.get_surface_count()): var material = StandardMaterial3D.new() material.albedo_color = random_color mesh_instance.mesh.surface_set_material(surface, material) else: if has_outside_faces: if not inherit_outer_material: for surface_id in range(1, mesh_instance.mesh.get_surface_count()): mesh_instance.mesh.surface_set_material(surface_id, outer_material) if inner_material: mesh_instance.mesh.surface_set_material(0, inner_material) call_deferred("_deferred_add_child", mesh_instance) func _deferred_add_child(mesh_instance: MeshInstance3D): current_collection.add_child(mesh_instance) mesh_instance.set_owner(owner) mesh_instance.set_block_signals(false) func get_config() -> VoronoiGeneratorConfig: var config = VoronoiGeneratorConfig.new() config.num_samples = samples config.random_seed = seed config.texture = sample_texture return config # INTERNAL FUNCTIONS - for showing things in the editor, e.g. var sample_visualizers: Array[CSGSphere3D] = [] func refresh_view(): if not Engine.is_editor_hint() or not is_instance_valid(voronoi_generator): return for visualizer in sample_visualizers: visualizer.queue_free() sample_visualizers = [] var target = get_target_mesh() if not EditorInterface.get_selection().get_selected_nodes().has(self) or not target: return var config = get_config() var material = StandardMaterial3D.new() material.albedo_color = Color.RED material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED material.disable_receive_shadows = true for sample in voronoi_generator.sample_points(target.mesh, config): var sphere = CSGSphere3D.new() sphere.set_block_signals(true) sphere.material = material sphere.radius = 0.02 sphere.rings = 4 sphere.radial_segments = 4 sphere.position = sample + target.position sphere.cast_shadow = false sample_visualizers += [sphere] add_child(sphere) func _get_configuration_warnings(): if not get_target_mesh(): return [NO_MESH_CHILD]