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/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index 4e9765c..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; @@ -24,6 +23,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 +124,6 @@ public unsafe void Execute(int index) } input.EndForEachIndex(); - - if (!basePolygonCache[brushNodeOrder].IsCreated) return; @@ -141,7 +140,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 +224,14 @@ 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,6 +274,7 @@ 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) @@ -283,17 +290,92 @@ public unsafe void Execute(int index) 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) + { + (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) uniqueVertexMapper.RegisterVertices( surfaceIndexList, prevCount, *brushVertices.m_Vertices, - map3DTo2D.normal, + finalFaceNormal, instanceID, interiorCat); + + // Normal smoothing logic (across the entire object) + if (normalSmoothingAngle > 0.0001f) + { + var renderVertices = uniqueVertexMapper.surfaceRenderVertices; + var positions = uniqueVertexMapper.surfaceColliderVertices; + float smoothingCos = math.cos(math.radians(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; + + // 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 + // 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) + { + smoothedNormal += comparisonNormal; + } + } + } + + var rv = renderVertices[v]; + rv.normal = math.normalize(smoothedNormal); + renderVertices[v] = rv; + } + } } catch (System.Exception ex) { Debug.LogException(ex); } } @@ -304,15 +386,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/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..bba2295 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 = modelSettings.NormalSmoothing ? math.clamp(modelSettings.NormalSmoothingAngle, 0.0f, 180.0f) : 0.0f; + } ref var compactHierarchy = ref CompactHierarchyManager.GetHierarchy(this.treeCompactNodeID); @@ -1572,7 +1620,9 @@ ref JobHandles.dataStream2JobHandle transformationCache = chiselLookupValues.transformationCache, input = dataStream2.AsReader(), meshQueries = Temporaries.meshQueries, - instanceIDLookup = CompactHierarchyManager.GetReadOnlyInstanceIDLookup(), + instanceIDLookup = GetReadOnlyInstanceIDLookup(), + 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); 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)