882 lines
30 KiB
C#
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]");
|
|
}
|
|
}
|