// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using FlaxEditor.Tools.Terrain.Brushes; using FlaxEngine; namespace FlaxEditor.Tools.Terrain.Paint { /// /// The base class for terran paint 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 paint operation to make it stronger or more subtle. /// [EditorOrder(0), Limit(0, 10, 0.01f), Tooltip("The tool strength (normalized to range 0-1). Defines the intensity of the paint operation to make it stronger or more subtle.")] public float Strength = 1.0f; /// /// Gets a value indicating whether this mode supports negative apply for terrain modification. /// public virtual bool SupportsNegativeApply => false; /// /// Gets the index of the active splatmap texture to modify by the tool. It must be equal or higher than zero bu less than . /// public abstract int ActiveSplatmapIndex { get; } /// /// Applies the modification to the terrain. /// /// The brush. /// The options. /// The gizmo. /// The terrain. public unsafe void Apply(Brush brush, ref Options options, PaintTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain) { // Combine final apply strength float strength = Strength * options.Strength * options.DeltaTime * 10.0f; if (strength <= 0.0f) return; if (options.Invert && SupportsNegativeApply) strength *= -1; // Prepare var splatmapIndex = ActiveSplatmapIndex; 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 = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes).ToPointer(); var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex; ApplyParams p = new ApplyParams { Terrain = terrain, Brush = brush, Gizmo = gizmo, Options = options, Strength = strength, SplatmapIndex = splatmapIndex, HeightmapSize = heightmapSize, TempBuffer = tempBuffer, }; // Get brush bounds in terrain local space var brushBounds = gizmo.CursorBrushBounds; terrain.GetLocalToWorldMatrix(out p.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; // 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, heightmapSize - modifiedOffset.X); modifiedSize.Y = Mathf.Min(modifiedSize.Y, 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) var sourceData = TerrainTools.GetSplatMapData(terrain, ref patch.PatchCoord, splatmapIndex); if (sourceData == null) throw new Exception("Cannot modify terrain. Loading splatmap failed. See log for more info."); // Record patch data before editing it if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord)) { gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord, splatmapIndex); } // Apply modification p.ModifiedOffset = modifiedOffset; p.ModifiedSize = modifiedSize; p.PatchCoord = patch.PatchCoord; p.PatchPositionLocal = patchPositionLocal; p.SourceData = sourceData; Apply(ref p); } } /// /// The mode apply parameters. /// public unsafe struct ApplyParams { /// /// The brush. /// public Brush Brush; /// /// The options. /// public Options Options; /// /// The gizmo. /// public PaintTerrainGizmoMode 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 splatmap texture index. /// public int SplatmapIndex; /// /// The temporary data buffer (for modified data). /// public Color32* TempBuffer; /// /// The source data buffer. /// public Color32* SourceData; /// /// 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 Apply(ref ApplyParams p); } }