@tool extends Node3D class_name LightmapProbeGrid signal size_changed signal probes_changed const max_probes: int = 1000 ## Only selected layers will be seen by LightmapProbeGrid. Works like Camera3D Cull Mask.[br][br] ## NOTE: NOT compatible with LightmapProbeGrid v1.0 @export_flags_3d_render var visual_cull_mask: int = 1048575 @export var size: Vector3 = Vector3.ONE: set(value): # size cannot be zero or negative size = value.clamp(Vector3(1E-6, 1E-6, 1E-6), Vector3.INF) size_changed.emit() get: return size @onready var depth_shader: Shader = preload("Depth.gdshader") var probes_number: Vector3i = Vector3i(2, 2, 2): set(value): probes_number = value set_probes_number() get: return probes_number var planned_probes: int = 8 var current_probes: int = 8 var old_size: Vector3 = Vector3.ONE var old_scale: Vector3 = Vector3.ONE var warned: bool = false var far_distance: float = 1 var object_size: float = 1 var gizmo_lines: PackedVector3Array = [] var godot_version: int = Engine.get_version_info().hex func _get_property_list() -> Array[Dictionary]: var properties: Array[Dictionary] = [] properties.append({ "name": "probes_number", "type": TYPE_VECTOR3I, "usage": PROPERTY_USAGE_STORAGE }) properties.append({ "name": "far_distance", "type": TYPE_FLOAT, "usage": PROPERTY_USAGE_STORAGE }) properties.append({ "name": "object_size", "type": TYPE_FLOAT, "usage": PROPERTY_USAGE_STORAGE }) return properties func _enter_tree() -> void: if not Engine.is_editor_hint(): return if get_child_count() < 1: generate_probes() func _ready() -> void: if not Engine.is_editor_hint(): return size_changed.connect(scale_probes) set_notify_local_transform(true) old_size = size old_scale = scale current_probes = get_child_count() # Keep local scale fixed. Reflect in "size" if the user try to scale func _notification(what: int) -> void: if (what == NOTIFICATION_LOCAL_TRANSFORM_CHANGED) and not scale.is_equal_approx(Vector3.ONE): if not warned: printerr("LightmapProbeGrid: Resetting Scale, please use the handles (red dots) or ", "the property \"Size\" in LightmapProbeGridsection") warned = true if(scale.x <= 0): scale = Vector3.ONE return # TODO take a look on this workaround var scale_diff: Vector3 = abs(scale - Vector3.ONE) var size_sign: Vector3 = sign(scale - old_scale) size += size_sign * scale_diff / 10.0 old_scale = scale scale = Vector3.ONE func _get_configuration_warnings() -> PackedStringArray: var warnings: PackedStringArray = [] if planned_probes > max_probes: var text: String = "LightmapProbeGrid: The maximum number of Probes must be " + \ str(max_probes) + ". Please consider add more instances of LightmapProbeGrid" warnings.append(text) printerr(text) # Returning an empty array means "no warning". return warnings func set_probes_number() -> void: planned_probes = probes_number.x * probes_number.y * probes_number.z update_configuration_warnings() probes_changed.emit() func scale_probes() -> void: var new_size: Vector3 = size / old_size # Scaling all probes for probe: Node3D in get_children(): probe.position *= new_size old_size = size func generate_probes() -> void: # check number of probes if planned_probes > max_probes: return # Clear all previews probes for i: int in get_child_count(): get_child(i).queue_free() # Wait for the last one to be cleaned if i == get_child_count() -1: await get_child(i).tree_exited # Defining probe arrays var probes_positions: Array[Vector3] = [] var probes_names: Array[String] = [] # Distance between probes # step = size / (probes_number - 1) var step: Vector3 = size / Vector3(probes_number - Vector3i.ONE) # Starting relative positions var start_position: Vector3 = Vector3.ONE * size / 2.0 var current_position: Vector3 = Vector3.ZERO # Defining Probes relative positions and names for x: float in probes_number.x: for y: float in probes_number.y: for z: float in probes_number.z: current_position = start_position - step * Vector3(x, y, z) probes_positions.append(current_position) probes_names.append("LightmapProbe %.f, %.f, %.f" % [x, y, z]) # Generating probes var root_node: Node = get_tree().edited_scene_root for i: int in range(probes_positions.size()): var probe: LightmapProbe = LightmapProbe.new() probe.position = probes_positions[i] probe.name = probes_names[i] add_child(probe) probe.set_owner(root_node) current_probes = probes_number.x * probes_number.y * probes_number.z set_probes_number() # Workaround to raycast without colliders. Consists in a camera with a filter in front that shows # the depth texture. The camera.far is the "ray" lenght and camera rotation is the "ray" orientation # https://docs.godotengine.org/en/stable/tutorials/shaders/advanced_postprocessing.html#depth-texture func add_GPU_raycaster(probe: Node3D) -> void: var root_node: Node = get_tree().edited_scene_root # SubViewport that will host the camera var sub_viewport: SubViewport = SubViewport.new() sub_viewport.name = "GPUraycast" sub_viewport.size = Vector2(2, 2) sub_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED sub_viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_NEVER sub_viewport.handle_input_locally = false sub_viewport.debug_draw = Viewport.DEBUG_DRAW_UNSHADED sub_viewport.positional_shadow_atlas_size = 0 sub_viewport.positional_shadow_atlas_quad_0 = Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_DISABLED sub_viewport.positional_shadow_atlas_quad_1 = Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_DISABLED sub_viewport.positional_shadow_atlas_quad_2 = Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_DISABLED sub_viewport.positional_shadow_atlas_quad_3 = Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_DISABLED probe.add_child(sub_viewport) sub_viewport.set_owner(root_node) # Camera for the viewport var camera_3d: Camera3D = Camera3D.new() camera_3d.projection = Camera3D.PROJECTION_ORTHOGONAL camera_3d.size = 0.001 camera_3d.near = 0.001 camera_3d.far = 1.0 sub_viewport.add_child(camera_3d) camera_3d.set_owner(root_node) camera_3d.position = probe.global_position camera_3d.rotation = Vector3.ZERO camera_3d.cull_mask = visual_cull_mask # Depth filter: A quad with a material that shows the Depth texture. This goes in front of the # camera var depth_material: ShaderMaterial = ShaderMaterial.new() depth_material.shader = depth_shader var depth_filter: MeshInstance3D = MeshInstance3D.new() var depth_mesh: QuadMesh = QuadMesh.new() depth_filter.mesh = depth_mesh depth_mesh.material = depth_material depth_mesh.size = Vector2.ONE * 0.001 camera_3d.add_child(depth_filter) depth_filter.set_owner(root_node) depth_filter.position = Vector3(0, 0, -0.002) depth_filter.rotation = Vector3.ZERO func generate_probes_raycasters() -> void: for probe in get_children(): add_GPU_raycaster(probe) func remove_probes_raycasters() -> void: for probe in get_children(): for child in probe.get_children(): child.queue_free(); # The function look_at not always work. Exceptions are handled here func rotate_camera(camera: Camera3D, to: Vector3) -> void: var from: Vector3 = camera.position # look_at don't work if the node and target have the same position. You cannot look at yourself if from == to: return # look_at don't work if the direction and rotation axix have same orientation. In this case, # change the rotation axis var direction: Vector3 = abs(to - from) var mag = (direction.normalized() - Vector3.UP).length() if mag > 0.001: camera.look_at(to) else: camera.look_at(to, Vector3.RIGHT) # Shoot rays from the center to all the probes. If any object is detected so the probe is # obstructed and will be cut func cut_obstructed() -> void: await generate_probes_raycasters() var probes_array: Array[LightmapProbe] = [] var camera_array: Array[Camera3D] = [] var subViewport_array: Array[SubViewport] = [] var results_array: Array[float] = [] # Populating arrays for probe: LightmapProbe in get_children(): probes_array.append(probe) gizmo_lines.append_array([Vector3.ZERO, probe.position]) var sub_viewport: SubViewport = probe.get_child(0) subViewport_array.append(sub_viewport) var camera: Camera3D = sub_viewport.get_child(0) camera_array.append(camera) # Rotating cameras and updating sub_viewports for i in range(camera_array.size()): var camera: Camera3D = camera_array[i] var probe_pos: Vector3 = probes_array[i].global_position var sub_viewport: SubViewport = subViewport_array[i] camera.position = position # The lenght of the "Ray" camera.far = maxf((probe_pos - position).length(), 0.0021) # The direction of the "Ray" rotate_camera(camera, probe_pos) sub_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE # Getting the values await RenderingServer.frame_post_draw for i in range(subViewport_array.size()): var sub_viewport = subViewport_array[i] var texture: Image = sub_viewport.get_texture().get_image() var color: Color = texture.get_pixel(0,0) var colorValue: float = color.r var result: float = colorValue results_array.append(result) # Cutting probes for i in range(subViewport_array.size()): var result: float = results_array[i] # On Godot 4.3+, the depth texture was inverted if godot_version >= 0x040300: result = 1.0 - result if result < 1.0: var probe = probes_array[i] probe.queue_free() current_probes -= 1 set_probes_number() remove_probes_raycasters() # Detect if the probe is far from any object. It will shoot rays on all 6 axis and 8 quadrants. # If there aren't any objects the probe will be cut func cut_far() -> void: await generate_probes_raycasters() # 6 axis and 8 quadrants var directions: Array[Vector3] = [ # 6 Axis Vector3(0, 0, 1), Vector3(0, 1, 0), Vector3(1, 0, 0), Vector3(0, 0, -1), Vector3(0, -1, 0), Vector3(-1, 0, 0), # 8 Quadrants Vector3(1, 1, 1).normalized(), Vector3(1, 1, -1).normalized(), Vector3(1, -1, 1).normalized(), Vector3(1, -1, -1).normalized(), Vector3(-1, 1, 1).normalized(), Vector3(-1, 1, -1).normalized(), Vector3(-1, -1, 1).normalized(), Vector3(-1, -1, -1).normalized() ] var probes_array: Array[LightmapProbe] = [] var camera_array: Array[Camera3D] = [] var subViewport_array: Array[SubViewport] = [] var collisions_number: Array[int] = [] # Populating arrays for probe: LightmapProbe in get_children(): probes_array.append(probe) var sub_viewport: SubViewport = probe.get_child(0) subViewport_array.append(sub_viewport) var camera: Camera3D = sub_viewport.get_child(0) camera_array.append(camera) collisions_number.resize(camera_array.size()) collisions_number.fill(0) # Getting data for all cameras on each direction for dir in directions: # Rotating all cameras to the same direction, and updating viewport for i in camera_array.size(): var probe: LightmapProbe = probes_array[i] var sub_viewport: SubViewport = subViewport_array[i] var camera: Camera3D = camera_array[i] camera.position = probe.global_position # The lenght of the "Ray" camera.far = far_distance # The direction of the "Ray" rotate_camera(camera, probe.global_position + dir) sub_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE gizmo_lines.append_array([probe.position, probe.position + dir * far_distance]) # Getting all values for the current direction await RenderingServer.frame_post_draw for i in range(subViewport_array.size()): var sub_viewport = subViewport_array[i] var texture: Image = sub_viewport.get_texture().get_image() var color: Color = texture.get_pixel(0,0) var colorValue: float = color.r var result: float = colorValue # On Godot 4.3+, the depth texture was inverted if godot_version >= 0x040300: result = 1.0 - result if result < 1.0: collisions_number[i] += 1 # Cut probes if there are no collisions for i in probes_array.size(): if collisions_number[i] < 1: var probe = probes_array[i] probe.queue_free() current_probes -= 1 set_probes_number() remove_probes_raycasters() # Detect if probe is inside an object. It will shoot rays from all 6 axis to the probe. If at least # 4 are obstructed, the probe will be cut func cut_inside() -> void: await generate_probes_raycasters() # 6 Axis var axis: Array[Vector3] = [ Vector3(0, 0, 1), Vector3(0, 1, 0), Vector3(1, 0, 0), Vector3(0, 0, -1), Vector3(0, -1, 0), Vector3(-1, 0, 0), ] var probes_array: Array[LightmapProbe] = [] var camera_array: Array[Camera3D] = [] var subViewport_array: Array[SubViewport] = [] var collisions_number: Array[int] = [] # Populating arrays for probe: LightmapProbe in get_children(): probes_array.append(probe) var sub_viewport: SubViewport = probe.get_child(0) subViewport_array.append(sub_viewport) var camera: Camera3D = sub_viewport.get_child(0) camera_array.append(camera) collisions_number.resize(camera_array.size()) collisions_number.fill(0) # Getting data for all cameras on each axis for dir in axis: # For each direction, position all cameras to look from outside # to each probe in object_size distance for i in camera_array.size(): var probe: LightmapProbe = probes_array[i] var sub_viewport: SubViewport = subViewport_array[i] var camera: Camera3D = camera_array[i] camera.position = probe.global_position + dir * object_size # The lenght of the "Ray" camera.far = object_size # The direction of the "Ray" rotate_camera(camera, probe.global_position) sub_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE gizmo_lines.append_array([probe.position, probe.position + dir * object_size]) # Getting all values for the current direction await RenderingServer.frame_post_draw for i in range(subViewport_array.size()): var sub_viewport = subViewport_array[i] var texture: Image = sub_viewport.get_texture().get_image() var color: Color = texture.get_pixel(0,0) var colorValue: float = color.r var result: float = colorValue # On Godot 4.3+, the depth texture was inverted if godot_version >= 0x040300: result = 1.0 - result if result < 1.0: collisions_number[i] += 1 # Cut probes if there are more than 4 collisions for i in probes_array.size(): if collisions_number[i] > 3: var probe = probes_array[i] probe.queue_free() current_probes -= 1 set_probes_number() remove_probes_raycasters()