Initial commit

This commit is contained in:
scorpion21211-cmd
2026-10-03 23:29:41 -04:00
commit e24bbbda5d
10586 changed files with 1030090 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
# Changelog
All notable changes to World Brush are documented here.
## 0.3.4 - 2026-08-14
- Added per-palette-entry Godot automatic mesh LOD controls.
- Added one-click `Generate LODs` import configuration and reimport for only
the active model source.
- Added optional distance culling per model.
- Added persistent Auto LOD controllers for painted instances.
- Added a centralized, batched runtime distance manager.
- Far instances now suspend rendering, shadows, GI, collision, areas, and child
processing, then restore their original state when the camera returns.
- Added `LOD Bias = 0` support for forcing the lightest generated LOD in tests.
- Improved Arabic and English Auto LOD guidance.
## 0.3.0
- Added independent manual near, medium, and distant LOD scenes per palette
entry.
- Added configurable transition and culling distances.
- Added editor preview and runtime camera switching.
## 0.2.0
- Added scene palette persistence and independent per-scene brush profiles.
- Added undo/redo for painting and erasing.
## 0.1.0
- Initial 3D paint and erase workflow.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ALSAHLY
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.
+99
View File
@@ -0,0 +1,99 @@
# World Brush
World Brush is a Godot 4 editor plugin for painting and erasing 3D scenes directly in the 3D viewport.
## Features
- Paint reusable 3D scenes directly in the editor viewport.
- Erase previously painted instances.
- Manage multiple scenes through a scene palette.
- Keep an independent profile for every scene, including its medium/distant models and all LOD distances.
- Enable Godot's automatic mesh LOD independently for any single active palette model.
- Configure native LOD bias and an optional culling distance per model.
- Apply `Generate LODs` and reimport only the active model source with one click.
- Optionally assign medium and distant scenes to an active scene.
- Keep only one LOD scene visible and switch it by camera distance at runtime.
- Configure medium, distant, and optional culling distances per scene.
- Adjust brush radius, instance count, and minimum spacing.
- Randomize Y rotation and scale.
- Rotate instances with the surface.
- Limit placement by maximum slope.
- Apply height offset along World Y or the surface normal.
- Undo and redo painting and erasing actions.
- Save palette and brush settings.
- Arabic and English interface support.
## Installation
1. Copy the `addons/world_brush` folder into your Godot project.
2. Open the project in Godot.
3. Go to `Project > Project Settings > Plugins`.
4. Enable `World Brush`.
## Distance LOD workflow
### Automatic LOD from one model
1. Add and activate a model or scene in the scene palette.
2. Enable `Godot Auto LOD — Single Model` for that palette entry.
3. Adjust `Transition Detail` (`LOD Bias`) and the optional `Hide After` distance.
4. Click `Apply & Reimport This Model` if the source needs to be regenerated.
The import action updates `Meshes > Generate LODs` only for the active model
source (GLB, GLTF, FBX, OBJ, or Blend). It never changes every palette model.
Every palette scene stores its own automatic LOD enabled state, bias, and cull
distance. Turning the option off restores the original GeometryInstance3D
values on painted instances managed by World Brush.
Set LOD bias to `0` to force the lightest generated mesh for visual testing;
use a higher value for normal gameplay quality.
World Brush stores a lightweight controller beside every painted Auto LOD
instance. This keeps native LOD bias and culling active after saving, reopening,
or running the edited scene without modifying the reusable source scene.
At runtime, culled instances also disable their collision shapes, collision
layers, area monitoring, shadows, global illumination, rendering, and child
processing. They are restored slightly
before the camera returns to the culling boundary to avoid visible or physical
popping.
A single runtime manager updates all painted instances in small batches. This
avoids per-instance timers and keeps distance checks smooth with hundreds of
props.
### Manual LOD from authored models
1. Add and activate the original high-detail scene in the scene palette.
2. Enable `Add Distant Scene`.
3. Assign the medium and distant versions of the same model.
4. Set the distances where the medium and distant scenes begin.
5. Optionally set `Hide After`; leave it at `0` to disable distance culling.
Newly painted instances show one LOD scene at a time. The editor preview follows
the 3D editor camera while painting and navigating, and the saved runtime
controller follows the active game camera. Older World Brush LOD groups are
upgraded automatically when their scene is opened.
Switching the active palette scene restores only that scene's LOD profile.
New scenes start with LOD disabled and empty medium/distant slots; settings are
never inherited from the previously active scene. Changing a profile updates
only painted instances whose original scene matches that profile.
## Compatibility
- Godot 4.6 or newer (tested with 4.6.3 and 4.7.1)
- 3D projects
- Editor plugin
## Support and known behavior
- Automatic mesh LOD depends on Godot's generated import LODs. Use `Apply &
Reimport This Model` for imported GLB, GLTF, FBX, OBJ, or Blend sources.
- Native LOD changes mesh detail; `Hide After` additionally disables rendering,
shadows, GI, collision, area monitoring, and child processing at long range.
- World Brush never changes the import settings of every palette entry at once.
Each scene keeps an independent Auto LOD profile.
- If an issue occurs, include the Godot version, renderer, reproduction steps,
and the editor Output log in the report.
## License
World Brush is released under the MIT License.
+7
View File
@@ -0,0 +1,7 @@
[plugin]
name="World Brush"
description="Paint, erase, scatter, and optimize reusable 3D scenes directly in the editor viewport."
author="ALSAHLY"
version="0.3.4"
script="world_brush_plugin.gd"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://4uis0jk0b1bv
@@ -0,0 +1,24 @@
[gd_scene format=3 uid="uid://cxihuk2blp8s"]
[ext_resource type="Script" uid="uid://4uis0jk0b1bv" path="res://addons/world_brush/ui/world_brush_dock.gd" id="1_u07ui"]
[node name="WorldBrushDock" type="VBoxContainer" unique_id=2039932584]
layout_direction = 3
offset_right = 280.0
offset_bottom = 400.0
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_u07ui")
[node name="TitleLabel" type="Label" parent="." unique_id=1893496441]
layout_mode = 2
text = "فرشاة العالم"
horizontal_alignment = 1
[node name="TopSeparator" type="HSeparator" parent="." unique_id=848279132]
layout_mode = 2
[node name="StatusLabel" type="Label" parent="." unique_id=1926505656]
layout_mode = 2
text = "تمت استعادة المكتبة: 2 Scene(s)"
horizontal_alignment = 2
@@ -0,0 +1,207 @@
@tool
extends Node
@export var auto_lod_enabled := true
@export_range(0.0, 4.0, 0.05) var lod_bias := 1.0
@export_range(0.0, 100000.0, 1.0) var cull_distance := 0.0
@export var original_settings: Dictionary = {}
const REACTIVATION_RATIO := 0.9
const RUNTIME_MANAGER_SCRIPT: Script = preload(
"res://addons/world_brush/world_brush_auto_lod_manager.gd"
)
var _runtime_states: Dictionary = {}
var _runtime_far := false
var _runtime_registered := false
func _ready() -> void:
call_deferred("_apply_settings")
if not Engine.is_editor_hint():
call_deferred("_register_with_runtime_manager")
func configure(enabled: bool, detail_bias: float, hide_after: float) -> void:
auto_lod_enabled = enabled
lod_bias = clampf(detail_bias, 0.0, 4.0)
cull_distance = maxf(hide_after, 0.0)
_apply_settings()
if not Engine.is_editor_hint() and is_inside_tree():
_register_with_runtime_manager()
func restore_original_settings() -> void:
_set_runtime_far(false)
var instance_root := get_parent()
if instance_root == null:
return
for raw_path in original_settings:
var geometry := instance_root.get_node_or_null(NodePath(str(raw_path))) as GeometryInstance3D
if geometry == null:
continue
var values: Dictionary = original_settings[raw_path]
geometry.lod_bias = float(values.get("lod_bias", 1.0))
geometry.visibility_range_end = float(values.get("visibility_range_end", 0.0))
func _register_with_runtime_manager() -> void:
if Engine.is_editor_hint() or _runtime_registered or not is_inside_tree():
return
_capture_runtime_states()
var manager := get_tree().get_first_node_in_group(
&"world_brush_auto_lod_manager"
)
if manager == null:
manager = Node.new()
manager.name = "WorldBrushAutoLODManager"
manager.set_script(RUNTIME_MANAGER_SCRIPT)
var host := get_tree().current_scene
if host == null:
host = get_tree().root
host.add_child(manager)
if not manager.has_method(&"register_controller"):
return
manager.call(&"register_controller", self)
_runtime_registered = true
func _update_runtime_distance_state(camera: Camera3D = null) -> void:
if not auto_lod_enabled or cull_distance <= 0.0:
_set_runtime_far(false)
return
var instance_root := get_parent() as Node3D
if camera == null:
camera = get_viewport().get_camera_3d()
if instance_root == null or camera == null:
return
var distance_squared := instance_root.global_position.distance_squared_to(
camera.global_position
)
var threshold := cull_distance
if _runtime_far:
threshold *= REACTIVATION_RATIO
_set_runtime_far(distance_squared > threshold * threshold)
func _capture_runtime_states() -> void:
if not _runtime_states.is_empty():
return
var instance_root := get_parent()
if instance_root == null:
return
_capture_runtime_node(instance_root, instance_root)
func _capture_runtime_node(instance_root: Node, node: Node) -> void:
if node == self:
return
var path := str(instance_root.get_path_to(node))
var values := {
"process_mode": node.process_mode,
}
if node is CollisionObject3D:
values["collision_layer"] = node.collision_layer
values["collision_mask"] = node.collision_mask
if node is CollisionShape3D:
values["disabled"] = node.disabled
if node is GeometryInstance3D:
values["visible"] = node.visible
values["cast_shadow"] = node.cast_shadow
values["gi_mode"] = node.gi_mode
if node is Area3D:
values["monitoring"] = node.monitoring
values["monitorable"] = node.monitorable
_runtime_states[path] = values
for child in node.get_children():
_capture_runtime_node(instance_root, child)
func _set_runtime_far(far: bool) -> void:
if _runtime_far == far:
return
_runtime_far = far
var instance_root := get_parent()
if instance_root == null:
return
if _runtime_states.is_empty():
_capture_runtime_states()
for raw_path in _runtime_states:
var node := instance_root.get_node_or_null(NodePath(str(raw_path)))
if node == null or node == self:
continue
var values: Dictionary = _runtime_states[raw_path]
# Keep the instance root alive so this controller can wake it again.
if node != instance_root:
node.process_mode = (
Node.PROCESS_MODE_DISABLED
if far
else int(values.get("process_mode", Node.PROCESS_MODE_INHERIT))
)
if node is CollisionObject3D:
node.collision_layer = 0 if far else int(values.get("collision_layer", 1))
node.collision_mask = 0 if far else int(values.get("collision_mask", 1))
if node is CollisionShape3D:
node.set_deferred(
"disabled",
true if far else bool(values.get("disabled", false))
)
if node is GeometryInstance3D:
node.visible = false if far else bool(values.get("visible", true))
node.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
if far
else int(values.get(
"cast_shadow",
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
))
)
node.gi_mode = (
GeometryInstance3D.GI_MODE_DISABLED
if far
else int(values.get(
"gi_mode",
GeometryInstance3D.GI_MODE_STATIC
))
)
if node is Area3D:
node.set_deferred(
"monitoring",
false if far else bool(values.get("monitoring", true))
)
node.set_deferred(
"monitorable",
false if far else bool(values.get("monitorable", true))
)
func _apply_settings() -> void:
if not auto_lod_enabled:
restore_original_settings()
return
var instance_root := get_parent()
if instance_root == null:
return
var geometry_instances: Array[GeometryInstance3D] = []
_collect_geometry_instances(instance_root, geometry_instances)
for geometry in geometry_instances:
var geometry_path := str(instance_root.get_path_to(geometry))
if not original_settings.has(geometry_path):
original_settings[geometry_path] = {
"lod_bias": geometry.lod_bias,
"visibility_range_end": geometry.visibility_range_end,
}
geometry.lod_bias = lod_bias
geometry.visibility_range_end = cull_distance
func _collect_geometry_instances(
node: Node,
result: Array[GeometryInstance3D]
) -> void:
if node is GeometryInstance3D:
result.append(node as GeometryInstance3D)
for child in node.get_children():
if child == self:
continue
_collect_geometry_instances(child, result)
@@ -0,0 +1 @@
uid://cywjw8jfuoqxd
@@ -0,0 +1,41 @@
extends Node
const TARGET_REFRESH_FRAMES := 15
const MAX_CHECKS_PER_FRAME := 64
var _controllers: Array[WeakRef] = []
var _cursor := 0
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
add_to_group(&"world_brush_auto_lod_manager")
func register_controller(controller: Node) -> void:
if controller == null:
return
_controllers.append(weakref(controller))
func _process(_delta: float) -> void:
if _controllers.is_empty():
return
var camera := get_viewport().get_camera_3d()
if camera == null:
return
var checks := clampi(
ceili(float(_controllers.size()) / float(TARGET_REFRESH_FRAMES)),
1,
MAX_CHECKS_PER_FRAME
)
var completed := 0
while completed < checks and not _controllers.is_empty():
_cursor %= _controllers.size()
var controller := _controllers[_cursor].get_ref() as Node
if controller == null or not controller.is_inside_tree():
_controllers.remove_at(_cursor)
continue
controller.call(&"_update_runtime_distance_state", camera)
_cursor += 1
completed += 1
@@ -0,0 +1 @@
uid://cdv8j33awshaq
@@ -0,0 +1,124 @@
@tool
extends Node3D
@export_range(1.0, 100000.0, 1.0, "or_greater")
var medium_distance: float = 25.0
@export_range(2.0, 100000.0, 1.0, "or_greater")
var distant_distance: float = 60.0
@export_range(0.0, 100000.0, 1.0, "or_greater")
var cull_distance: float = 0.0
var _elapsed: float = 0.0
var _active_level: int = -2
func _ready() -> void:
if Engine.is_editor_hint():
show_editor_preview()
set_process(false)
return
set_process(true)
_update_from_camera()
func _process(delta: float) -> void:
_elapsed += delta
if _elapsed < 0.15:
return
_elapsed = 0.0
_update_from_camera()
func configure(
new_medium_distance: float,
new_distant_distance: float,
new_cull_distance: float
) -> void:
medium_distance = maxf(new_medium_distance, 1.0)
distant_distance = maxf(
new_distant_distance,
medium_distance + 1.0
)
cull_distance = maxf(new_cull_distance, 0.0)
show_editor_preview()
func show_editor_preview() -> void:
_active_level = -2
_set_active_level(0)
func update_for_distance(distance: float) -> void:
var near_level := get_node_or_null(^"Near") as Node3D
var medium_level := get_node_or_null(^"Medium") as Node3D
var distant_level := get_node_or_null(^"Distant") as Node3D
var target_level := 0
if cull_distance > 0.0 and distance >= cull_distance:
target_level = -1
elif distant_level != null and distance >= distant_distance:
target_level = 2
elif medium_level != null and distance >= medium_distance:
target_level = 1
elif near_level == null:
if medium_level != null:
target_level = 1
elif distant_level != null:
target_level = 2
else:
target_level = -1
_set_active_level(target_level)
func _update_from_camera() -> void:
var viewport := get_viewport()
if viewport == null:
return
var camera := viewport.get_camera_3d()
if camera == null:
return
update_for_distance(
camera.global_position.distance_to(global_position)
)
func _set_active_level(level: int) -> void:
if bool(get_meta("harvest_hidden", false)):
_active_level = -2
_set_level_visible(^"Near", false)
_set_level_visible(^"Medium", false)
_set_level_visible(^"Distant", false)
return
if level == _active_level:
return
_active_level = level
_set_level_visible(^"Near", level == 0)
_set_level_visible(^"Medium", level == 1)
_set_level_visible(^"Distant", level == 2)
func set_harvest_hidden(hidden: bool) -> void:
set_meta("harvest_hidden", hidden)
_active_level = -2
if hidden:
_set_level_visible(^"Near", false)
_set_level_visible(^"Medium", false)
_set_level_visible(^"Distant", false)
else:
_update_from_camera()
func _set_level_visible(path: NodePath, level_visible: bool) -> void:
var level := get_node_or_null(path) as Node3D
if level != null:
level.visible = level_visible
@@ -0,0 +1 @@
uid://cnwnnkdv60i55
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://dkff06xnj76fl