Files
voider/Scripts/Tools/CollisionGenerator.cs

882 lines
30 KiB
C#

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]");
}
}