This commit is contained in:
2025-05-09 23:06:36 +02:00
parent f3d332f39c
commit dda8eac39a
135 changed files with 8297 additions and 186 deletions

View File

@@ -34,10 +34,10 @@ const GRAPH_MAX_FRAMETIME = 1.0 / GRAPH_MAX_FPS
## Debug menu display style.
enum Style {
HIDDEN, ## Debug menu is hidden.
VISIBLE_COMPACT, ## Debug menu is visible, with only the FPS, FPS cap (if any) and time taken to render the last frame.
VISIBLE_DETAILED, ## Debug menu is visible with full information, including graphs.
MAX, ## Represents the size of the Style enum.
HIDDEN, ## Debug menu is hidden.
VISIBLE_COMPACT, ## Debug menu is visible, with only the FPS, FPS cap (if any) and time taken to render the last frame.
VISIBLE_DETAILED, ## Debug menu is visible with full information, including graphs.
MAX, ## Represents the size of the Style enum.
}
## The style to use when drawing the debug menu.
@@ -70,7 +70,7 @@ var sum_func := func avg(accum: float, number: float) -> float: return accum + n
var frame_history_total: Array[float] = []
var frame_history_cpu: Array[float] = []
var frame_history_gpu: Array[float] = []
var fps_history: Array[float] = [] # Only used for graphs.
var fps_history: Array[float] = [] # Only used for graphs.
var frametime_avg := GRAPH_MIN_FRAMETIME
var frametime_cpu_avg := GRAPH_MAX_FRAMETIME
@@ -106,10 +106,10 @@ func _ready() -> void:
# (red = 10 FPS, yellow = 60 FPS, green = 110 FPS, cyan = 160 FPS).
# This makes the color gradient non-linear.
# Colors are taken from <https://tailwindcolor.com/>.
frame_time_gradient.set_color(0, Color8(239, 68, 68)) # red-500
frame_time_gradient.set_color(1, Color8(56, 189, 248)) # light-blue-400
frame_time_gradient.add_point(0.3333, Color8(250, 204, 21)) # yellow-400
frame_time_gradient.add_point(0.6667, Color8(128, 226, 95)) # 50-50 mix of lime-400 and green-400
frame_time_gradient.set_color(0, Color(239, 68, 68)) # red-500
frame_time_gradient.set_color(1, Color(56, 189, 248)) # light-blue-400
frame_time_gradient.add_point(0.3333, Color(250, 204, 21)) # yellow-400
frame_time_gradient.add_point(0.6667, Color(128, 226, 95)) # 50-50 mix of lime-400 and green-400
get_viewport().size_changed.connect(update_settings_label)
@@ -274,8 +274,8 @@ func update_information_label() -> void:
information.text = (
"%s, %d threads\n" % [OS.get_processor_name().replace("(R)", "").replace("(TM)", ""), OS.get_processor_count()]
+ "%s %s (%s %s), %s %s\n" % [OS.get_name(), "64-bit" if OS.has_feature("64") else "32-bit", release_string, "double" if OS.has_feature("double") else "single", graphics_api_string, RenderingServer.get_video_adapter_api_version()]
+ "%s, %s" % [adapter_string, driver_info_string]
+"%s %s (%s %s), %s %s\n" % [OS.get_name(), "64-bit" if OS.has_feature("64") else "32-bit", release_string, "double" if OS.has_feature("double") else "single", graphics_api_string, RenderingServer.get_video_adapter_api_version()]
+"%s, %s" % [adapter_string, driver_info_string]
)

View File

@@ -0,0 +1 @@
uid://uy4ruqo71tqe

View File

