Merge branch 'feature/817-terrain-smoothing' of https://github.com/envision3d/FlaxEngine into envision3d-feature/817-terrain-smoothing

This commit is contained in:
Wojtek Figat
2024-02-06 16:34:15 +01:00
6 changed files with 214 additions and 67 deletions

View File

@@ -18,7 +18,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public float TargetHeight = 0.0f;
/// <inheritdoc />
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)

View File

@@ -26,7 +26,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
}
/// <inheritdoc />
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;

View File

@@ -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;
/// <summary>
/// Applies the modification to the terrain.
/// Gets all patches that will be affected by the brush
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="options">The options.</param>
/// <param name="gizmo">The gizmo.</param>
/// <param name="terrain">The terrain.</param>
public unsafe void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
/// <param name="brush"></param>
/// <param name="options"></param>
/// <param name="gizmo"></param>
/// <param name="terrain"></param>
public unsafe virtual List<ApplyParams> GetAffectedPatches(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
{
List<ApplyParams> 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;
}
/// <summary>
/// Applies the modification to the terrain.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="options">The options.</param>
/// <param name="gizmo">The gizmo.</param>
/// <param name="terrain">The terrain.</param>
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);
}
/// <summary>
/// Applies the brush to all affected patches
/// </summary>
/// <param name="gizmo"></param>
/// <param name="affectedPatches"></param>
public unsafe virtual void ApplyBrush(SculptTerrainGizmoMode gizmo, List<ApplyParams> 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);
}
}
/// <summary>
/// The mode apply parameters.
/// </summary>
@@ -231,6 +276,6 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
/// Applies the modification to the terrain.
/// </summary>
/// <param name="p">The parameters to use.</param>
public abstract void Apply(ref ApplyParams p);
public abstract void ApplyBrushToPatch(ref ApplyParams p);
}
}

View File

@@ -29,7 +29,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public override bool SupportsNegativeApply => true;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
// Prepare
var brushPosition = p.Gizmo.CursorPosition;

View File

@@ -15,7 +15,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public override bool SupportsNegativeApply => true;
/// <inheritdoc />
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;

View File

@@ -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;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
public override unsafe void ApplyBrush(SculptTerrainGizmoMode gizmo, List<ApplyParams> 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();
}
/// <inheritdoc />
public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
// noop; unused
}
}
}