MagicNStuff/source/addons/lightmap_probe_grid/lightmap_probe_grid.gd

469 lines
14 KiB
GDScript

@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()