@@ -1,7 +1,20 @@
[gd_scene load_steps=3 format=3 uid="uid://cggqb75a8w8r"]
[gd_scene load_steps=6 format=3 uid="uid://cggqb75a8w8r"]
[ext_resource type="Script" path="res://addons/debug_menu/debug_menu.gd" id="1_p440y"]
[sub_resource type="SystemFont" id="SystemFont_f56mb"]
font_names = PackedStringArray("Sans-Serif", "", "", "", "", "", "", "", "", "")
[sub_resource type="Theme" id="Theme_qdnc2"]
default_font = SubResource("SystemFont_f56mb")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6qqxv"]
bg_color = Color(0, 0, 0, 0.498039)
expand_margin_left = 8.0
expand_margin_top = 8.0
expand_margin_right = 8.0
expand_margin_bottom = 8.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ki0n8"]
bg_color = Color(0, 0, 0, 0.25098)
@@ -9,20 +22,19 @@ bg_color = Color(0, 0, 0, 0.25098)
process_mode = 3
layer = 128
[node name="DebugMenu" type="Control" parent="." node_paths=PackedStringArray("fps", "frame_time", "frame_number", "frame_history_total_avg", "frame_history_total_min", "frame_history_total_max", "frame_history_total_last", "frame_history_cpu_avg", "frame_history_cpu_min", "frame_history_cpu_max", "frame_history_cpu_last", "frame_history_gpu_avg", "frame_history_gpu_min", "frame_history_gpu_max", "frame_history_gpu_last", "fps_graph", "total_graph", "cpu_graph", "gpu_graph", "information", "settings")]
[node name="DebugMenu" type="PanelContainer" parent="." node_paths=PackedStringArray("fps", "frame_time", "frame_number", "frame_history_total_avg", "frame_history_total_min", "frame_history_total_max", "frame_history_total_last", "frame_history_cpu_avg", "frame_history_cpu_min", "frame_history_cpu_max", "frame_history_cpu_last", "frame_history_gpu_avg", "frame_history_gpu_min", "frame_history_gpu_max", "frame_history_gpu_last", "fps_graph", "total_graph", "cpu_graph", "gpu_graph", "information", "settings")]
custom_minimum_size = Vector2(400, 400)
layout_mode = 3
anchors_preset = 1
anchors_preset = 11
anchor_left = 1.0
anchor_right = 1.0
offset_left = -416.0
offset_top = 8.0
offset_right = -16.0
offset_bottom = 408.0
anchor_bottom = 1.0
offset_left = -277.0
grow_horizontal = 0
grow_vertical = 2
size_flags_horizontal = 8
size_flags_vertical = 4
mouse_filter = 2
theme = SubResource("Theme_qdnc2")
theme_override_styles/panel = SubResource("StyleBoxFlat_6qqxv")
script = ExtResource("1_p440y")
fps = NodePath("VBoxContainer/FPS")
frame_time = NodePath("VBoxContainer/FrameTime")
@@ -47,13 +59,7 @@ information = NodePath("VBoxContainer/Information")
settings = NodePath("VBoxContainer/Settings")
[node name="VBoxContainer" type="VBoxContainer" parent="DebugMenu"]
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -300.0
offset_bottom = 374.0
grow_horizontal = 0
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 0
@@ -61,8 +67,8 @@ theme_override_constants/separation = 0
modulate = Color(0, 1, 0, 1)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 5
theme_override_constants/line_spacing = 0
theme_override_constants/outline_size = 5
theme_override_font_sizes/font_size = 18
text = "60 FPS"
horizontal_alignment = 2

View File

@@ -0,0 +1 @@
uid://c15ljqrjuqgvh

View File

@@ -0,0 +1,9 @@
shader_type spatial;
render_mode unshaded;
uniform sampler2D depth_texture : source_color, hint_depth_texture, filter_nearest, repeat_disable;
void fragment() {
float depth = texture(depth_texture, SCREEN_UV).x;
ALBEDO = vec3(depth);
}

View File

