diff --git a/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs b/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs index 610a336b7..bf80eb34d 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs @@ -18,7 +18,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public float TargetHeight = 0.0f; /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) { // If used with invert mode pick the target height level if (p.Options.Invert) diff --git a/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs b/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs index d4c10f00e..3d75a56f2 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs @@ -26,7 +26,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt } /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) { var strength = p.Strength * -10.0f; var brushPosition = p.Gizmo.CursorPosition; diff --git a/Source/Editor/Tools/Terrain/Sculpt/Mode.cs b/Source/Editor/Tools/Terrain/Sculpt/Mode.cs index 9a42d2ae8..a92b899e1 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/Mode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/Mode.cs @@ -1,6 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using System.Collections.Generic; using FlaxEditor.Tools.Terrain.Brushes; using FlaxEngine; @@ -50,18 +51,20 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public virtual bool EditHoles => false; /// - /// Applies the modification to the terrain. + /// Gets all patches that will be affected by the brush /// - /// The brush. - /// The options. - /// The gizmo. - /// The terrain. - public unsafe void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain) + /// + /// + /// + /// + public unsafe virtual List GetAffectedPatches(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain) { + List affectedPatches = new(); + // Combine final apply strength float strength = Strength * options.Strength * options.DeltaTime; if (strength <= 0.0f) - return; + return affectedPatches; if (options.Invert && SupportsNegativeApply) strength *= -1; @@ -72,20 +75,10 @@ namespace FlaxEditor.Tools.Terrain.Sculpt var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount; var tempBuffer = (float*)gizmo.GetHeightmapTempBuffer(heightmapLength * sizeof(float)).ToPointer(); var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex; - ApplyParams p = new ApplyParams - { - Terrain = terrain, - Brush = brush, - Gizmo = gizmo, - Options = options, - Strength = strength, - HeightmapSize = heightmapSize, - TempBuffer = tempBuffer, - }; // Get brush bounds in terrain local space var brushBounds = gizmo.CursorBrushBounds; - terrain.GetLocalToWorldMatrix(out p.TerrainWorld); + terrain.GetLocalToWorldMatrix(out var terrainWorld); terrain.GetWorldToLocalMatrix(out var terrainInvWorld); BoundingBox.Transform(ref brushBounds, ref terrainInvWorld, out var brushBoundsLocal); @@ -131,26 +124,78 @@ namespace FlaxEditor.Tools.Terrain.Sculpt if (sourceHeights == null && sourceHoles == null) throw new Exception("Cannot modify terrain. Loading heightmap failed. See log for more info."); - // Record patch data before editing it - if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord)) + ApplyParams p = new ApplyParams { - gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord); - } + Terrain = terrain, + TerrainWorld = terrainWorld, + Brush = brush, + Gizmo = gizmo, + Options = options, + Strength = strength, + HeightmapSize = heightmapSize, + TempBuffer = tempBuffer, + ModifiedOffset = modifiedOffset, + ModifiedSize = modifiedSize, + PatchCoord = patch.PatchCoord, + PatchPositionLocal = patchPositionLocal, + SourceHeightMap = sourceHeights, + SourceHolesMask = sourceHoles, + }; - // Apply modification - p.ModifiedOffset = modifiedOffset; - p.ModifiedSize = modifiedSize; - p.PatchCoord = patch.PatchCoord; - p.PatchPositionLocal = patchPositionLocal; - p.SourceHeightMap = sourceHeights; - p.SourceHolesMask = sourceHoles; - Apply(ref p); + affectedPatches.Add(p); } + return affectedPatches; + } + + /// + /// Applies the modification to the terrain. + /// + /// The brush. + /// The options. + /// The gizmo. + /// The terrain. + public unsafe void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain) + { + var affectedPatches = GetAffectedPatches(brush, ref options, gizmo, terrain); + + if (affectedPatches.Count == 0) + { + return; + } + + ApplyBrush(gizmo, affectedPatches); + // Auto NavMesh rebuild + var brushBounds = gizmo.CursorBrushBounds; gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds); } + /// + /// Applies the brush to all affected patches + /// + /// + /// + public unsafe virtual void ApplyBrush(SculptTerrainGizmoMode gizmo, List affectedPatches) + { + for (int i = 0; i < affectedPatches.Count; i++) + { + ApplyParams patchApplyParams = affectedPatches[i]; + + // Record patch data before editing it + if (!gizmo.CurrentEditUndoAction.HashPatch(ref patchApplyParams.PatchCoord)) + { + gizmo.CurrentEditUndoAction.AddPatch(ref patchApplyParams.PatchCoord); + } + + ApplyBrushToPatch(ref patchApplyParams); + + // Auto NavMesh rebuild + var brushBounds = gizmo.CursorBrushBounds; + gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds); + } + } + /// /// The mode apply parameters. /// @@ -231,6 +276,6 @@ namespace FlaxEditor.Tools.Terrain.Sculpt /// Applies the modification to the terrain. /// /// The parameters to use. - public abstract void Apply(ref ApplyParams p); + public abstract void ApplyBrushToPatch(ref ApplyParams p); } } diff --git a/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs b/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs index 32d02bb3f..7fcb09288 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs @@ -29,7 +29,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public override bool SupportsNegativeApply => true; /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) { // Prepare var brushPosition = p.Gizmo.CursorPosition; diff --git a/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs b/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs index c3409f30c..381b39ebd 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs @@ -15,7 +15,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public override bool SupportsNegativeApply => true; /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) { var strength = p.Strength * 1000.0f; var brushPosition = p.Gizmo.CursorPosition; diff --git a/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs b/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs index d23eae8dd..49ccfe0de 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs @@ -1,7 +1,9 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. -using System; +using System.Collections.Generic; using FlaxEngine; +using FlaxEditor.Tools.Terrain.Brushes; +using System; namespace FlaxEditor.Tools.Terrain.Sculpt { @@ -19,43 +21,135 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public float FilterRadius = 0.4f; /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrush(SculptTerrainGizmoMode gizmo, List affectedPatches) { - // Prepare - var brushPosition = p.Gizmo.CursorPosition; - var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * p.Brush.Size), 2); - var max = p.HeightmapSize - 1; - var strength = Mathf.Saturate(p.Strength); - - // Apply brush modification Profiler.BeginEvent("Apply Brush"); - for (int z = 0; z < p.ModifiedSize.Y; z++) + + // TODO: don't need these on each patch; just need them once + var heightmapSize = affectedPatches[0].HeightmapSize; + var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * affectedPatches[0].Brush.Size), 2); + + + ///// + /// Calculate bounding coordinates of the total affected area + /// + + + Int2 modifieedAreaMinCoord = Int2.Maximum; + Int2 modifiedAreaMaxCoord = Int2.Minimum; + + for (int i = 0; i < affectedPatches.Count; i++) { - var zz = z + p.ModifiedOffset.Y; - for (int x = 0; x < p.ModifiedSize.X; x++) + var patch = affectedPatches[i]; + + var tl = (patch.PatchCoord * (heightmapSize - 1)) + patch.ModifiedOffset; + var br = tl + patch.ModifiedSize; + + if (tl.X <= modifieedAreaMinCoord.X && tl.Y <= modifieedAreaMinCoord.Y) { - var xx = x + p.ModifiedOffset.X; - var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx]; + modifieedAreaMinCoord = tl; + } - var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex); - Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld); + if (br.X >= modifiedAreaMaxCoord.X && br.Y >= modifiedAreaMaxCoord.Y) + { + modifiedAreaMaxCoord = br; + } + } - var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength; - if (paintAmount > 0) + var totalModifiedSize = modifiedAreaMaxCoord - modifieedAreaMinCoord; + + + ///// + /// Build map of heights in affected area + /// + + + var modifiedHeights = new float[totalModifiedSize.X * totalModifiedSize.Y]; + + for (int i = 0; i < affectedPatches.Count; i++) + { + var patch = affectedPatches[i]; + + for (int z = 0; z < patch.ModifiedSize.Y; z++) + { + for (int x = 0; x < patch.ModifiedSize.X; x++) { + // read height from current patch + var localCoordX = (x + patch.ModifiedOffset.X); + var localCoordY = (z + patch.ModifiedOffset.Y); + var coordHeight = patch.SourceHeightMap[(localCoordY * heightmapSize) + localCoordX]; + + // calculate the absolute coordinate of the terrain point + var absoluteCurrentPointCoord = patch.PatchCoord * (heightmapSize - 1) + patch.ModifiedOffset + new Int2(x, z); + var currentPointCoordRelativeToModifiedArea = absoluteCurrentPointCoord - modifieedAreaMinCoord; + + // store height + var index = (currentPointCoordRelativeToModifiedArea.Y * totalModifiedSize.X) + currentPointCoordRelativeToModifiedArea.X; + modifiedHeights[index] = coordHeight; + } + } + } + + + ///// + /// Iterate through modified points and smooth now that we have height information for all necessary points + /// + + + for (int i = 0; i < affectedPatches.Count; i++) + { + var patch = affectedPatches[i]; + + var brushPosition = patch.Gizmo.CursorPosition; + var strength = Mathf.Saturate(patch.Strength); + + for (int z = 0; z < patch.ModifiedSize.Y; z++) + { + for (int x = 0; x < patch.ModifiedSize.X; x++) + { + // read height from current patch + var localCoordX = (x + patch.ModifiedOffset.X); + var localCoordY = (z + patch.ModifiedOffset.Y); + var coordHeight = patch.SourceHeightMap[(localCoordY * heightmapSize) + localCoordX]; + + // calculate the absolute coordinate of the terrain point + var absoluteCurrentPointCoord = patch.PatchCoord * (heightmapSize - 1) + patch.ModifiedOffset + new Int2(x, z); + var currentPointCoordRelativeToModifiedArea = absoluteCurrentPointCoord - modifieedAreaMinCoord; + + // calculate brush influence at the current position + var samplePositionLocal = patch.PatchPositionLocal + new Vector3(localCoordX * FlaxEngine.Terrain.UnitsPerVertex, coordHeight, localCoordY * FlaxEngine.Terrain.UnitsPerVertex); + Vector3.Transform(ref samplePositionLocal, ref patch.TerrainWorld, out Vector3 samplePositionWorld); + var paintAmount = patch.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength; + + if (paintAmount == 0) + { + patch.TempBuffer[z * patch.ModifiedSize.X + x] = coordHeight; + continue; + } + + // Record patch data before editing it + if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord)) + { + gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord); + } + // Sum the nearby values float smoothValue = 0; int smoothValueSamples = 0; - int minX = Math.Max(x - radius + p.ModifiedOffset.X, 0); - int minZ = Math.Max(z - radius + p.ModifiedOffset.Y, 0); - int maxX = Math.Min(x + radius + p.ModifiedOffset.X, max); - int maxZ = Math.Min(z + radius + p.ModifiedOffset.Y, max); + + var minX = Math.Max(0, currentPointCoordRelativeToModifiedArea.X - radius); + var maxX = Math.Min(totalModifiedSize.X - 1, currentPointCoordRelativeToModifiedArea.X + radius); + var minZ = Math.Max(0, currentPointCoordRelativeToModifiedArea.Y - radius); + var maxZ = Math.Min(totalModifiedSize.Y - 1, currentPointCoordRelativeToModifiedArea.Y + radius); + for (int dz = minZ; dz <= maxZ; dz++) { for (int dx = minX; dx <= maxX; dx++) { - var height = p.SourceHeightMap[dz * p.HeightmapSize + dx]; + var coordIndex = (dz * totalModifiedSize.X) + dx; + var height = modifiedHeights[coordIndex]; + smoothValue += height; smoothValueSamples++; } @@ -65,18 +159,26 @@ namespace FlaxEditor.Tools.Terrain.Sculpt smoothValue /= smoothValueSamples; // Blend between the height and smooth value - p.TempBuffer[z * p.ModifiedSize.X + x] = Mathf.Lerp(sourceHeight, smoothValue, paintAmount); - } - else - { - p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight; + var newHeight = Mathf.Lerp(coordHeight, smoothValue, paintAmount); + patch.TempBuffer[z * patch.ModifiedSize.X + x] = newHeight; } } - } - Profiler.EndEvent(); - // Update terrain patch - TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize); + // Update terrain patch + TerrainTools.ModifyHeightMap(patch.Terrain, ref patch.PatchCoord, patch.TempBuffer, ref patch.ModifiedOffset, ref patch.ModifiedSize); + } + + // Auto NavMesh rebuild + var brushBounds = gizmo.CursorBrushBounds; + gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds); + + Profiler.EndEvent(); + } + + /// + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) + { + // noop; unused } } }