// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEditor.Tools.Terrain.Brushes; using FlaxEngine; namespace FlaxEditor.Tools.Terrain.Sculpt { /// /// The base class for terran sculpt tool modes. /// [HideInEditor] public abstract class Mode { /// /// The options container for the terrain editing apply. /// public struct Options { /// /// If checked, modification apply method should be inverted. /// public bool Invert; /// /// The master strength parameter to apply when editing the terrain. /// public float Strength; /// /// The delta time (in seconds) for the terrain modification apply. Used to scale the strength. Adjusted to handle low-FPS. /// public float DeltaTime; } /// /// The tool strength (normalized to range 0-1). Defines the intensity of the sculpt operation to make it stronger or more subtle. /// [EditorOrder(0), Limit(0, 6, 0.01f), Tooltip("The tool strength (normalized to range 0-1). Defines the intensity of the sculpt operation to make it stronger or more subtle.")] public float Strength = 1.2f; /// /// Gets a value indicating whether this mode supports negative apply for terrain modification. /// public virtual bool SupportsNegativeApply => false; /// /// Gets a value indicating whether this mode modifies the terrain holes mask rather than heightmap. /// public virtual bool EditHoles => false; /// /// Gets all patches that will be affected by the brush /// /// The brush. /// The options. /// The gizmo. /// The terrain. public virtual unsafe 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 affectedPatches; if (options.Invert && SupportsNegativeApply) strength *= -1; // Prepare var chunkSize = terrain.ChunkSize; var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1; var heightmapLength = heightmapSize * heightmapSize; 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; // Get brush bounds in terrain local space var brushBounds = gizmo.CursorBrushBounds; terrain.GetLocalToWorldMatrix(out var terrainWorld); terrain.GetWorldToLocalMatrix(out var terrainInvWorld); BoundingBox.Transform(ref brushBounds, ref terrainInvWorld, out var brushBoundsLocal); // TODO: try caching brush weights before apply to reduce complexity and batch brush sampling // Process all the patches under the cursor for (int patchIndex = 0; patchIndex < gizmo.PatchesUnderCursor.Count; patchIndex++) { var patch = gizmo.PatchesUnderCursor[patchIndex]; var patchPositionLocal = new Vector3(patch.PatchCoord.X * patchSize, 0, patch.PatchCoord.Y * patchSize); // Transform brush bounds from local terrain space into local patch vertex space var brushBoundsPatchLocalMin = (brushBoundsLocal.Minimum - patchPositionLocal) * unitsPerVertexInv; var brushBoundsPatchLocalMax = (brushBoundsLocal.Maximum - patchPositionLocal) * unitsPerVertexInv; // Calculate patch heightmap area to modify by brush var brushPatchMin = new Int2((int)Math.Floor(brushBoundsPatchLocalMin.X), (int)Math.Floor(brushBoundsPatchLocalMin.Z)); var brushPatchMax = new Int2((int)Math.Ceiling(brushBoundsPatchLocalMax.X), (int)Math.Ceiling(brushBoundsPatchLocalMax.Z)); var modifiedOffset = brushPatchMin; var modifiedSize = brushPatchMax - brushPatchMin; // Expand the modification area by one vertex in each direction to ensure normal vectors are updated for edge cases, also clamp to prevent overflows if (modifiedOffset.X < 0) { modifiedSize.X += modifiedOffset.X; modifiedOffset.X = 0; } if (modifiedOffset.Y < 0) { modifiedSize.Y += modifiedOffset.Y; modifiedOffset.Y = 0; } modifiedSize.X = Mathf.Min(modifiedSize.X + 2, heightmapSize - modifiedOffset.X); modifiedSize.Y = Mathf.Min(modifiedSize.Y + 2, heightmapSize - modifiedOffset.Y); // Skip patch won't be modified at all if (modifiedSize.X <= 0 || modifiedSize.Y <= 0) continue; // Get the patch data (cached internally by the c++ core in editor) float* sourceHeights = EditHoles ? null : TerrainTools.GetHeightmapData(terrain, ref patch.PatchCoord); byte* sourceHoles = EditHoles ? TerrainTools.GetHolesMaskData(terrain, ref patch.PatchCoord) : null; if (sourceHeights == null && sourceHoles == null) throw new Exception("Cannot modify terrain. Loading heightmap failed. See log for more info."); ApplyParams p = new ApplyParams { 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, }; affectedPatches.Add(p); } return affectedPatches; } /// /// Applies the modification to the terrain. /// /// The brush. /// The options. /// The gizmo. /// The terrain. public 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 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. /// public unsafe struct ApplyParams { /// /// The brush. /// public Brush Brush; /// /// The options. /// public Options Options; /// /// The gizmo. /// public SculptTerrainGizmoMode Gizmo; /// /// The terrain. /// public FlaxEngine.Terrain Terrain; /// /// The patch coordinates. /// public Int2 PatchCoord; /// /// The modified offset. /// public Int2 ModifiedOffset; /// /// The modified size. /// public Int2 ModifiedSize; /// /// The final calculated strength of the effect to apply (can be negative for inverted terrain modification if is set). /// public float Strength; /// /// The temporary data buffer (for modified data). Has size of array of floats that has size of heightmap length. /// public float* TempBuffer; /// /// The source heightmap data buffer. May be null if modified is holes mask. /// public float* SourceHeightMap; /// /// The source holes mask data buffer. May be null if modified is. /// public byte* SourceHolesMask; /// /// The heightmap size (edge). /// public int HeightmapSize; /// /// The patch position in terrain local-space. /// public Vector3 PatchPositionLocal; /// /// The terrain local-to-world matrix. /// public Matrix TerrainWorld; } /// /// Applies the modification to the terrain. /// /// The parameters to use. public abstract void ApplyBrushToPatch(ref ApplyParams p); } }