Optimize Player UI updates with threshold-based rendering and add performance profiler autoload: implement UI_UPDATE_THRESHOLD constant to reduce unnecessary label updates, add conditional DEBUG compilation for debug prints, wrap debug output in preprocessor directives, add PerformanceProfiler autoload, switch to JoltPhysics3D engine, enable VSync, configure rendering quality settings (TAA, SSAA, anisotropic filtering, shadow atlas), and update main scene path

This commit is contained in:
William Stuckey
2026-01-03 16:07:49 -06:00
parent 0d69ad30fb
commit 04dcf8d0e6
182 changed files with 9654 additions and 8 deletions

View File

@@ -0,0 +1,881 @@
using Godot;
using System.Collections.Generic;
namespace Voider.Tools;
/// <summary>
/// Standalone tool for generating collision shapes from MeshInstance3D nodes in any scene.
/// Run the CollisionGenerator.tscn scene, select a target scene, and generate collision.
/// </summary>
public partial class CollisionGenerator : Control
{
public enum CollisionMode
{
Trimesh, // Accurate concave collision - best for static geometry
Convex, // Fast convex hull - good for simple objects
MultiConvex, // Multiple convex shapes - balanced (falls back to trimesh)
Merged // Single merged trimesh - best for large maps with many meshes
}
// UI Elements
private LineEdit _scenePathEdit;
private Button _browseButton;
private Button _generateButton;
private Button _clearButton;
private Button _mergeButton;
private OptionButton _collisionModeOption;
private SpinBox _collisionLayerSpin;
private SpinBox _collisionMaskSpin;
private SpinBox _minSizeThresholdSpin;
private CheckBox _skipSmallMeshesCheck;
private RichTextLabel _logOutput;
private FileDialog _fileDialog;
private Panel _previewPanel;
private SubViewport _previewViewport;
private Node3D _previewRoot;
private Camera3D _previewCamera;
// State
private string _targetScenePath = "";
private PackedScene _loadedScene;
private Node _sceneInstance;
public override void _Ready()
{
BuildUI();
Log("[b]Collision Generator Tool[/b]\nSelect a scene file to generate collision shapes.");
}
private void BuildUI()
{
// Main container
var mainContainer = new VBoxContainer();
mainContainer.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
mainContainer.AddThemeConstantOverride("separation", 10);
AddChild(mainContainer);
// Add some padding
var marginContainer = new MarginContainer();
marginContainer.AddThemeConstantOverride("margin_left", 20);
marginContainer.AddThemeConstantOverride("margin_right", 20);
marginContainer.AddThemeConstantOverride("margin_top", 20);
marginContainer.AddThemeConstantOverride("margin_bottom", 20);
marginContainer.SizeFlagsHorizontal = SizeFlags.ExpandFill;
marginContainer.SizeFlagsVertical = SizeFlags.ExpandFill;
mainContainer.AddChild(marginContainer);
var innerContainer = new VBoxContainer();
innerContainer.AddThemeConstantOverride("separation", 15);
marginContainer.AddChild(innerContainer);
// Title
var title = new Label();
title.Text = "Collision Generator";
title.AddThemeFontSizeOverride("font_size", 24);
innerContainer.AddChild(title);
// Scene selection section
var sceneSection = CreateSection("Target Scene");
innerContainer.AddChild(sceneSection);
var sceneRow = new HBoxContainer();
sceneRow.AddThemeConstantOverride("separation", 10);
sceneSection.AddChild(sceneRow);
_scenePathEdit = new LineEdit();
_scenePathEdit.PlaceholderText = "Select a .tscn file...";
_scenePathEdit.SizeFlagsHorizontal = SizeFlags.ExpandFill;
_scenePathEdit.Editable = false;
sceneRow.AddChild(_scenePathEdit);
_browseButton = new Button();
_browseButton.Text = "Browse...";
_browseButton.Pressed += OnBrowsePressed;
sceneRow.AddChild(_browseButton);
// Options section
var optionsSection = CreateSection("Options");
innerContainer.AddChild(optionsSection);
var optionsGrid = new GridContainer();
optionsGrid.Columns = 2;
optionsGrid.AddThemeConstantOverride("h_separation", 20);
optionsGrid.AddThemeConstantOverride("v_separation", 10);
optionsSection.AddChild(optionsGrid);
// Collision Mode
optionsGrid.AddChild(CreateLabel("Collision Mode:"));
_collisionModeOption = new OptionButton();
_collisionModeOption.AddItem("Trimesh (Accurate)", 0);
_collisionModeOption.AddItem("Convex (Fast)", 1);
_collisionModeOption.AddItem("Multi-Convex (Balanced)", 2);
_collisionModeOption.Selected = 0;
_collisionModeOption.SizeFlagsHorizontal = SizeFlags.ExpandFill;
optionsGrid.AddChild(_collisionModeOption);
// Collision Layer
optionsGrid.AddChild(CreateLabel("Collision Layer:"));
_collisionLayerSpin = new SpinBox();
_collisionLayerSpin.MinValue = 1;
_collisionLayerSpin.MaxValue = 32;
_collisionLayerSpin.Value = 1;
optionsGrid.AddChild(_collisionLayerSpin);
// Collision Mask
optionsGrid.AddChild(CreateLabel("Collision Mask:"));
_collisionMaskSpin = new SpinBox();
_collisionMaskSpin.MinValue = 1;
_collisionMaskSpin.MaxValue = 32;
_collisionMaskSpin.Value = 1;
optionsGrid.AddChild(_collisionMaskSpin);
// Skip Small Meshes
optionsGrid.AddChild(CreateLabel("Skip Small Meshes:"));
_skipSmallMeshesCheck = new CheckBox();
_skipSmallMeshesCheck.ButtonPressed = true;
_skipSmallMeshesCheck.Text = "Filter out tiny objects";
optionsGrid.AddChild(_skipSmallMeshesCheck);
// Min Size Threshold
optionsGrid.AddChild(CreateLabel("Min Size (meters):"));
_minSizeThresholdSpin = new SpinBox();
_minSizeThresholdSpin.MinValue = 0.0;
_minSizeThresholdSpin.MaxValue = 10.0;
_minSizeThresholdSpin.Step = 0.1;
_minSizeThresholdSpin.Value = 0.1;
optionsGrid.AddChild(_minSizeThresholdSpin);
// Actions section
var actionsSection = CreateSection("Actions");
innerContainer.AddChild(actionsSection);
var actionsRow = new HBoxContainer();
actionsRow.AddThemeConstantOverride("separation", 10);
actionsSection.AddChild(actionsRow);
_generateButton = new Button();
_generateButton.Text = "Generate Collision";
_generateButton.Disabled = true;
_generateButton.SizeFlagsHorizontal = SizeFlags.ExpandFill;
_generateButton.Pressed += OnGeneratePressed;
actionsRow.AddChild(_generateButton);
_clearButton = new Button();
_clearButton.Text = "Clear Collision";
_clearButton.Disabled = true;
_clearButton.SizeFlagsHorizontal = SizeFlags.ExpandFill;
_clearButton.Pressed += OnClearPressed;
actionsRow.AddChild(_clearButton);
_mergeButton = new Button();
_mergeButton.Text = "Merge Existing";
_mergeButton.Disabled = true;
_mergeButton.SizeFlagsHorizontal = SizeFlags.ExpandFill;
_mergeButton.Pressed += OnMergePressed;
actionsRow.AddChild(_mergeButton);
// Log output section
var logSection = CreateSection("Log");
logSection.SizeFlagsVertical = SizeFlags.ExpandFill;
innerContainer.AddChild(logSection);
var logScroll = new ScrollContainer();
logScroll.SizeFlagsVertical = SizeFlags.ExpandFill;
logScroll.SizeFlagsHorizontal = SizeFlags.ExpandFill;
logSection.AddChild(logScroll);
_logOutput = new RichTextLabel();
_logOutput.BbcodeEnabled = true;
_logOutput.SizeFlagsHorizontal = SizeFlags.ExpandFill;
_logOutput.SizeFlagsVertical = SizeFlags.ExpandFill;
_logOutput.FitContent = true;
_logOutput.ScrollFollowing = true;
logScroll.AddChild(_logOutput);
// File dialog
_fileDialog = new FileDialog();
_fileDialog.FileMode = FileDialog.FileModeEnum.OpenFile;
_fileDialog.Access = FileDialog.AccessEnum.Resources;
_fileDialog.Filters = new string[] { "*.tscn ; Godot Scene Files" };
_fileDialog.Title = "Select Scene to Process";
_fileDialog.Size = new Vector2I(800, 600);
_fileDialog.FileSelected += OnFileSelected;
AddChild(_fileDialog);
}
private VBoxContainer CreateSection(string title)
{
var container = new VBoxContainer();
container.AddThemeConstantOverride("separation", 8);
var label = new Label();
label.Text = title;
label.AddThemeFontSizeOverride("font_size", 16);
container.AddChild(label);
var separator = new HSeparator();
container.AddChild(separator);
return container;
}
private Label CreateLabel(string text)
{
var label = new Label();
label.Text = text;
return label;
}
private void OnBrowsePressed()
{
_fileDialog.CurrentDir = "res://Scenes";
_fileDialog.PopupCentered();
}
private void OnFileSelected(string path)
{
_targetScenePath = path;
_scenePathEdit.Text = path;
_generateButton.Disabled = false;
_clearButton.Disabled = false;
_mergeButton.Disabled = false;
// Load and analyze the scene
LoadAndAnalyzeScene(path);
}
private void LoadAndAnalyzeScene(string path)
{
Log($"\n[b]Loading:[/b] {path}");
_loadedScene = GD.Load<PackedScene>(path);
if (_loadedScene == null)
{
LogError($"Failed to load scene: {path}");
return;
}
_sceneInstance = _loadedScene.Instantiate();
if (_sceneInstance == null)
{
LogError("Failed to instantiate scene");
return;
}
// Count meshes and existing collision
var meshInstances = new List<MeshInstance3D>();
var existingCollision = new List<Node>();
CollectMeshInstances(_sceneInstance, meshInstances);
CollectExistingCollision(_sceneInstance, existingCollision);
Log($"[color=cyan]Found {meshInstances.Count} mesh instances[/color]");
Log($"[color=yellow]Found {existingCollision.Count} existing collision nodes[/color]");
// Don't add to tree, just keep reference for processing
}
private void OnGeneratePressed()
{
if (string.IsNullOrEmpty(_targetScenePath))
{
LogError("No scene selected");
return;
}
GenerateCollisionForScene();
}
private void OnClearPressed()
{
if (string.IsNullOrEmpty(_targetScenePath))
{
LogError("No scene selected");
return;
}
ClearCollisionFromScene();
}
private void OnMergePressed()
{
if (string.IsNullOrEmpty(_targetScenePath))
{
LogError("No scene selected");
return;
}
MergeExistingCollision();
}
private void GenerateCollisionForScene()
{
Log($"\n[b]Generating collision for:[/b] {_targetScenePath}");
// Reload fresh instance
_loadedScene = GD.Load<PackedScene>(_targetScenePath);
_sceneInstance = _loadedScene.Instantiate();
// We need to add scene to tree temporarily to calculate global transforms
AddChild(_sceneInstance);
var meshInstances = new List<MeshInstance3D>();
CollectMeshInstances(_sceneInstance, meshInstances);
var collisionMode = (CollisionMode)_collisionModeOption.Selected;
uint collisionLayer = (uint)_collisionLayerSpin.Value;
uint collisionMask = (uint)_collisionMaskSpin.Value;
bool skipSmall = _skipSmallMeshesCheck.ButtonPressed;
float minSize = (float)_minSizeThresholdSpin.Value;
// Create a container for all collision at the scene root level
var collisionContainer = _sceneInstance.GetNodeOrNull<Node3D>("_GeneratedCollision");
if (collisionContainer == null)
{
collisionContainer = new Node3D();
collisionContainer.Name = "_GeneratedCollision";
_sceneInstance.AddChild(collisionContainer);
collisionContainer.Owner = _sceneInstance;
}
GeneratePerMeshCollision(meshInstances, collisionContainer, collisionMode, collisionLayer, collisionMask, skipSmall, minSize);
// Remove from tree before saving
RemoveChild(_sceneInstance);
// Restore mouse cursor (Player script may have captured it)
Input.MouseMode = Input.MouseModeEnum.Visible;
// Save the modified scene
SaveScene();
}
private void MergeExistingCollision()
{
Log($"\n[b]Merging existing collision in:[/b] {_targetScenePath}");
// Reload fresh instance - bypass cache
ResourceLoader.Load<PackedScene>(_targetScenePath, cacheMode: ResourceLoader.CacheMode.Ignore);
_loadedScene = GD.Load<PackedScene>(_targetScenePath);
_sceneInstance = _loadedScene.Instantiate();
// Add to tree temporarily
AddChild(_sceneInstance);
// Find the collision container
var collisionContainer = _sceneInstance.GetNodeOrNull<Node3D>("_GeneratedCollision");
if (collisionContainer == null || collisionContainer.GetChildCount() == 0)
{
RemoveChild(_sceneInstance);
_sceneInstance.Free();
Log("[color=yellow]No existing collision found to merge[/color]");
return;
}
// Check if already merged
if (collisionContainer.GetChildCount() == 1 && collisionContainer.GetChild(0).Name == "MergedCollision")
{
RemoveChild(_sceneInstance);
_sceneInstance.Free();
Log("[color=yellow]Collision is already merged[/color]");
return;
}
uint collisionLayer = (uint)_collisionLayerSpin.Value;
uint collisionMask = (uint)_collisionMaskSpin.Value;
Log($"[color=cyan]Merging {collisionContainer.GetChildCount()} collision nodes into single shape[/color]");
// Collect all faces from existing collision shapes
var allFaces = new List<Vector3>();
var nodesToRemove = new List<Node>();
// Handle new structure: single StaticBody3D with multiple CollisionShape3D children
var generatedBody = collisionContainer.GetNodeOrNull<StaticBody3D>("GeneratedStaticBody");
if (generatedBody != null)
{
foreach (var child in generatedBody.GetChildren())
{
if (child is CollisionShape3D collisionShape && collisionShape.Shape is ConcavePolygonShape3D concaveShape)
{
var globalTransform = collisionShape.GlobalTransform;
var faces = concaveShape.GetFaces();
foreach (var vertex in faces)
{
allFaces.Add(globalTransform * vertex);
}
}
}
nodesToRemove.Add(generatedBody);
}
// Also handle legacy structure: multiple StaticBody3D nodes
foreach (var child in collisionContainer.GetChildren())
{
if (child is StaticBody3D staticBody && child.Name != "GeneratedStaticBody" && child.Name != "MergedCollision")
{
var globalTransform = staticBody.GlobalTransform;
foreach (var bodyChild in staticBody.GetChildren())
{
if (bodyChild is CollisionShape3D collisionShape && collisionShape.Shape is ConcavePolygonShape3D concaveShape)
{
var faces = concaveShape.GetFaces();
foreach (var vertex in faces)
{
allFaces.Add(globalTransform * vertex);
}
}
}
nodesToRemove.Add(child);
}
}
Log($" Collected {allFaces.Count / 3} triangles from existing shapes");
if (allFaces.Count < 9)
{
RemoveChild(_sceneInstance);
_sceneInstance.Free();
LogError("Not enough faces to create merged collision");
return;
}
// Remove old collision nodes
foreach (var node in nodesToRemove)
{
collisionContainer.RemoveChild(node);
node.Free();
}
// Create the merged collision shape
var shape = new ConcavePolygonShape3D();
shape.SetFaces(allFaces.ToArray());
var mergedBody = new StaticBody3D();
mergedBody.Name = "MergedCollision";
mergedBody.CollisionLayer = collisionLayer;
mergedBody.CollisionMask = collisionMask;
var mergedShape = new CollisionShape3D();
mergedShape.Name = "CollisionShape3D";
mergedShape.Shape = shape;
mergedBody.AddChild(mergedShape);
collisionContainer.AddChild(mergedBody);
mergedBody.Owner = _sceneInstance;
mergedShape.Owner = _sceneInstance;
// Remove from tree before saving
RemoveChild(_sceneInstance);
// Restore mouse cursor
Input.MouseMode = Input.MouseModeEnum.Visible;
// Save the modified scene
SaveScene();
Log($"\n[b]Complete![/b] Merged into single collision with {allFaces.Count / 3} triangles");
}
private void GenerateMergedCollision(
List<MeshInstance3D> meshInstances,
Node3D collisionContainer,
uint collisionLayer,
uint collisionMask,
bool skipSmall,
float minSize)
{
Log("[color=cyan]Using MERGED collision mode - combining all meshes into single shape[/color]");
// Collect all vertices from all meshes, transformed to world space
var allVertices = new List<Vector3>();
int meshCount = 0;
int skipped = 0;
foreach (var meshInstance in meshInstances)
{
if (meshInstance.Mesh == null)
continue;
// Filter by size if enabled
if (skipSmall)
{
var aabb = meshInstance.Mesh.GetAabb();
float maxExtent = Mathf.Max(aabb.Size.X, Mathf.Max(aabb.Size.Y, aabb.Size.Z));
if (maxExtent < minSize)
{
skipped++;
continue;
}
}
// Get mesh data and transform vertices to world space
var globalTransform = meshInstance.GlobalTransform;
var mesh = meshInstance.Mesh;
for (int surfaceIdx = 0; surfaceIdx < mesh.GetSurfaceCount(); surfaceIdx++)
{
var arrays = mesh.SurfaceGetArrays(surfaceIdx);
if (arrays == null || arrays.Count == 0)
continue;
var vertices = arrays[(int)Mesh.ArrayType.Vertex].AsVector3Array();
if (vertices == null)
continue;
foreach (var vertex in vertices)
{
// Transform vertex to world space
allVertices.Add(globalTransform * vertex);
}
}
meshCount++;
}
Log($" Processed {meshCount} meshes, skipped {skipped} small meshes");
Log($" Total vertices: {allVertices.Count}");
if (allVertices.Count < 3)
{
LogError("Not enough vertices to create collision shape");
return;
}
// Create a single ConcavePolygonShape3D from all vertices
// We need to reconstruct faces - for merged collision, we'll collect faces instead
var allFaces = new List<Vector3>();
foreach (var meshInstance in meshInstances)
{
if (meshInstance.Mesh == null)
continue;
if (skipSmall)
{
var aabb = meshInstance.Mesh.GetAabb();
float maxExtent = Mathf.Max(aabb.Size.X, Mathf.Max(aabb.Size.Y, aabb.Size.Z));
if (maxExtent < minSize)
continue;
}
var globalTransform = meshInstance.GlobalTransform;
var mesh = meshInstance.Mesh;
for (int surfaceIdx = 0; surfaceIdx < mesh.GetSurfaceCount(); surfaceIdx++)
{
var arrays = mesh.SurfaceGetArrays(surfaceIdx);
if (arrays == null || arrays.Count == 0)
continue;
var vertices = arrays[(int)Mesh.ArrayType.Vertex].AsVector3Array();
var indices = arrays[(int)Mesh.ArrayType.Index].AsInt32Array();
if (vertices == null)
continue;
if (indices != null && indices.Length > 0)
{
// Indexed mesh - use indices to build triangles
for (int i = 0; i + 2 < indices.Length; i += 3)
{
allFaces.Add(globalTransform * vertices[indices[i]]);
allFaces.Add(globalTransform * vertices[indices[i + 1]]);
allFaces.Add(globalTransform * vertices[indices[i + 2]]);
}
}
else
{
// Non-indexed mesh - vertices are already in triangle order
for (int i = 0; i + 2 < vertices.Length; i += 3)
{
allFaces.Add(globalTransform * vertices[i]);
allFaces.Add(globalTransform * vertices[i + 1]);
allFaces.Add(globalTransform * vertices[i + 2]);
}
}
}
}
Log($" Total triangles: {allFaces.Count / 3}");
if (allFaces.Count < 9)
{
LogError("Not enough faces to create collision shape");
return;
}
// Create the merged collision shape
var shape = new ConcavePolygonShape3D();
shape.SetFaces(allFaces.ToArray());
// Create single StaticBody3D at origin (vertices are already in world space)
var staticBody = new StaticBody3D();
staticBody.Name = "MergedCollision";
staticBody.CollisionLayer = collisionLayer;
staticBody.CollisionMask = collisionMask;
var collisionShape = new CollisionShape3D();
collisionShape.Name = "CollisionShape3D";
collisionShape.Shape = shape;
staticBody.AddChild(collisionShape);
collisionContainer.AddChild(staticBody);
staticBody.Owner = _sceneInstance;
collisionShape.Owner = _sceneInstance;
Log($"\n[b]Complete![/b] Created single merged collision with {allFaces.Count / 3} triangles");
}
private void GeneratePerMeshCollision(
List<MeshInstance3D> meshInstances,
Node3D collisionContainer,
CollisionMode collisionMode,
uint collisionLayer,
uint collisionMask,
bool skipSmall,
float minSize)
{
int generated = 0;
int skipped = 0;
// Create or get a single StaticBody3D to hold all collision shapes
var staticBody = collisionContainer.GetNodeOrNull<StaticBody3D>("GeneratedStaticBody");
if (staticBody == null)
{
staticBody = new StaticBody3D();
staticBody.Name = "GeneratedStaticBody";
staticBody.CollisionLayer = collisionLayer;
staticBody.CollisionMask = collisionMask;
collisionContainer.AddChild(staticBody);
staticBody.Owner = _sceneInstance;
}
foreach (var meshInstance in meshInstances)
{
if (meshInstance.Mesh == null)
continue;
// Filter by size if enabled
if (skipSmall)
{
var aabb = meshInstance.Mesh.GetAabb();
float maxExtent = Mathf.Max(aabb.Size.X, Mathf.Max(aabb.Size.Y, aabb.Size.Z));
if (maxExtent < minSize)
{
skipped++;
continue;
}
}
// Check if we already generated collision for this mesh
var collisionName = GetUniqueMeshName(meshInstance);
if (staticBody.HasNode(collisionName))
{
Log($" [color=yellow]Skipping[/color] {GetNodePath(meshInstance)} - already has collision");
skipped++;
continue;
}
var shape = CreateCollisionShape(meshInstance.Mesh, collisionMode);
if (shape == null)
{
LogError($" Failed to create shape for {meshInstance.Name}");
continue;
}
var collisionShape = new CollisionShape3D();
collisionShape.Name = collisionName;
collisionShape.Shape = shape;
collisionShape.GlobalTransform = meshInstance.GlobalTransform;
staticBody.AddChild(collisionShape);
collisionShape.Owner = _sceneInstance;
generated++;
}
Log($"\n[b]Complete![/b] Generated: {generated}, Skipped: {skipped}");
}
private string GetUniqueMeshName(MeshInstance3D mesh)
{
// Create a unique name based on the mesh's path in the scene
var path = GetNodePath(mesh);
return path.Replace("/", "_").Replace(" ", "_");
}
private void ClearCollisionFromScene()
{
Log($"\n[b]Clearing collision from:[/b] {_targetScenePath}");
// Reload fresh instance - bypass cache
ResourceLoader.Load<PackedScene>(_targetScenePath, cacheMode: ResourceLoader.CacheMode.Ignore);
_loadedScene = GD.Load<PackedScene>(_targetScenePath);
_sceneInstance = _loadedScene.Instantiate();
// Add to tree temporarily (required for proper scene packing)
AddChild(_sceneInstance);
// Simply remove the _GeneratedCollision container if it exists
var collisionContainer = _sceneInstance.GetNodeOrNull<Node3D>("_GeneratedCollision");
if (collisionContainer == null)
{
RemoveChild(_sceneInstance);
_sceneInstance.Free();
Log("[color=yellow]No generated collision found in this scene[/color]");
return;
}
int cleared = collisionContainer.GetChildCount();
// Must use RemoveChild + Free, not QueueFree (which waits until next frame)
_sceneInstance.RemoveChild(collisionContainer);
collisionContainer.Free();
// Remove from tree before saving
RemoveChild(_sceneInstance);
// Restore mouse cursor
Input.MouseMode = Input.MouseModeEnum.Visible;
// Save the modified scene
SaveScene();
Log($"\n[b]Complete![/b] Cleared {cleared} collision nodes");
}
private void SaveScene()
{
var packedScene = new PackedScene();
var error = packedScene.Pack(_sceneInstance);
if (error != Error.Ok)
{
LogError($"Failed to pack scene: {error}");
return;
}
error = ResourceSaver.Save(packedScene, _targetScenePath);
if (error != Error.Ok)
{
LogError($"Failed to save scene: {error}");
return;
}
Log($"[color=green]Saved:[/color] {_targetScenePath}");
}
private void CollectMeshInstances(Node node, List<MeshInstance3D> meshInstances)
{
if (node is MeshInstance3D meshInstance)
{
meshInstances.Add(meshInstance);
}
foreach (var child in node.GetChildren())
{
CollectMeshInstances(child, meshInstances);
}
}
private void CollectExistingCollision(Node node, List<Node> collisionNodes)
{
if (node is CollisionShape3D || node is StaticBody3D)
{
collisionNodes.Add(node);
}
foreach (var child in node.GetChildren())
{
CollectExistingCollision(child, collisionNodes);
}
}
private void CollectGeneratedCollision(Node node, List<Node> toRemove, List<StaticBody3D> toUnwrap)
{
// StaticBody3D nodes we created (named *_collision)
if (node is StaticBody3D staticBody && staticBody.Name.ToString().EndsWith("_collision"))
{
toUnwrap.Add(staticBody);
return;
}
// CollisionShape3D nodes that are direct children of MeshInstance3D
if (node is CollisionShape3D && node.GetParent() is MeshInstance3D)
{
toRemove.Add(node);
}
foreach (var child in node.GetChildren())
{
CollectGeneratedCollision(child, toRemove, toUnwrap);
}
}
private bool HasExistingCollision(MeshInstance3D meshInstance)
{
// Check for collision shape as child
foreach (var child in meshInstance.GetChildren())
{
if (child is CollisionShape3D)
return true;
}
// Check for sibling StaticBody3D with matching name
var parent = meshInstance.GetParent();
if (parent != null)
{
var collisionName = $"{meshInstance.Name}_collision";
foreach (var sibling in parent.GetChildren())
{
if (sibling is StaticBody3D && sibling.Name == collisionName)
return true;
}
}
// Check if parent is a StaticBody3D we created
if (parent is StaticBody3D sb && sb.Name.ToString().EndsWith("_collision"))
return true;
return false;
}
private Shape3D CreateCollisionShape(Mesh mesh, CollisionMode mode)
{
return mode switch
{
CollisionMode.Trimesh => mesh.CreateTrimeshShape(),
CollisionMode.Convex => mesh.CreateConvexShape(),
CollisionMode.MultiConvex => mesh.CreateTrimeshShape(), // Fallback
_ => mesh.CreateTrimeshShape()
};
}
private string GetNodePath(Node node)
{
var path = node.Name.ToString();
var parent = node.GetParent();
while (parent != null && parent != _sceneInstance)
{
path = $"{parent.Name}/{path}";
parent = parent.GetParent();
}
return path;
}
private void Log(string message)
{
_logOutput.AppendText(message + "\n");
GD.Print(message.Replace("[b]", "").Replace("[/b]", "")
.Replace("[color=green]", "").Replace("[color=yellow]", "")
.Replace("[color=cyan]", "").Replace("[color=orange]", "")
.Replace("[color=red]", "").Replace("[/color]", ""));
}
private void LogError(string message)
{
Log($"[color=red]ERROR: {message}[/color]");
}
}