@@ -0,0 +1 @@
uid://0dexnhbddxfl

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 SpockBauru
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,79 @@
# LightmapProbeGrid
Create a grid of Lightmap Probes and cut unwanted ones!
LightmapProbeGrid is an extension for [Godot Engine](https://godotengine.org/) that helps on the demanding task of placing Lightmap Probes where LightmapGI fails to do it.
**Disclaimer:** If you are getting the error `scene/3d/lightmap_gi.cpp:529 - Inconsistency found in triangulation...` is because [Godot Issue 82642](https://github.com/godotengine/godot/issues/82642). If you have the knowledge, would you kindly contrubute to fix the issue please? \o/
### Video Tutorial
https://www.youtube.com/watch?v=HzZSQ0BPpuk
# Index
* [What's new](#whats-new)
* [How to install](#how-to-install)
* [Making a grid of Light Probes](#making-a-grid-of-light-probes)
* [Cut Obstructed Probes](#cut-obstructed-probes)
* [Cut probes inside objects](#cut-probes-inside-objects)
* [Cut probes far from objects](#cut-probes-far-from-objects)
* [Limitations](#limitations)
* [Compatibility](#compatibility)
* [Ending notes](#ending-notes)
* [Changelog](#changelog)
# What's New
Thanks to [dwarfses](https://twitter.com/dwarfses/status/1731691097263362513), LightmapProbeGrid v2.0 now uses GPU raycast instead of the regular Physics raycast. It may be slower but means that colliders are not needed anymore! If the camera can see, it should work!
NOTE: The Cull Mask from v1.0 is not compatible with v2.0.
# How to install
1) Download the file `LightmapProbeGrid_v2.0.zip` from the [Download Page](https://github.com/SpockBauru/LightmapProbeGrid/releases)
2) Extract the `addons` folder on the root of your project (`res://`). Other files/folders are optional.
3) Go to Godot's "Project" menu -> "Project Settings" -> "Plugins" tab -> enable "LightmapProbeGrid".
4) Restart Godot.
You can also open the `DemoScene` to see how it works.
# Making a grid of Light Probes
- Place the LightmapProbeGrid Node in the scene. It's located at "Add Node" -> Node3D -> LightmapProbeGrid.
- Use the handles (red dots) to resize the grid.
- In the LightmapProbeGrid Inspector you can set the number of Light Probes on each axis with the minimum of 2. Press "Generate Probes" to apply the settings and place your grid of Light Probes in the Scene.
Now you can cut unwanted probes with the methods bellow.
## Cut Obstructed Probes
This method is designed to cut probes that are placed beyond visual limits such as the ground or the walls of a cave.
On LightmapProbeGrid Inscpector click on "Cut Obstructed Probes". It will test each Light Probe from the center of the grid to the probe and see if the line intercepts an object. The probe will be cut if there's something blocking the line.
## Cut probes inside objects
This method is designed to delete probes that are inside objects. It will test all 6 axis of each Light Probe: Up, Down, Left, Right, Forward and Backward by the distance indicated in `Max Object Size`.
If at least 4 of these lines hit something the probe will be cut. It considers only 4 hits to cut probes on long objects like pillars and trees.
## Cut probes far from objects
This method is designed to delete probes that are far away from any object. Normally these probes don't contain any relevant light information but use with care in places that have a high usage of spotlights.
When you click the button the area around the Light Probe is tested on various directions by the distance indicated in `Max Distance`. The probe will be cut if none of the rays intercept an object.
## Using masks
You can select which 3D render layers LightmapProbeGrid will interact on the section Visual Cull Mask. Only selected layers will be used on detection for the Cut methods above.
Use masks to filter out objects to not interact with the rays, like characters or moving objects.
# Limitations
LightmapProbeGrid is not designed to work with a huge number of Light Probes at once covering a vast area. It is designed to be placed multiple times in a scene with relatively small grids (less than 1,000 probes).
# Compatibility
LightmapProbeGrid is compatible with Godot 4.2 and there are plans to continue supporting onward.
# Ending notes
This tool was entirely made on my free time. If you want to support me, please make an awesome asset and publish for free to the community!
# Changelog
v2.0:
- Major changes: now uses GPU Raycast instead of Physics raycast
- This means that colliders are not needed anymore!
- The Cull Mask from v1.0 is not compatible with v2.0
v1.0:
- First release.

View File

@@ -0,0 +1,89 @@
@tool
extends Node
@export var probes_x: SpinBox
@export var probes_y: SpinBox
@export var probes_z: SpinBox
@export var planned_probes: RichTextLabel
@export var generate_button: Button
@export var far_distance: SpinBox
@export var object_size: SpinBox
var root_node: Node
var probe_grid: LightmapProbeGrid
func _ready() -> void:
root_node = EditorInterface.get_edited_scene_root()
if EditorInterface.get_selection().get_selected_nodes().size() == 1:
probe_grid = EditorInterface.get_selection().get_selected_nodes()[0] as LightmapProbeGrid
else:
return
# connecting signals
if not probe_grid.probes_changed.is_connected(_get_probes):
probe_grid.probes_changed.connect(_get_probes)
if not probe_grid.probes_changed.is_connected(planned_probes_text):
probe_grid.probes_changed.connect(planned_probes_text)
# initializing values
far_distance.value = probe_grid.far_distance
object_size.value = probe_grid.object_size
_get_probes()
planned_probes_text()
planned_probes.tooltip_text = "Maximum is %s" % probe_grid.max_probes
func planned_probes_text() -> void:
var total: int = probe_grid.planned_probes
var current: int = probe_grid.current_probes
var max_probes: int = probe_grid.max_probes
if total <= max_probes:
planned_probes.text = "Probes Planned/Current: " + str(total) + " / " + str(current)
generate_button.disabled = false
else:
planned_probes.text = "[color=red]Planned Probes: %s [/color] \
\nWarning: Max number of probes is %s" % [total, max_probes]
generate_button.disabled = true
func _set_probes_number(_value: float) -> void:
var number_of_probes: Vector3i = Vector3i.ONE
number_of_probes.x = int(probes_x.value)
number_of_probes.y = int(probes_y.value)
number_of_probes.z = int(probes_z.value)
probe_grid.probes_number = number_of_probes
func _on_generate_probes_pressed() -> void:
probe_grid.generate_probes()
func _get_probes() -> void:
var number: Vector3i = probe_grid.probes_number
probes_x.value = number.x
probes_y.value = number.y
probes_z.value = number.z
func _on_cut_by_mask_pressed() -> void:
probe_grid.cut_obstructed()
func _cut_far_probes():
probe_grid.cut_far()
func _set_far_distance(value):
probe_grid.far_distance = value
func _cut_inside():
probe_grid.cut_inside()
func _set_object_size(value):
probe_grid.object_size = value

View File

@@ -0,0 +1 @@
uid://l34dvbnsrsna

View File

@@ -0,0 +1,198 @@
[gd_scene load_steps=3 format=3 uid="uid://xp1820rv0uy"]
[ext_resource type="Script" path="res://addons/lightmap_probe_grid/UI.gd" id="1_4a60v"]
[ext_resource type="StyleBox" uid="uid://4nfudftbnexo" path="res://addons/lightmap_probe_grid/style_box_flat.tres" id="2_rdj4j"]
[node name="RootContainer" type="VBoxContainer" node_paths=PackedStringArray("probes_x", "probes_y", "probes_z", "planned_probes", "generate_button", "far_distance", "object_size")]
offset_right = 257.0
offset_bottom = 346.0
script = ExtResource("1_4a60v")
probes_x = NodePath("ProbesContainer/ProbesX")
probes_y = NodePath("ProbesContainer/ProbesY")
probes_z = NodePath("ProbesContainer/ProbesZ")
planned_probes = NodePath("PlannedLabel")
generate_button = NodePath("GenerateProbes")
far_distance = NodePath("CutFarProbesContainer/Distance")
object_size = NodePath("CutInsideContainer/Size")
[node name="ProbesLabel" type="Label" parent="."]
clip_contents = true
layout_mode = 2
tooltip_text = "Number of probes on each axis"
mouse_filter = 0
text = "Number of Probes"
vertical_alignment = 1
[node name="ProbesContainer" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="ProbesX" type="SpinBox" parent="ProbesContainer"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Minimum is 2"
min_value = 2.0
max_value = 250.0
value = 2.0
alignment = 2
prefix = "x:"
select_all_on_focus = true
[node name="ProbesY" type="SpinBox" parent="ProbesContainer"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Minimum is 2"
min_value = 2.0
max_value = 250.0
value = 2.0
alignment = 2
prefix = "y:"
select_all_on_focus = true
[node name="ProbesZ" type="SpinBox" parent="ProbesContainer"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Minimum is 2"
min_value = 2.0
max_value = 250.0
value = 2.0
alignment = 2
prefix = "z:"
select_all_on_focus = true
[node name="PlannedLabel" type="RichTextLabel" parent="."]
layout_mode = 2
tooltip_text = "Maximum is xxxx"
bbcode_enabled = true
text = "Planned Probes: xxxx"
fit_content = true
[node name="GenerateProbes" type="Button" parent="."]
layout_mode = 2
theme_override_styles/normal = ExtResource("2_rdj4j")
text = "Generate Probes"
[node name="HSeparator" type="HSeparator" parent="."]
layout_mode = 2
[node name="ObscuredLabel" type="Label" parent="."]
layout_mode = 2
tooltip_text = "Test from center to each probe,
and cut the probe if the path is obstructed by an object.
Use Collision Mask to filter objects."
mouse_filter = 0
text = "Probes obscured from center"
vertical_alignment = 1
[node name="Cut by Mask" type="Button" parent="."]
layout_mode = 2
tooltip_text = "Test from center to each probe,
and cut the probe if the path is obstructed by an object.
Use Collision Mask to filter objects."
theme_override_styles/normal = ExtResource("2_rdj4j")
text = "Cut Obstructed Probes"
[node name="HSeparator2" type="HSeparator" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="."]
layout_mode = 2
tooltip_text = "For each probe, test if there is any object within an Max distance.
If there isn't any object the probe is cut.
Use Collision Mask to filter objects."
mouse_filter = 0
text = "Probes far from any object"
[node name="CutFarProbesContainer" type="HBoxContainer" parent="."]
layout_mode = 2
alignment = 1
[node name="DistanceLabel" type="Label" parent="CutFarProbesContainer"]
custom_minimum_size = Vector2(67, 0)
layout_mode = 2
tooltip_text = "The distance from the probe that objects will be detected"
mouse_filter = 0
text = "Max distance"
[node name="Distance" type="SpinBox" parent="CutFarProbesContainer"]
layout_mode = 2
tooltip_text = "The distance from the probe that objects will be detected"
step = 0.0
value = 1.0
allow_greater = true
suffix = "m"
custom_arrow_step = 1.0
select_all_on_focus = true
[node name="CutFarProbes" type="Button" parent="."]
custom_minimum_size = Vector2(148, 0)
layout_mode = 2
tooltip_text = "For each probe, test if there is any object within an Max distance.
If there isn't any object the probe is cut.
Use Collision Mask to filter objects."
theme_override_styles/normal = ExtResource("2_rdj4j")
text = "Cut Far Probes"
[node name="HSeparator3" type="HSeparator" parent="."]
layout_mode = 2
[node name="Label2" type="Label" parent="."]
layout_mode = 2
tooltip_text = "For each probe, test if the same object is intercepted by the yellow lines.
If the same object is detected 4 times or more,
the probe is considered to be inside and will be cut.
Only 4 lines are tested instead of 6 because is common for
objects to not have 2 faces, like pillars.
Use Collision Mask to filter objects."
mouse_filter = 0
text = "Probes inside objects"
[node name="CutInsideContainer" type="HBoxContainer" parent="."]
layout_mode = 2
alignment = 1
[node name="SizeLabel" type="Label" parent="CutInsideContainer"]
layout_mode = 2
tooltip_text = "Distance tested from the probes (size of yellow lines)"
mouse_filter = 0
text = "Max object size"
[node name="Size" type="SpinBox" parent="CutInsideContainer"]
layout_mode = 2
tooltip_text = "Distance tested from the probes (size of yellow lines)"
step = 0.0
value = 1.0
allow_greater = true
suffix = "m"
custom_arrow_step = 1.0
[node name="CutInsideObjects" type="Button" parent="."]
custom_minimum_size = Vector2(148, 0)
layout_mode = 2
tooltip_text = "For each probe, test if the same object is intercepted by the yellow lines.
If the same object is detected 4 times or more,
the probe is considered to be inside and will be cut.
Only 4 lines are tested instead of 6 because is common for
objects to not have 2 faces, like pillars.
Use Collision Mask to filter objects."
theme_override_styles/normal = ExtResource("2_rdj4j")
text = "Cut Inside Objects"
[connection signal="value_changed" from="ProbesContainer/ProbesX" to="." method="_set_probes_number"]
[connection signal="value_changed" from="ProbesContainer/ProbesY" to="." method="_set_probes_number"]
[connection signal="value_changed" from="ProbesContainer/ProbesZ" to="." method="_set_probes_number"]
[connection signal="pressed" from="GenerateProbes" to="." method="_on_generate_probes_pressed"]
[connection signal="pressed" from="Cut by Mask" to="." method="_on_cut_by_mask_pressed"]
[connection signal="value_changed" from="CutFarProbesContainer/Distance" to="." method="_set_far_distance"]
[connection signal="pressed" from="CutFarProbes" to="." method="_cut_far_probes"]
[connection signal="value_changed" from="CutInsideContainer/Size" to="." method="_set_object_size"]
[connection signal="pressed" from="CutInsideObjects" to="." method="_cut_inside"]

View File

@@ -0,0 +1,16 @@
extends EditorInspectorPlugin
var ui_control: PackedScene = preload("controls_scene.tscn")
var ui: Control = null
func _can_handle(object: Object) -> bool:
if object is LightmapProbeGrid:
return true
else:
return false
func _parse_category(_object: Object, category: String) -> void:
if category.begins_with("lightmap_probe_grid"):
if ui == null:
ui = ui_control.instantiate()
add_property_editor("LighmapProbeGrid", ui)

View File

@@ -0,0 +1 @@
uid://cyh1thwnyn2ra

View File

@@ -0,0 +1,159 @@
# my_custom_gizmo_plugin.gd
extends EditorNode3DGizmoPlugin
const handles_axis: PackedVector3Array = [
Vector3(1, 0, 0),
Vector3(0, 1, 0),
Vector3(0, 0, 1),
Vector3(-1, 0, 0),
Vector3(0, -1, 0),
Vector3(0, 0, -1)
]
const box_lines: PackedVector3Array = [
# plane -x
Vector3(-1, -1, -1), Vector3(-1, -1, 1),
Vector3(-1, -1, 1), Vector3(-1, 1, 1),
Vector3(-1, 1, 1), Vector3(-1, 1, -1),
Vector3(-1, 1, -1), Vector3(-1, -1, -1),
# plane +x
Vector3(1, -1, -1), Vector3(1, -1, 1),
Vector3(1, -1, 1), Vector3(1, 1, 1),
Vector3(1, 1, 1), Vector3(1, 1, -1),
Vector3(1, 1, -1), Vector3(1, -1, -1),
# connecting plane x with -x
Vector3(1, -1, -1), Vector3(-1, -1, -1),
Vector3(1, -1, 1), Vector3(-1, -1, 1),
Vector3(1, 1, -1), Vector3(-1, 1, -1),
Vector3(1, 1, 1), Vector3(-1, 1, 1),
]
var icon: Texture2D = preload("lightmap_probe_grid_icon.svg")
var timer: Timer = Timer.new()
var is_awayting: bool = false
func _get_gizmo_name() -> String:
return "LightmapProbeGrid"
func _init() -> void:
create_material("main_material", Color(0,0,0))
create_material("tool_material", Color(1, 0.9, 0))
create_handle_material("handles_material")
create_icon_material("icon_material", icon)
func _has_gizmo(node: Node3D) -> bool:
if node is LightmapProbeGrid:
if not node.size_changed.is_connected(node.update_gizmos):
node.size_changed.connect(node.update_gizmos)
if not node.probes_changed.is_connected(node.update_gizmos):
node.probes_changed.connect(node.update_gizmos)
return true
else:
return false
func _redraw(gizmo: EditorNode3DGizmo) -> void:
gizmo.clear()
var box: LightmapProbeGrid = gizmo.get_node_3d() as LightmapProbeGrid
var size: Vector3 = box.size
var icon_gizmo: Material = get_material("icon_material")
gizmo.add_unscaled_billboard(icon_gizmo, 0.05)
# Setting box lines
var lines: PackedVector3Array = []
for pos: Vector3 in box_lines:
var scaled: Vector3 = 0.5 * pos * size
lines.append(scaled)
gizmo.add_lines(lines, get_material("main_material", gizmo))
# Setting handles
var handles: PackedVector3Array = []
for pos: Vector3 in handles_axis:
var scaled: Vector3 = 0.5 * pos * size
handles.append(scaled)
gizmo.add_handles(handles, get_material("handles_material", gizmo), [])
# Setting extra tool lines from main script
var tool_lines: PackedVector3Array = box.gizmo_lines
if not tool_lines.is_empty():
gizmo.add_lines(tool_lines, get_material("tool_material", gizmo))
clear_tool_await(box)
# Wait 3 seconds before clear the main script gizmos. If called twice the timer is reset
func clear_tool_await(box: LightmapProbeGrid):
if timer == null:
timer = Timer.new()
# Add timer to the scene
if timer.get_parent() == null:
var root_node = EditorInterface.get_edited_scene_root()
root_node.add_child(timer)
timer.name = "lightmap_probe_grid_timer"
timer.wait_time = 3.0
timer.start()
if is_awayting:
return
is_awayting = true
await timer.timeout
is_awayting = false
timer.stop
box.gizmo_lines.clear()
box.update_gizmos()
# Based on github.com/godotengine/godot/blob/master/editor/plugins/gizmos/gizmo_3d_helper.cpp
# please, make it available to GDScript plugin developers...
func _set_handle(gizmo: EditorNode3DGizmo, index: int, _sec: bool, camera: Camera3D, point: Vector2) -> void:
var box: LightmapProbeGrid = gizmo.get_node_3d() as LightmapProbeGrid
var axis: Vector3 = handles_axis[index]
var axis_index: int = axis.abs().max_axis_index()
var inverse: Transform3D = box.global_transform.affine_inverse()
var ray_from: Vector3 = camera.project_ray_origin(point)
var ray_to: Vector3 = camera.project_ray_normal(point)
var camera_position: Vector3 = inverse * ray_from
var camera_to_mouse: Vector3 = inverse * (ray_from + ray_to * 5000)
var segment1: Vector3 = axis * 5000
var segment2: Vector3 = axis * -5000
var intersection: PackedVector3Array = Geometry3D.get_closest_points_between_segments(segment2,
segment1, camera_position, camera_to_mouse)
# Distance between the center and the handle (without scale)
var distance: float = intersection[0][axis_index]
# multiply axis signal to cancel distance signal
distance *= axis[axis_index]
var old_distance: float = 0.5 * box.size[axis_index]
# Defining new size and positions
var new_size: float = distance + old_distance
# Translate halfway through the size difference
var translate: Vector3 = 0.5 * (distance - old_distance) * axis
# Updating size and position
box.size[axis_index] = new_size
box.translate(translate)
# Update Gizmo
box.update_gizmos()
func _get_handle_name(_gizmo: EditorNode3DGizmo, _handle_id: int, _sec: bool) -> String:
return "Probe Grid Size"
func _get_handle_value(gizmo: EditorNode3DGizmo, _id: int, _sec: bool) -> Vector3:
var box: LightmapProbeGrid = gizmo.get_node_3d() as LightmapProbeGrid
return box.size

View File

@@ -0,0 +1 @@
uid://cww446exifamp

View File

@@ -0,0 +1,453 @@
@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 = []
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 get_child_count() < 1:
generate_probes()
func _ready() -> void:
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] = []
var probes_x: float = probes_number.x
var probes_y: float = probes_number.y
var probes_z: float = probes_number.z
# Distance between probes
var step_x: float = size.x / (probes_x - 1)
var step_y: float = size.y / (probes_y - 1)
var step_z: float = size.z / (probes_z - 1)
# 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_x:
for y: float in probes_y:
for z: float in probes_z:
current_position.x = start_position.x - step_x * x
current_position.y = start_position.y - step_y * y
current_position.z = start_position.z - step_z * 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(distance: float) -> 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(far_distance)
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 = (probe_pos - position).length()
# 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]
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(far_distance)
# 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
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(far_distance)
# 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
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()

View File

@@ -0,0 +1 @@
uid://dy5lhlfkbk5t6

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 16" width="16">
<path d="M7 10A1 1 0 0 0 13 10 1 1 0 0 0 7 10zM10 4A1 1 0 0 1 10 16 V14A1 1 0 0 0 10 6zM4 9h2v2H4z M5 6.6 6.4 5.2 7.8 6.6 6.4 8z M5 13.4 6.4 12 7.8 13.4 6.4 14.8z" fill="#fc7f7f"></path> <!-- Probe -->
<path d="M3.7 1 H12.3A2 2 0 1 1 12.3 3H3.7 A2 2 0 0 1 3 3.7 V12.3A2 2 0 1 1 1 12.3 V3.7A2 2 0 1 1 3.7 1z" fill="#fc7f7f"></path> <!-- Frame -->
</svg>

After

Width:  |  Height:  |  Size: 436 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dtmcycli3j3j1"
path="res://.godot/imported/lightmap_probe_grid_icon.svg-71f84c7339e4d62a065aeef79112e123.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/lightmap_probe_grid/lightmap_probe_grid_icon.svg"
dest_files=["res://.godot/imported/lightmap_probe_grid_icon.svg-71f84c7339e4d62a065aeef79112e123.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,7 @@
[plugin]
name="LightmapProbeGrid"
description="Create a grid of LightmapProbes, and cut unwanted ones!"
author="SpockBauru"
version="2.0"
script="plugin.gd"

View File

@@ -0,0 +1,25 @@
@tool
extends EditorPlugin
var custom_node: Script = preload("lightmap_probe_grid.gd")
var icon: Texture2D = preload("lightmap_probe_grid_icon.svg")
var inspector_script: Script = preload("editor_inspector_plugin.gd")
var inspector_plugin: EditorInspectorPlugin = inspector_script.new()
var gizmo_script: Script = preload("gizmo.gd")
var gizmo_plugin: EditorNode3DGizmoPlugin = gizmo_script.new()
func _get_plugin_name() -> String:
return "LightmapProbeGrid"
func _enter_tree() -> void:
add_custom_type("LightmapProbeGrid", "Node3D", custom_node, icon)
add_inspector_plugin(inspector_plugin)
add_node_3d_gizmo_plugin(gizmo_plugin)
func _exit_tree() -> void:
remove_custom_type("LightmapProbeGrid")
remove_inspector_plugin(inspector_plugin)
remove_node_3d_gizmo_plugin(gizmo_plugin)

View File

@@ -0,0 +1 @@
uid://df4lt60aa1ac8

View File

@@ -0,0 +1,13 @@
[gd_resource type="StyleBoxFlat" format=3 uid="uid://4nfudftbnexo"]
[resource]
draw_center = false
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.5, 0.5, 0.5, 1)
corner_radius_top_left = 2
corner_radius_top_right = 2
corner_radius_bottom_right = 2
corner_radius_bottom_left = 2