From 247d7f5af3117d685412176a5680be19eff6cf64 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 6 Dec 2025 11:58:11 +0100 Subject: [PATCH 1/6] Added inspector details --- .../Containers/ChiselModelComponent.cs | 75 ++++++++++++++++++- .../Jobs/JobData/ChiselBrushRenderBuffer.cs | 12 ++- .../Managers/CSGManager.UpdateTreeMeshes.cs | 51 +++++++++++++ .../Containers/ChiselModelEditor.cs | 25 +++++++ 4 files changed, 159 insertions(+), 4 deletions(-) diff --git a/Components/Components/Containers/ChiselModelComponent.cs b/Components/Components/Containers/ChiselModelComponent.cs index e323c14..1ef9616 100644 --- a/Components/Components/Containers/ChiselModelComponent.cs +++ b/Components/Components/Containers/ChiselModelComponent.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Runtime.CompilerServices; using Chisel.Core; @@ -107,6 +109,9 @@ public sealed class ChiselGeneratedRenderSettings public const string kLightProbeVolumeOverrideName = nameof(lightProbeProxyVolumeOverride); public const string kProbeAnchorName = nameof(probeAnchor); public const string kReceiveGIName = nameof(receiveGI); + public const string kSubtractiveWorkflowName = nameof(subtractiveWorkflow); + public const string kNormalSmoothingName = nameof(normalSmoothing); + public const string kNormalSmoothingAngleName = nameof(normalSmoothingAngle); #if UNITY_EDITOR public const string kLightmapParametersName = nameof(lightmapParameters); @@ -128,6 +133,9 @@ public sealed class ChiselGeneratedRenderSettings public bool allowOcclusionWhenDynamic = true; public uint renderingLayerMask = ~(uint)0; public ReceiveGI receiveGI = ReceiveGI.LightProbes; + public bool subtractiveWorkflow = false; + public bool normalSmoothing = false; + [Range(0, 180)] public float normalSmoothingAngle = 45.0f; #if UNITY_EDITOR // SerializedObject access Only @@ -174,6 +182,9 @@ public void Reset() allowOcclusionWhenDynamic = true; renderingLayerMask = ~(uint)0; receiveGI = ReceiveGI.LightProbes; + subtractiveWorkflow = false; + normalSmoothing = false; + normalSmoothingAngle = 45.0f; #if UNITY_EDITOR lightmapParameters = new UnityEditor.LightmapParameters(); importantGI = false; @@ -231,7 +242,7 @@ public sealed class ChiselModelComponent : ChiselNodeComponent // TODO: put all bools in flags (makes it harder to work with in the ModelEditor though) - public bool CreateRenderComponents = true; + public bool CreateRenderComponents = true; public bool CreateColliderComponents = true; public bool AutoRebuildUVs = true; public VertexChannelFlags VertexChannelMask = VertexChannelFlags.All; @@ -241,7 +252,7 @@ public ChiselModelComponent() : base() { } // Will show a warning icon in hierarchy when generator has a problem (do not make this method slow, it is called a lot!) - public override void GetMessages(IChiselMessageHandler messages) + public override void GetMessages(IChiselMessageHandler messages) { // TODO: improve warning messages const string kModelHasNoChildrenMessage = kNodeTypeName + " has no children and will not have an effect"; @@ -271,6 +282,7 @@ public override void GetMessages(IChiselMessageHandler messages) protected override void OnCleanup() { + ModelSettingsStore.Remove(GetInstanceID()); if (generated != null) { if (!this && generated.generatedDataContainer) @@ -303,6 +315,7 @@ public override void OnInitialize() renderSettings = new ChiselGeneratedRenderSettings(); renderSettings.Reset(); } + UpdateModelSettingsLookup(); if (generated != null && !generated.generatedDataContainer) @@ -330,6 +343,64 @@ public override void OnInitialize() IsInitialized = true; } + void MarkAllBrushesDirty() + { + if (!Node.Valid) + return; + + var stack = new Stack(); + stack.Push(Node); + while (stack.Count > 0) + { + var current = stack.Pop(); + if (!current.Valid) + continue; + + switch (current.Type) + { + case CSGNodeType.Brush: + ((CSGTreeBrush)current).SetDirty(); + break; + case CSGNodeType.Branch: + case CSGNodeType.Tree: + for (int i = 0; i < current.Count; i++) + stack.Push(current[i]); + break; + } + } + } + + internal void UpdateModelSettingsLookup() + { + if (renderSettings == null) + { + renderSettings = new ChiselGeneratedRenderSettings(); + renderSettings.Reset(); + } + + ModelSettingsStore.Set(GetInstanceID(), new ModelSettings + { + SubtractiveWorkflow = renderSettings.subtractiveWorkflow, + NormalSmoothing = renderSettings.normalSmoothing, + NormalSmoothingAngle = renderSettings.normalSmoothingAngle + }); + SetDirty(); + MarkAllBrushesDirty(); + if (ChiselModelManager.Instance != null) + ChiselModelManager.Instance.UpdateModels(); + } + + protected override void OnValidateState() + { + base.OnValidateState(); + UpdateModelSettingsLookup(); + } + + public void SyncModelSettingsStore() + { + UpdateModelSettingsLookup(); + } + #if UNITY_EDITOR // TODO: remove from here, shouldn't be public public MaterialPropertyBlock materialPropertyBlock; diff --git a/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs b/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs index 96b2ab0..7acf87b 100644 --- a/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs +++ b/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs @@ -134,8 +134,16 @@ public void Construct(BlobBuilder builder, this.vertexCount = colliderVertices.Length; this.indexCount = indices.Length; - // TODO: properly compute hash again, AND USE IT - this.surfaceHashValue = 0;// math.hash(new uint3(normalHash, tangentHash, uv0Hash)); + uint surfaceHash = 0; + for (int i = 0; i < renderVertices.Length; i++) + { + var renderVertex = renderVertices[i]; + surfaceHash = math.hash(new uint2(surfaceHash, math.hash(renderVertex.normal))); + surfaceHash = math.hash(new uint2(surfaceHash, math.hash(renderVertex.tangent))); + surfaceHash = math.hash(new uint2(surfaceHash, math.hash(renderVertex.uv0))); + } + + this.surfaceHashValue = surfaceHash; this.geometryHashValue = geometryHashValue; this.aabb = colliderVertices.GetMinMax(); diff --git a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs index 5b35b61..fc37379 100644 --- a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs +++ b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs @@ -5,9 +5,47 @@ using Unity.Profiling; using Unity.Entities; using System.Buffers; +using System.Collections.Generic; namespace Chisel.Core { + public struct ModelSettings + { + public bool SubtractiveWorkflow; + public bool NormalSmoothing; + public float NormalSmoothingAngle; + } + + public static class ModelSettingsStore + { + static readonly Dictionary s_Settings = new(); + static readonly object s_Lock = new(); + + public static void Set(int instanceID, ModelSettings settings) + { + lock (s_Lock) + { + s_Settings[instanceID] = settings; + } + } + + public static bool TryGet(int instanceID, out ModelSettings settings) + { + lock (s_Lock) + { + return s_Settings.TryGetValue(instanceID, out settings); + } + } + + public static void Remove(int instanceID) + { + lock (s_Lock) + { + s_Settings.Remove(instanceID); + } + } + } + static partial class CompactHierarchyManager { const bool runInParallelDefault = true; @@ -58,6 +96,8 @@ internal struct TreeUpdate public int brushCount; public int maxNodeOrder; public int updateCount; + public bool subtractiveWorkflow; + public float normalSmoothingAngle; public JobHandle dependencies; @@ -234,6 +274,14 @@ public void Initialize() // Reset everything JobHandles = default; Temporaries = default; + subtractiveWorkflow = false; + normalSmoothingAngle = -1f; // -1 means no smoothing + + if (ModelSettingsStore.TryGet(tree.InstanceID, out var modelSettings)) + { + subtractiveWorkflow = modelSettings.SubtractiveWorkflow; + normalSmoothingAngle = math.clamp(modelSettings.NormalSmoothingAngle, 0.0f, 180.0f); + } ref var compactHierarchy = ref CompactHierarchyManager.GetHierarchy(this.treeCompactNodeID); @@ -1573,6 +1621,9 @@ ref JobHandles.dataStream2JobHandle input = dataStream2.AsReader(), meshQueries = Temporaries.meshQueries, instanceIDLookup = CompactHierarchyManager.GetReadOnlyInstanceIDLookup(), + // TODO turn these on later + // subtractiveWorkflow = subtractiveWorkflow, + // normalSmoothingAngle = normalSmoothingAngle, // Write brushRenderBufferCache = chiselLookupValues.brushRenderBufferCache diff --git a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs index 39217ae..b6c588f 100644 --- a/Editor/ComponentEditors/Containers/ChiselModelEditor.cs +++ b/Editor/ComponentEditors/Containers/ChiselModelEditor.cs @@ -52,6 +52,9 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) readonly static GUIContent kCreateRenderComponentsContents = new("Renderable"); readonly static GUIContent kCreateColliderComponentsContents = new("Collidable"); readonly static GUIContent kUnwrapParamsContents = new("UV Generation"); + readonly static GUIContent kSubtractiveWorkflowContents = new("Subtractive Workflow", "Flip generated surface orientations to support subtractive workflows."); + readonly static GUIContent kNormalSmoothingContents = new("Normal Smoothing", "Smooth generated normals across adjacent faces."); + readonly static GUIContent kNormalSmoothingAngleContents = new("Angle", "Smoothing angle in degrees (0-180)."); readonly static GUIContent kForceBuildUVsContents = new("Build", "Manually build lightmap UVs for generated meshes. This operation can be slow for more complicated meshes"); readonly static GUIContent kForceRebuildUVsContents = new("Rebuild", "Manually rebuild lightmap UVs for generated meshes. This operation can be slow for more complicated meshes"); @@ -150,6 +153,9 @@ internal static bool ValidateActiveModel(MenuCommand menuCommand) SerializedProperty lightProbeVolumeOverrideProp; SerializedProperty probeAnchorProp; SerializedProperty stitchLightmapSeamsProp; + SerializedProperty subtractiveWorkflowProp; + SerializedProperty normalSmoothingProp; + SerializedProperty normalSmoothingAngleProp; SerializedObject gameObjectsSerializedObject; SerializedProperty staticEditorFlagsProp; @@ -218,6 +224,9 @@ internal void OnEnable() createRenderComponentsProp = serializedObject.FindProperty($"{ChiselModelComponent.kCreateRenderComponentsName}"); createColliderComponentsProp = serializedObject.FindProperty($"{ChiselModelComponent.kCreateColliderComponentsName}"); autoRebuildUVsProp = serializedObject.FindProperty($"{ChiselModelComponent.kAutoRebuildUVsName}"); + subtractiveWorkflowProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kSubtractiveWorkflowName}"); + normalSmoothingProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kNormalSmoothingName}"); + normalSmoothingAngleProp = serializedObject.FindProperty($"{ChiselModelComponent.kRenderSettingsName}.{ChiselGeneratedRenderSettings.kNormalSmoothingAngleName}"); 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}"); @@ -1209,6 +1218,14 @@ public override void OnInspectorGUI() EditorGUI.BeginDisabledGroup(!createRenderComponentsProp.boolValue); { + EditorGUILayout.PropertyField(subtractiveWorkflowProp, kSubtractiveWorkflowContents); + EditorGUILayout.PropertyField(normalSmoothingProp, kNormalSmoothingContents); + if (normalSmoothingProp.boolValue) + { + EditorGUI.indentLevel++; + EditorGUILayout.Slider(normalSmoothingAngleProp, 0, 180, kNormalSmoothingAngleContents); + EditorGUI.indentLevel--; + } RenderGenerationSettingsGUI(); } EditorGUI.EndDisabledGroup(); @@ -1246,7 +1263,15 @@ public override void OnInspectorGUI() gameObjectsSerializedObject.ApplyModifiedProperties(); if (serializedObject != null) serializedObject.ApplyModifiedProperties(); + foreach (var t in targets) + { + if (t is ChiselModelComponent model) + { + model.SyncModelSettingsStore(); + } + } ForceUpdateNodeContents(serializedObject); + ChiselModelManager.Instance.UpdateModels(); } if (showGenerationSettings != oldShowGenerationSettings) SessionState.SetBool(kDisplayGenerationSettingsKey, showGenerationSettings); From 71b44186e66273af41b0c8dc42570382e6eca35c Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 6 Dec 2025 12:38:54 +0100 Subject: [PATCH 2/6] It works but not always --- .../Jobs/GenerateSurfaceTrianglesJob.cs | 123 +++++++++++++++--- .../Managers/CSGManager.UpdateTreeMeshes.cs | 7 +- 2 files changed, 110 insertions(+), 20 deletions(-) diff --git a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index 4e9765c..50c5f48 100644 --- a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs +++ b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs @@ -24,6 +24,8 @@ struct GenerateSurfaceTrianglesJob : IJobParallelForDefer [NoAlias, ReadOnly] public NativeStream.Reader input; [NoAlias, ReadOnly] public NativeArray meshQueries; [NoAlias, ReadOnly] public CompactHierarchyManagerInstance.ReadOnlyInstanceIDLookup instanceIDLookup; + [NoAlias, ReadOnly] public bool subtractiveWorkflow; + [NoAlias, ReadOnly] public float normalSmoothingAngle; // Write [NativeDisableParallelForRestriction] @@ -123,8 +125,6 @@ public unsafe void Execute(int index) } input.EndForEachIndex(); - - if (!basePolygonCache[brushNodeOrder].IsCreated) return; @@ -141,7 +141,6 @@ public unsafe void Execute(int index) maxLoops = math.max(maxLoops, length); } - ref var baseSurfaces = ref basePolygonCache[brushNodeOrder].Value.surfaces; var transform = transformationCache[brushNodeOrder]; var treeToNode = transform.treeToNode; @@ -226,6 +225,15 @@ public unsafe void Execute(int index) var planeNormalMap = math.mul(nodeToTreeInvTrans, plane); var map3DTo2D = new Map3DTo2D(planeNormalMap.xyz); + // --- NORMAL FLIP LOGIC PREPARATION --- + float3 finalFaceNormal = map3DTo2D.normal; + if (subtractiveWorkflow) + { + // Flip the normal direction so lighting is correct for the "inside" + finalFaceNormal = -finalFaceNormal; + } + // ------------------------------------- + surfaceIndexList.Clear(); for (int li = 0; li < loops.Length; li++) { @@ -268,32 +276,113 @@ public unsafe void Execute(int index) output, settings, Allocator.Temp); + #if UNITY_EDITOR && DEBUG - // Inside the loop after calling Triangulate: - if (output.Status.Value != Status.OK) - { - // Log the specific status enum value/name for more detail! - Debug.LogError($"Triangulator failed for surface {surf}, loop index {loopIdx} with status {output.Status.Value.ToString()} ({output.Status.Value})"); - } - else if (output.Triangles.Length == 0) + if (output.Status.Value != Status.OK || output.Triangles.Length == 0) { - Debug.LogError($"Triangulator returned zero triangles for surface {surf}, loop index {loopIdx}."); + // Debug.LogError(...) } #endif if (output.Status.Value != Status.OK || output.Triangles.Length == 0) continue; + // --- WINDING ORDER FLIP FOR SUBTRACTIVE --- + if (subtractiveWorkflow) + { + // Flip winding order (0,1,2 -> 0,2,1) to face "inwards" + for (int ti = 0; ti < output.Triangles.Length; ti += 3) + { + var tmp = output.Triangles[ti + 1]; + output.Triangles[ti + 1] = output.Triangles[ti + 2]; + output.Triangles[ti + 2] = tmp; + } + } + // ------------------------------------------ + // Map triangles back var prevCount = surfaceIndexList.Length; var interiorCat = (CategoryIndex)info.interiorCategory; roVerts.RemapTriangles(interiorCat, output.Triangles, surfaceIndexList); + + // --- REGISTER VERTICES (Pass calculated/flipped normal) --- uniqueVertexMapper.RegisterVertices( surfaceIndexList, prevCount, *brushVertices.m_Vertices, - map3DTo2D.normal, + finalFaceNormal, instanceID, interiorCat); + + // --- NORMAL SMOOTHING LOGIC (GLOBAL) --- + if (normalSmoothingAngle > 0.0001f) + { + var renderVertices = uniqueVertexMapper.surfaceRenderVertices; + var positions = uniqueVertexMapper.surfaceColliderVertices; + float smoothingCos = math.cos(normalSmoothingAngle); + + int totalVerts = renderVertices.Length; + + for (int v = 0; v < totalVerts; v++) + { + float3 vertPos = positions[v]; + float3 smoothedNormal = finalFaceNormal; + + // Iterate ALL brushes to smooth across the entire model + for (int otherBrushIdx = 0; otherBrushIdx < basePolygonCache.Length; otherBrushIdx++) + { + if (!basePolygonCache[otherBrushIdx].IsCreated) continue; + ref var otherSurfaces = ref basePolygonCache[otherBrushIdx].Value.surfaces; + var otherTransform = transformationCache[otherBrushIdx]; + var otherNodeToTreeInvTrans = math.transpose(otherTransform.treeToNode); + + for(int otherSurf = 0; otherSurf < otherSurfaces.Length; otherSurf++) + { + // Optimization: if we are checking against ourselves (same brush, same surface), skip + // However, we are in a loop over all brushes. + // 'brushNodeOrder' is the index of the current brush in basePolygonCache? + // 'brushIndexOrder.nodeOrder' was used to get current brush. + if (otherBrushIdx == brushNodeOrder && otherSurf == surf) continue; + + float4 otherPlaneLocal = otherSurfaces[otherSurf].localPlane; + float4 otherPlaneTree = math.mul(otherNodeToTreeInvTrans, otherPlaneLocal); + float3 otherNormal = otherPlaneTree.xyz; + + // Check Angle + // If we subtractive, the contribution of this surface is also inverted? + // The normal of the surface ITSELF is fixed. + // If that surface is part of a subtractive brush, its visual normal is inverted. + // We don't easily know if 'otherBrushIdx' is subtractive here unless we pass that info. + // Limitation: We assume other surfaces contribute 'as is' or we need 'subtractive' status of ALL brushes. + // 'subtractiveWorkflow' is a global flag for the *Tree* in this context? + // If so, then 'otherNormal' should also be flipped if 'subtractiveWorkflow' is true. + + // Normal Alignment Check + float dotAngle = math.dot(finalFaceNormal, otherNormal); + + // If purely subtractive workflow, both normals are flipped relative to 'true' geometry. + // So dot product is (-N1).(-N2) = N1.N2. The angle is the same. + + if (dotAngle < smoothingCos) + continue; + + // Plane Distance Check + float dist = math.dot(otherNormal, vertPos) + otherPlaneTree.w; + if (math.abs(dist) < 0.005f) + { + if (subtractiveWorkflow) + otherNormal = -otherNormal; + + smoothedNormal += otherNormal; + } + } + } + + var rv = renderVertices[v]; + rv.normal = math.normalize(smoothedNormal); + renderVertices[v] = rv; + } + } + // ------------------------------ } catch (System.Exception ex) { Debug.LogException(ex); } } @@ -304,15 +393,17 @@ public unsafe void Execute(int index) var parms = baseSurfaces[surf].destinationParameters; var UV0 = baseSurfaces[surf].UV0; var uvMat = math.mul(UV0.ToFloat4x4(), treeToPlane); + + // Normals are now correct (flipped/smoothed) before Tangents are calculated MeshAlgorithms.ComputeUVs(uniqueVertexMapper.surfaceRenderVertices, uvMat); MeshAlgorithms.ComputeTangents(surfaceIndexList, uniqueVertexMapper.surfaceRenderVertices); ref var buf = ref surfaceBuffers[surf]; buf.Construct(builder, surfaceIndexList, - uniqueVertexMapper.surfaceColliderVertices, - uniqueVertexMapper.surfaceSelectVertices, - uniqueVertexMapper.surfaceRenderVertices, - surf, flags, parms); + uniqueVertexMapper.surfaceColliderVertices, + uniqueVertexMapper.surfaceSelectVertices, + uniqueVertexMapper.surfaceRenderVertices, + surf, flags, parms); } using var queryList = new NativeList(surfaceBuffers.Length, Allocator.Temp); diff --git a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs index fc37379..4f6cb3c 100644 --- a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs +++ b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs @@ -1620,10 +1620,9 @@ ref JobHandles.dataStream2JobHandle transformationCache = chiselLookupValues.transformationCache, input = dataStream2.AsReader(), meshQueries = Temporaries.meshQueries, - instanceIDLookup = CompactHierarchyManager.GetReadOnlyInstanceIDLookup(), - // TODO turn these on later - // subtractiveWorkflow = subtractiveWorkflow, - // normalSmoothingAngle = normalSmoothingAngle, + instanceIDLookup = GetReadOnlyInstanceIDLookup(), + subtractiveWorkflow = subtractiveWorkflow, + normalSmoothingAngle = normalSmoothingAngle, // Write brushRenderBufferCache = chiselLookupValues.brushRenderBufferCache From a298fae7189d26a0efed72d6f85025fa68210b3d Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 6 Dec 2025 12:43:57 +0100 Subject: [PATCH 3/6] Update GenerateSurfaceTrianglesJob.cs --- .../2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index 50c5f48..40e32ff 100644 --- a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs +++ b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs @@ -278,9 +278,15 @@ public unsafe void Execute(int index) Allocator.Temp); #if UNITY_EDITOR && DEBUG - if (output.Status.Value != Status.OK || output.Triangles.Length == 0) + // Inside the loop after calling Triangulate: + if (output.Status.Value != Status.OK) + { + // Log the specific status enum value/name for more detail! + Debug.LogError($"Triangulator failed for surface {surf}, loop index {loopIdx} with status {output.Status.Value.ToString()} ({output.Status.Value})"); + } + else if (output.Triangles.Length == 0) { - // Debug.LogError(...) + Debug.LogError($"Triangulator returned zero triangles for surface {surf}, loop index {loopIdx}."); } #endif if (output.Status.Value != Status.OK || output.Triangles.Length == 0) @@ -318,7 +324,7 @@ public unsafe void Execute(int index) { var renderVertices = uniqueVertexMapper.surfaceRenderVertices; var positions = uniqueVertexMapper.surfaceColliderVertices; - float smoothingCos = math.cos(normalSmoothingAngle); + float smoothingCos = math.cos(math.radians(normalSmoothingAngle)); int totalVerts = renderVertices.Length; From 0d891e83271a592267d4cc0ec386944f2b07283e Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 6 Dec 2025 12:51:59 +0100 Subject: [PATCH 4/6] Fixed the issues when both flip and smooth are enabled --- .../Jobs/GenerateSurfaceTrianglesJob.cs | 26 +++++++------------ .../Managers/CSGManager.UpdateTreeMeshes.cs | 2 +- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index 40e32ff..75f2e04 100644 --- a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs +++ b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs @@ -353,32 +353,24 @@ public unsafe void Execute(int index) float4 otherPlaneTree = math.mul(otherNodeToTreeInvTrans, otherPlaneLocal); float3 otherNormal = otherPlaneTree.xyz; - // Check Angle - // If we subtractive, the contribution of this surface is also inverted? - // The normal of the surface ITSELF is fixed. - // If that surface is part of a subtractive brush, its visual normal is inverted. - // We don't easily know if 'otherBrushIdx' is subtractive here unless we pass that info. - // Limitation: We assume other surfaces contribute 'as is' or we need 'subtractive' status of ALL brushes. - // 'subtractiveWorkflow' is a global flag for the *Tree* in this context? - // If so, then 'otherNormal' should also be flipped if 'subtractiveWorkflow' is true. - + // If subtractive workflow, we must flip the neighbor normal effectively + // to compare "Inwards vs Inwards" rather than "Inwards vs Outwards" + float3 comparisonNormal = subtractiveWorkflow ? -otherNormal : otherNormal; + // Normal Alignment Check - float dotAngle = math.dot(finalFaceNormal, otherNormal); - - // If purely subtractive workflow, both normals are flipped relative to 'true' geometry. - // So dot product is (-N1).(-N2) = N1.N2. The angle is the same. + // This now compares the correctly oriented normals + float dotAngle = math.dot(finalFaceNormal, comparisonNormal); if (dotAngle < smoothingCos) continue; // Plane Distance Check + // distance = dot(N_raw, P) + D_raw. + // We use the raw plane normal and D from the cache for geometric distance. float dist = math.dot(otherNormal, vertPos) + otherPlaneTree.w; if (math.abs(dist) < 0.005f) { - if (subtractiveWorkflow) - otherNormal = -otherNormal; - - smoothedNormal += otherNormal; + smoothedNormal += comparisonNormal; } } } diff --git a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs index 4f6cb3c..bba2295 100644 --- a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs +++ b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs @@ -280,7 +280,7 @@ public void Initialize() if (ModelSettingsStore.TryGet(tree.InstanceID, out var modelSettings)) { subtractiveWorkflow = modelSettings.SubtractiveWorkflow; - normalSmoothingAngle = math.clamp(modelSettings.NormalSmoothingAngle, 0.0f, 180.0f); + normalSmoothingAngle = modelSettings.NormalSmoothing ? math.clamp(modelSettings.NormalSmoothingAngle, 0.0f, 180.0f) : 0.0f; } ref var compactHierarchy = ref CompactHierarchyManager.GetHierarchy(this.treeCompactNodeID); From 6804f04e1cf629b7810b75c50edb686bbded2e19 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 6 Dec 2025 12:56:14 +0100 Subject: [PATCH 5/6] Cleanup --- .../Jobs/GenerateSurfaceTrianglesJob.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index 75f2e04..104131b 100644 --- a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs +++ b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs @@ -5,7 +5,6 @@ using Unity.Mathematics; using Debug = UnityEngine.Debug; using ReadOnlyAttribute = Unity.Collections.ReadOnlyAttribute; -using WriteOnlyAttribute = Unity.Collections.WriteOnlyAttribute; using Unity.Entities; using andywiecko.BurstTriangulator.LowLevel.Unsafe; using andywiecko.BurstTriangulator; @@ -225,14 +224,13 @@ public unsafe void Execute(int index) var planeNormalMap = math.mul(nodeToTreeInvTrans, plane); var map3DTo2D = new Map3DTo2D(planeNormalMap.xyz); - // --- NORMAL FLIP LOGIC PREPARATION --- + // Normal flip logic preparation float3 finalFaceNormal = map3DTo2D.normal; if (subtractiveWorkflow) { // Flip the normal direction so lighting is correct for the "inside" finalFaceNormal = -finalFaceNormal; } - // ------------------------------------- surfaceIndexList.Clear(); for (int li = 0; li < loops.Length; li++) @@ -292,25 +290,23 @@ public unsafe void Execute(int index) if (output.Status.Value != Status.OK || output.Triangles.Length == 0) continue; - // --- WINDING ORDER FLIP FOR SUBTRACTIVE --- + // Winding order flip for subtractive if (subtractiveWorkflow) { - // Flip winding order (0,1,2 -> 0,2,1) to face "inwards" + // Flip winding order (0,1,2 -> 0,2,1) to face inwards for (int ti = 0; ti < output.Triangles.Length; ti += 3) { - var tmp = output.Triangles[ti + 1]; - output.Triangles[ti + 1] = output.Triangles[ti + 2]; - output.Triangles[ti + 2] = tmp; + (output.Triangles[ti + 1], output.Triangles[ti + 2]) = + (output.Triangles[ti + 2], output.Triangles[ti + 1]); } } - // ------------------------------------------ // Map triangles back var prevCount = surfaceIndexList.Length; var interiorCat = (CategoryIndex)info.interiorCategory; roVerts.RemapTriangles(interiorCat, output.Triangles, surfaceIndexList); - // --- REGISTER VERTICES (Pass calculated/flipped normal) --- + // Register vertices (Pass calculated/flipped normal) uniqueVertexMapper.RegisterVertices( surfaceIndexList, prevCount, @@ -319,7 +315,7 @@ public unsafe void Execute(int index) instanceID, interiorCat); - // --- NORMAL SMOOTHING LOGIC (GLOBAL) --- + // Normal smoothing logic (across the entire object) if (normalSmoothingAngle > 0.0001f) { var renderVertices = uniqueVertexMapper.surfaceRenderVertices; @@ -380,7 +376,6 @@ public unsafe void Execute(int index) renderVertices[v] = rv; } } - // ------------------------------ } catch (System.Exception ex) { Debug.LogException(ex); } } From 13c46bcf9e369732147267b4e9f9dcc78462fff1 Mon Sep 17 00:00:00 2001 From: Coen Hacking Date: Sat, 6 Dec 2025 13:17:29 +0100 Subject: [PATCH 6/6] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 713a49f..dec25b3 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,14 @@ 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 +* Normal smoothing 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 * [2D shape editor](https://github.com/RadicalCSG/Chisel.Prototype/issues/260) * [Hotspot mapping](https://github.com/RadicalCSG/Chisel.Prototype/issues/173)