From 6affd23615fcb3d28d692f0d42b510bb7541dd4f Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 17:29:00 +0200 Subject: [PATCH 01/17] fix: ignore invalid editor resource paths --- Editor/ExtensionMethods/ChiselEditorResources.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Editor/ExtensionMethods/ChiselEditorResources.cs b/Editor/ExtensionMethods/ChiselEditorResources.cs index 5e92370..8736f1e 100644 --- a/Editor/ExtensionMethods/ChiselEditorResources.cs +++ b/Editor/ExtensionMethods/ChiselEditorResources.cs @@ -242,7 +242,9 @@ static string[] GetResourcePaths() if (System.IO.Directory.Exists(packagePath)) { var localPath = ToLocalPath(packagePath); - if (foundPaths.Add(localPath)) paths.Add(localPath); + if ((localPath.StartsWith("Assets/") || localPath.StartsWith("Packages/")) && + foundPaths.Add(localPath)) + paths.Add(localPath); } } return paths.ToArray(); From 4d94b61a4e2dff2a624f2bb6950912b7cf23ee69 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 17:40:36 +0200 Subject: [PATCH 02/17] Ignore resource paths outside project --- .../ExtensionMethods/ChiselEditorResources.cs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/Editor/ExtensionMethods/ChiselEditorResources.cs b/Editor/ExtensionMethods/ChiselEditorResources.cs index 8736f1e..5a21493 100644 --- a/Editor/ExtensionMethods/ChiselEditorResources.cs +++ b/Editor/ExtensionMethods/ChiselEditorResources.cs @@ -242,6 +242,8 @@ static string[] GetResourcePaths() if (System.IO.Directory.Exists(packagePath)) { var localPath = ToLocalPath(packagePath); + if (localPath == null) + continue; if ((localPath.StartsWith("Assets/") || localPath.StartsWith("Packages/")) && foundPaths.Add(localPath)) paths.Add(localPath); @@ -262,21 +264,30 @@ static string ToLocalPath(string path) if (assetsPathIndex != -1) { path = path.Substring(assetsPathIndex + 1); - } else + } + else { var packagePathIndex = path.IndexOf(@"/Packages/"); if (packagePathIndex != -1) { path = path.Substring(packagePathIndex + 1); - } - var packageCacheIndex = path.IndexOf(@"/PackageCache/"); - if (packageCacheIndex != -1) - { - path = "Packages/" + path.Substring(packageCacheIndex + @"/PackageCache/".Length); - } - } + } + else + { + var packageCacheIndex = path.IndexOf(@"/PackageCache/"); + if (packageCacheIndex != -1) + { + path = "Packages/" + + path.Substring(packageCacheIndex + @"/PackageCache/".Length); + } + else + { + return null; // outside project + } + } + } if (!path.EndsWith("/")) - path = path + "/"; + path += "/"; return path; } #endregion From eccb27f03e6fbfbcfab7191da84a6697e3a5a8b5 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 17:59:04 +0200 Subject: [PATCH 03/17] Add subtractive editing option to model --- .../Containers/ChiselModelComponent.cs | 2 ++ .../ComponentEditors/Base/ChiselNodeEditor.cs | 5 +++++ .../Containers/ChiselModelEditor.cs | 4 ++++ .../SceneView/ChiselDefaultPlacementTools.cs | 20 +++++++++++++------ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Components/Components/Containers/ChiselModelComponent.cs b/Components/Components/Containers/ChiselModelComponent.cs index e323c14..d57f871 100644 --- a/Components/Components/Containers/ChiselModelComponent.cs +++ b/Components/Components/Containers/ChiselModelComponent.cs @@ -209,6 +209,7 @@ public sealed class ChiselModelComponent : ChiselNodeComponent public const string kCreateColliderComponentsName = nameof(CreateColliderComponents); public const string kAutoRebuildUVsName = nameof(AutoRebuildUVs); public const string kVertexChannelMaskName = nameof(VertexChannelMask); + public const string kSubtractiveEditingName = nameof(SubtractiveEditing); public const string kNodeTypeName = "Model"; @@ -235,6 +236,7 @@ public sealed class ChiselModelComponent : ChiselNodeComponent public bool CreateColliderComponents = true; public bool AutoRebuildUVs = true; public VertexChannelFlags VertexChannelMask = VertexChannelFlags.All; + public bool SubtractiveEditing = false; public ChiselModelComponent() : base() { } diff --git a/Editor/ComponentEditors/Base/ChiselNodeEditor.cs b/Editor/ComponentEditors/Base/ChiselNodeEditor.cs index 9379e57..1a0bd0e 100644 --- a/Editor/ComponentEditors/Base/ChiselNodeEditor.cs +++ b/Editor/ComponentEditors/Base/ChiselNodeEditor.cs @@ -326,6 +326,11 @@ protected static void CreateAsGameObjectMenuCommand(MenuCommand menuCommand, str Undo.RegisterCreatedObjectUndo(modelGameObject, "Create " + modelGameObject.name); MoveTargetsUnderModel(new[] { component }, model); } + if (model.SubtractiveEditing && component is IChiselHasOperation hasOperation) + { + Undo.RecordObject(component, "Set Operation"); + hasOperation.Operation = CSGOperationType.Subtractive; + } } else model = component as ChiselModelComponent; diff --git a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs index 39217ae..3b1c2b4 100644 --- a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs +++ b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs @@ -51,6 +51,7 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) readonly static GUIContent kColliderSettingsContent = new("Collider"); readonly static GUIContent kCreateRenderComponentsContents = new("Renderable"); readonly static GUIContent kCreateColliderComponentsContents = new("Collidable"); + readonly static GUIContent kSubtractiveEditingContents = new("Subtractive Editing", "New brushes are created as subtractive when enabled"); readonly static GUIContent kUnwrapParamsContents = new("UV Generation"); readonly static GUIContent kForceBuildUVsContents = new("Build", "Manually build lightmap UVs for generated meshes. This operation can be slow for more complicated meshes"); @@ -128,6 +129,7 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) SerializedProperty vertexChannelMaskProp; SerializedProperty createRenderComponentsProp; SerializedProperty createColliderComponentsProp; + SerializedProperty subtractiveEditingProp; SerializedProperty autoRebuildUVsProp; SerializedProperty angleErrorProp; SerializedProperty areaErrorProp; @@ -217,6 +219,7 @@ internal void OnEnable() vertexChannelMaskProp = serializedObject.FindProperty($"{ChiselModelComponent.kVertexChannelMaskName}"); createRenderComponentsProp = serializedObject.FindProperty($"{ChiselModelComponent.kCreateRenderComponentsName}"); createColliderComponentsProp = serializedObject.FindProperty($"{ChiselModelComponent.kCreateColliderComponentsName}"); + subtractiveEditingProp = serializedObject.FindProperty($"{ChiselModelComponent.kSubtractiveEditingName}"); autoRebuildUVsProp = serializedObject.FindProperty($"{ChiselModelComponent.kAutoRebuildUVsName}"); angleErrorProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kAngleErrorName}"); areaErrorProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kAreaErrorName}"); @@ -1206,6 +1209,7 @@ public override void OnInspectorGUI() EditorGUI.indentLevel++; EditorGUILayout.PropertyField(createColliderComponentsProp, kCreateColliderComponentsContents); EditorGUILayout.PropertyField(createRenderComponentsProp, kCreateRenderComponentsContents); + EditorGUILayout.PropertyField(subtractiveEditingProp, kSubtractiveEditingContents); EditorGUI.BeginDisabledGroup(!createRenderComponentsProp.boolValue); { diff --git a/Editor/SceneView/ChiselDefaultPlacementTools.cs b/Editor/SceneView/ChiselDefaultPlacementTools.cs index b906394..193c7c2 100644 --- a/Editor/SceneView/ChiselDefaultPlacementTools.cs +++ b/Editor/SceneView/ChiselDefaultPlacementTools.cs @@ -60,17 +60,19 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) shape.Center = Vector2.zero; generatedComponent.definition.Reset(); generatedComponent.SurfaceDefinition?.Reset(); - generatedComponent.Operation = forceOperation ?? CSGOperationType.Additive; + var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; + generatedComponent.Operation = forceOperation ?? defaultOperation; PlacementToolDefinition.OnCreate(ref generatedComponent.definition, shape); PlacementToolDefinition.OnUpdate(ref generatedComponent.definition, height); generatedComponent.ResetTreeNodes(); } } else { + var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; generatedComponent.Operation = forceOperation ?? ((height < 0 && modelBeneathCursor) ? CSGOperationType.Subtractive : - CSGOperationType.Additive); + defaultOperation); PlacementToolDefinition.OnUpdate(ref generatedComponent.definition, height); generatedComponent.OnValidate(); generatedComponent.ClearHashes(); // TODO: remove need for this @@ -153,7 +155,8 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) generatedComponent.definition.Reset(); generatedComponent.surfaceArray?.Reset(); - generatedComponent.Operation = forceOperation ?? CSGOperationType.Additive; + var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; + generatedComponent.Operation = forceOperation ?? defaultOperation; PlacementToolDefinition.OnCreate(ref generatedComponent.definition); PlacementToolDefinition.OnUpdate(ref generatedComponent.definition, bounds); generatedComponent.OnValidate(); @@ -176,12 +179,17 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) // Update the generator GameObject ChiselComponentFactory.SetTransform(generatedComponent, transformation); if ((generatoreModeFlags & PlacementFlags.AlwaysFaceUp) == PlacementFlags.AlwaysFaceCameraXZ) - generatedComponent.Operation = forceOperation ?? CSGOperationType.Additive; - else + { + var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; + generatedComponent.Operation = forceOperation ?? defaultOperation; + } else + { + var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; generatedComponent.Operation = forceOperation ?? ((height < 0 && modelBeneathCursor) ? CSGOperationType.Subtractive : - CSGOperationType.Additive); + defaultOperation); + } PlacementToolDefinition.OnUpdate(ref generatedComponent.definition, bounds); generatedComponent.OnValidate(); From 7e6b500920b76cd92c00ce15084c7f85ce275931 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 18:02:42 +0200 Subject: [PATCH 04/17] Updated mats version --- .../Materials/URP/DebugVisualization/Collision.mat | 2 +- .../Materials/URP/DebugVisualization/Discarded.mat | 2 +- .../Materials/URP/DebugVisualization/ShadowCasting.mat | 2 +- .../Materials/URP/DebugVisualization/ShadowOnly.mat | 2 +- .../Materials/URP/DebugVisualization/ShadowReceiving.mat | 2 +- Package Resources/Materials/URP/DebugVisualization/Trigger.mat | 2 +- .../Materials/URP/DebugVisualization/UserHidden.mat | 2 +- Package Resources/Materials/URP/Defaults/Floor.mat | 2 +- Package Resources/Materials/URP/Defaults/Step.mat | 2 +- Package Resources/Materials/URP/Defaults/Tread.mat | 2 +- Package Resources/Materials/URP/Defaults/Wall.mat | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Package Resources/Materials/URP/DebugVisualization/Collision.mat b/Package Resources/Materials/URP/DebugVisualization/Collision.mat index 504eb34..e146b72 100644 --- a/Package Resources/Materials/URP/DebugVisualization/Collision.mat +++ b/Package Resources/Materials/URP/DebugVisualization/Collision.mat @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 --- !u!21 &2100000 Material: serializedVersion: 8 diff --git a/Package Resources/Materials/URP/DebugVisualization/Discarded.mat b/Package Resources/Materials/URP/DebugVisualization/Discarded.mat index 86cf889..0fa8224 100644 --- a/Package Resources/Materials/URP/DebugVisualization/Discarded.mat +++ b/Package Resources/Materials/URP/DebugVisualization/Discarded.mat @@ -157,4 +157,4 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 diff --git a/Package Resources/Materials/URP/DebugVisualization/ShadowCasting.mat b/Package Resources/Materials/URP/DebugVisualization/ShadowCasting.mat index 329315d..b053c96 100644 --- a/Package Resources/Materials/URP/DebugVisualization/ShadowCasting.mat +++ b/Package Resources/Materials/URP/DebugVisualization/ShadowCasting.mat @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 --- !u!114 &-1506755939743460727 MonoBehaviour: m_ObjectHideFlags: 1 diff --git a/Package Resources/Materials/URP/DebugVisualization/ShadowOnly.mat b/Package Resources/Materials/URP/DebugVisualization/ShadowOnly.mat index 4903e4b..754e9c5 100644 --- a/Package Resources/Materials/URP/DebugVisualization/ShadowOnly.mat +++ b/Package Resources/Materials/URP/DebugVisualization/ShadowOnly.mat @@ -157,4 +157,4 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 diff --git a/Package Resources/Materials/URP/DebugVisualization/ShadowReceiving.mat b/Package Resources/Materials/URP/DebugVisualization/ShadowReceiving.mat index 149e24e..37dc475 100644 --- a/Package Resources/Materials/URP/DebugVisualization/ShadowReceiving.mat +++ b/Package Resources/Materials/URP/DebugVisualization/ShadowReceiving.mat @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 --- !u!114 &-2819722843381972991 MonoBehaviour: m_ObjectHideFlags: 1 diff --git a/Package Resources/Materials/URP/DebugVisualization/Trigger.mat b/Package Resources/Materials/URP/DebugVisualization/Trigger.mat index 41a5731..e4be9a8 100644 --- a/Package Resources/Materials/URP/DebugVisualization/Trigger.mat +++ b/Package Resources/Materials/URP/DebugVisualization/Trigger.mat @@ -28,7 +28,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 --- !u!21 &2100000 Material: serializedVersion: 8 diff --git a/Package Resources/Materials/URP/DebugVisualization/UserHidden.mat b/Package Resources/Materials/URP/DebugVisualization/UserHidden.mat index 47b0e35..83dd607 100644 --- a/Package Resources/Materials/URP/DebugVisualization/UserHidden.mat +++ b/Package Resources/Materials/URP/DebugVisualization/UserHidden.mat @@ -28,7 +28,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 --- !u!21 &2100000 Material: serializedVersion: 8 diff --git a/Package Resources/Materials/URP/Defaults/Floor.mat b/Package Resources/Materials/URP/Defaults/Floor.mat index 3a23368..15dcce1 100644 --- a/Package Resources/Materials/URP/Defaults/Floor.mat +++ b/Package Resources/Materials/URP/Defaults/Floor.mat @@ -157,4 +157,4 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 diff --git a/Package Resources/Materials/URP/Defaults/Step.mat b/Package Resources/Materials/URP/Defaults/Step.mat index e29993e..03348b3 100644 --- a/Package Resources/Materials/URP/Defaults/Step.mat +++ b/Package Resources/Materials/URP/Defaults/Step.mat @@ -157,4 +157,4 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 diff --git a/Package Resources/Materials/URP/Defaults/Tread.mat b/Package Resources/Materials/URP/Defaults/Tread.mat index 21bc68f..6f92701 100644 --- a/Package Resources/Materials/URP/Defaults/Tread.mat +++ b/Package Resources/Materials/URP/Defaults/Tread.mat @@ -157,4 +157,4 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 diff --git a/Package Resources/Materials/URP/Defaults/Wall.mat b/Package Resources/Materials/URP/Defaults/Wall.mat index 4e30756..86326ec 100644 --- a/Package Resources/Materials/URP/Defaults/Wall.mat +++ b/Package Resources/Materials/URP/Defaults/Wall.mat @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 9 + version: 10 --- !u!114 &-1617261777445278378 MonoBehaviour: m_ObjectHideFlags: 1 From ed88df376242fe7a41f839c2acf0843339881b7d Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 18:08:10 +0200 Subject: [PATCH 05/17] Fix default operation model variable --- Editor/SceneView/ChiselDefaultPlacementTools.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Editor/SceneView/ChiselDefaultPlacementTools.cs b/Editor/SceneView/ChiselDefaultPlacementTools.cs index 193c7c2..001340f 100644 --- a/Editor/SceneView/ChiselDefaultPlacementTools.cs +++ b/Editor/SceneView/ChiselDefaultPlacementTools.cs @@ -44,6 +44,8 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) { case ShapeExtrusionState.Modified: case ShapeExtrusionState.Create: + ChiselModelComponent model = generatedComponent ? + generatedComponent.GetComponentInParent() : null; { if (!generatedComponent) { @@ -52,7 +54,7 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) var center2D = shape.Center; var center3D = new Vector3(center2D.x, 0, center2D.y); Transform parentTransform = null; - var model = ChiselModelManager.Instance.GetActiveModelOrCreate(modelBeneathCursor); + model = ChiselModelManager.Instance.GetActiveModelOrCreate(modelBeneathCursor); if (model != null) parentTransform = model.transform; generatedComponent = ChiselComponentFactory.Create(generatorType, ToolName, parentTransform, transformation * Matrix4x4.TRS(center3D, Quaternion.identity, Vector3.one)) @@ -68,6 +70,8 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) } } else { + if (!model) + model = generatedComponent.GetComponentInParent(); var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; generatedComponent.Operation = forceOperation ?? ((height < 0 && modelBeneathCursor) ? @@ -137,6 +141,8 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) { case GeneratorModeState.Update: { + ChiselModelComponent model = generatedComponent ? + generatedComponent.GetComponentInParent() : null; if (!generatedComponent) { var size = bounds.size; @@ -146,7 +152,7 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) { // Create the generator GameObject Transform parentTransform = null; - var model = ChiselModelManager.Instance.GetActiveModelOrCreate(modelBeneathCursor); + model = ChiselModelManager.Instance.GetActiveModelOrCreate(modelBeneathCursor); if (model != null) parentTransform = model.transform; generatedComponent = ChiselComponentFactory.Create(generatorType, ToolName, parentTransform, transformation) as ChiselNodeGeneratorComponent; @@ -184,6 +190,8 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) generatedComponent.Operation = forceOperation ?? defaultOperation; } else { + if (!model) + model = generatedComponent.GetComponentInParent(); var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; generatedComponent.Operation = forceOperation ?? ((height < 0 && modelBeneathCursor) ? From 1472931dae1756522c9cd47cb24cc61366b2aa5c Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 18:16:31 +0200 Subject: [PATCH 06/17] Fix variable scope in placement tool cases --- Editor/SceneView/ChiselDefaultPlacementTools.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Editor/SceneView/ChiselDefaultPlacementTools.cs b/Editor/SceneView/ChiselDefaultPlacementTools.cs index 001340f..6456488 100644 --- a/Editor/SceneView/ChiselDefaultPlacementTools.cs +++ b/Editor/SceneView/ChiselDefaultPlacementTools.cs @@ -44,9 +44,9 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) { case ShapeExtrusionState.Modified: case ShapeExtrusionState.Create: + { ChiselModelComponent model = generatedComponent ? generatedComponent.GetComponentInParent() : null; - { if (!generatedComponent) { if (height != 0) From bd1fcaff102a0049942535bcfc67be373e9bc751 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 18:35:43 +0200 Subject: [PATCH 07/17] Add subtractive mode behavior --- .../Components/Containers/ChiselModelComponent.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Components/Components/Containers/ChiselModelComponent.cs b/Components/Components/Containers/ChiselModelComponent.cs index d57f871..05922bd 100644 --- a/Components/Components/Containers/ChiselModelComponent.cs +++ b/Components/Components/Containers/ChiselModelComponent.cs @@ -287,8 +287,16 @@ internal override CSGTreeNode RebuildTreeNodes() Debug.LogWarning($"{nameof(ChiselModelComponent)} already has a treeNode, but trying to create a new one?", this); var instanceID = GetInstanceID(); Node = CSGTree.Create(instanceID: instanceID); + Node.Operation = SubtractiveEditing ? CSGOperationType.Subtractive : CSGOperationType.Additive; return Node; - } + } + + protected override void OnValidateState() + { + if (Node.Valid) + Node.Operation = SubtractiveEditing ? CSGOperationType.Subtractive : CSGOperationType.Additive; + base.OnValidateState(); + } public override void OnInitialize() { From 2f39c96ec225ee54762a31ead2ad7b9af4ca9219 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 19:52:41 +0200 Subject: [PATCH 08/17] Implement subtractive editing by flipping normals --- .../Containers/ChiselModelComponent.cs | 3 -- .../Generated/ChiselGeneratedObjects.cs | 6 ++++ Components/Utility/ChiselMeshUtility.cs | 29 +++++++++++++++++++ Components/Utility/ChiselMeshUtility.cs.meta | 2 ++ .../ComponentEditors/Base/ChiselNodeEditor.cs | 5 ---- .../SceneView/ChiselDefaultPlacementTools.cs | 15 ++++------ 6 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 Components/Utility/ChiselMeshUtility.cs create mode 100644 Components/Utility/ChiselMeshUtility.cs.meta diff --git a/Components/Components/Containers/ChiselModelComponent.cs b/Components/Components/Containers/ChiselModelComponent.cs index 05922bd..621affc 100644 --- a/Components/Components/Containers/ChiselModelComponent.cs +++ b/Components/Components/Containers/ChiselModelComponent.cs @@ -287,14 +287,11 @@ internal override CSGTreeNode RebuildTreeNodes() Debug.LogWarning($"{nameof(ChiselModelComponent)} already has a treeNode, but trying to create a new one?", this); var instanceID = GetInstanceID(); Node = CSGTree.Create(instanceID: instanceID); - Node.Operation = SubtractiveEditing ? CSGOperationType.Subtractive : CSGOperationType.Additive; return Node; } protected override void OnValidateState() { - if (Node.Valid) - Node.Operation = SubtractiveEditing ? CSGOperationType.Subtractive : CSGOperationType.Additive; base.OnValidateState(); } diff --git a/Components/Components/Generated/ChiselGeneratedObjects.cs b/Components/Components/Generated/ChiselGeneratedObjects.cs index 71d7886..74bb0c5 100644 --- a/Components/Components/Generated/ChiselGeneratedObjects.cs +++ b/Components/Components/Generated/ChiselGeneratedObjects.cs @@ -607,6 +607,12 @@ public int FinishMeshUpdates(ChiselModelComponent model, meshUpdates.meshDataArray = default; Profiler.EndSample(); + if (model.SubtractiveEditing) + { + for (int i = 0; i < foundMeshes.Count; i++) + ChiselMeshUtility.FlipNormals(foundMeshes[i]); + } + // TODO: user meshDataArray data to determine if colliders are visible or not, then we can move this before the Apply Profiler.BeginSample("UpdateColliders"); ChiselColliderObjects.UpdateProperties(model, this.colliders); diff --git a/Components/Utility/ChiselMeshUtility.cs b/Components/Utility/ChiselMeshUtility.cs new file mode 100644 index 0000000..1e8cdcb --- /dev/null +++ b/Components/Utility/ChiselMeshUtility.cs @@ -0,0 +1,29 @@ +using System; +using UnityEngine; + +namespace Chisel.Components +{ + internal static class ChiselMeshUtility + { + public static void FlipNormals(Mesh mesh) + { + if (!mesh) + return; + + var normals = mesh.normals; + if (normals != null && normals.Length > 0) + { + for (int i = 0; i < normals.Length; i++) + normals[i] = -normals[i]; + mesh.normals = normals; + } + + for (int s = 0; s < mesh.subMeshCount; s++) + { + var indices = mesh.GetTriangles(s); + Array.Reverse(indices); + mesh.SetTriangles(indices, s); + } + } + } +} diff --git a/Components/Utility/ChiselMeshUtility.cs.meta b/Components/Utility/ChiselMeshUtility.cs.meta new file mode 100644 index 0000000..c3ddb67 --- /dev/null +++ b/Components/Utility/ChiselMeshUtility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d34ada735574463d9cdc828678a82cce diff --git a/Editor/ComponentEditors/Base/ChiselNodeEditor.cs b/Editor/ComponentEditors/Base/ChiselNodeEditor.cs index 1a0bd0e..9379e57 100644 --- a/Editor/ComponentEditors/Base/ChiselNodeEditor.cs +++ b/Editor/ComponentEditors/Base/ChiselNodeEditor.cs @@ -326,11 +326,6 @@ protected static void CreateAsGameObjectMenuCommand(MenuCommand menuCommand, str Undo.RegisterCreatedObjectUndo(modelGameObject, "Create " + modelGameObject.name); MoveTargetsUnderModel(new[] { component }, model); } - if (model.SubtractiveEditing && component is IChiselHasOperation hasOperation) - { - Undo.RecordObject(component, "Set Operation"); - hasOperation.Operation = CSGOperationType.Subtractive; - } } else model = component as ChiselModelComponent; diff --git a/Editor/SceneView/ChiselDefaultPlacementTools.cs b/Editor/SceneView/ChiselDefaultPlacementTools.cs index 6456488..3ec2a22 100644 --- a/Editor/SceneView/ChiselDefaultPlacementTools.cs +++ b/Editor/SceneView/ChiselDefaultPlacementTools.cs @@ -62,8 +62,7 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) shape.Center = Vector2.zero; generatedComponent.definition.Reset(); generatedComponent.SurfaceDefinition?.Reset(); - var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; - generatedComponent.Operation = forceOperation ?? defaultOperation; + generatedComponent.Operation = forceOperation ?? CSGOperationType.Additive; PlacementToolDefinition.OnCreate(ref generatedComponent.definition, shape); PlacementToolDefinition.OnUpdate(ref generatedComponent.definition, height); generatedComponent.ResetTreeNodes(); @@ -72,11 +71,10 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) { if (!model) model = generatedComponent.GetComponentInParent(); - var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; generatedComponent.Operation = forceOperation ?? ((height < 0 && modelBeneathCursor) ? CSGOperationType.Subtractive : - defaultOperation); + CSGOperationType.Additive); PlacementToolDefinition.OnUpdate(ref generatedComponent.definition, height); generatedComponent.OnValidate(); generatedComponent.ClearHashes(); // TODO: remove need for this @@ -161,8 +159,7 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) generatedComponent.definition.Reset(); generatedComponent.surfaceArray?.Reset(); - var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; - generatedComponent.Operation = forceOperation ?? defaultOperation; + generatedComponent.Operation = forceOperation ?? CSGOperationType.Additive; PlacementToolDefinition.OnCreate(ref generatedComponent.definition); PlacementToolDefinition.OnUpdate(ref generatedComponent.definition, bounds); generatedComponent.OnValidate(); @@ -186,17 +183,15 @@ public override void OnSceneGUI(SceneView sceneView, Rect dragArea) ChiselComponentFactory.SetTransform(generatedComponent, transformation); if ((generatoreModeFlags & PlacementFlags.AlwaysFaceUp) == PlacementFlags.AlwaysFaceCameraXZ) { - var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; - generatedComponent.Operation = forceOperation ?? defaultOperation; + generatedComponent.Operation = forceOperation ?? CSGOperationType.Additive; } else { if (!model) model = generatedComponent.GetComponentInParent(); - var defaultOperation = (model != null && model.SubtractiveEditing) ? CSGOperationType.Subtractive : CSGOperationType.Additive; generatedComponent.Operation = forceOperation ?? ((height < 0 && modelBeneathCursor) ? CSGOperationType.Subtractive : - defaultOperation); + CSGOperationType.Additive); } PlacementToolDefinition.OnUpdate(ref generatedComponent.definition, bounds); generatedComponent.OnValidate(); From 272eea1c41b6adf3a423a81e61969bafc1c70dcd Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 19:59:33 +0200 Subject: [PATCH 09/17] Flip meshes when SubtractiveEditing toggled --- .../Containers/ChiselModelComponent.cs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Components/Components/Containers/ChiselModelComponent.cs b/Components/Components/Containers/ChiselModelComponent.cs index 621affc..2749e59 100644 --- a/Components/Components/Containers/ChiselModelComponent.cs +++ b/Components/Components/Containers/ChiselModelComponent.cs @@ -237,6 +237,7 @@ public sealed class ChiselModelComponent : ChiselNodeComponent public bool AutoRebuildUVs = true; public VertexChannelFlags VertexChannelMask = VertexChannelFlags.All; public bool SubtractiveEditing = false; + [NonSerialized] bool prevSubtractiveEditing; public ChiselModelComponent() : base() { } @@ -293,6 +294,12 @@ internal override CSGTreeNode RebuildTreeNodes() protected override void OnValidateState() { base.OnValidateState(); + + if (prevSubtractiveEditing != SubtractiveEditing && generated != null) + { + FlipGeneratedMeshes(); + prevSubtractiveEditing = SubtractiveEditing; + } } public override void OnInitialize() @@ -334,7 +341,35 @@ public override void OnInitialize() name == ChiselModelManager.kGeneratedDefaultModelName) IsDefaultModel = true; - IsInitialized = true; + prevSubtractiveEditing = SubtractiveEditing; + IsInitialized = true; + } + + void FlipGeneratedMeshes() + { + if (generated == null) + return; + + if (generated.renderables != null) + { + foreach (var renderable in generated.renderables) + if (renderable != null && renderable.sharedMesh) + ChiselMeshUtility.FlipNormals(renderable.sharedMesh); + } + + if (generated.debugVisualizationRenderables != null) + { + foreach (var renderable in generated.debugVisualizationRenderables) + if (renderable != null && renderable.sharedMesh) + ChiselMeshUtility.FlipNormals(renderable.sharedMesh); + } + + if (generated.colliders != null) + { + foreach (var collider in generated.colliders) + if (collider != null && collider.sharedMesh) + ChiselMeshUtility.FlipNormals(collider.sharedMesh); + } } #if UNITY_EDITOR From 42d903fb7ad99238abf06e49018fc44a1f2b1516 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Fri, 18 Jul 2025 20:01:59 +0200 Subject: [PATCH 10/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 713a49f..8623709 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,12 @@ Features (incomplete) * Draw 2D shapes (possible to turn straight lines into curves) on existing CSG surfaces and extrude them * Precise snapping to surfaces, edges, vertices and grid lines * Rotatable & movable grid +* Subtractive Workflow **(NEW)** Planned Features (incomplete, and in random order): * [Debug Visualizations](https://github.com/RadicalCSG/Chisel.Prototype/issues/118) to see shadow only surfaces, collider surfaces etc. (partially implemented) * [Double sided surfaces](https://github.com/RadicalCSG/Chisel.Prototype/issues/226) * [Extrusion from existing surface](https://github.com/RadicalCSG/Chisel.Prototype/issues/19) -* [Subtractive Workflow](https://github.com/RadicalCSG/Chisel.Prototype/issues/14) * [Clip Tool](https://github.com/RadicalCSG/Chisel.Prototype/issues/15) * [Normal smoothing](https://github.com/RadicalCSG/Chisel.Prototype/issues/184) * [Node Based Generators](https://github.com/RadicalCSG/Chisel.Prototype/issues/94) for easy procedural content generation From 4adf9a323b96ba708cdb9401fd115f197d629183 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 19 Jul 2025 18:42:12 +0200 Subject: [PATCH 11/17] Update Wall.mat --- .../Materials/builtin/Defaults/Wall.mat | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/Package Resources/Materials/builtin/Defaults/Wall.mat b/Package Resources/Materials/builtin/Defaults/Wall.mat index 228422d..9fe2409 100644 --- a/Package Resources/Materials/builtin/Defaults/Wall.mat +++ b/Package Resources/Materials/builtin/Defaults/Wall.mat @@ -24,7 +24,7 @@ Material: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: Wall - m_Shader: {fileID: 45, guid: 0000000000000000f000000000000000, type: 0} + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} m_Parent: {fileID: 0} m_ModifiedSerializedProperties: 0 m_ValidKeywords: @@ -34,8 +34,10 @@ Material: m_EnableInstancingVariants: 0 m_DoubleSidedGI: 0 m_CustomRenderQueue: -1 - stringTagMap: {} - disabledShaderPasses: [] + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS m_LockedProperties: m_SavedProperties: serializedVersion: 3 @@ -44,6 +46,10 @@ Material: m_Texture: {fileID: 0} m_Scale: {x: 1, y: 1} m_Offset: {x: 0, y: 0} + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} - _BumpMap: m_Texture: {fileID: 0} m_Scale: {x: 1, y: 1} @@ -65,7 +71,7 @@ Material: m_Scale: {x: 1, y: 1} m_Offset: {x: 0, y: 0} - _MainTex: - m_Texture: {fileID: 2800000, guid: 449ae3f15d4b14242bd974e568363f23, type: 3} + m_Texture: {fileID: 0} m_Scale: {x: 1, y: 1} m_Offset: {x: 0, y: 0} - _MetallicGlossMap: @@ -84,13 +90,36 @@ Material: m_Texture: {fileID: 0} m_Scale: {x: 1, y: 1} m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} m_Ints: [] m_Floats: - : 1 + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 - _DetailNormalMapScale: 1 - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 - _GlossMapScale: 1 - _Glossiness: 0.183 - _GlossyReflections: 1 @@ -98,13 +127,21 @@ Material: - _Mode: 0 - _OcclusionStrength: 1 - _Parallax: 0.02 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0.5 - _SmoothnessTextureChannel: 0 - _SpecularHighlights: 0 - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 - _UVSec: 0 + - _WorkflowMode: 1 + - _XRMotionVectorsPass: 1 - _ZWrite: 1 m_Colors: - : {r: 1, g: 1, b: 1, a: 1} + - _BaseColor: {r: 1, g: 1, b: 1, a: 1} - _Color: {r: 1, g: 1, b: 1, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - _SpecColor: {r: 0.09433961, g: 0.09433961, b: 0.09433961, a: 1} From 6923008cddf42895355829f4c301a16f3c75471f Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 19 Jul 2025 23:16:39 +0200 Subject: [PATCH 12/17] Add normal smoothing support --- .../Containers/ChiselModelComponent.cs | 45 +++++++++++++++++++ .../Generated/ChiselGeneratedObjects.cs | 6 +++ Components/Utility/ChiselMeshUtility.cs | 7 +++ .../Containers/ChiselModelEditor.cs | 17 +++++-- README.md | 2 +- 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/Components/Components/Containers/ChiselModelComponent.cs b/Components/Components/Containers/ChiselModelComponent.cs index 2749e59..d78d914 100644 --- a/Components/Components/Containers/ChiselModelComponent.cs +++ b/Components/Components/Containers/ChiselModelComponent.cs @@ -210,6 +210,8 @@ public sealed class ChiselModelComponent : ChiselNodeComponent public const string kAutoRebuildUVsName = nameof(AutoRebuildUVs); public const string kVertexChannelMaskName = nameof(VertexChannelMask); public const string kSubtractiveEditingName = nameof(SubtractiveEditing); + public const string kSmoothNormalsName = nameof(SmoothNormals); + public const string kSmoothingAngleName = nameof(SmoothingAngle); public const string kNodeTypeName = "Model"; @@ -237,7 +239,12 @@ public sealed class ChiselModelComponent : ChiselNodeComponent public bool AutoRebuildUVs = true; public VertexChannelFlags VertexChannelMask = VertexChannelFlags.All; public bool SubtractiveEditing = false; + public bool SmoothNormals = false; + [Range(0, 180)] + public float SmoothingAngle = 45.0f; [NonSerialized] bool prevSubtractiveEditing; + [NonSerialized] bool prevSmoothNormals; + [NonSerialized] float prevSmoothingAngle; public ChiselModelComponent() : base() { } @@ -300,6 +307,13 @@ protected override void OnValidateState() FlipGeneratedMeshes(); prevSubtractiveEditing = SubtractiveEditing; } + + if ((prevSmoothNormals != SmoothNormals || !Mathf.Approximately(prevSmoothingAngle, SmoothingAngle)) && generated != null) + { + SmoothGeneratedMeshes(); + prevSmoothNormals = SmoothNormals; + prevSmoothingAngle = SmoothingAngle; + } } public override void OnInitialize() @@ -342,6 +356,8 @@ public override void OnInitialize() IsDefaultModel = true; prevSubtractiveEditing = SubtractiveEditing; + prevSmoothNormals = SmoothNormals; + prevSmoothingAngle = SmoothingAngle; IsInitialized = true; } @@ -372,6 +388,35 @@ void FlipGeneratedMeshes() } } + void SmoothGeneratedMeshes() + { + if (generated == null) + return; + + float angle = SmoothNormals ? SmoothingAngle : 0.0f; + + if (generated.renderables != null) + { + foreach (var renderable in generated.renderables) + if (renderable != null && renderable.sharedMesh) + ChiselMeshUtility.SmoothNormals(renderable.sharedMesh, angle); + } + + if (generated.debugVisualizationRenderables != null) + { + foreach (var renderable in generated.debugVisualizationRenderables) + if (renderable != null && renderable.sharedMesh) + ChiselMeshUtility.SmoothNormals(renderable.sharedMesh, angle); + } + + if (generated.colliders != null) + { + foreach (var collider in generated.colliders) + if (collider != null && collider.sharedMesh) + ChiselMeshUtility.SmoothNormals(collider.sharedMesh, angle); + } + } + #if UNITY_EDITOR // TODO: remove from here, shouldn't be public public MaterialPropertyBlock materialPropertyBlock; diff --git a/Components/Components/Generated/ChiselGeneratedObjects.cs b/Components/Components/Generated/ChiselGeneratedObjects.cs index 74bb0c5..fb2e734 100644 --- a/Components/Components/Generated/ChiselGeneratedObjects.cs +++ b/Components/Components/Generated/ChiselGeneratedObjects.cs @@ -613,6 +613,12 @@ public int FinishMeshUpdates(ChiselModelComponent model, ChiselMeshUtility.FlipNormals(foundMeshes[i]); } + if (model.SmoothNormals) + { + for (int i = 0; i < foundMeshes.Count; i++) + ChiselMeshUtility.SmoothNormals(foundMeshes[i], model.SmoothingAngle); + } + // TODO: user meshDataArray data to determine if colliders are visible or not, then we can move this before the Apply Profiler.BeginSample("UpdateColliders"); ChiselColliderObjects.UpdateProperties(model, this.colliders); diff --git a/Components/Utility/ChiselMeshUtility.cs b/Components/Utility/ChiselMeshUtility.cs index 1e8cdcb..f457b33 100644 --- a/Components/Utility/ChiselMeshUtility.cs +++ b/Components/Utility/ChiselMeshUtility.cs @@ -25,5 +25,12 @@ public static void FlipNormals(Mesh mesh) mesh.SetTriangles(indices, s); } } + + public static void SmoothNormals(Mesh mesh, float angle) + { + if (!mesh) + return; + mesh.RecalculateNormals(angle); + } } } diff --git a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs index 3b1c2b4..30bc565 100644 --- a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs +++ b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs @@ -52,6 +52,8 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) readonly static GUIContent kCreateRenderComponentsContents = new("Renderable"); readonly static GUIContent kCreateColliderComponentsContents = new("Collidable"); readonly static GUIContent kSubtractiveEditingContents = new("Subtractive Editing", "New brushes are created as subtractive when enabled"); + readonly static GUIContent kSmoothNormalsContents = new("Smooth Normals"); + readonly static GUIContent kSmoothingAngleContents = new("Smoothing Angle"); readonly static GUIContent kUnwrapParamsContents = new("UV Generation"); readonly static GUIContent kForceBuildUVsContents = new("Build", "Manually build lightmap UVs for generated meshes. This operation can be slow for more complicated meshes"); @@ -130,6 +132,8 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) SerializedProperty createRenderComponentsProp; SerializedProperty createColliderComponentsProp; SerializedProperty subtractiveEditingProp; + SerializedProperty smoothNormalsProp; + SerializedProperty smoothingAngleProp; SerializedProperty autoRebuildUVsProp; SerializedProperty angleErrorProp; SerializedProperty areaErrorProp; @@ -217,10 +221,12 @@ internal void OnEnable() } vertexChannelMaskProp = serializedObject.FindProperty($"{ChiselModelComponent.kVertexChannelMaskName}"); - createRenderComponentsProp = serializedObject.FindProperty($"{ChiselModelComponent.kCreateRenderComponentsName}"); - createColliderComponentsProp = serializedObject.FindProperty($"{ChiselModelComponent.kCreateColliderComponentsName}"); - subtractiveEditingProp = serializedObject.FindProperty($"{ChiselModelComponent.kSubtractiveEditingName}"); - autoRebuildUVsProp = serializedObject.FindProperty($"{ChiselModelComponent.kAutoRebuildUVsName}"); + createRenderComponentsProp = serializedObject.FindProperty($"{ChiselModelComponent.kCreateRenderComponentsName}"); + createColliderComponentsProp = serializedObject.FindProperty($"{ChiselModelComponent.kCreateColliderComponentsName}"); + subtractiveEditingProp = serializedObject.FindProperty($"{ChiselModelComponent.kSubtractiveEditingName}"); + smoothNormalsProp = serializedObject.FindProperty($"{ChiselModelComponent.kSmoothNormalsName}"); + smoothingAngleProp = serializedObject.FindProperty($"{ChiselModelComponent.kSmoothingAngleName}"); + autoRebuildUVsProp = serializedObject.FindProperty($"{ChiselModelComponent.kAutoRebuildUVsName}"); angleErrorProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kAngleErrorName}"); areaErrorProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kAreaErrorName}"); hardAngleProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kHardAngleName}"); @@ -1210,6 +1216,9 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(createColliderComponentsProp, kCreateColliderComponentsContents); EditorGUILayout.PropertyField(createRenderComponentsProp, kCreateRenderComponentsContents); EditorGUILayout.PropertyField(subtractiveEditingProp, kSubtractiveEditingContents); + EditorGUILayout.PropertyField(smoothNormalsProp, kSmoothNormalsContents); + if (smoothNormalsProp.boolValue) + EditorGUILayout.PropertyField(smoothingAngleProp, kSmoothingAngleContents); EditorGUI.BeginDisabledGroup(!createRenderComponentsProp.boolValue); { diff --git a/README.md b/README.md index 8623709..6de7fd6 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Planned Features (incomplete, and in random order): * [Double sided surfaces](https://github.com/RadicalCSG/Chisel.Prototype/issues/226) * [Extrusion from existing surface](https://github.com/RadicalCSG/Chisel.Prototype/issues/19) * [Clip Tool](https://github.com/RadicalCSG/Chisel.Prototype/issues/15) -* [Normal smoothing](https://github.com/RadicalCSG/Chisel.Prototype/issues/184) +* Normal smoothing **(NEW)** * [Node Based Generators](https://github.com/RadicalCSG/Chisel.Prototype/issues/94) for easy procedural content generation * [2D shape editor](https://github.com/RadicalCSG/Chisel.Prototype/issues/260) * [Hotspot mapping](https://github.com/RadicalCSG/Chisel.Prototype/issues/173) From 6fa306ebaab233e45dffcf075c01b61df5d54041 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 19 Jul 2025 23:25:40 +0200 Subject: [PATCH 13/17] Fix normal smoothing using custom NormalSolver --- Components/Utility/ChiselMeshUtility.cs | 2 +- Components/Utility/NormalSolver.cs | 257 ++++++++++++++++++++++++ Components/Utility/NormalSolver.cs.meta | 2 + 3 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 Components/Utility/NormalSolver.cs create mode 100644 Components/Utility/NormalSolver.cs.meta diff --git a/Components/Utility/ChiselMeshUtility.cs b/Components/Utility/ChiselMeshUtility.cs index f457b33..130974f 100644 --- a/Components/Utility/ChiselMeshUtility.cs +++ b/Components/Utility/ChiselMeshUtility.cs @@ -30,7 +30,7 @@ public static void SmoothNormals(Mesh mesh, float angle) { if (!mesh) return; - mesh.RecalculateNormals(angle); + NormalSolver.RecalculateNormals(mesh, angle); } } } diff --git a/Components/Utility/NormalSolver.cs b/Components/Utility/NormalSolver.cs new file mode 100644 index 0000000..b229b77 --- /dev/null +++ b/Components/Utility/NormalSolver.cs @@ -0,0 +1,257 @@ +/* + * The following code was taken from: https://schemingdeveloper.com + * + * Visit our game studio website: http://stopthegnomes.com + * + * License: You may use this code however you see fit, as long as you include this notice + * without any modifications. + * + * You may not publish a paid asset on Unity store if its main function is based on + * the following code, but you may publish a paid asset that uses this code. + * + * If you intend to use this in a Unity store asset or a commercial project, it would + * be appreciated, but not required, if you let me know with a link to the asset. If I + * don't get back to you just go ahead and use it anyway! + */ + +using System.Collections.Generic; +using UnityEngine; + +public static class NormalSolver +{ + /// + /// Recalculate the normals of a mesh based on an angle threshold. This takes + /// into account distinct vertices that have the same position. + /// + /// + /// + /// The smoothing angle. Note that triangles that already share + /// the same vertex will be smooth regardless of the angle! + /// + public static void RecalculateNormals(this Mesh mesh, float angle) + { + var cosineThreshold = Mathf.Cos(angle * Mathf.Deg2Rad); + + var vertices = mesh.vertices; + var normals = new Vector3[vertices.Length]; + + // Holds the normal of each triangle in each sub mesh. + var triNormals = new Vector3[mesh.subMeshCount][]; + + var dictionary = new Dictionary>(vertices.Length); + + for (var subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; ++subMeshIndex) + { + + var triangles = mesh.GetTriangles(subMeshIndex); + + triNormals[subMeshIndex] = new Vector3[triangles.Length / 3]; + + for (var i = 0; i < triangles.Length; i += 3) + { + int i1 = triangles[i]; + int i2 = triangles[i + 1]; + int i3 = triangles[i + 2]; + + // Calculate the normal of the triangle + Vector3 p1 = vertices[i2] - vertices[i1]; + Vector3 p2 = vertices[i3] - vertices[i1]; + Vector3 normal = Vector3.Cross(p1, p2).normalized; + int triIndex = i / 3; + triNormals[subMeshIndex][triIndex] = normal; + + List entry; + VertexKey key; + + if (!dictionary.TryGetValue(key = new VertexKey(vertices[i1]), out entry)) + { + entry = new List(4); + dictionary.Add(key, entry); + } + entry.Add(new VertexEntry(subMeshIndex, triIndex, i1)); + + if (!dictionary.TryGetValue(key = new VertexKey(vertices[i2]), out entry)) + { + entry = new List(); + dictionary.Add(key, entry); + } + entry.Add(new VertexEntry(subMeshIndex, triIndex, i2)); + + if (!dictionary.TryGetValue(key = new VertexKey(vertices[i3]), out entry)) + { + entry = new List(); + dictionary.Add(key, entry); + } + entry.Add(new VertexEntry(subMeshIndex, triIndex, i3)); + } + } + + // Each entry in the dictionary represents a unique vertex position. + + foreach (var vertList in dictionary.Values) + { + for (var i = 0; i < vertList.Count; ++i) + { + + var sum = new Vector3(); + var lhsEntry = vertList[i]; + + for (var j = 0; j < vertList.Count; ++j) + { + var rhsEntry = vertList[j]; + + if (lhsEntry.VertexIndex == rhsEntry.VertexIndex) + { + sum += triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]; + } + else + { + // The dot product is the cosine of the angle between the two triangles. + // A larger cosine means a smaller angle. + var dot = Vector3.Dot( + triNormals[lhsEntry.MeshIndex][lhsEntry.TriangleIndex], + triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]); + if (dot >= cosineThreshold) + { + sum += triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]; + } + } + } + + normals[lhsEntry.VertexIndex] = sum.normalized; + } + } + + mesh.normals = normals; + } + + private struct VertexKey + { + private readonly long _x; + private readonly long _y; + private readonly long _z; + + // Change this if you require a different precision. + private const int Tolerance = 100000; + + // Magic FNV values. Do not change these. + private const long FNV32Init = 0x811c9dc5; + private const long FNV32Prime = 0x01000193; + + public VertexKey(Vector3 position) + { + _x = (long)(Mathf.Round(position.x * Tolerance)); + _y = (long)(Mathf.Round(position.y * Tolerance)); + _z = (long)(Mathf.Round(position.z * Tolerance)); + } + + public override bool Equals(object obj) + { + var key = (VertexKey)obj; + return _x == key._x && _y == key._y && _z == key._z; + } + + public override int GetHashCode() + { + long rv = FNV32Init; + rv ^= _x; + rv *= FNV32Prime; + rv ^= _y; + rv *= FNV32Prime; + rv ^= _z; + rv *= FNV32Prime; + + return rv.GetHashCode(); + } + } + + private struct VertexEntry + { + public int MeshIndex; + public int TriangleIndex; + public int VertexIndex; + + public VertexEntry(int meshIndex, int triIndex, int vertIndex) + { + MeshIndex = meshIndex; + TriangleIndex = triIndex; + VertexIndex = vertIndex; + } + } + + public static void AutoWeld(this Mesh mesh, float threshold, float bucketStep) + { + Vector3[] oldVertices = mesh.vertices; + Vector3[] newVertices = new Vector3[oldVertices.Length]; + int[] old2new = new int[oldVertices.Length]; + int newSize = 0; + + // Find AABB + Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); + Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue); + for (int i = 0; i < oldVertices.Length; i++) + { + if (oldVertices[i].x < min.x) min.x = oldVertices[i].x; + if (oldVertices[i].y < min.y) min.y = oldVertices[i].y; + if (oldVertices[i].z < min.z) min.z = oldVertices[i].z; + if (oldVertices[i].x > max.x) max.x = oldVertices[i].x; + if (oldVertices[i].y > max.y) max.y = oldVertices[i].y; + if (oldVertices[i].z > max.z) max.z = oldVertices[i].z; + } + + // Make cubic buckets, each with dimensions "bucketStep" + int bucketSizeX = Mathf.FloorToInt((max.x - min.x) / bucketStep) + 1; + int bucketSizeY = Mathf.FloorToInt((max.y - min.y) / bucketStep) + 1; + int bucketSizeZ = Mathf.FloorToInt((max.z - min.z) / bucketStep) + 1; + List[,,] buckets = new List[bucketSizeX, bucketSizeY, bucketSizeZ]; + + // Make new vertices + for (int i = 0; i < oldVertices.Length; i++) + { + // Determine which bucket it belongs to + int x = Mathf.FloorToInt((oldVertices[i].x - min.x) / bucketStep); + int y = Mathf.FloorToInt((oldVertices[i].y - min.y) / bucketStep); + int z = Mathf.FloorToInt((oldVertices[i].z - min.z) / bucketStep); + + // Check to see if it's already been added + if (buckets[x, y, z] == null) + buckets[x, y, z] = new List(); // Make buckets lazily + + for (int j = 0; j < buckets[x, y, z].Count; j++) + { + Vector3 to = newVertices[buckets[x, y, z][j]] - oldVertices[i]; + if (Vector3.SqrMagnitude(to) < threshold) + { + old2new[i] = buckets[x, y, z][j]; + goto skip; // Skip to next old vertex if this one is already there + } + } + + // Add new vertex + newVertices[newSize] = oldVertices[i]; + buckets[x, y, z].Add(newSize); + old2new[i] = newSize; + newSize++; + + skip:; + } + + // Make new triangles + int[] oldTris = mesh.triangles; + int[] newTris = new int[oldTris.Length]; + for (int i = 0; i < oldTris.Length; i++) + { + newTris[i] = old2new[oldTris[i]]; + } + + Vector3[] finalVertices = new Vector3[newSize]; + for (int i = 0; i < newSize; i++) + finalVertices[i] = newVertices[i]; + + mesh.Clear(); + mesh.vertices = finalVertices; + mesh.triangles = newTris; + mesh.RecalculateNormals(); + mesh.Optimize(); + } +} diff --git a/Components/Utility/NormalSolver.cs.meta b/Components/Utility/NormalSolver.cs.meta new file mode 100644 index 0000000..b0fed5c --- /dev/null +++ b/Components/Utility/NormalSolver.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 99408dc071d443e38d8f009614c08c24 From 455869b1563da30a3b387b6471fd71e5ff5af514 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 19 Jul 2025 23:32:59 +0200 Subject: [PATCH 14/17] Update NormalSolver.cs --- Components/Utility/NormalSolver.cs | 369 +++++++++++++++-------------- 1 file changed, 186 insertions(+), 183 deletions(-) diff --git a/Components/Utility/NormalSolver.cs b/Components/Utility/NormalSolver.cs index b229b77..019939b 100644 --- a/Components/Utility/NormalSolver.cs +++ b/Components/Utility/NormalSolver.cs @@ -17,241 +17,244 @@ using System.Collections.Generic; using UnityEngine; -public static class NormalSolver +namespace Chisel.Components { - /// - /// Recalculate the normals of a mesh based on an angle threshold. This takes - /// into account distinct vertices that have the same position. - /// - /// - /// - /// The smoothing angle. Note that triangles that already share - /// the same vertex will be smooth regardless of the angle! - /// - public static void RecalculateNormals(this Mesh mesh, float angle) + public static class NormalSolver { - var cosineThreshold = Mathf.Cos(angle * Mathf.Deg2Rad); + /// + /// Recalculate the normals of a mesh based on an angle threshold. This takes + /// into account distinct vertices that have the same position. + /// + /// + /// + /// The smoothing angle. Note that triangles that already share + /// the same vertex will be smooth regardless of the angle! + /// + public static void RecalculateNormals(this Mesh mesh, float angle) + { + var cosineThreshold = Mathf.Cos(angle * Mathf.Deg2Rad); - var vertices = mesh.vertices; - var normals = new Vector3[vertices.Length]; + var vertices = mesh.vertices; + var normals = new Vector3[vertices.Length]; - // Holds the normal of each triangle in each sub mesh. - var triNormals = new Vector3[mesh.subMeshCount][]; + // Holds the normal of each triangle in each sub mesh. + var triNormals = new Vector3[mesh.subMeshCount][]; - var dictionary = new Dictionary>(vertices.Length); + var dictionary = new Dictionary>(vertices.Length); - for (var subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; ++subMeshIndex) - { + for (var subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; ++subMeshIndex) + { - var triangles = mesh.GetTriangles(subMeshIndex); + var triangles = mesh.GetTriangles(subMeshIndex); - triNormals[subMeshIndex] = new Vector3[triangles.Length / 3]; + triNormals[subMeshIndex] = new Vector3[triangles.Length / 3]; - for (var i = 0; i < triangles.Length; i += 3) - { - int i1 = triangles[i]; - int i2 = triangles[i + 1]; - int i3 = triangles[i + 2]; + for (var i = 0; i < triangles.Length; i += 3) + { + int i1 = triangles[i]; + int i2 = triangles[i + 1]; + int i3 = triangles[i + 2]; - // Calculate the normal of the triangle - Vector3 p1 = vertices[i2] - vertices[i1]; - Vector3 p2 = vertices[i3] - vertices[i1]; - Vector3 normal = Vector3.Cross(p1, p2).normalized; - int triIndex = i / 3; - triNormals[subMeshIndex][triIndex] = normal; + // Calculate the normal of the triangle + Vector3 p1 = vertices[i2] - vertices[i1]; + Vector3 p2 = vertices[i3] - vertices[i1]; + Vector3 normal = Vector3.Cross(p1, p2).normalized; + int triIndex = i / 3; + triNormals[subMeshIndex][triIndex] = normal; - List entry; - VertexKey key; + List entry; + VertexKey key; - if (!dictionary.TryGetValue(key = new VertexKey(vertices[i1]), out entry)) - { - entry = new List(4); - dictionary.Add(key, entry); - } - entry.Add(new VertexEntry(subMeshIndex, triIndex, i1)); + if (!dictionary.TryGetValue(key = new VertexKey(vertices[i1]), out entry)) + { + entry = new List(4); + dictionary.Add(key, entry); + } + entry.Add(new VertexEntry(subMeshIndex, triIndex, i1)); - if (!dictionary.TryGetValue(key = new VertexKey(vertices[i2]), out entry)) - { - entry = new List(); - dictionary.Add(key, entry); - } - entry.Add(new VertexEntry(subMeshIndex, triIndex, i2)); + if (!dictionary.TryGetValue(key = new VertexKey(vertices[i2]), out entry)) + { + entry = new List(); + dictionary.Add(key, entry); + } + entry.Add(new VertexEntry(subMeshIndex, triIndex, i2)); - if (!dictionary.TryGetValue(key = new VertexKey(vertices[i3]), out entry)) - { - entry = new List(); - dictionary.Add(key, entry); + if (!dictionary.TryGetValue(key = new VertexKey(vertices[i3]), out entry)) + { + entry = new List(); + dictionary.Add(key, entry); + } + entry.Add(new VertexEntry(subMeshIndex, triIndex, i3)); } - entry.Add(new VertexEntry(subMeshIndex, triIndex, i3)); } - } - // Each entry in the dictionary represents a unique vertex position. + // Each entry in the dictionary represents a unique vertex position. - foreach (var vertList in dictionary.Values) - { - for (var i = 0; i < vertList.Count; ++i) + foreach (var vertList in dictionary.Values) { - - var sum = new Vector3(); - var lhsEntry = vertList[i]; - - for (var j = 0; j < vertList.Count; ++j) + for (var i = 0; i < vertList.Count; ++i) { - var rhsEntry = vertList[j]; - if (lhsEntry.VertexIndex == rhsEntry.VertexIndex) - { - sum += triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]; - } - else + var sum = new Vector3(); + var lhsEntry = vertList[i]; + + for (var j = 0; j < vertList.Count; ++j) { - // The dot product is the cosine of the angle between the two triangles. - // A larger cosine means a smaller angle. - var dot = Vector3.Dot( - triNormals[lhsEntry.MeshIndex][lhsEntry.TriangleIndex], - triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]); - if (dot >= cosineThreshold) + var rhsEntry = vertList[j]; + + if (lhsEntry.VertexIndex == rhsEntry.VertexIndex) { sum += triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]; } + else + { + // The dot product is the cosine of the angle between the two triangles. + // A larger cosine means a smaller angle. + var dot = Vector3.Dot( + triNormals[lhsEntry.MeshIndex][lhsEntry.TriangleIndex], + triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]); + if (dot >= cosineThreshold) + { + sum += triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]; + } + } } - } - normals[lhsEntry.VertexIndex] = sum.normalized; + normals[lhsEntry.VertexIndex] = sum.normalized; + } } + + mesh.normals = normals; } - mesh.normals = normals; - } + private struct VertexKey + { + private readonly long _x; + private readonly long _y; + private readonly long _z; - private struct VertexKey - { - private readonly long _x; - private readonly long _y; - private readonly long _z; + // Change this if you require a different precision. + private const int Tolerance = 100000; - // Change this if you require a different precision. - private const int Tolerance = 100000; + // Magic FNV values. Do not change these. + private const long FNV32Init = 0x811c9dc5; + private const long FNV32Prime = 0x01000193; - // Magic FNV values. Do not change these. - private const long FNV32Init = 0x811c9dc5; - private const long FNV32Prime = 0x01000193; + public VertexKey(Vector3 position) + { + _x = (long)(Mathf.Round(position.x * Tolerance)); + _y = (long)(Mathf.Round(position.y * Tolerance)); + _z = (long)(Mathf.Round(position.z * Tolerance)); + } - public VertexKey(Vector3 position) - { - _x = (long)(Mathf.Round(position.x * Tolerance)); - _y = (long)(Mathf.Round(position.y * Tolerance)); - _z = (long)(Mathf.Round(position.z * Tolerance)); - } + public override bool Equals(object obj) + { + var key = (VertexKey)obj; + return _x == key._x && _y == key._y && _z == key._z; + } - public override bool Equals(object obj) - { - var key = (VertexKey)obj; - return _x == key._x && _y == key._y && _z == key._z; + public override int GetHashCode() + { + long rv = FNV32Init; + rv ^= _x; + rv *= FNV32Prime; + rv ^= _y; + rv *= FNV32Prime; + rv ^= _z; + rv *= FNV32Prime; + + return rv.GetHashCode(); + } } - public override int GetHashCode() + private struct VertexEntry { - long rv = FNV32Init; - rv ^= _x; - rv *= FNV32Prime; - rv ^= _y; - rv *= FNV32Prime; - rv ^= _z; - rv *= FNV32Prime; - - return rv.GetHashCode(); - } - } - - private struct VertexEntry - { - public int MeshIndex; - public int TriangleIndex; - public int VertexIndex; + public int MeshIndex; + public int TriangleIndex; + public int VertexIndex; - public VertexEntry(int meshIndex, int triIndex, int vertIndex) - { - MeshIndex = meshIndex; - TriangleIndex = triIndex; - VertexIndex = vertIndex; + public VertexEntry(int meshIndex, int triIndex, int vertIndex) + { + MeshIndex = meshIndex; + TriangleIndex = triIndex; + VertexIndex = vertIndex; + } } - } - public static void AutoWeld(this Mesh mesh, float threshold, float bucketStep) - { - Vector3[] oldVertices = mesh.vertices; - Vector3[] newVertices = new Vector3[oldVertices.Length]; - int[] old2new = new int[oldVertices.Length]; - int newSize = 0; - - // Find AABB - Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); - Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue); - for (int i = 0; i < oldVertices.Length; i++) + public static void AutoWeld(this Mesh mesh, float threshold, float bucketStep) { - if (oldVertices[i].x < min.x) min.x = oldVertices[i].x; - if (oldVertices[i].y < min.y) min.y = oldVertices[i].y; - if (oldVertices[i].z < min.z) min.z = oldVertices[i].z; - if (oldVertices[i].x > max.x) max.x = oldVertices[i].x; - if (oldVertices[i].y > max.y) max.y = oldVertices[i].y; - if (oldVertices[i].z > max.z) max.z = oldVertices[i].z; - } + Vector3[] oldVertices = mesh.vertices; + Vector3[] newVertices = new Vector3[oldVertices.Length]; + int[] old2new = new int[oldVertices.Length]; + int newSize = 0; + + // Find AABB + Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); + Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue); + for (int i = 0; i < oldVertices.Length; i++) + { + if (oldVertices[i].x < min.x) min.x = oldVertices[i].x; + if (oldVertices[i].y < min.y) min.y = oldVertices[i].y; + if (oldVertices[i].z < min.z) min.z = oldVertices[i].z; + if (oldVertices[i].x > max.x) max.x = oldVertices[i].x; + if (oldVertices[i].y > max.y) max.y = oldVertices[i].y; + if (oldVertices[i].z > max.z) max.z = oldVertices[i].z; + } - // Make cubic buckets, each with dimensions "bucketStep" - int bucketSizeX = Mathf.FloorToInt((max.x - min.x) / bucketStep) + 1; - int bucketSizeY = Mathf.FloorToInt((max.y - min.y) / bucketStep) + 1; - int bucketSizeZ = Mathf.FloorToInt((max.z - min.z) / bucketStep) + 1; - List[,,] buckets = new List[bucketSizeX, bucketSizeY, bucketSizeZ]; + // Make cubic buckets, each with dimensions "bucketStep" + int bucketSizeX = Mathf.FloorToInt((max.x - min.x) / bucketStep) + 1; + int bucketSizeY = Mathf.FloorToInt((max.y - min.y) / bucketStep) + 1; + int bucketSizeZ = Mathf.FloorToInt((max.z - min.z) / bucketStep) + 1; + List[,,] buckets = new List[bucketSizeX, bucketSizeY, bucketSizeZ]; - // Make new vertices - for (int i = 0; i < oldVertices.Length; i++) - { - // Determine which bucket it belongs to - int x = Mathf.FloorToInt((oldVertices[i].x - min.x) / bucketStep); - int y = Mathf.FloorToInt((oldVertices[i].y - min.y) / bucketStep); - int z = Mathf.FloorToInt((oldVertices[i].z - min.z) / bucketStep); + // Make new vertices + for (int i = 0; i < oldVertices.Length; i++) + { + // Determine which bucket it belongs to + int x = Mathf.FloorToInt((oldVertices[i].x - min.x) / bucketStep); + int y = Mathf.FloorToInt((oldVertices[i].y - min.y) / bucketStep); + int z = Mathf.FloorToInt((oldVertices[i].z - min.z) / bucketStep); - // Check to see if it's already been added - if (buckets[x, y, z] == null) - buckets[x, y, z] = new List(); // Make buckets lazily + // Check to see if it's already been added + if (buckets[x, y, z] == null) + buckets[x, y, z] = new List(); // Make buckets lazily - for (int j = 0; j < buckets[x, y, z].Count; j++) - { - Vector3 to = newVertices[buckets[x, y, z][j]] - oldVertices[i]; - if (Vector3.SqrMagnitude(to) < threshold) + for (int j = 0; j < buckets[x, y, z].Count; j++) { - old2new[i] = buckets[x, y, z][j]; - goto skip; // Skip to next old vertex if this one is already there + Vector3 to = newVertices[buckets[x, y, z][j]] - oldVertices[i]; + if (Vector3.SqrMagnitude(to) < threshold) + { + old2new[i] = buckets[x, y, z][j]; + goto skip; // Skip to next old vertex if this one is already there + } } - } - // Add new vertex - newVertices[newSize] = oldVertices[i]; - buckets[x, y, z].Add(newSize); - old2new[i] = newSize; - newSize++; + // Add new vertex + newVertices[newSize] = oldVertices[i]; + buckets[x, y, z].Add(newSize); + old2new[i] = newSize; + newSize++; - skip:; - } + skip:; + } - // Make new triangles - int[] oldTris = mesh.triangles; - int[] newTris = new int[oldTris.Length]; - for (int i = 0; i < oldTris.Length; i++) - { - newTris[i] = old2new[oldTris[i]]; - } + // Make new triangles + int[] oldTris = mesh.triangles; + int[] newTris = new int[oldTris.Length]; + for (int i = 0; i < oldTris.Length; i++) + { + newTris[i] = old2new[oldTris[i]]; + } - Vector3[] finalVertices = new Vector3[newSize]; - for (int i = 0; i < newSize; i++) - finalVertices[i] = newVertices[i]; + Vector3[] finalVertices = new Vector3[newSize]; + for (int i = 0; i < newSize; i++) + finalVertices[i] = newVertices[i]; - mesh.Clear(); - mesh.vertices = finalVertices; - mesh.triangles = newTris; - mesh.RecalculateNormals(); - mesh.Optimize(); + mesh.Clear(); + mesh.vertices = finalVertices; + mesh.triangles = newTris; + mesh.RecalculateNormals(); + mesh.Optimize(); + } } } From 7e7c6ef1c7d177a86faab09af6bbb1fac6be27b2 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 19 Jul 2025 23:36:08 +0200 Subject: [PATCH 15/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6de7fd6..48e44ca 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ Features (incomplete) * Precise snapping to surfaces, edges, vertices and grid lines * Rotatable & movable grid * Subtractive Workflow **(NEW)** +* Normal smoothing **(NEW)** Planned Features (incomplete, and in random order): * [Debug Visualizations](https://github.com/RadicalCSG/Chisel.Prototype/issues/118) to see shadow only surfaces, collider surfaces etc. (partially implemented) * [Double sided surfaces](https://github.com/RadicalCSG/Chisel.Prototype/issues/226) * [Extrusion from existing surface](https://github.com/RadicalCSG/Chisel.Prototype/issues/19) * [Clip Tool](https://github.com/RadicalCSG/Chisel.Prototype/issues/15) -* Normal smoothing **(NEW)** * [Node Based Generators](https://github.com/RadicalCSG/Chisel.Prototype/issues/94) for easy procedural content generation * [2D shape editor](https://github.com/RadicalCSG/Chisel.Prototype/issues/260) * [Hotspot mapping](https://github.com/RadicalCSG/Chisel.Prototype/issues/173) From 6f42085303ac04e3b6513ef242b01df1ef77ec1e Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sun, 20 Jul 2025 21:08:46 +0200 Subject: [PATCH 16/17] Move debug settings to dedicated inspector section --- .../Containers/ChiselModelComponent.cs | 7 +++ .../Managers/CSGManager.UpdateTreeMeshes.cs | 58 ++++++++++++++++++- .../Containers/ChiselModelEditor.cs | 18 ++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/Components/Components/Containers/ChiselModelComponent.cs b/Components/Components/Containers/ChiselModelComponent.cs index d78d914..923e972 100644 --- a/Components/Components/Containers/ChiselModelComponent.cs +++ b/Components/Components/Containers/ChiselModelComponent.cs @@ -212,6 +212,7 @@ public sealed class ChiselModelComponent : ChiselNodeComponent public const string kSubtractiveEditingName = nameof(SubtractiveEditing); public const string kSmoothNormalsName = nameof(SmoothNormals); public const string kSmoothingAngleName = nameof(SmoothingAngle); + public const string kDebugLogBrushesName = nameof(DebugLogBrushes); public const string kNodeTypeName = "Model"; @@ -246,6 +247,12 @@ public sealed class ChiselModelComponent : ChiselNodeComponent [NonSerialized] bool prevSmoothNormals; [NonSerialized] float prevSmoothingAngle; + #region Debug + // When enabled all brush geometry will be printed out to the console + // at the start of the CSG job update + public bool DebugLogBrushes = false; + #endregion + public ChiselModelComponent() : base() { } diff --git a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs index 5b35b61..28ccdec 100644 --- a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs +++ b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs @@ -5,6 +5,8 @@ using Unity.Profiling; using Unity.Entities; using System.Buffers; +using UnityEngine; +using System.Reflection; namespace Chisel.Core { @@ -634,6 +636,7 @@ public void RunMeshInitJobs(JobHandle dependsOn) var chiselLookupValues = ChiselTreeLookup.Value[this.tree]; ref var brushMeshBlobs = ref ChiselMeshLookup.Value.brushMeshBlobCache; + { #region Build Lookup Tables using (kJobBuildLookupTablesJobProfilerMarker.Auto()) @@ -839,10 +842,61 @@ public void RunMeshInitJobs(JobHandle dependsOn) } public void RunMeshUpdateJobs() - { - var chiselLookupValues = ChiselTreeLookup.Value[this.tree]; + { + var chiselLookupValues = ChiselTreeLookup.Value[this.tree]; ref var brushMeshBlobs = ref ChiselMeshLookup.Value.brushMeshBlobCache; + // Debug logging of all brush geometry when enabled on the model + var modelObj = UnityEngine.Resources.InstanceIDToObject(this.tree.InstanceID); + bool debugLogBrushes = false; + if (modelObj != null) + { + var type = modelObj.GetType(); + var prop = type.GetProperty("DebugLogBrushes", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (prop != null && prop.PropertyType == typeof(bool)) + debugLogBrushes = (bool)prop.GetValue(modelObj); + else + { + var field = type.GetField("DebugLogBrushes", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (field != null && field.FieldType == typeof(bool)) + debugLogBrushes = (bool)field.GetValue(modelObj); + } + } + if (debugLogBrushes) + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine("Brush Debug Info:"); + for (int i = 0; i < Temporaries.brushes.Length; i++) + { + var brushID = Temporaries.brushes[i]; + var brush = CSGTreeBrush.FindNoErrors(brushID); + if (!brush.Valid) + continue; + sb.AppendLine($"Brush {i} Operation: {brush.Operation}"); + var brushMeshBlob = BrushMeshManager.GetBrushMeshBlob(brush.BrushMesh); + if (!brushMeshBlob.IsCreated) + continue; + ref var vertices = ref brushMeshBlob.Value.localVertices; + for (int v = 0; v < vertices.Length; v++) + sb.AppendLine($" v{v}: {vertices[v]}"); + ref var halfEdges = ref brushMeshBlob.Value.halfEdges; + ref var polygons = ref brushMeshBlob.Value.polygons; + for (int p = 0; p < polygons.Length; p++) + { + var polygon = polygons[p]; + sb.Append($" f{p}:"); + for (int e = 0; e < polygon.edgeCount; e++) + { + var edgeIndex = polygon.firstEdge + e; + sb.Append(' '); + sb.Append(halfEdges[edgeIndex].vertexIndex); + } + sb.AppendLine(); + } + } + UnityEngine.Debug.Log(sb.ToString()); + } + #region Perform CSG #region Prepare diff --git a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs index 30bc565..a2b8d57 100644 --- a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs +++ b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs @@ -49,11 +49,13 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) readonly static GUIContent kAdditionalSettingsContent = new("Additional Settings"); readonly static GUIContent kGenerationSettingsContent = new("Geometry Output"); readonly static GUIContent kColliderSettingsContent = new("Collider"); + readonly static GUIContent kDebugContent = new("Debug"); readonly static GUIContent kCreateRenderComponentsContents = new("Renderable"); readonly static GUIContent kCreateColliderComponentsContents = new("Collidable"); readonly static GUIContent kSubtractiveEditingContents = new("Subtractive Editing", "New brushes are created as subtractive when enabled"); readonly static GUIContent kSmoothNormalsContents = new("Smooth Normals"); readonly static GUIContent kSmoothingAngleContents = new("Smoothing Angle"); + readonly static GUIContent kDebugLogBrushesContents = new("Debug Log Brushes"); readonly static GUIContent kUnwrapParamsContents = new("UV Generation"); readonly static GUIContent kForceBuildUVsContents = new("Build", "Manually build lightmap UVs for generated meshes. This operation can be slow for more complicated meshes"); @@ -126,6 +128,7 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) const string kDisplayLightmapKey = "ChiselModelEditor.ShowLightmapSettings"; const string kDisplayChartingKey = "ChiselModelEditor.ShowChartingSettings"; const string kDisplayUnwrapParamsKey = "ChiselModelEditor.ShowUnwrapParams"; + const string kDisplayDebugKey = "ChiselModelEditor.ShowDebugSettings"; SerializedProperty vertexChannelMaskProp; @@ -134,6 +137,7 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) SerializedProperty subtractiveEditingProp; SerializedProperty smoothNormalsProp; SerializedProperty smoothingAngleProp; + SerializedProperty debugLogBrushesProp; SerializedProperty autoRebuildUVsProp; SerializedProperty angleErrorProp; SerializedProperty areaErrorProp; @@ -174,6 +178,7 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) bool showLightmapSettings; bool showChartingSettings; bool showUnwrapParams; + bool showDebug; UnityEngine.Object[] childNodes; @@ -213,6 +218,7 @@ internal void OnEnable() showLightmapSettings = SessionState.GetBool(kDisplayLightmapKey, true); showChartingSettings = SessionState.GetBool(kDisplayChartingKey, true); showUnwrapParams = SessionState.GetBool(kDisplayUnwrapParamsKey, true); + showDebug = SessionState.GetBool(kDisplayDebugKey, false); if (!target) { @@ -226,6 +232,7 @@ internal void OnEnable() subtractiveEditingProp = serializedObject.FindProperty($"{ChiselModelComponent.kSubtractiveEditingName}"); smoothNormalsProp = serializedObject.FindProperty($"{ChiselModelComponent.kSmoothNormalsName}"); smoothingAngleProp = serializedObject.FindProperty($"{ChiselModelComponent.kSmoothingAngleName}"); + debugLogBrushesProp = serializedObject.FindProperty($"{ChiselModelComponent.kDebugLogBrushesName}"); autoRebuildUVsProp = serializedObject.FindProperty($"{ChiselModelComponent.kAutoRebuildUVsName}"); angleErrorProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kAngleErrorName}"); areaErrorProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kAreaErrorName}"); @@ -1193,6 +1200,7 @@ public override void OnInspectorGUI() var oldShowGenerationSettings = showGenerationSettings; var oldShowColliderSettings = showColliderSettings; + var oldShowDebug = showDebug; if (gameObjectsSerializedObject != null) gameObjectsSerializedObject.Update(); if (serializedObject != null) serializedObject.Update(); @@ -1252,6 +1260,15 @@ public override void OnInspectorGUI() } EditorGUILayout.EndFoldoutHeaderGroup(); } + + showDebug = EditorGUILayout.BeginFoldoutHeaderGroup(showDebug, kDebugContent); + if (showDebug) + { + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(debugLogBrushesProp, kDebugLogBrushesContents); + EditorGUI.indentLevel--; + } + EditorGUILayout.EndFoldoutHeaderGroup(); } if (EditorGUI.EndChangeCheck()) { @@ -1264,6 +1281,7 @@ public override void OnInspectorGUI() if (showGenerationSettings != oldShowGenerationSettings) SessionState.SetBool(kDisplayGenerationSettingsKey, showGenerationSettings); if (showColliderSettings != oldShowColliderSettings ) SessionState.SetBool(kDisplayColliderSettingsKey, showColliderSettings); + if (showDebug != oldShowDebug ) SessionState.SetBool(kDisplayDebugKey, showDebug); } finally { From 6d6430b7fbcc61d8a119676c3d9f64dec78f4749 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Mon, 21 Jul 2025 10:50:48 +0200 Subject: [PATCH 17/17] Add DebugLogOutput option and logging --- .../Containers/ChiselModelComponent.cs | 3 +++ .../Generated/ChiselGeneratedObjects.cs | 19 +++++++++++++++++++ .../Containers/ChiselModelEditor.cs | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/Components/Components/Containers/ChiselModelComponent.cs b/Components/Components/Containers/ChiselModelComponent.cs index 923e972..80fdb29 100644 --- a/Components/Components/Containers/ChiselModelComponent.cs +++ b/Components/Components/Containers/ChiselModelComponent.cs @@ -213,6 +213,7 @@ public sealed class ChiselModelComponent : ChiselNodeComponent public const string kSmoothNormalsName = nameof(SmoothNormals); public const string kSmoothingAngleName = nameof(SmoothingAngle); public const string kDebugLogBrushesName = nameof(DebugLogBrushes); + public const string kDebugLogOutputName = nameof(DebugLogOutput); public const string kNodeTypeName = "Model"; @@ -251,6 +252,8 @@ public sealed class ChiselModelComponent : ChiselNodeComponent // When enabled all brush geometry will be printed out to the console // at the start of the CSG job update public bool DebugLogBrushes = false; + // When enabled the generated output mesh data will be printed out after rebuilding + public bool DebugLogOutput = false; #endregion diff --git a/Components/Components/Generated/ChiselGeneratedObjects.cs b/Components/Components/Generated/ChiselGeneratedObjects.cs index fb2e734..70fa887 100644 --- a/Components/Components/Generated/ChiselGeneratedObjects.cs +++ b/Components/Components/Generated/ChiselGeneratedObjects.cs @@ -5,6 +5,7 @@ using UnityEngine.Profiling; using Unity.Jobs; using UnityEngine.Pool; +using System.Text; namespace Chisel.Components { @@ -672,6 +673,24 @@ public int FinishMeshUpdates(ChiselModelComponent model, colliderDebugVisualization.renderMaterials = new Material[] { ChiselProjectSettings.CollisionSurfacesMaterial }; // }} + if (model.DebugLogOutput) + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine("Output Mesh Debug Info:"); + for (int m = 0; m < foundMeshes.Count; m++) + { + var mesh = foundMeshes[m]; + sb.AppendLine($"Mesh {m} vertices:"); + var verts = mesh.vertices; + for (int v = 0; v < verts.Length; v++) + sb.AppendLine($" v{v}: {verts[v]}"); + var tris = mesh.triangles; + for (int t = 0; t < tris.Length; t += 3) + sb.AppendLine($" t{t / 3}: {tris[t]}, {tris[t + 1]}, {tris[t + 2]}"); + } + UnityEngine.Debug.Log(sb.ToString()); + } + var foundMeshCount = foundMeshes.Count; foundMeshes.Clear(); return foundMeshCount; diff --git a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs index a2b8d57..32df53c 100644 --- a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs +++ b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs @@ -56,6 +56,7 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) readonly static GUIContent kSmoothNormalsContents = new("Smooth Normals"); readonly static GUIContent kSmoothingAngleContents = new("Smoothing Angle"); readonly static GUIContent kDebugLogBrushesContents = new("Debug Log Brushes"); + readonly static GUIContent kDebugLogOutputContents = new("Debug Log Output"); readonly static GUIContent kUnwrapParamsContents = new("UV Generation"); readonly static GUIContent kForceBuildUVsContents = new("Build", "Manually build lightmap UVs for generated meshes. This operation can be slow for more complicated meshes"); @@ -138,6 +139,7 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) SerializedProperty smoothNormalsProp; SerializedProperty smoothingAngleProp; SerializedProperty debugLogBrushesProp; + SerializedProperty debugLogOutputProp; SerializedProperty autoRebuildUVsProp; SerializedProperty angleErrorProp; SerializedProperty areaErrorProp; @@ -233,6 +235,7 @@ internal void OnEnable() smoothNormalsProp = serializedObject.FindProperty($"{ChiselModelComponent.kSmoothNormalsName}"); smoothingAngleProp = serializedObject.FindProperty($"{ChiselModelComponent.kSmoothingAngleName}"); debugLogBrushesProp = serializedObject.FindProperty($"{ChiselModelComponent.kDebugLogBrushesName}"); + debugLogOutputProp = serializedObject.FindProperty($"{ChiselModelComponent.kDebugLogOutputName}"); autoRebuildUVsProp = serializedObject.FindProperty($"{ChiselModelComponent.kAutoRebuildUVsName}"); angleErrorProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kAngleErrorName}"); areaErrorProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kUVGenerationSettingsName}.{SerializableUnwrapParam.kAreaErrorName}"); @@ -1266,6 +1269,7 @@ public override void OnInspectorGUI() { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(debugLogBrushesProp, kDebugLogBrushesContents); + EditorGUILayout.PropertyField(debugLogOutputProp, kDebugLogOutputContents); EditorGUI.indentLevel--; } EditorGUILayout.EndFoldoutHeaderGroup();