You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Brushes
{
/// <summary>
/// Terrain sculpture or paint brush logic descriptor.
/// </summary>
[HideInEditor]
public abstract class Brush
{
/// <summary>
/// The cached material instance for the brush usage.
/// </summary>
protected MaterialInstance _material;
/// <summary>
/// The brush size (in world units). Within this area, the brush will have at least some effect.
/// </summary>
[EditorOrder(0), Limit(0.0f, 1000000.0f, 10.0f), Tooltip("The brush size (in world units). Within this area, the brush will have at least some effect.")]
public float Size = 4000.0f;
/// <summary>
/// Gets the brush material for the terrain chunk rendering. It must have domain set to Terrain. Setup material parameters within this call.
/// </summary>
/// <param name="position">The world-space brush position.</param>
/// <param name="color">The brush position.</param>
/// <returns>The ready to render material for terrain chunks overlay on top of the terrain.</returns>
public abstract MaterialInstance GetBrushMaterial(ref Vector3 position, ref Color color);
/// <summary>
/// Loads the brush material from the internal location. It's later cached by the object and reused.
/// </summary>
/// <param name="internalPath">The brush material path (for in-build editor brushes).</param>
/// <returns>The brush material instance or null if cannot load or missing.</returns>
protected MaterialInstance CacheMaterial(string internalPath)
{
if (!_material)
{
var material = FlaxEngine.Content.LoadAsyncInternal<Material>(internalPath);
material.WaitForLoaded();
_material = material.CreateVirtualInstance();
}
return _material;
}
/// <summary>
/// Samples the brush intensity at the specified position.
/// </summary>
/// <param name="brushPosition">The brush center position (world-space).</param>
/// <param name="samplePosition">The sample position (world-space).</param>
/// <returns>The sampled value. Normalized to range 0-1 as an intensity to apply.</returns>
public abstract float Sample(ref Vector3 brushPosition, ref Vector3 samplePosition);
}
}

View File

@@ -0,0 +1,135 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Brushes
{
/// <summary>
/// Terrain brush that has circle shape and uses radial falloff.
/// </summary>
/// <seealso cref="FlaxEditor.Tools.Terrain.Brushes.Brush" />
[HideInEditor]
public class CircleBrush : Brush
{
/// <summary>
/// Circle brush falloff types.
/// </summary>
public enum FalloffTypes
{
/// <summary>
/// A linear falloff that has been smoothed to round off the sharp edges where the falloff begins and ends.
/// </summary>
Smooth = 0,
/// <summary>
/// A sharp linear falloff, without rounded edges.
/// </summary>
Linear = 1,
/// <summary>
/// A half-ellipsoid-shaped falloff that begins smoothly and ends sharply.
/// </summary>
Spherical = 2,
/// <summary>
/// A falloff with an abrupt start and a smooth ellipsoidal end. The opposite of the Sphere falloff.
/// </summary>
Tip = 3,
}
/// <summary>
/// The brush falloff that defines the percentage from the brush's extents where the falloff should begin. Essentially, this describes how hard the brush's edges are. A falloff of 0 means the brush will have full effect throughout with hard edges. A falloff of 1 means the brush will only have full effect at its center, and the effect will be reduced throughout its entire area to the edge.
/// </summary>
[EditorOrder(10), Limit(0, 1, 0.01f), Tooltip("The brush falloff that defines the percentage from the brush's extents where the falloff should begin.")]
public float Falloff = 0.5f;
/// <summary>
/// The brush falloff type. Defines circle brush falloff mode.
/// </summary>
[EditorOrder(20), Tooltip("The brush falloff type. Defines circle brush falloff mode.")]
public FalloffTypes FalloffType = FalloffTypes.Smooth;
private delegate float CalculateFalloffDelegate(float distance, float radius, float falloff);
private float CalculateFalloff_Smooth(float distance, float radius, float falloff)
{
// Smooth-step linear falloff
float alpha = CalculateFalloff_Linear(distance, radius, falloff);
return alpha * alpha * (3 - 2 * alpha);
}
private float CalculateFalloff_Linear(float distance, float radius, float falloff)
{
return distance < radius ? 1.0f : falloff > 0.0f ? Mathf.Max(0.0f, 1.0f - (distance - radius) / falloff) : 0.0f;
}
private float CalculateFalloff_Spherical(float distance, float radius, float falloff)
{
if (distance <= radius)
{
return 1.0f;
}
if (distance > radius + falloff)
{
return 0.0f;
}
// Elliptical falloff
return Mathf.Sqrt(1.0f - Mathf.Square((distance - radius) / falloff));
}
private float CalculateFalloff_Tip(float distance, float radius, float falloff)
{
if (distance <= radius)
{
return 1.0f;
}
if (distance > radius + falloff)
{
return 0.0f;
}
// Inverse elliptical falloff
return 1.0f - Mathf.Sqrt(1.0f - Mathf.Square((falloff + radius - distance) / falloff));
}
/// <inheritdoc />
public override MaterialInstance GetBrushMaterial(ref Vector3 position, ref Color color)
{
var material = CacheMaterial(EditorAssets.TerrainCircleBrushMaterial);
if (material)
{
// Data 0: XYZ: position, W: radius
// Data 1: X: falloff, Y: type
float halfSize = Size * 0.5f;
float falloff = halfSize * Falloff;
float radius = halfSize - falloff;
material.SetParameterValue("Color", color);
material.SetParameterValue("BrushData0", new Vector4(position, radius));
material.SetParameterValue("BrushData1", new Vector4(falloff, (float)FalloffType, 0, 0));
}
return material;
}
/// <inheritdoc />
public override float Sample(ref Vector3 brushPosition, ref Vector3 samplePosition)
{
Vector3.DistanceXZ(ref brushPosition, ref samplePosition, out var distanceXZ);
float halfSize = Size * 0.5f;
float falloff = halfSize * Falloff;
float radius = halfSize - falloff;
switch (FalloffType)
{
case FalloffTypes.Smooth: return CalculateFalloff_Smooth(distanceXZ, radius, falloff);
case FalloffTypes.Linear: return CalculateFalloff_Linear(distanceXZ, radius, falloff);
case FalloffTypes.Spherical: return CalculateFalloff_Spherical(distanceXZ, radius, falloff);
case FalloffTypes.Tip: return CalculateFalloff_Tip(distanceXZ, radius, falloff);
default: throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -0,0 +1,187 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI.Tabs;
using FlaxEditor.Modules;
using FlaxEditor.SceneGraph.Actors;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Terrain carving tab. Supports different modes for terrain editing including: carving, painting and managing tools.
/// </summary>
/// <seealso cref="Tab" />
[HideInEditor]
public class CarveTab : Tab
{
private readonly Tabs _modes;
private readonly ContainerControl _noTerrainPanel;
/// <summary>
/// The editor instance.
/// </summary>
public readonly Editor Editor;
/// <summary>
/// The cached selected terrain. It's synchronized with <see cref="SceneEditingModule.Selection"/>.
/// </summary>
public FlaxEngine.Terrain SelectedTerrain;
/// <summary>
/// Occurs when selected terrain gets changed (to a different value).
/// </summary>
public event Action SelectedTerrainChanged;
/// <summary>
/// The sculpt tab;
/// </summary>
public SculptTab Sculpt;
/// <summary>
/// The paint tab;
/// </summary>
public PaintTab Paint;
/// <summary>
/// The edit tab;
/// </summary>
public EditTab Edit;
/// <summary>
/// Initializes a new instance of the <see cref="CarveTab"/> class.
/// </summary>
/// <param name="icon">The icon.</param>
/// <param name="editor">The editor instance.</param>
public CarveTab(SpriteHandle icon, Editor editor)
: base(string.Empty, icon)
{
Editor = editor;
Editor.SceneEditing.SelectionChanged += OnSelectionChanged;
Selected += OnSelected;
_modes = new Tabs
{
Orientation = Orientation.Vertical,
UseScroll = true,
Offsets = Margin.Zero,
AnchorPreset = AnchorPresets.StretchAll,
TabsSize = new Vector2(50, 32),
Parent = this
};
// Init tool modes
InitSculptMode();
InitPaintMode();
InitEditMode();
_modes.SelectedTabIndex = 0;
_noTerrainPanel = new ContainerControl
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
BackgroundColor = Style.Current.Background,
Parent = this
};
var noTerrainLabel = new Label
{
Text = "Select terrain to edit\nor\n\n\n\n",
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = _noTerrainPanel
};
var noTerrainButton = new Button
{
Text = "Create new terrain",
AnchorPreset = AnchorPresets.MiddleCenter,
Offsets = new Margin(-60, 120, -12, 24),
Parent = _noTerrainPanel
};
noTerrainButton.Clicked += OnCreateNewTerrainClicked;
}
private void OnSelected(Tab tab)
{
// Auto select first terrain actor to make usage easier
if (Editor.SceneEditing.SelectionCount == 1 && Editor.SceneEditing.Selection[0] is SceneGraph.ActorNode actorNode && actorNode.Actor is FlaxEngine.Terrain)
return;
var actor = Level.FindActor<FlaxEngine.Terrain>();
if (actor)
{
Editor.SceneEditing.Select(actor);
}
}
private void OnCreateNewTerrainClicked()
{
Editor.UI.CreateTerrain();
}
private void OnSelectionChanged()
{
var terrainNode = Editor.SceneEditing.SelectionCount > 0 ? Editor.SceneEditing.Selection[0] as TerrainNode : null;
var terrain = terrainNode?.Actor as FlaxEngine.Terrain;
if (terrain != SelectedTerrain)
{
SelectedTerrain = terrain;
SelectedTerrainChanged?.Invoke();
}
_noTerrainPanel.Visible = terrain == null;
}
private void InitSculptMode()
{
var tab = _modes.AddTab(Sculpt = new SculptTab(this, Editor.Windows.EditWin.Viewport.SculptTerrainGizmo));
tab.Selected += OnTabSelected;
}
private void InitPaintMode()
{
var tab = _modes.AddTab(Paint = new PaintTab(this, Editor.Windows.EditWin.Viewport.PaintTerrainGizmo));
tab.Selected += OnTabSelected;
}
private void InitEditMode()
{
var tab = _modes.AddTab(Edit = new EditTab(this, Editor.Windows.EditWin.Viewport.EditTerrainGizmo));
tab.Selected += OnTabSelected;
}
/// <inheritdoc />
public override void OnSelected()
{
base.OnSelected();
UpdateGizmoMode();
}
private void OnTabSelected(Tab tab)
{
UpdateGizmoMode();
}
/// <summary>
/// Updates the active viewport gizmo mode based on the current mode.
/// </summary>
private void UpdateGizmoMode()
{
switch (_modes.SelectedTabIndex)
{
case 0:
Editor.Windows.EditWin.Viewport.SetActiveMode<SculptTerrainGizmoMode>();
break;
case 1:
Editor.Windows.EditWin.Viewport.SetActiveMode<PaintTerrainGizmoMode>();
break;
case 2:
Editor.Windows.EditWin.Viewport.SetActiveMode<EditTerrainGizmoMode>();
break;
default: throw new IndexOutOfRangeException("Invalid carve tab mode.");
}
}
}
}

View File

@@ -0,0 +1,254 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.ComponentModel;
using FlaxEditor.CustomEditors;
using FlaxEditor.GUI.Dialogs;
using FlaxEngine;
using FlaxEngine.GUI;
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
// ReSharper disable UnusedMember.Global
#pragma warning disable 649
#pragma warning disable 414
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Terrain creator dialog. Allows user to specify initial terrain properties perform proper setup.
/// </summary>
/// <seealso cref="FlaxEditor.GUI.Dialogs.Dialog" />
sealed class CreateTerrainDialog : Dialog
{
private enum ChunkSizes
{
_31 = 31,
_63 = 63,
_127 = 127,
_255 = 255,
}
private class Options
{
[EditorOrder(100), EditorDisplay("Layout", "Number Of Patches"), DefaultValue(typeof(Int2), "1,1"), Limit(0, 512), Tooltip("Amount of terrain patches in each direction (X and Z). Each terrain patch contains a grid of 16 chunks. Patches can be later added or removed from terrain using a terrain editor tool.")]
public Int2 NumberOfPatches = new Int2(1, 1);
[EditorOrder(110), EditorDisplay("Layout"), DefaultValue(ChunkSizes._127), Tooltip("The size of the chunk (amount of quads per edge for the highest LOD). Must be power of two minus one (eg. 63).")]
public ChunkSizes ChunkSize = ChunkSizes._127;
[EditorOrder(120), EditorDisplay("Layout", "LOD Count"), DefaultValue(6), Limit(1, FlaxEngine.Terrain.MaxLODs), Tooltip("The maximum Level Of Details count. The actual amount of LODs may be lower due to provided chunk size (each LOD has 4 times less quads).")]
public int LODCount = 6;
[EditorOrder(130), EditorDisplay("Layout"), DefaultValue(null), Tooltip("The default material used for terrain rendering (chunks can override this). It must have Domain set to terrain.")]
public MaterialBase Material;
[EditorOrder(200), EditorDisplay("Collision"), DefaultValue(null), AssetReference(typeof(PhysicalMaterial), true), Tooltip("Terrain default physical material used to define the collider physical properties.")]
public JsonAsset PhysicalMaterial;
[EditorOrder(210), EditorDisplay("Collision", "Collision LOD"), DefaultValue(-1), Limit(-1, 100, 0.1f), Tooltip("Terrain geometry LOD index used for collision.")]
public int CollisionLOD = -1;
[EditorOrder(300), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom heightmap texture to import. Used as a source for height field values (from channel Red).")]
public Texture Heightmap;
[EditorOrder(310), EditorDisplay("Import Data"), DefaultValue(5000.0f), Tooltip("Custom heightmap texture values scale. Applied to adjust the normalized heightmap values into the world units.")]
public float HeightmapScale = 5000.0f;
[EditorOrder(320), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers compositing.")]
public Texture Splatmap1;
[EditorOrder(330), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers compositing.")]
public Texture Splatmap2;
[EditorOrder(400), EditorDisplay("Transform", "Position"), DefaultValue(typeof(Vector3), "0,0,0"), Tooltip("Position of the terrain (importer offset it on the Y axis.)")]
public Vector3 Position = new Vector3(0.0f, 0.0f, 0.0f);
[EditorOrder(410), EditorDisplay("Transform", "Rotation"), DefaultValue(typeof(Quaternion), "0,0,0,1"), Tooltip("Orientation of the terrain")]
public Quaternion Orientation = Quaternion.Identity;
[EditorOrder(420), EditorDisplay("Transform", "Scale"), DefaultValue(typeof(Vector3), "1,1,1"), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("Scale of the terrain")]
public Vector3 Scale = Vector3.One;
}
private readonly Options _options = new Options();
private bool _isDone;
private bool _isWorking;
private FlaxEngine.Terrain _terrain;
private CustomEditorPresenter _editor;
/// <summary>
/// Initializes a new instance of the <see cref="CreateTerrainDialog"/> class.
/// </summary>
public CreateTerrainDialog()
: base("Create terrain")
{
const float TotalWidth = 450;
const float EditorHeight = 600;
Width = TotalWidth;
// Header and help description
var headerLabel = new Label
{
Text = "New Terrain",
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = new Margin(0, 0, 0, 40),
Parent = this,
Font = new FontReference(Style.Current.FontTitle)
};
var infoLabel = new Label
{
Text = "Specify options for new terrain.\nIt will be added to the first opened scene.\nMany of the following settings can be adjusted later.\nYou can also create terrain at runtime from code.",
HorizontalAlignment = TextAlignment.Near,
Margin = new Margin(7),
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = new Margin(10, -20, 45, 70),
Parent = this
};
// Buttons
const float ButtonsWidth = 60;
const float ButtonsHeight = 24;
const float ButtonsMargin = 8;
var importButton = new Button
{
Text = "Create",
AnchorPreset = AnchorPresets.BottomRight,
Offsets = new Margin(-ButtonsWidth - ButtonsMargin, ButtonsWidth, -ButtonsHeight - ButtonsMargin, ButtonsHeight),
Parent = this
};
importButton.Clicked += OnCreate;
var cancelButton = new Button
{
Text = "Cancel",
AnchorPreset = AnchorPresets.BottomRight,
Offsets = new Margin(-ButtonsWidth - ButtonsMargin - ButtonsWidth - ButtonsMargin, ButtonsWidth, -ButtonsHeight - ButtonsMargin, ButtonsHeight),
Parent = this
};
cancelButton.Clicked += OnCancel;
// Settings editor
var settingsEditor = new CustomEditorPresenter(null);
settingsEditor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
settingsEditor.Panel.Offsets = new Margin(2, 2, infoLabel.Bottom + 2, EditorHeight);
settingsEditor.Panel.Parent = this;
_editor = settingsEditor;
_dialogSize = new Vector2(TotalWidth, settingsEditor.Panel.Bottom);
settingsEditor.Select(_options);
}
private void OnCreate()
{
if (_isWorking)
return;
var scene = Level.GetScene(0);
if (scene == null)
throw new InvalidOperationException("No scene found to add terrain to it!");
// Create terrain object and setup some options
var terrain = new FlaxEngine.Terrain();
terrain.Setup(_options.LODCount, (int)_options.ChunkSize);
terrain.Transform = new Transform(_options.Position, _options.Orientation, _options.Scale);
terrain.Material = _options.Material;
terrain.PhysicalMaterial = _options.PhysicalMaterial;
terrain.CollisionLOD = _options.CollisionLOD;
if (_options.Heightmap)
terrain.Position -= new Vector3(0, _options.HeightmapScale * 0.5f, 0);
// Add to scene (even if generation fails user gets a terrain in the scene)
terrain.Parent = scene;
Editor.Instance.Scene.MarkSceneEdited(scene);
// Show loading label
var label = new Label
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Text = "Generating terrain...",
BackgroundColor = Style.Current.ForegroundDisabled,
Parent = this,
};
// Lock UI
_editor.Panel.Enabled = false;
_isWorking = true;
_isDone = false;
// Start async work
_terrain = terrain;
var thread = new System.Threading.Thread(Generate);
thread.Name = "Terrain Generator";
thread.Start();
}
private void Generate()
{
_isWorking = true;
_isDone = false;
// Call tool to generate the terrain patches from the input data
if (TerrainTools.GenerateTerrain(_terrain, ref _options.NumberOfPatches, _options.Heightmap, _options.HeightmapScale, _options.Splatmap1, _options.Splatmap2))
{
Editor.LogError("Failed to generate terrain. See log for more info.");
}
_isWorking = false;
_isDone = true;
}
private void OnCancel()
{
if (_isWorking)
return;
Close(DialogResult.Cancel);
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
if (_isDone)
{
Editor.Instance.SceneEditing.Select(_terrain);
_terrain = null;
_isDone = false;
Close(DialogResult.OK);
return;
}
base.Update(deltaTime);
}
/// <inheritdoc />
protected override bool CanCloseWindow(ClosingReason reason)
{
if (_isWorking && reason == ClosingReason.User)
return false;
return base.CanCloseWindow(reason);
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (_isWorking)
return true;
switch (key)
{
case KeyboardKeys.Escape:
OnCancel();
return true;
case KeyboardKeys.Return:
OnCreate();
return true;
}
return base.OnKeyDown(key);
}
}
}

View File

@@ -0,0 +1,405 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Tabs;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Carve tab related to terrain editing. Allows to pick a terrain patch and remove it or add new patches. Can be used to modify selected chunk properties.
/// </summary>
/// <seealso cref="Tab" />
[HideInEditor]
public class EditTab : Tab
{
/// <summary>
/// The parent carve tab.
/// </summary>
public readonly CarveTab CarveTab;
/// <summary>
/// The related edit terrain gizmo.
/// </summary>
public readonly EditTerrainGizmoMode Gizmo;
private readonly ComboBox _modeComboBox;
private readonly Label _selectionInfoLabel;
private readonly Button _deletePatchButton;
private readonly Button _exportTerrainButton;
private readonly ContainerControl _chunkProperties;
private readonly AssetPicker _chunkOverrideMaterial;
private bool _isUpdatingUI;
/// <summary>
/// Initializes a new instance of the <see cref="EditTab"/> class.
/// </summary>
/// <param name="tab">The parent tab.</param>
/// <param name="gizmo">The related gizmo.</param>
public EditTab(CarveTab tab, EditTerrainGizmoMode gizmo)
: base("Edit")
{
CarveTab = tab;
Gizmo = gizmo;
CarveTab.SelectedTerrainChanged += OnSelectionChanged;
Gizmo.SelectedChunkCoordChanged += OnSelectionChanged;
// Main panel
var panel = new Panel(ScrollBars.Both)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
// Mode
var modeLabel = new Label(4, 4, 40, 18)
{
HorizontalAlignment = TextAlignment.Near,
Text = "Mode:",
Parent = panel,
};
_modeComboBox = new ComboBox(modeLabel.Right + 4, 4, 110)
{
Parent = panel,
};
_modeComboBox.AddItem("Edit Chunk");
_modeComboBox.AddItem("Add Patch");
_modeComboBox.AddItem("Remove Patch");
_modeComboBox.AddItem("Export terrain");
_modeComboBox.SelectedIndex = 0;
_modeComboBox.SelectedIndexChanged += (combobox) => Gizmo.EditMode = (EditTerrainGizmoMode.Modes)combobox.SelectedIndex;
Gizmo.ModeChanged += OnGizmoModeChanged;
// Info
_selectionInfoLabel = new Label(modeLabel.X, modeLabel.Bottom + 4, 40, 18 * 3)
{
VerticalAlignment = TextAlignment.Near,
HorizontalAlignment = TextAlignment.Near,
Parent = panel,
};
// Chunk Properties
_chunkProperties = new Panel(ScrollBars.None)
{
Location = new Vector2(_selectionInfoLabel.X, _selectionInfoLabel.Bottom + 4),
Parent = panel,
};
var chunkOverrideMaterialLabel = new Label(0, 0, 90, 64)
{
HorizontalAlignment = TextAlignment.Near,
Text = "Override Material",
Parent = _chunkProperties,
};
_chunkOverrideMaterial = new AssetPicker(new ScriptType(typeof(MaterialBase)), new Vector2(chunkOverrideMaterialLabel.Right + 4, 0))
{
Width = 300.0f,
Parent = _chunkProperties,
};
_chunkOverrideMaterial.SelectedItemChanged += OnSelectedChunkOverrideMaterialChanged;
_chunkProperties.Size = new Vector2(_chunkOverrideMaterial.Right + 4, _chunkOverrideMaterial.Bottom + 4);
// Delete patch
_deletePatchButton = new Button(_selectionInfoLabel.X, _selectionInfoLabel.Bottom + 4)
{
Text = "Delete Patch",
Parent = panel,
};
_deletePatchButton.Clicked += OnDeletePatchButtonClicked;
// Export terrain
_exportTerrainButton = new Button(_selectionInfoLabel.X, _selectionInfoLabel.Bottom + 4)
{
Text = "Export terrain",
Parent = panel,
};
_exportTerrainButton.Clicked += OnExportTerrainButtonClicked;
// Update UI to match the current state
OnSelectionChanged();
OnGizmoModeChanged();
}
[Serializable]
private class DeletePatchAction : IUndoAction
{
[Serialize]
private Guid _terrainId;
[Serialize]
private Int2 _patchCoord;
[Serialize]
private string _data;
/// <inheritdoc />
public string ActionString => "Delete terrain patch";
public DeletePatchAction(FlaxEngine.Terrain terrain, ref Int2 patchCoord)
{
if (terrain == null)
throw new ArgumentException(nameof(terrain));
_terrainId = terrain.ID;
_patchCoord = patchCoord;
_data = TerrainTools.SerializePatch(terrain, ref patchCoord);
}
/// <inheritdoc />
public void Do()
{
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
if (terrain == null)
{
Editor.LogError("Missing terrain actor.");
return;
}
terrain.GetPatchBounds(terrain.GetPatchIndex(ref _patchCoord), out var patchBounds);
terrain.RemovePatch(ref _patchCoord);
OnPatchEdit(terrain, ref patchBounds);
}
/// <inheritdoc />
public void Undo()
{
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
if (terrain == null)
{
Editor.LogError("Missing terrain actor.");
return;
}
terrain.AddPatch(ref _patchCoord);
TerrainTools.DeserializePatch(terrain, ref _patchCoord, _data);
terrain.GetPatchBounds(terrain.GetPatchIndex(ref _patchCoord), out var patchBounds);
OnPatchEdit(terrain, ref patchBounds);
}
private void OnPatchEdit(FlaxEngine.Terrain terrain, ref BoundingBox patchBounds)
{
Editor.Instance.Scene.MarkSceneEdited(terrain.Scene);
var editorOptions = Editor.Instance.Options.Options;
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
// Auto NavMesh rebuild
if (!isPlayMode && editorOptions.General.AutoRebuildNavMesh)
{
if (terrain.Scene && (terrain.StaticFlags & StaticFlags.Navigation) == StaticFlags.Navigation)
{
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
/// <inheritdoc />
public void Dispose()
{
_data = null;
}
}
private void OnDeletePatchButtonClicked()
{
if (_isUpdatingUI)
return;
var patchCoord = Gizmo.SelectedPatchCoord;
if (!CarveTab.SelectedTerrain.HasPatch(ref patchCoord))
return;
var action = new DeletePatchAction(CarveTab.SelectedTerrain, ref patchCoord);
action.Do();
CarveTab.Editor.Undo.AddAction(action);
}
[Serializable]
private class EditChunkMaterialAction : IUndoAction
{
[Serialize]
private Guid _terrainId;
[Serialize]
private Int2 _patchCoord;
[Serialize]
private Int2 _chunkCoord;
[Serialize]
private Guid _beforeMaterial;
[Serialize]
private Guid _afterMaterial;
/// <inheritdoc />
public string ActionString => "Edit terrain chunk material";
public EditChunkMaterialAction(FlaxEngine.Terrain terrain, ref Int2 patchCoord, ref Int2 chunkCoord, MaterialBase toSet)
{
if (terrain == null)
throw new ArgumentException(nameof(terrain));
_terrainId = terrain.ID;
_patchCoord = patchCoord;
_chunkCoord = chunkCoord;
_beforeMaterial = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord)?.ID ?? Guid.Empty;
_afterMaterial = toSet?.ID ?? Guid.Empty;
}
/// <inheritdoc />
public void Do()
{
Set(ref _afterMaterial);
}
/// <inheritdoc />
public void Undo()
{
Set(ref _beforeMaterial);
}
/// <inheritdoc />
public void Dispose()
{
}
private void Set(ref Guid id)
{
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
if (terrain == null)
{
Editor.LogError("Missing terrain actor.");
return;
}
terrain.SetChunkOverrideMaterial(ref _patchCoord, ref _chunkCoord, FlaxEngine.Content.LoadAsync<MaterialBase>(id));
Editor.Instance.Scene.MarkSceneEdited(terrain.Scene);
}
}
private void OnSelectedChunkOverrideMaterialChanged()
{
if (_isUpdatingUI)
return;
var patchCoord = Gizmo.SelectedPatchCoord;
var chunkCoord = Gizmo.SelectedChunkCoord;
var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.SelectedAsset as MaterialBase);
action.Do();
CarveTab.Editor.Undo.AddAction(action);
}
private void OnExportTerrainButtonClicked()
{
if (_isUpdatingUI)
return;
if (FileSystem.ShowBrowseFolderDialog(null, null, "Select the output folder", out var outputFolder))
return;
TerrainTools.ExportTerrain(CarveTab.SelectedTerrain, outputFolder);
}
private void OnSelectionChanged()
{
var terrain = CarveTab.SelectedTerrain;
if (terrain == null)
{
_selectionInfoLabel.Text = "Select a terrain to modify its properties.";
_chunkProperties.Visible = false;
_deletePatchButton.Visible = false;
_exportTerrainButton.Visible = false;
}
else
{
var patchCoord = Gizmo.SelectedPatchCoord;
switch (Gizmo.EditMode)
{
case EditTerrainGizmoMode.Modes.Edit:
{
var chunkCoord = Gizmo.SelectedChunkCoord;
_selectionInfoLabel.Text = string.Format(
"Selected terrain: {0}\nPatch: {1}x{2}\nChunk: {3}x{4}",
terrain.Name,
patchCoord.X, patchCoord.Y,
chunkCoord.X, chunkCoord.Y
);
_chunkProperties.Visible = true;
_deletePatchButton.Visible = false;
_exportTerrainButton.Visible = false;
_isUpdatingUI = true;
if (terrain.HasPatch(ref patchCoord))
{
_chunkOverrideMaterial.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord);
_chunkOverrideMaterial.Enabled = true;
}
else
{
_chunkOverrideMaterial.SelectedAsset = null;
_chunkOverrideMaterial.Enabled = false;
}
_isUpdatingUI = false;
break;
}
case EditTerrainGizmoMode.Modes.Add:
{
if (terrain.HasPatch(ref patchCoord))
{
_selectionInfoLabel.Text = string.Format(
"Selected terrain: {0}\nMove mouse cursor at location without a patch.",
terrain.Name
);
}
else
{
_selectionInfoLabel.Text = string.Format(
"Selected terrain: {0}\nPatch to add: {1}x{2}\nTo add a new patch press the left mouse button.",
terrain.Name,
patchCoord.X, patchCoord.Y
);
}
_chunkProperties.Visible = false;
_deletePatchButton.Visible = false;
_exportTerrainButton.Visible = false;
break;
}
case EditTerrainGizmoMode.Modes.Remove:
{
_selectionInfoLabel.Text = string.Format(
"Selected terrain: {0}\nPatch: {1}x{2}",
terrain.Name,
patchCoord.X, patchCoord.Y
);
_chunkProperties.Visible = false;
_deletePatchButton.Visible = true;
_exportTerrainButton.Visible = false;
break;
}
case EditTerrainGizmoMode.Modes.Export:
{
_selectionInfoLabel.Text = string.Format(
"Selected terrain: {0}",
terrain.Name
);
_chunkProperties.Visible = false;
_deletePatchButton.Visible = false;
_exportTerrainButton.Visible = true;
break;
}
}
}
}
private void OnGizmoModeChanged()
{
_modeComboBox.SelectedIndex = (int)Gizmo.EditMode;
OnSelectionChanged();
}
}
}

View File

@@ -0,0 +1,277 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEngine;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Gizmo for picking terrain chunks and patches. Managed by the <see cref="EditTerrainGizmoMode"/>.
/// </summary>
/// <seealso cref="FlaxEditor.Gizmo.GizmoBase" />
[HideInEditor]
public sealed class EditTerrainGizmo : GizmoBase
{
private Model _planeModel;
private MaterialBase _highlightMaterial;
private MaterialBase _highlightTerrainMaterial;
/// <summary>
/// The parent mode.
/// </summary>
public readonly EditTerrainGizmoMode Mode;
/// <summary>
/// Initializes a new instance of the <see cref="EditTerrainGizmo"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="mode">The mode.</param>
public EditTerrainGizmo(IGizmoOwner owner, EditTerrainGizmoMode mode)
: base(owner)
{
Mode = mode;
}
private void GetAssets()
{
if (_planeModel)
return;
_planeModel = FlaxEngine.Content.LoadAsyncInternal<Model>("Editor/Primitives/Plane");
_highlightTerrainMaterial = FlaxEngine.Content.LoadAsyncInternal<MaterialBase>(EditorAssets.HighlightTerrainMaterial);
_highlightMaterial = EditorAssets.Cache.HighlightMaterialInstance;
}
private FlaxEngine.Terrain SelectedTerrain
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
return (FlaxEngine.Terrain)terrainNode?.Actor;
}
}
/// <inheritdoc />
public override void Draw(ref RenderContext renderContext)
{
if (!IsActive)
return;
var terrain = SelectedTerrain;
if (!terrain)
return;
GetAssets();
switch (Mode.EditMode)
{
case EditTerrainGizmoMode.Modes.Edit:
{
// Highlight selected chunk
var patchCoord = Mode.SelectedPatchCoord;
if (terrain.HasPatch(ref patchCoord))
{
var chunkCoord = Mode.SelectedChunkCoord;
terrain.DrawChunk(ref renderContext, ref patchCoord, ref chunkCoord, _highlightTerrainMaterial);
}
break;
}
case EditTerrainGizmoMode.Modes.Add:
{
// Highlight patch to add location
var patchCoord = Mode.SelectedPatchCoord;
if (!terrain.HasPatch(ref patchCoord) && _planeModel)
{
var planeSize = 256.0f;
var patchSize = terrain.ChunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
Matrix world = Matrix.RotationZ(-Mathf.PiOverTwo) *
Matrix.Scaling(patchSize / planeSize) *
Matrix.Translation(patchSize * (0.5f + patchCoord.X), 0, patchSize * (0.5f + patchCoord.Y)) *
Matrix.Scaling(terrain.Scale) *
Matrix.RotationQuaternion(terrain.Orientation) *
Matrix.Translation(terrain.Position);
_planeModel.Draw(ref renderContext, _highlightMaterial, ref world);
}
break;
}
case EditTerrainGizmoMode.Modes.Remove:
{
// Highlight selected patch
var patchCoord = Mode.SelectedPatchCoord;
if (terrain.HasPatch(ref patchCoord))
{
terrain.DrawPatch(ref renderContext, ref patchCoord, _highlightTerrainMaterial);
}
break;
}
}
}
/// <inheritdoc />
public override void Update(float dt)
{
base.Update(dt);
if (!IsActive)
return;
if (Mode.EditMode == EditTerrainGizmoMode.Modes.Add)
{
var sceneEditing = Editor.Instance.SceneEditing;
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
if (terrainNode != null)
{
// Check if mouse ray hits any of the terrain patches sides to add a new patch there
var mouseRay = Owner.MouseRay;
var terrain = (FlaxEngine.Terrain)terrainNode.Actor;
var chunkCoord = Int2.Zero;
if (!TerrainTools.TryGetPatchCoordToAdd(terrain, mouseRay, out var patchCoord))
{
// If terrain has no patches TryGetPatchCoordToAdd will always return the default patch to add, otherwise fallback to already used location
terrain.GetPatchCoord(0, out patchCoord);
}
Mode.SetSelectedChunk(ref patchCoord, ref chunkCoord);
}
}
}
[Serializable]
private class AddPatchAction : IUndoAction
{
[Serialize]
private Guid _terrainId;
[Serialize]
private Int2 _patchCoord;
/// <inheritdoc />
public string ActionString => "Add terrain patch";
public AddPatchAction(FlaxEngine.Terrain terrain, ref Int2 patchCoord)
{
if (terrain == null)
throw new ArgumentException(nameof(terrain));
_terrainId = terrain.ID;
_patchCoord = patchCoord;
}
/// <inheritdoc />
public void Do()
{
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
if (terrain == null)
{
Editor.LogError("Missing terrain actor.");
return;
}
terrain.AddPatch(ref _patchCoord);
if (TerrainTools.InitializePatch(terrain, ref _patchCoord))
{
Editor.LogError("Failed to initialize terrain patch.");
}
terrain.GetPatchBounds(terrain.GetPatchIndex(ref _patchCoord), out var patchBounds);
OnPatchEdit(terrain, ref patchBounds);
}
/// <inheritdoc />
public void Undo()
{
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
if (terrain == null)
{
Editor.LogError("Missing terrain actor.");
return;
}
terrain.GetPatchBounds(terrain.GetPatchIndex(ref _patchCoord), out var patchBounds);
terrain.RemovePatch(ref _patchCoord);
OnPatchEdit(terrain, ref patchBounds);
}
private void OnPatchEdit(FlaxEngine.Terrain terrain, ref BoundingBox patchBounds)
{
Editor.Instance.Scene.MarkSceneEdited(terrain.Scene);
var editorOptions = Editor.Instance.Options.Options;
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
// Auto NavMesh rebuild
if (!isPlayMode && editorOptions.General.AutoRebuildNavMesh)
{
if (terrain.Scene && (terrain.StaticFlags & StaticFlags.Navigation) == StaticFlags.Navigation)
{
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
/// <inheritdoc />
public void Dispose()
{
}
}
private bool TryAddPatch()
{
var terrain = SelectedTerrain;
if (terrain)
{
var patchCoord = Mode.SelectedPatchCoord;
if (!terrain.HasPatch(ref patchCoord))
{
// Add a new patch (with undo)
var action = new AddPatchAction(terrain, ref patchCoord);
action.Do();
Editor.Instance.Undo.AddAction(action);
return true;
}
}
return false;
}
/// <inheritdoc />
public override void Pick()
{
if (Mode.EditMode == EditTerrainGizmoMode.Modes.Add && TryAddPatch())
{
// Patch added!
}
else
{
// Get mouse ray and try to hit terrain
var ray = Owner.MouseRay;
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders;
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags) as TerrainNode;
// Update selection
var sceneEditing = Editor.Instance.SceneEditing;
if (hit != null)
{
if (Mode.EditMode != EditTerrainGizmoMode.Modes.Add)
{
// Perform detailed tracing
var terrain = (FlaxEngine.Terrain)hit.Actor;
terrain.RayCast(ray, out _, out var patchCoord, out var chunkCoord);
Mode.SetSelectedChunk(ref patchCoord, ref chunkCoord);
}
sceneEditing.Select(hit);
}
else
{
sceneEditing.Deselect();
}
}
}
}
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Terrain management and editing tool.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
[HideInEditor]
public class EditTerrainGizmoMode : EditorGizmoMode
{
/// <summary>
/// The terrain properties editing modes.
/// </summary>
public enum Modes
{
/// <summary>
/// Terrain chunks editing mode.
/// </summary>
Edit,
/// <summary>
/// Terrain patches adding mode.
/// </summary>
Add,
/// <summary>
/// Terrain patches removing mode.
/// </summary>
Remove,
/// <summary>
/// Terrain exporting mode.
/// </summary>
Export,
}
private Modes _mode;
/// <summary>
/// The terrain editing gizmo.
/// </summary>
public EditTerrainGizmo Gizmo;
/// <summary>
/// The patch coordinates of the last picked patch.
/// </summary>
public Int2 SelectedPatchCoord { get; private set; }
/// <summary>
/// The chunk coordinates (relative to the patch) of the last picked chunk.
/// </summary>
public Int2 SelectedChunkCoord { get; private set; }
/// <summary>
/// Occurs when mode gets changed.
/// </summary>
public event Action ModeChanged;
/// <summary>
/// The active edit mode.
/// </summary>
public Modes EditMode
{
get => _mode;
set
{
if (_mode != value)
{
_mode = value;
ModeChanged?.Invoke();
}
}
}
/// <summary>
/// Occurs when selected patch or/and chunk coord gets changed (after picking by user).
/// </summary>
public event Action SelectedChunkCoordChanged;
/// <inheritdoc />
public override void Init(MainEditorGizmoViewport viewport)
{
base.Init(viewport);
EditMode = Modes.Edit;
Gizmo = new EditTerrainGizmo(viewport, this);
}
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
Viewport.Gizmos.Active = Gizmo;
}
/// <summary>
/// Sets the selected chunk coordinates.
/// </summary>
/// <param name="patchCoord">The patch coord.</param>
/// <param name="chunkCoord">The chunk coord.</param>
public void SetSelectedChunk(ref Int2 patchCoord, ref Int2 chunkCoord)
{
if (SelectedPatchCoord != patchCoord || SelectedChunkCoord != chunkCoord)
{
SelectedPatchCoord = patchCoord;
SelectedChunkCoord = chunkCoord;
OnSelectedTerrainChunkChanged();
}
}
private void OnSelectedTerrainChunkChanged()
{
SelectedChunkCoordChanged?.Invoke();
}
}
}

View File

@@ -0,0 +1,234 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Paint
{
/// <summary>
/// The base class for terran paint tool modes.
/// </summary>
[HideInEditor]
public abstract class Mode
{
/// <summary>
/// The options container for the terrain editing apply.
/// </summary>
public struct Options
{
/// <summary>
/// If checked, modification apply method should be inverted.
/// </summary>
public bool Invert;
/// <summary>
/// The master strength parameter to apply when editing the terrain.
/// </summary>
public float Strength;
/// <summary>
/// The delta time (in seconds) for the terrain modification apply. Used to scale the strength. Adjusted to handle low-FPS.
/// </summary>
public float DeltaTime;
}
/// <summary>
/// The tool strength (normalized to range 0-1). Defines the intensity of the paint operation to make it stronger or mre subtle.
/// </summary>
[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;
/// <summary>
/// Gets a value indicating whether this mode supports negative apply for terrain modification.
/// </summary>
public virtual bool SupportsNegativeApply => false;
/// <summary>
/// Gets the index of the active splatmap texture to modify by the tool. It must be equal or higher than zero bu less than <see cref="FlaxEngine.Terrain.MaxSplatmapsCount"/>.
/// </summary>
public abstract int ActiveSplatmapIndex { get; }
/// <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, 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(Mathf.FloorToInt(brushBoundsPatchLocalMin.X), Mathf.FloorToInt(brushBoundsPatchLocalMin.Z));
var brushPatchMax = new Int2(Mathf.CeilToInt(brushBoundsPatchLocalMax.X), Mathf.FloorToInt(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 FlaxException("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);
}
}
/// <summary>
/// The mode apply parameters.
/// </summary>
public unsafe struct ApplyParams
{
/// <summary>
/// The brush.
/// </summary>
public Brush Brush;
/// <summary>
/// The options.
/// </summary>
public Options Options;
/// <summary>
/// The gizmo.
/// </summary>
public PaintTerrainGizmoMode Gizmo;
/// <summary>
/// The terrain.
/// </summary>
public FlaxEngine.Terrain Terrain;
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
/// <summary>
/// The modified offset.
/// </summary>
public Int2 ModifiedOffset;
/// <summary>
/// The modified size.
/// </summary>
public Int2 ModifiedSize;
/// <summary>
/// The final calculated strength of the effect to apply (can be negative for inverted terrain modification if <see cref="SupportsNegativeApply"/> is set).
/// </summary>
public float Strength;
/// <summary>
/// The splatmap texture index.
/// </summary>
public int SplatmapIndex;
/// <summary>
/// The temporary data buffer (for modified data).
/// </summary>
public Color32* TempBuffer;
/// <summary>
/// The source data buffer.
/// </summary>
public Color32* SourceData;
/// <summary>
/// The heightmap size (edge).
/// </summary>
public int HeightmapSize;
/// <summary>
/// The patch position in terrain local-space.
/// </summary>
public Vector3 PatchPositionLocal;
/// <summary>
/// The terrain local-to-world matrix.
/// </summary>
public Matrix TerrainWorld;
}
/// <summary>
/// Applies the modification to the terrain.
/// </summary>
/// <param name="p">The parameters to use.</param>
public abstract void Apply(ref ApplyParams p);
}
}

View File

@@ -0,0 +1,125 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Paint
{
/// <summary>
/// Paint tool mode. Edits terrain splatmap by painting with the single layer on top of the others.
/// </summary>
/// <seealso cref="FlaxEditor.Tools.Terrain.Paint.Mode" />
[HideInEditor]
public sealed class SingleLayerMode : Mode
{
/// <summary>
/// The paint layers.
/// </summary>
public enum Layers
{
/// <summary>
/// The layer 0.
/// </summary>
Layer0,
/// <summary>
/// The layer 0.
/// </summary>
Layer1,
/// <summary>
/// The layer 2.
/// </summary>
Layer2,
/// <summary>
/// The layer 3.
/// </summary>
Layer3,
/// <summary>
/// The layer 4.
/// </summary>
Layer4,
/// <summary>
/// The layer 5.
/// </summary>
Layer5,
/// <summary>
/// The layer 6.
/// </summary>
Layer6,
/// <summary>
/// The layer 7.
/// </summary>
Layer7,
}
/// <summary>
/// The layer to paint with it.
/// </summary>
[EditorOrder(10), Tooltip("The layer to paint with it. Terrain material can access per-layer blend weight to perform materials or textures blending.")]
public Layers Layer = Layers.Layer0;
/// <inheritdoc />
public override int ActiveSplatmapIndex => (int)Layer < 4 ? 0 : 1;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
{
var strength = p.Strength;
var layer = (int)Layer;
var brushPosition = p.Gizmo.CursorPosition;
var layerComponent = layer % 4;
// Apply brush modification
Profiler.BeginEvent("Apply Brush");
for (int z = 0; z < p.ModifiedSize.Y; z++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
{
var xx = x + p.ModifiedOffset.X;
var src = p.SourceData[zz * p.HeightmapSize + xx];
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, 0, zz * FlaxEngine.Terrain.UnitsPerVertex);
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
// Extract layer weight
byte* srcPtr = &src.R;
var srcWeight = *(srcPtr + layerComponent) / 255.0f;
// Accumulate weight
float dstWeight = srcWeight + paintAmount;
// Check for solid layer case
if (dstWeight >= 1.0f)
{
// Erase other layers
// TODO: maybe erase only the higher layers?
// TODO: need to erase also weights form the other splatmaps
src = Color32.Transparent;
// Use limit value
dstWeight = 1.0f;
}
// Modify packed weight
*(srcPtr + layerComponent) = (byte)(dstWeight * 255.0f);
// Write back
p.TempBuffer[z * p.ModifiedSize.X + x] = src;
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifySplatMap(p.Terrain, ref p.PatchCoord, p.SplatmapIndex, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
}
}
}

View File

@@ -0,0 +1,124 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Carve tab related to terrain painting. Allows to modify terrain splatmap using brush.
/// </summary>
/// <seealso cref="Tab" />
[HideInEditor]
public class PaintTab : Tab
{
/// <summary>
/// The object for paint mode settings adjusting via Custom Editor.
/// </summary>
private sealed class ProxyObject
{
private readonly PaintTerrainGizmoMode _mode;
private object _currentMode, _currentBrush;
public ProxyObject(PaintTerrainGizmoMode mode)
{
_mode = mode;
SyncData();
}
public void SyncData()
{
_currentMode = _mode.CurrentMode;
_currentBrush = _mode.CurrentBrush;
}
[EditorOrder(0), EditorDisplay("Tool"), Tooltip("Paint tool mode to use.")]
public PaintTerrainGizmoMode.ModeTypes ToolMode
{
get => _mode.ToolModeType;
set => _mode.ToolModeType = value;
}
[EditorOrder(100), EditorDisplay("Tool", EditorDisplayAttribute.InlineStyle)]
public object Mode
{
get => _currentMode;
set { }
}
[EditorOrder(1000), EditorDisplay("Brush"), Tooltip("Paint brush type to use.")]
public PaintTerrainGizmoMode.BrushTypes BrushTypeType
{
get => _mode.ToolBrushType;
set => _mode.ToolBrushType = value;
}
[EditorOrder(1100), EditorDisplay("Brush", EditorDisplayAttribute.InlineStyle)]
public object Brush
{
get => _currentBrush;
set { }
}
}
private readonly ProxyObject _proxy;
private readonly CustomEditorPresenter _presenter;
/// <summary>
/// The parent carve tab.
/// </summary>
public readonly CarveTab CarveTab;
/// <summary>
/// The related sculp terrain gizmo.
/// </summary>
public readonly PaintTerrainGizmoMode Gizmo;
/// <summary>
/// Initializes a new instance of the <see cref="PaintTab"/> class.
/// </summary>
/// <param name="tab">The parent tab.</param>
/// <param name="gizmo">The related gizmo.</param>
public PaintTab(CarveTab tab, PaintTerrainGizmoMode gizmo)
: base("Paint")
{
CarveTab = tab;
Gizmo = gizmo;
Gizmo.ToolModeChanged += OnToolModeChanged;
_proxy = new ProxyObject(gizmo);
// Main panel
var panel = new Panel(ScrollBars.Both)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
// Options editor
// TODO: use editor undo for changing brush options
var editor = new CustomEditorPresenter(null);
editor.Panel.Parent = panel;
editor.Select(_proxy);
_presenter = editor;
}
private void OnToolModeChanged()
{
_presenter.BuildLayoutOnUpdate();
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
if (_presenter.BuildOnUpdate)
{
_proxy.SyncData();
}
base.Update(deltaTime);
}
}
}

View File

@@ -0,0 +1,206 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Tools.Terrain.Paint;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Gizmo for painting terrain. Managed by the <see cref="PaintTerrainGizmoMode"/>.
/// </summary>
/// <seealso cref="FlaxEditor.Gizmo.GizmoBase" />
[HideInEditor]
public sealed class PaintTerrainGizmo : GizmoBase
{
private FlaxEngine.Terrain _paintTerrain;
private Ray _prevRay;
/// <summary>
/// The parent mode.
/// </summary>
public readonly PaintTerrainGizmoMode Mode;
/// <summary>
/// Gets a value indicating whether gizmo tool is painting the terrain splatmap.
/// </summary>
public bool IsPainting => _paintTerrain != null;
/// <summary>
/// Occurs when terrain paint has been started.
/// </summary>
public event Action PaintStarted;
/// <summary>
/// Occurs when terrain paint has been ended.
/// </summary>
public event Action PaintEnded;
/// <summary>
/// Initializes a new instance of the <see cref="PaintTerrainGizmo"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="mode">The mode.</param>
public PaintTerrainGizmo(IGizmoOwner owner, PaintTerrainGizmoMode mode)
: base(owner)
{
Mode = mode;
}
private FlaxEngine.Terrain SelectedTerrain
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
return (FlaxEngine.Terrain)terrainNode?.Actor;
}
}
/// <inheritdoc />
public override void Draw(ref RenderContext renderContext)
{
if (!IsActive)
return;
var terrain = SelectedTerrain;
if (!terrain)
return;
if (Mode.HasValidHit)
{
var brushPosition = Mode.CursorPosition;
var brushColor = new Color(1.0f, 0.85f, 0.0f); // TODO: expose to editor options
var brushMaterial = Mode.CurrentBrush.GetBrushMaterial(ref brushPosition, ref brushColor);
if (!brushMaterial)
return;
for (int i = 0; i < Mode.ChunksUnderCursor.Count; i++)
{
var chunk = Mode.ChunksUnderCursor[i];
terrain.DrawChunk(ref renderContext, ref chunk.PatchCoord, ref chunk.ChunkCoord, brushMaterial, 0);
}
}
}
/// <summary>
/// Called to start terrain painting
/// </summary>
/// <param name="terrain">The terrain.</param>
private void PaintStart(FlaxEngine.Terrain terrain)
{
// Skip if already is painting
if (IsPainting)
return;
_paintTerrain = terrain;
PaintStarted?.Invoke();
}
/// <summary>
/// Called to update terrain painting logic.
/// </summary>
/// <param name="dt">The delta time (in seconds).</param>
private void PaintUpdate(float dt)
{
// Skip if is not painting
if (!IsPainting)
return;
// Edit the terrain
Profiler.BeginEvent("Paint Terrain");
var options = new Mode.Options
{
Strength = 1.0f,
DeltaTime = dt,
Invert = Owner.IsControlDown
};
Mode.CurrentMode.Apply(Mode.CurrentBrush, ref options, Mode, _paintTerrain);
Profiler.EndEvent();
}
/// <summary>
/// Called to end terrain painting.
/// </summary>
private void PaintEnd()
{
// Skip if nothing was painted
if (!IsPainting)
return;
_paintTerrain = null;
PaintEnded?.Invoke();
}
/// <inheritdoc />
public override void Update(float dt)
{
base.Update(dt);
// Check if gizmo is not active
if (!IsActive)
{
PaintEnd();
return;
}
// Check if no terrain is selected
var terrain = SelectedTerrain;
if (!terrain)
{
PaintEnd();
return;
}
// Check if selected terrain was changed during painting
if (terrain != _paintTerrain && IsPainting)
{
PaintEnd();
}
// Special case if user is sculpting terrain and mouse is not moving then freeze the brush location to help painting vertical tip objects
var mouseRay = Owner.MouseRay;
if (IsPainting && _prevRay == mouseRay)
{
// Freeze cursor
}
// Perform detailed tracing to find cursor location on the terrain
else if (terrain.RayCast(mouseRay, out var closest, out var patchCoord, out var chunkCoord))
{
var hitLocation = mouseRay.GetPoint(closest);
Mode.SetCursor(ref hitLocation);
}
// No hit
else
{
Mode.ClearCursor();
}
_prevRay = mouseRay;
// Handle painting
if (Owner.IsLeftMouseButtonDown)
PaintStart(terrain);
else
PaintEnd();
PaintUpdate(dt);
}
/// <inheritdoc />
public override void Pick()
{
// Get mouse ray and try to hit terrain
var ray = Owner.MouseRay;
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders;
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags);
// Update selection
var sceneEditing = Editor.Instance.SceneEditing;
if (hit is TerrainNode)
sceneEditing.Select(hit);
}
}
}

View File

@@ -0,0 +1,357 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEditor.Tools.Terrain.Paint;
using FlaxEditor.Tools.Terrain.Undo;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Terrain painting tool mode.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
[HideInEditor]
public class PaintTerrainGizmoMode : EditorGizmoMode
{
/// <summary>
/// The terrain layer names.
/// </summary>
public static readonly string[] TerrainLayerNames =
{
"Layer 0",
"Layer 1",
"Layer 2",
"Layer 3",
"Layer 4",
"Layer 5",
"Layer 6",
"Layer 7",
};
private IntPtr _cachedSplatmapData;
private int _cachedSplatmapDataSize;
private EditTerrainMapAction _activeAction;
/// <summary>
/// The terrain painting gizmo.
/// </summary>
public PaintTerrainGizmo Gizmo;
/// <summary>
/// The tool modes.
/// </summary>
public enum ModeTypes
{
/// <summary>
/// The single layer mode.
/// </summary>
SingleLayer,
}
/// <summary>
/// The brush types.
/// </summary>
public enum BrushTypes
{
/// <summary>
/// The circle brush.
/// </summary>
CircleBrush,
}
private readonly Mode[] _modes =
{
new SingleLayerMode(),
};
private readonly Brush[] _brushes =
{
new CircleBrush(),
};
private ModeTypes _modeType = ModeTypes.SingleLayer;
private BrushTypes _brushType = BrushTypes.CircleBrush;
/// <summary>
/// Occurs when tool mode gets changed.
/// </summary>
public event Action ToolModeChanged;
/// <summary>
/// Gets the current tool mode (enum).
/// </summary>
public ModeTypes ToolModeType
{
get => _modeType;
set
{
if (_modeType != value)
{
if (_activeAction != null)
throw new InvalidOperationException("Cannot change paint tool mode during terrain editing.");
_modeType = value;
ToolModeChanged?.Invoke();
}
}
}
/// <summary>
/// Gets the current tool mode.
/// </summary>
public Mode CurrentMode => _modes[(int)_modeType];
/// <summary>
/// Gets the single layer mode instance.
/// </summary>
public SingleLayerMode SingleLayerMode => _modes[(int)ModeTypes.SingleLayer] as SingleLayerMode;
/// <summary>
/// Occurs when tool brush gets changed.
/// </summary>
public event Action ToolBrushChanged;
/// <summary>
/// Gets the current tool brush (enum).
/// </summary>
public BrushTypes ToolBrushType
{
get => _brushType;
set
{
if (_brushType != value)
{
if (_activeAction != null)
throw new InvalidOperationException("Cannot change sculpt tool brush type during terrain editing.");
_brushType = value;
ToolBrushChanged?.Invoke();
}
}
}
/// <summary>
/// Gets the current brush.
/// </summary>
public Brush CurrentBrush => _brushes[(int)_brushType];
/// <summary>
/// Gets the circle brush instance.
/// </summary>
public CircleBrush CircleBrush => _brushes[(int)BrushTypes.CircleBrush] as CircleBrush;
/// <summary>
/// The last valid cursor position of the brush (in world space).
/// </summary>
public Vector3 CursorPosition { get; private set; }
/// <summary>
/// Flag used to indicate whenever last cursor position of the brush is valid.
/// </summary>
public bool HasValidHit { get; private set; }
/// <summary>
/// Describes the terrain patch link.
/// </summary>
public struct PatchLocation
{
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
}
/// <summary>
/// The selected terrain patches collection that are under cursor (affected by the brush).
/// </summary>
public readonly List<PatchLocation> PatchesUnderCursor = new List<PatchLocation>();
/// <summary>
/// Describes the terrain chunk link.
/// </summary>
public struct ChunkLocation
{
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
/// <summary>
/// The chunk coordinates.
/// </summary>
public Int2 ChunkCoord;
}
/// <summary>
/// The selected terrain chunk collection that are under cursor (affected by the brush).
/// </summary>
public readonly List<ChunkLocation> ChunksUnderCursor = new List<ChunkLocation>();
/// <summary>
/// Gets the selected terrain actor (see <see cref="Modules.SceneEditingModule"/>).
/// </summary>
public FlaxEngine.Terrain SelectedTerrain
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
return (FlaxEngine.Terrain)terrainNode?.Actor;
}
}
/// <summary>
/// Gets the world bounds of the brush located at the current cursor position (defined by <see cref="CursorPosition"/>). Valid only if <see cref="HasValidHit"/> is set to true.
/// </summary>
public BoundingBox CursorBrushBounds
{
get
{
const float brushExtentY = 10000.0f;
float brushSizeHalf = CurrentBrush.Size * 0.5f;
Vector3 center = CursorPosition;
BoundingBox box;
box.Minimum = new Vector3(center.X - brushSizeHalf, center.Y - brushSizeHalf - brushExtentY, center.Z - brushSizeHalf);
box.Maximum = new Vector3(center.X + brushSizeHalf, center.Y + brushSizeHalf + brushExtentY, center.Z + brushSizeHalf);
return box;
}
}
/// <summary>
/// Gets the splatmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
/// </summary>
/// <param name="size">The minimum buffer size (in bytes).</param>
/// <returns>The allocated memory using <see cref="Marshal"/> interface.</returns>
public IntPtr GetSplatmapTempBuffer(int size)
{
if (_cachedSplatmapDataSize < size)
{
if (_cachedSplatmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedSplatmapData);
}
_cachedSplatmapData = Marshal.AllocHGlobal(size);
_cachedSplatmapDataSize = size;
}
return _cachedSplatmapData;
}
/// <summary>
/// Gets the current edit terrain undo system action. Use it to record the data for the undo restoring after terrain editing.
/// </summary>
internal EditTerrainMapAction CurrentEditUndoAction => _activeAction;
/// <inheritdoc />
public override void Init(MainEditorGizmoViewport viewport)
{
base.Init(viewport);
Gizmo = new PaintTerrainGizmo(viewport, this);
Gizmo.PaintStarted += OnPaintStarted;
Gizmo.PaintEnded += OnPaintEnded;
}
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
Viewport.Gizmos.Active = Gizmo;
ClearCursor();
}
/// <inheritdoc />
public override void OnDeactivated()
{
base.OnDeactivated();
// Free temporary memory buffer
if (_cachedSplatmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedSplatmapData);
_cachedSplatmapData = IntPtr.Zero;
_cachedSplatmapDataSize = 0;
}
}
/// <summary>
/// Clears the cursor location information cached within the gizmo mode.
/// </summary>
public void ClearCursor()
{
HasValidHit = false;
PatchesUnderCursor.Clear();
ChunksUnderCursor.Clear();
}
/// <summary>
/// Sets the cursor location in the world space. Updates the brush location and cached affected chunks.
/// </summary>
/// <param name="hitPosition">The cursor hit location on the selected terrain.</param>
public void SetCursor(ref Vector3 hitPosition)
{
HasValidHit = true;
CursorPosition = hitPosition;
PatchesUnderCursor.Clear();
ChunksUnderCursor.Clear();
// Find patches and chunks affected by the brush
var terrain = SelectedTerrain;
if (terrain == null)
throw new InvalidOperationException("Cannot set cursor then no terrain is selected.");
var brushBounds = CursorBrushBounds;
var patchesCount = terrain.PatchesCount;
BoundingBox tmp;
for (int patchIndex = 0; patchIndex < patchesCount; patchIndex++)
{
terrain.GetPatchBounds(patchIndex, out tmp);
if (!tmp.Intersects(ref brushBounds))
continue;
terrain.GetPatchCoord(patchIndex, out var patchCoord);
PatchesUnderCursor.Add(new PatchLocation() { PatchCoord = patchCoord });
for (int chunkIndex = 0; chunkIndex < FlaxEngine.Terrain.PatchChunksCount; chunkIndex++)
{
terrain.GetChunkBounds(patchIndex, chunkIndex, out tmp);
if (!tmp.Intersects(ref brushBounds))
continue;
var chunkCoord = new Int2(chunkIndex % FlaxEngine.Terrain.PatchEdgeChunksCount, chunkIndex / FlaxEngine.Terrain.PatchEdgeChunksCount);
ChunksUnderCursor.Add(new ChunkLocation() { PatchCoord = patchCoord, ChunkCoord = chunkCoord });
}
}
}
private void OnPaintStarted()
{
if (_activeAction != null)
throw new InvalidOperationException("Terrain paint start/end resynchronization.");
var terrain = SelectedTerrain;
_activeAction = new EditTerrainSplatMapAction(terrain);
}
private void OnPaintEnded()
{
if (_activeAction != null)
{
if (_activeAction.HasAnyModification)
{
_activeAction.OnEditingEnd();
Editor.Instance.Undo.AddAction(_activeAction);
}
_activeAction = null;
}
}
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
/// <summary>
/// Sculpt tool mode that flattens the terrain heightmap area affected by brush to the target value.
/// </summary>
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
[HideInEditor]
public sealed class FlattenMode : Mode
{
/// <summary>
/// The target terrain height to blend to.
/// </summary>
[EditorOrder(10), Tooltip("The target terrain height to blend to.")]
public float TargetHeight = 0.0f;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
{
// If used with invert mode pick the target height level
if (p.Options.Invert)
{
var center = p.ModifiedOffset + p.ModifiedSize / 2;
TargetHeight = p.SourceHeightMap[center.Y * p.HeightmapSize + center.X];
return;
}
// Prepare
var brushPosition = p.Gizmo.CursorPosition;
var targetHeight = TargetHeight;
var strength = Mathf.Saturate(p.Strength);
// Apply brush modification
Profiler.BeginEvent("Apply Brush");
for (int z = 0; z < p.ModifiedSize.Y; z++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
{
var xx = x + p.ModifiedOffset.X;
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
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);
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
// Blend between the height and the target value
p.TempBuffer[z * p.ModifiedSize.X + x] = Mathf.Lerp(sourceHeight, targetHeight, paintAmount);
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
/// <summary>
/// Terrain holes creating tool mode edits terrain holes mask by changing area affected by brush.
/// </summary>
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
[HideInEditor]
public sealed class HolesMode : Mode
{
/// <inheritdoc />
public override bool SupportsNegativeApply => true;
/// <inheritdoc />
public override bool EditHoles => true;
/// <summary>
/// Initializes a new instance of the <see cref="HolesMode"/> class.
/// </summary>
public HolesMode()
{
Strength = 6.0f;
}
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
{
var strength = p.Strength * -10.0f;
var brushPosition = p.Gizmo.CursorPosition;
var tempBuffer = (byte*)p.TempBuffer;
// Apply brush modification
Profiler.BeginEvent("Apply Brush");
for (int z = 0; z < p.ModifiedSize.Y; z++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
{
var xx = x + p.ModifiedOffset.X;
var sourceMask = p.SourceHolesMask[zz * p.HeightmapSize + xx] != 0 ? 1.0f : 0.0f;
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, 0, zz * FlaxEngine.Terrain.UnitsPerVertex);
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
samplePositionWorld.Y = brushPosition.Y;
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld);
tempBuffer[z * p.ModifiedSize.X + x] = (byte)((sourceMask + paintAmount * strength) < 0.8f ? 0 : 255);
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifyHolesMask(p.Terrain, ref p.PatchCoord, tempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
}
}
}

View File

@@ -0,0 +1,246 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
/// <summary>
/// The base class for terran sculpt tool modes.
/// </summary>
[HideInEditor]
public abstract class Mode
{
/// <summary>
/// The options container for the terrain editing apply.
/// </summary>
public struct Options
{
/// <summary>
/// If checked, modification apply method should be inverted.
/// </summary>
public bool Invert;
/// <summary>
/// The master strength parameter to apply when editing the terrain.
/// </summary>
public float Strength;
/// <summary>
/// The delta time (in seconds) for the terrain modification apply. Used to scale the strength. Adjusted to handle low-FPS.
/// </summary>
public float DeltaTime;
}
/// <summary>
/// The tool strength (normalized to range 0-1). Defines the intensity of the sculpt operation to make it stronger or mre subtle.
/// </summary>
[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;
/// <summary>
/// Gets a value indicating whether this mode supports negative apply for terrain modification.
/// </summary>
public virtual bool SupportsNegativeApply => false;
/// <summary>
/// Gets a value indicating whether this mode modifies the terrain holes mask rather than heightmap.
/// </summary>
public virtual bool EditHoles => false;
/// <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)
{
// Combine final apply strength
float strength = Strength * options.Strength * options.DeltaTime;
if (strength <= 0.0f)
return;
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;
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.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(Mathf.FloorToInt(brushBoundsPatchLocalMin.X), Mathf.FloorToInt(brushBoundsPatchLocalMin.Z));
var brushPatchMax = new Int2(Mathf.CeilToInt(brushBoundsPatchLocalMax.X), Mathf.FloorToInt(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 FlaxException("Cannot modify terrain. Loading heightmap 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);
}
// Apply modification
p.ModifiedOffset = modifiedOffset;
p.ModifiedSize = modifiedSize;
p.PatchCoord = patch.PatchCoord;
p.PatchPositionLocal = patchPositionLocal;
p.SourceHeightMap = sourceHeights;
p.SourceHolesMask = sourceHoles;
Apply(ref p);
}
var editorOptions = Editor.Instance.Options.Options;
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
// Auto NavMesh rebuild
if (!isPlayMode && editorOptions.General.AutoRebuildNavMesh)
{
if (terrain.Scene && (terrain.StaticFlags & StaticFlags.Navigation) == StaticFlags.Navigation)
{
Navigation.BuildNavMesh(terrain.Scene, brushBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
/// <summary>
/// The mode apply parameters.
/// </summary>
public unsafe struct ApplyParams
{
/// <summary>
/// The brush.
/// </summary>
public Brush Brush;
/// <summary>
/// The options.
/// </summary>
public Options Options;
/// <summary>
/// The gizmo.
/// </summary>
public SculptTerrainGizmoMode Gizmo;
/// <summary>
/// The terrain.
/// </summary>
public FlaxEngine.Terrain Terrain;
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
/// <summary>
/// The modified offset.
/// </summary>
public Int2 ModifiedOffset;
/// <summary>
/// The modified size.
/// </summary>
public Int2 ModifiedSize;
/// <summary>
/// The final calculated strength of the effect to apply (can be negative for inverted terrain modification if <see cref="SupportsNegativeApply"/> is set).
/// </summary>
public float Strength;
/// <summary>
/// The temporary data buffer (for modified data). Has size of array of floats that has size of heightmap length.
/// </summary>
public float* TempBuffer;
/// <summary>
/// The source heightmap data buffer. May be null if modified is holes mask.
/// </summary>
public float* SourceHeightMap;
/// <summary>
/// The source holes mask data buffer. May be null if modified is.
/// </summary>
public byte* SourceHolesMask;
/// <summary>
/// The heightmap size (edge).
/// </summary>
public int HeightmapSize;
/// <summary>
/// The patch position in terrain local-space.
/// </summary>
public Vector3 PatchPositionLocal;
/// <summary>
/// The terrain local-to-world matrix.
/// </summary>
public Matrix TerrainWorld;
}
/// <summary>
/// Applies the modification to the terrain.
/// </summary>
/// <param name="p">The parameters to use.</param>
public abstract void Apply(ref ApplyParams p);
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.Utilities;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
/// <summary>
/// Sculpt tool mode that applies the noise to the terrain heightmap area affected by brush.
/// </summary>
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
[HideInEditor]
public sealed class NoiseMode : Mode
{
/// <summary>
/// The tool noise scale. Adjusts the noise pattern scale.
/// </summary>
[EditorOrder(10), Limit(0, 10000), Tooltip("The tool noise scale. Adjusts the noise pattern scale.")]
public float NoiseScale = 128.0f;
/// <summary>
/// The tool noise amount scale. Adjusts the noise amplitude scale.
/// </summary>
[EditorOrder(10), Limit(0, 10000000), Tooltip("The tool noise amount scale. Adjusts the noise amplitude scale.")]
public float NoiseAmount = 10000.0f;
/// <inheritdoc />
public override bool SupportsNegativeApply => true;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
{
// Prepare
var brushPosition = p.Gizmo.CursorPosition;
var noise = new PerlinNoise(0, NoiseScale, p.Strength * NoiseAmount);
var chunkSize = p.Terrain.ChunkSize;
var patchSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount;
var patchOffset = p.PatchCoord * patchSize;
// Apply brush modification
Profiler.BeginEvent("Apply Brush");
for (int z = 0; z < p.ModifiedSize.Y; z++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
{
var xx = x + p.ModifiedOffset.X;
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
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);
var noiseSample = noise.Sample(xx + patchOffset.X, zz + patchOffset.Y);
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld);
p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight + noiseSample * paintAmount;
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
}
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
/// <summary>
/// Sculpt tool mode. Edits terrain heightmap by moving area affected by brush up or down.
/// </summary>
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
[HideInEditor]
public sealed class SculptMode : Mode
{
/// <inheritdoc />
public override bool SupportsNegativeApply => true;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
{
var strength = p.Strength * 1000.0f;
var brushPosition = p.Gizmo.CursorPosition;
// Apply brush modification
Profiler.BeginEvent("Apply Brush");
for (int z = 0; z < p.ModifiedSize.Y; z++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
{
var xx = x + p.ModifiedOffset.X;
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
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);
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld);
p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight + paintAmount * strength;
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
}
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
/// <summary>
/// Sculpt tool mode that smooths the terrain heightmap area affected by brush.
/// </summary>
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
[HideInEditor]
public sealed class SmoothMode : Mode
{
/// <summary>
/// The tool smoothing radius. Defines the size of smoothing kernel, the higher value the more nearby samples is included into normalized sum. Scaled by the brush size.
/// </summary>
[EditorOrder(10), Limit(0, 1, 0.01f), Tooltip("The tool smoothing radius. Defines the size of smoothing kernel, the higher value the more nearby samples is included into normalized sum. Scaled by the brush size.")]
public float FilterRadius = 0.4f;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
{
// 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++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
{
var xx = x + p.ModifiedOffset.X;
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
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);
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
if (paintAmount > 0)
{
// 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);
for (int dz = minZ; dz <= maxZ; dz++)
{
for (int dx = minX; dx <= maxX; dx++)
{
var height = p.SourceHeightMap[dz * p.HeightmapSize + dx];
smoothValue += height;
smoothValueSamples++;
}
}
// Normalize
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;
}
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
}
}
}

View File

@@ -0,0 +1,124 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Carve tab related to terrain carving. Allows modifying terrain height and visibility using a brush.
/// </summary>
/// <seealso cref="Tab" />
[HideInEditor]
public class SculptTab : Tab
{
/// <summary>
/// The object for sculp mode settings adjusting via Custom Editor.
/// </summary>
private sealed class ProxyObject
{
private readonly SculptTerrainGizmoMode _mode;
private object _currentMode, _currentBrush;
public ProxyObject(SculptTerrainGizmoMode mode)
{
_mode = mode;
SyncData();
}
public void SyncData()
{
_currentMode = _mode.CurrentMode;
_currentBrush = _mode.CurrentBrush;
}
[EditorOrder(0), EditorDisplay("Tool"), Tooltip("Sculpt tool mode to use.")]
public SculptTerrainGizmoMode.ModeTypes ToolMode
{
get => _mode.ToolModeType;
set => _mode.ToolModeType = value;
}
[EditorOrder(100), EditorDisplay("Tool", EditorDisplayAttribute.InlineStyle)]
public object Mode
{
get => _currentMode;
set { }
}
[EditorOrder(1000), EditorDisplay("Brush"), Tooltip("Sculpt brush type to use.")]
public SculptTerrainGizmoMode.BrushTypes BrushTypeType
{
get => _mode.ToolBrushType;
set => _mode.ToolBrushType = value;
}
[EditorOrder(1100), EditorDisplay("Brush", EditorDisplayAttribute.InlineStyle)]
public object Brush
{
get => _currentBrush;
set { }
}
}
private readonly ProxyObject _proxy;
private readonly CustomEditorPresenter _presenter;
/// <summary>
/// The parent carve tab.
/// </summary>
public readonly CarveTab CarveTab;
/// <summary>
/// The related sculp terrain gizmo.
/// </summary>
public readonly SculptTerrainGizmoMode Gizmo;
/// <summary>
/// Initializes a new instance of the <see cref="SculptTab"/> class.
/// </summary>
/// <param name="tab">The parent tab.</param>
/// <param name="gizmo">The related gizmo.</param>
public SculptTab(CarveTab tab, SculptTerrainGizmoMode gizmo)
: base("Sculpt")
{
CarveTab = tab;
Gizmo = gizmo;
Gizmo.ToolModeChanged += OnToolModeChanged;
_proxy = new ProxyObject(gizmo);
// Main panel
var panel = new Panel(ScrollBars.Both)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
// Options editor
// TODO: use editor undo for changing brush options
var editor = new CustomEditorPresenter(null);
editor.Panel.Parent = panel;
editor.Select(_proxy);
_presenter = editor;
}
private void OnToolModeChanged()
{
_presenter.BuildLayoutOnUpdate();
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
if (_presenter.BuildOnUpdate)
{
_proxy.SyncData();
}
base.Update(deltaTime);
}
}
}

View File

@@ -0,0 +1,206 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Tools.Terrain.Sculpt;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Gizmo for carving terrain. Managed by the <see cref="SculptTerrainGizmoMode"/>.
/// </summary>
/// <seealso cref="FlaxEditor.Gizmo.GizmoBase" />
[HideInEditor]
public sealed class SculptTerrainGizmo : GizmoBase
{
private FlaxEngine.Terrain _paintTerrain;
private Ray _prevRay;
/// <summary>
/// The parent mode.
/// </summary>
public readonly SculptTerrainGizmoMode Mode;
/// <summary>
/// Gets a value indicating whether gizmo tool is painting the terrain heightmap.
/// </summary>
public bool IsPainting => _paintTerrain != null;
/// <summary>
/// Occurs when terrain paint has been started.
/// </summary>
public event Action PaintStarted;
/// <summary>
/// Occurs when terrain paint has been ended.
/// </summary>
public event Action PaintEnded;
/// <summary>
/// Initializes a new instance of the <see cref="SculptTerrainGizmo"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="mode">The mode.</param>
public SculptTerrainGizmo(IGizmoOwner owner, SculptTerrainGizmoMode mode)
: base(owner)
{
Mode = mode;
}
private FlaxEngine.Terrain SelectedTerrain
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
return (FlaxEngine.Terrain)terrainNode?.Actor;
}
}
/// <inheritdoc />
public override void Draw(ref RenderContext renderContext)
{
if (!IsActive)
return;
var terrain = SelectedTerrain;
if (!terrain)
return;
if (Mode.HasValidHit)
{
var brushPosition = Mode.CursorPosition;
var brushColor = new Color(1.0f, 0.85f, 0.0f); // TODO: expose to editor options
var brushMaterial = Mode.CurrentBrush.GetBrushMaterial(ref brushPosition, ref brushColor);
if (!brushMaterial)
return;
for (int i = 0; i < Mode.ChunksUnderCursor.Count; i++)
{
var chunk = Mode.ChunksUnderCursor[i];
terrain.DrawChunk(ref renderContext, ref chunk.PatchCoord, ref chunk.ChunkCoord, brushMaterial);
}
}
}
/// <summary>
/// Called to start terrain painting
/// </summary>
/// <param name="terrain">The terrain.</param>
private void PaintStart(FlaxEngine.Terrain terrain)
{
// Skip if already is painting
if (IsPainting)
return;
_paintTerrain = terrain;
PaintStarted?.Invoke();
}
/// <summary>
/// Called to update terrain painting logic.
/// </summary>
/// <param name="dt">The delta time (in seconds).</param>
private void PaintUpdate(float dt)
{
// Skip if is not painting
if (!IsPainting)
return;
// Edit the terrain
Profiler.BeginEvent("Edit Terrain");
var options = new Mode.Options
{
Strength = 1.0f,
DeltaTime = dt,
Invert = Owner.IsControlDown
};
Mode.CurrentMode.Apply(Mode.CurrentBrush, ref options, Mode, _paintTerrain);
Profiler.EndEvent();
}
/// <summary>
/// Called to end terrain painting.
/// </summary>
private void PaintEnd()
{
// Skip if nothing was painted
if (!IsPainting)
return;
_paintTerrain = null;
PaintEnded?.Invoke();
}
/// <inheritdoc />
public override void Update(float dt)
{
base.Update(dt);
// Check if gizmo is not active
if (!IsActive)
{
PaintEnd();
return;
}
// Check if no terrain is selected
var terrain = SelectedTerrain;
if (!terrain)
{
PaintEnd();
return;
}
// Check if selected terrain was changed during painting
if (terrain != _paintTerrain && IsPainting)
{
PaintEnd();
}
// Special case if user is sculpting terrain and mouse is not moving then freeze the brush location to help painting vertical tip objects
var mouseRay = Owner.MouseRay;
if (IsPainting && _prevRay == mouseRay)
{
// Freeze cursor
}
// Perform detailed tracing to find cursor location on the terrain
else if (terrain.RayCast(mouseRay, out var closest, out var patchCoord, out var chunkCoord))
{
var hitLocation = mouseRay.GetPoint(closest);
Mode.SetCursor(ref hitLocation);
}
// No hit
else
{
Mode.ClearCursor();
}
_prevRay = mouseRay;
// Handle painting
if (Owner.IsLeftMouseButtonDown)
PaintStart(terrain);
else
PaintEnd();
PaintUpdate(dt);
}
/// <inheritdoc />
public override void Pick()
{
// Get mouse ray and try to hit terrain
var ray = Owner.MouseRay;
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders;
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags);
// Update selection
var sceneEditing = Editor.Instance.SceneEditing;
if (hit is TerrainNode)
sceneEditing.Select(hit);
}
}
}

View File

@@ -0,0 +1,379 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEditor.Tools.Terrain.Sculpt;
using FlaxEditor.Tools.Terrain.Undo;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Terrain carving tool mode.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
[HideInEditor]
public class SculptTerrainGizmoMode : EditorGizmoMode
{
private IntPtr _cachedHeightmapData;
private int _cachedHeightmapDataSize;
private EditTerrainMapAction _activeAction;
/// <summary>
/// The terrain carving gizmo.
/// </summary>
public SculptTerrainGizmo Gizmo;
/// <summary>
/// The tool modes.
/// </summary>
public enum ModeTypes
{
/// <summary>
/// The sculpt mode.
/// </summary>
[Tooltip("Sculpt tool mode. Edits terrain heightmap by moving area affected by brush up or down.")]
Sculpt,
/// <summary>
/// The smooth mode.
/// </summary>
[Tooltip("Sculpt tool mode that smooths the terrain heightmap area affected by brush.")]
Smooth,
/// <summary>
/// The flatten mode.
/// </summary>
[Tooltip("Sculpt tool mode that flattens the terrain heightmap area affected by brush to the target value.")]
Flatten,
/// <summary>
/// The noise mode.
/// </summary>
[Tooltip("Sculpt tool mode that applies the noise to the terrain heightmap area affected by brush.")]
Noise,
/// <summary>
/// The holes mode.
/// </summary>
[Tooltip("Terrain holes creating tool mode edits terrain holes mask by changing area affected by brush.")]
Holes,
}
/// <summary>
/// The brush types.
/// </summary>
public enum BrushTypes
{
/// <summary>
/// The circle brush.
/// </summary>
CircleBrush,
}
private readonly Mode[] _modes =
{
new SculptMode(),
new SmoothMode(),
new FlattenMode(),
new NoiseMode(),
new HolesMode(),
};
private readonly Brush[] _brushes =
{
new CircleBrush(),
};
private ModeTypes _modeType = ModeTypes.Sculpt;
private BrushTypes _brushType = BrushTypes.CircleBrush;
/// <summary>
/// Occurs when tool mode gets changed.
/// </summary>
public event Action ToolModeChanged;
/// <summary>
/// Gets the current tool mode (enum).
/// </summary>
public ModeTypes ToolModeType
{
get => _modeType;
set
{
if (_modeType != value)
{
if (_activeAction != null)
throw new InvalidOperationException("Cannot change sculpt tool mode during terrain editing.");
_modeType = value;
ToolModeChanged?.Invoke();
}
}
}
/// <summary>
/// Gets the current tool mode.
/// </summary>
public Mode CurrentMode => _modes[(int)_modeType];
/// <summary>
/// Gets the sculpt mode instance.
/// </summary>
public SculptMode SculptMode => _modes[(int)ModeTypes.Sculpt] as SculptMode;
/// <summary>
/// Gets the smooth mode instance.
/// </summary>
public SmoothMode SmoothMode => _modes[(int)ModeTypes.Smooth] as SmoothMode;
/// <summary>
/// Occurs when tool brush gets changed.
/// </summary>
public event Action ToolBrushChanged;
/// <summary>
/// Gets the current tool brush (enum).
/// </summary>
public BrushTypes ToolBrushType
{
get => _brushType;
set
{
if (_brushType != value)
{
if (_activeAction != null)
throw new InvalidOperationException("Cannot change sculpt tool brush type during terrain editing.");
_brushType = value;
ToolBrushChanged?.Invoke();
}
}
}
/// <summary>
/// Gets the current brush.
/// </summary>
public Brush CurrentBrush => _brushes[(int)_brushType];
/// <summary>
/// Gets the circle brush instance.
/// </summary>
public CircleBrush CircleBrush => _brushes[(int)BrushTypes.CircleBrush] as CircleBrush;
/// <summary>
/// The last valid cursor position of the brush (in world space).
/// </summary>
public Vector3 CursorPosition { get; private set; }
/// <summary>
/// Flag used to indicate whenever last cursor position of the brush is valid.
/// </summary>
public bool HasValidHit { get; private set; }
/// <summary>
/// Describes the terrain patch link.
/// </summary>
public struct PatchLocation
{
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
}
/// <summary>
/// The selected terrain patches collection that are under cursor (affected by the brush).
/// </summary>
public readonly List<PatchLocation> PatchesUnderCursor = new List<PatchLocation>();
/// <summary>
/// Describes the terrain chunk link.
/// </summary>
public struct ChunkLocation
{
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
/// <summary>
/// The chunk coordinates.
/// </summary>
public Int2 ChunkCoord;
}
/// <summary>
/// The selected terrain chunk collection that are under cursor (affected by the brush).
/// </summary>
public readonly List<ChunkLocation> ChunksUnderCursor = new List<ChunkLocation>();
/// <summary>
/// Gets the selected terrain actor (see <see cref="Modules.SceneEditingModule"/>).
/// </summary>
public FlaxEngine.Terrain SelectedTerrain
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
return (FlaxEngine.Terrain)terrainNode?.Actor;
}
}
/// <summary>
/// Gets the world bounds of the brush located at the current cursor position (defined by <see cref="CursorPosition"/>). Valid only if <see cref="HasValidHit"/> is set to true.
/// </summary>
public BoundingBox CursorBrushBounds
{
get
{
const float brushExtentY = 10000.0f;
float brushSizeHalf = CurrentBrush.Size * 0.5f;
Vector3 center = CursorPosition;
BoundingBox box;
box.Minimum = new Vector3(center.X - brushSizeHalf, center.Y - brushSizeHalf - brushExtentY, center.Z - brushSizeHalf);
box.Maximum = new Vector3(center.X + brushSizeHalf, center.Y + brushSizeHalf + brushExtentY, center.Z + brushSizeHalf);
return box;
}
}
/// <summary>
/// Gets the heightmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
/// </summary>
/// <param name="size">The minimum buffer size (in bytes).</param>
/// <returns>The allocated memory using <see cref="Marshal"/> interface.</returns>
public IntPtr GetHeightmapTempBuffer(int size)
{
if (_cachedHeightmapDataSize < size)
{
if (_cachedHeightmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedHeightmapData);
}
_cachedHeightmapData = Marshal.AllocHGlobal(size);
_cachedHeightmapDataSize = size;
}
return _cachedHeightmapData;
}
/// <summary>
/// Gets the current edit terrain undo system action. Use it to record the data for the undo restoring after terrain editing.
/// </summary>
internal EditTerrainMapAction CurrentEditUndoAction => _activeAction;
/// <inheritdoc />
public override void Init(MainEditorGizmoViewport viewport)
{
base.Init(viewport);
Gizmo = new SculptTerrainGizmo(viewport, this);
Gizmo.PaintStarted += OnPaintStarted;
Gizmo.PaintEnded += OnPaintEnded;
}
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
Viewport.Gizmos.Active = Gizmo;
ClearCursor();
}
/// <inheritdoc />
public override void OnDeactivated()
{
base.OnDeactivated();
// Free temporary memory buffer
if (_cachedHeightmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedHeightmapData);
_cachedHeightmapData = IntPtr.Zero;
_cachedHeightmapDataSize = 0;
}
}
/// <summary>
/// Clears the cursor location information cached within the gizmo mode.
/// </summary>
public void ClearCursor()
{
HasValidHit = false;
PatchesUnderCursor.Clear();
ChunksUnderCursor.Clear();
}
/// <summary>
/// Sets the cursor location in the world space. Updates the brush location and cached affected chunks.
/// </summary>
/// <param name="hitPosition">The cursor hit location on the selected terrain.</param>
public void SetCursor(ref Vector3 hitPosition)
{
HasValidHit = true;
CursorPosition = hitPosition;
PatchesUnderCursor.Clear();
ChunksUnderCursor.Clear();
// Find patches and chunks affected by the brush
var terrain = SelectedTerrain;
if (terrain == null)
throw new InvalidOperationException("Cannot set cursor then no terrain is selected.");
var brushBounds = CursorBrushBounds;
var patchesCount = terrain.PatchesCount;
BoundingBox tmp;
for (int patchIndex = 0; patchIndex < patchesCount; patchIndex++)
{
terrain.GetPatchBounds(patchIndex, out tmp);
if (!tmp.Intersects(ref brushBounds))
continue;
terrain.GetPatchCoord(patchIndex, out var patchCoord);
PatchesUnderCursor.Add(new PatchLocation { PatchCoord = patchCoord });
for (int chunkIndex = 0; chunkIndex < FlaxEngine.Terrain.PatchChunksCount; chunkIndex++)
{
terrain.GetChunkBounds(patchIndex, chunkIndex, out tmp);
if (!tmp.Intersects(ref brushBounds))
continue;
var chunkCoord = new Int2(chunkIndex % FlaxEngine.Terrain.PatchEdgeChunksCount, chunkIndex / FlaxEngine.Terrain.PatchEdgeChunksCount);
ChunksUnderCursor.Add(new ChunkLocation { PatchCoord = patchCoord, ChunkCoord = chunkCoord });
}
}
}
private void OnPaintStarted()
{
if (_activeAction != null)
throw new InvalidOperationException("Terrain paint start/end resynchronization.");
var terrain = SelectedTerrain;
if (CurrentMode.EditHoles)
_activeAction = new EditTerrainHolesMapAction(terrain);
else
_activeAction = new EditTerrainHeightMapAction(terrain);
}
private void OnPaintEnded()
{
if (_activeAction != null)
{
if (_activeAction.HasAnyModification)
{
_activeAction.OnEditingEnd();
Editor.Instance.Undo.AddAction(_activeAction);
}
_activeAction = null;
}
}
}
}

View File

@@ -0,0 +1,479 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "TerrainTools.h"
#include "Engine/Core/Cache.h"
#include "Engine/Core/Math/VectorInt.h"
#include "Engine/Core/Math/Color32.h"
#include "Engine/Core/Collections/CollectionPoolCache.h"
#include "Engine/Terrain/TerrainPatch.h"
#include "Engine/Terrain/Terrain.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Platform/FileSystem.h"
#include "FlaxEngine.Gen.h"
bool TerrainTools::TryGetPatchCoordToAdd(Terrain* terrain, const Ray& ray, Int2& result)
{
CHECK_RETURN(terrain, true);
result = Int2::Zero;
const float patchSize = terrain->GetChunkSize() * TERRAIN_UNITS_PER_VERTEX * TerrainPatch::CHUNKS_COUNT_EDGE;
// Try to pick any of the patch edges
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
const auto patch = terrain->GetPatch(patchIndex);
const auto x = patch->GetX();
const auto z = patch->GetZ();
const auto bounds = patch->GetBounds();
// TODO: use chunk neighbors to reduce algorithm complexity
#define CHECK_EDGE(dx, dz) \
if (terrain->GetPatch(x + dx, z + dz) == nullptr) \
{ \
if (bounds.MakeOffsetted(Vector3(patchSize * dx, 0, patchSize * dz)).Intersects(ray)) \
{ \
result = Int2(x + dx, z + dz); \
return true; \
} \
}
CHECK_EDGE(1, 0);
CHECK_EDGE(-1, 0);
CHECK_EDGE(0, 1);
CHECK_EDGE(0, -1);
CHECK_EDGE(1, 1);
CHECK_EDGE(1, -1);
CHECK_EDGE(1, 1);
CHECK_EDGE(-1, -1);
CHECK_EDGE(-1, 1);
#undef CHECK_EDGE
}
// Use the default patch if none added
if (terrain->GetPatchesCount() == 0)
{
return true;
}
return false;
}
struct TextureDataResult
{
FlaxStorage::LockData Lock;
BytesContainer Mip0Data;
TextureData Tmp;
uint32 RowPitch, SlicePitch;
PixelFormat Format;
Int2 Mip0Size;
BytesContainer* Mip0DataPtr;
TextureDataResult()
: Lock(FlaxStorage::LockData::Invalid)
{
}
};
bool GetTextureDataForSampling(Texture* texture, TextureDataResult& data)
{
// Lock asset chunks (if not virtual)
data.Lock = texture->LockData();
// Get the highest mip
{
PROFILE_CPU_NAMED("GetMipData");
texture->GetMipDataWithLoading(0, data.Mip0Data);
if (data.Mip0Data.IsInvalid())
{
LOG(Warning, "Failed to get texture data.");
return true;
}
if (!texture->GetMipDataCustomPitch(0, data.RowPitch, data.SlicePitch))
texture->GetTexture()->ComputePitch(0, data.RowPitch, data.SlicePitch);
}
data.Mip0Size = Int2(texture->GetTexture()->Size());
data.Format = texture->GetTexture()->Format();
// Decompress or convert data if need to
data.Mip0DataPtr = &data.Mip0Data;
if (PixelFormatExtensions::IsCompressed(data.Format))
{
PROFILE_CPU_NAMED("Decompress");
// Prepare source data descriptor (no data copy, just link the mip data)
TextureData src;
src.Width = data.Mip0Size.X;
src.Height = data.Mip0Size.Y;
src.Depth = 1;
src.Format = data.Format;
src.Items.Resize(1);
src.Items[0].Mips.Resize(1);
auto& srcMip = src.Items[0].Mips[0];
srcMip.Data.Link(data.Mip0Data);
srcMip.DepthPitch = data.SlicePitch;
srcMip.RowPitch = data.RowPitch;
srcMip.Lines = src.Height;
// Decompress texture
if (TextureTool::Convert(data.Tmp, src, PixelFormat::R8G8B8A8_UNorm))
{
LOG(Warning, "Failed to decompress data.");
return true;
}
// Override source data and format
data.Format = data.Tmp.Format;
data.RowPitch = data.Tmp.Items[0].Mips[0].RowPitch;
data.SlicePitch = data.Tmp.Items[0].Mips[0].DepthPitch;
data.Mip0DataPtr = &data.Tmp.Items[0].Mips[0].Data;
}
// TODO: convert to RGBA from other formats that cannot be sampled?
// Check if can even sample the given format
const auto sampler = TextureTool::GetSampler(data.Format);
if (sampler == nullptr)
{
LOG(Warning, "Texture format {0} cannot be sampled.", (int32)data.Format);
return true;
}
return false;
}
bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches, Texture* heightmap, float heightmapScale, Texture* splatmap1, Texture* splatmap2)
{
CHECK_RETURN(terrain && terrain->GetChunkSize() != 0, true);
if (numberOfPatches.X < 1 || numberOfPatches.Y < 1)
{
LOG(Warning, "Cannot setup terain with no patches.");
return false;
}
PROFILE_CPU_NAMED("Terrain.GenerateTerrain");
// Wait for assets to be loaded
if (heightmap && heightmap->WaitForLoaded())
{
LOG(Warning, "Loading heightmap texture failed.");
return true;
}
if (splatmap1 && splatmap1->WaitForLoaded())
{
LOG(Warning, "Loading splatmap texture failed.");
return true;
}
if (splatmap2 && splatmap2->WaitForLoaded())
{
LOG(Warning, "Loading splatmap texture failed.");
return true;
}
// Spawn patches
terrain->AddPatches(numberOfPatches);
// Prepare data
const auto heightmapSize = terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1;
Array<float> heightmapData;
heightmapData.Resize(heightmapSize * heightmapSize);
// Get heightmap data
if (heightmap && !Math::IsZero(heightmapScale))
{
// Get data
TextureDataResult dataHeightmap;
if (GetTextureDataForSampling(heightmap, dataHeightmap))
return true;
const auto sampler = TextureTool::GetSampler(dataHeightmap.Format);
// Initialize with sub-range of the input heightmap
const Vector2 uvPerPatch = Vector2::One / Vector2(numberOfPatches);
const float heightmapSizeInv = 1.0f / (heightmapSize - 1);
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
auto patch = terrain->GetPatch(patchIndex);
const Vector2 uvStart = Vector2((float)patch->GetX(), (float)patch->GetZ()) * uvPerPatch;
// Sample heightmap pixels with interpolation to get actual heightmap vertices locations
for (int32 z = 0; z < heightmapSize; z++)
{
for (int32 x = 0; x < heightmapSize; x++)
{
const Vector2 uv = uvStart + Vector2(x * heightmapSizeInv, z * heightmapSizeInv) * uvPerPatch;
const Color color = TextureTool::SampleLinear(sampler, uv, dataHeightmap.Mip0DataPtr->Get(), dataHeightmap.Mip0Size, dataHeightmap.RowPitch);
heightmapData[z * heightmapSize + x] = color.R * heightmapScale;
}
}
if (patch->SetupHeightMap(heightmapData.Count(), heightmapData.Get(), nullptr))
return true;
}
}
else
{
// Initialize flat heightmap data
heightmapData.SetAll(0.0f);
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
auto patch = terrain->GetPatch(patchIndex);
if (patch->SetupHeightMap(heightmapData.Count(), heightmapData.Get(), nullptr))
return true;
}
}
// Initialize terrain layers weights
Texture* splatmaps[2] = { splatmap1, splatmap2 };
Array<Color32> splatmapData;
TextureDataResult data1;
const Vector2 uvPerPatch = Vector2::One / Vector2(numberOfPatches);
const float heightmapSizeInv = 1.0f / (heightmapSize - 1);
for (int32 index = 0; index < ARRAY_COUNT(splatmaps); index++)
{
const auto splatmap = splatmaps[index];
if (!splatmap)
continue;
// Prepare data
if (splatmapData.IsEmpty())
splatmapData.Resize(heightmapSize * heightmapSize);
// Get splatmap data
if (GetTextureDataForSampling(splatmap, data1))
return true;
const auto sampler = TextureTool::GetSampler(data1.Format);
// Modify heightmap splatmaps with sub-range of the input splatmaps
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
auto patch = terrain->GetPatch(patchIndex);
const Vector2 uvStart = Vector2((float)patch->GetX(), (float)patch->GetZ()) * uvPerPatch;
// Sample splatmap pixels with interpolation to get actual splatmap values
for (int32 z = 0; z < heightmapSize; z++)
{
for (int32 x = 0; x < heightmapSize; x++)
{
const Vector2 uv = uvStart + Vector2(x * heightmapSizeInv, z * heightmapSizeInv) * uvPerPatch;
const Color color = TextureTool::SampleLinear(sampler, uv, data1.Mip0DataPtr->Get(), data1.Mip0Size, data1.RowPitch);
Color32 layers;
layers.R = (byte)(Math::Min(1.0f, color.R) * 255.0f);
layers.G = (byte)(Math::Min(1.0f, color.G) * 255.0f);
layers.B = (byte)(Math::Min(1.0f, color.B) * 255.0f);
layers.A = (byte)(Math::Min(1.0f, color.A) * 255.0f);
splatmapData[z * heightmapSize + x] = layers;
}
}
if (patch->ModifySplatMap(index, splatmapData.Get(), Int2::Zero, Int2(heightmapSize)))
return true;
}
}
return false;
}
StringAnsi TerrainTools::SerializePatch(Terrain* terrain, const Int2& patchCoord)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK_RETURN(patch, StringAnsi::Empty);
rapidjson_flax::StringBuffer buffer;
CompactJsonWriter writerObj(buffer);
JsonWriter& writer = writerObj;
writer.StartObject();
patch->Serialize(writer, nullptr);
writer.EndObject();
return StringAnsi(buffer.GetString());
}
void TerrainTools::DeserializePatch(Terrain* terrain, const Int2& patchCoord, const StringAnsiView& value)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK(patch);
rapidjson_flax::Document document;
document.Parse(value.Get(), value.Length());
CHECK(!document.HasParseError());
auto modifier = Cache::ISerializeModifier.Get();
modifier->EngineBuild = FLAXENGINE_VERSION_BUILD;
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
patch->Deserialize(document, modifier.Value);
patch->UpdatePostManualDeserialization();
}
bool TerrainTools::InitializePatch(Terrain* terrain, const Int2& patchCoord)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK_RETURN(patch, true);
return patch->InitializeHeightMap();
}
bool TerrainTools::ModifyHeightMap(Terrain* terrain, const Int2& patchCoord, float* samples, const Int2& offset, const Int2& size)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK_RETURN(patch, true);
return patch->ModifyHeightMap(samples, offset, size);
}
bool TerrainTools::ModifyHolesMask(Terrain* terrain, const Int2& patchCoord, byte* samples, const Int2& offset, const Int2& size)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK_RETURN(patch, true);
return patch->ModifyHolesMask(samples, offset, size);
}
bool TerrainTools::ModifySplatMap(Terrain* terrain, const Int2& patchCoord, int32 index, Color32* samples, const Int2& offset, const Int2& size)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK_RETURN(patch, true);
CHECK_RETURN(index >= 0 && index < TERRAIN_MAX_SPLATMAPS_COUNT, true);
return patch->ModifySplatMap(index, samples, offset, size);
}
float* TerrainTools::GetHeightmapData(Terrain* terrain, const Int2& patchCoord)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK_RETURN(patch, nullptr);
return patch->GetHeightmapData();
}
byte* TerrainTools::GetHolesMaskData(Terrain* terrain, const Int2& patchCoord)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK_RETURN(patch, nullptr);
return patch->GetHolesMaskData();
}
Color32* TerrainTools::GetSplatMapData(Terrain* terrain, const Int2& patchCoord, int32 index)
{
auto patch = terrain->GetPatch(patchCoord);
CHECK_RETURN(patch, nullptr);
return patch->GetSplatMapData(index);
}
bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
{
CHECK_RETURN(terrain && terrain->GetPatchesCount() != 0, true);
const auto firstPatch = terrain->GetPatch(0);
// Calculate texture size
const int32 patchEdgeVertexCount = terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1;
const int32 patchVertexCount = patchEdgeVertexCount * patchEdgeVertexCount;
// Find size of heightmap in patches
Int2 start(firstPatch->GetX(), firstPatch->GetZ());
Int2 end(start);
for (int32 i = 0; i < terrain->GetPatchesCount(); i++)
{
const int32 x = terrain->GetPatch(i)->GetX();
const int32 y = terrain->GetPatch(i)->GetZ();
if (x < start.X)
start.X = x;
if (y < start.Y)
start.Y = y;
if (x > end.X)
end.X = x;
if (y > end.Y)
end.Y = y;
}
const Int2 size = (end + 1) - start;
// Allocate - with space for non-existent patches
Array<float> heightmap;
heightmap.Resize(patchVertexCount * size.X * size.Y);
// Set to any element, where: min < elem < max
heightmap.SetAll(firstPatch->GetHeightmapData()[0]);
const int32 heightmapWidth = patchEdgeVertexCount * size.X;
// Fill heightmap with data
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
// Pick a patch
const auto patch = terrain->GetPatch(patchIndex);
const float* data = patch->GetHeightmapData();
// Beginning of patch
int32 dstIndex = (patch->GetX() - start.X) * patchEdgeVertexCount +
(patch->GetZ() - start.Y) * size.Y * patchVertexCount;
// Iterate over lines in patch
for (int32 z = 0; z < patchEdgeVertexCount; z++)
{
// Iterate over vertices in line
for (int32 x = 0; x < patchEdgeVertexCount; x++)
{
heightmap[dstIndex + x] = data[z * patchEdgeVertexCount + x];
}
dstIndex += heightmapWidth;
}
}
// Interpolate to 16-bit int
float maxHeight, minHeight;
maxHeight = minHeight = heightmap[0];
for (int32 i = 1; i < heightmap.Count(); i++)
{
float h = heightmap[i];
if (maxHeight < h)
maxHeight = h;
else if (minHeight > h)
minHeight = h;
}
const float maxValue = 65535.0f;
const float alpha = maxValue / (maxHeight - minHeight);
// Storage for pixel data
Array<uint16> byteHeightmap(heightmap.Capacity());
for (auto& elem : heightmap)
{
byteHeightmap.Add(static_cast<uint16>(alpha * (elem - minHeight)));
}
// Create texture
TextureData textureData;
textureData.Height = textureData.Width = heightmapWidth;
textureData.Depth = 1;
textureData.Format = PixelFormat::R16_UNorm;
textureData.Items.Resize(1);
textureData.Items[0].Mips.Resize(1);
// Fill mip data
TextureMipData* srcMip = textureData.GetData(0, 0);
srcMip->Data.Link(byteHeightmap.Get());
srcMip->Lines = textureData.Height;
srcMip->RowPitch = textureData.Width * 2; // 2 bytes per pixel for format R16
srcMip->DepthPitch = srcMip->Lines * srcMip->RowPitch;
// Find next non-existing file heightmap file
FileSystem::NormalizePath(outputFolder);
const String baseFileName(TEXT("heightmap"));
String outputPath;
for (int32 i = 0; i < MAX_int32; i++)
{
outputPath = outputFolder / baseFileName + StringUtils::ToString(i) + TEXT(".png");
if (!FileSystem::FileExists(outputPath))
break;
}
return TextureTool::ExportTexture(outputPath, textureData);
}

View File

@@ -0,0 +1,128 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingType.h"
class Terrain;
class Texture;
/// <summary>
/// Terrain* tools for editor. Allows to create and modify terrain.
/// </summary>
API_CLASS(Static, Namespace="FlaxEditor") class TerrainTools
{
DECLARE_SCRIPTING_TYPE_NO_SPAWN(TerrainTools);
/// <summary>
/// Checks if a given ray hits any of the terrain patches sides to add a new patch there.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="ray">The ray to use for tracing (eg. mouse ray in world space).</param>
/// <param name="result">The result patch coordinates (x and z). Valid only when method returns true.</param>
/// <returns>True if result is valid, otherwise nothing to add there.</returns>
API_FUNCTION() static bool TryGetPatchCoordToAdd(Terrain* terrain, const Ray& ray, API_PARAM(Out) Int2& result);
/// <summary>
/// Generates the terrain from the input heightmap and splat maps.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="numberOfPatches">The number of patches (X and Z axis).</param>
/// <param name="heightmap">The heightmap texture.</param>
/// <param name="heightmapScale">The heightmap scale. Applied to adjust the normalized heightmap values into the world units.</param>
/// <param name="splatmap1">The custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers composting. It's optional.</param>
/// <param name="splatmap2">The custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers composting. It's optional.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() static bool GenerateTerrain(Terrain* terrain, API_PARAM(Ref) const Int2& numberOfPatches, Texture* heightmap, float heightmapScale, Texture* splatmap1, Texture* splatmap2);
/// <summary>
/// Serializes the terrain chunk data to JSON string.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z).</param>
/// <returns>The serialized chunk data.</returns>
API_FUNCTION() static StringAnsi SerializePatch(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord);
/// <summary>
/// Deserializes the terrain chunk data from the JSON string.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z).</param>
/// <param name="value">The JSON string with serialized patch data.</param>
API_FUNCTION() static void DeserializePatch(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, const StringAnsiView& value);
/// <summary>
/// Initializes the patch heightmap and collision to the default flat level.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z) to initialize it.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() static bool InitializePatch(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord);
/// <summary>
/// Modifies the terrain patch heightmap with the given samples.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z) to modify it.</param>
/// <param name="samples">The samples. The array length is size.X*size.Y. It has to be type of float.</param>
/// <param name="offset">The offset from the first row and column of the heightmap data (offset destination x and z start position).</param>
/// <param name="size">The size of the heightmap to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() static bool ModifyHeightMap(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, float* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size);
/// <summary>
/// Modifies the terrain patch holes mask with the given samples.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z) to modify it.</param>
/// <param name="samples">The samples. The array length is size.X*size.Y. It has to be type of byte.</param>
/// <param name="offset">The offset from the first row and column of the mask data (offset destination x and z start position).</param>
/// <param name="size">The size of the mask to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() static bool ModifyHolesMask(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, byte* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size);
/// <summary>
/// Modifies the terrain patch splat map (layers mask) with the given samples.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z) to modify it.</param>
/// <param name="index">The zero-based splatmap texture index.</param>
/// <param name="samples">The samples. The array length is size.X*size.Y. It has to be type of <see cref="Color32"/>.</param>
/// <param name="offset">The offset from the first row and column of the splatmap data (offset destination x and z start position).</param>
/// <param name="size">The size of the splatmap to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() static bool ModifySplatMap(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, int32 index, Color32* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size);
/// <summary>
/// Gets the raw pointer to the heightmap data (cached internally by the c++ core in editor).
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z) to gather it.</param>
/// <returns>The pointer to the array of floats with terrain patch heights data. Null if failed to gather the data.</returns>
API_FUNCTION() static float* GetHeightmapData(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord);
/// <summary>
/// Gets the raw pointer to the holes mask data (cached internally by the c++ core in editor).
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z) to gather it.</param>
/// <returns>The pointer to the array of bytes with terrain patch holes mask data. Null if failed to gather the data.</returns>
API_FUNCTION() static byte* GetHolesMaskData(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord);
/// <summary>
/// Gets the raw pointer to the splatmap data (cached internally by the c++ core in editor).
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="patchCoord">The patch coordinates (x and z) to gather it.</param>
/// <param name="index">The zero-based splatmap texture index.</param>
/// <returns>The pointer to the array of Color32 with terrain patch packed splatmap data. Null if failed to gather the data.</returns>
API_FUNCTION() static Color32* GetSplatMapData(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, int32 index);
/// <summary>
/// Export terrain's heightmap as a texture.
/// </summary>
/// <param name="terrain">The terrain.</param>
/// <param name="outputFolder">The output folder path</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() static bool ExportTerrain(Terrain* terrain, String outputFolder);
};

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Undo
{
/// <summary>
/// The terrain heightmap editing action that records before and after states to swap between unmodified and modified terrain data.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
/// <seealso cref="EditTerrainMapAction" />
[Serializable]
unsafe class EditTerrainHeightMapAction : EditTerrainMapAction
{
/// <summary>
/// Initializes a new instance of the <see cref="EditTerrainHeightMapAction"/> class.
/// </summary>
/// <param name="terrain">The terrain.</param>
public EditTerrainHeightMapAction(FlaxEngine.Terrain terrain)
: base(terrain, sizeof(float))
{
}
/// <inheritdoc />
public override string ActionString => "Edit terrain heightmap";
/// <inheritdoc />
protected override IntPtr GetData(ref Int2 patchCoord, object tag)
{
return new IntPtr(TerrainTools.GetHeightmapData(Terrain, ref patchCoord));
}
/// <inheritdoc />
protected override void SetData(ref Int2 patchCoord, IntPtr data, object tag)
{
var offset = Int2.Zero;
var size = new Int2((int)Mathf.Sqrt(_heightmapLength));
if (TerrainTools.ModifyHeightMap(Terrain, ref patchCoord, (float*)data, ref offset, ref size))
throw new FlaxException("Failed to modify the heightmap.");
}
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Undo
{
/// <summary>
/// The terrain holes mask editing action that records before and after states to swap between unmodified and modified terrain data.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
/// <seealso cref="EditTerrainMapAction" />
[Serializable]
unsafe class EditTerrainHolesMapAction : EditTerrainMapAction
{
/// <summary>
/// Initializes a new instance of the <see cref="EditTerrainHolesMapAction"/> class.
/// </summary>
/// <param name="terrain">The terrain.</param>
public EditTerrainHolesMapAction(FlaxEngine.Terrain terrain)
: base(terrain, sizeof(byte))
{
}
/// <inheritdoc />
public override string ActionString => "Edit terrain holes";
/// <inheritdoc />
protected override IntPtr GetData(ref Int2 patchCoord, object tag)
{
return new IntPtr(TerrainTools.GetHolesMaskData(Terrain, ref patchCoord));
}
/// <inheritdoc />
protected override void SetData(ref Int2 patchCoord, IntPtr data, object tag)
{
var offset = Int2.Zero;
var size = new Int2((int)Mathf.Sqrt(_heightmapLength));
if (TerrainTools.ModifyHolesMask(Terrain, ref patchCoord, (byte*)data, ref offset, ref size))
throw new FlaxException("Failed to modify the terrain holes.");
}
}
}

View File

@@ -0,0 +1,216 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Undo
{
/// <summary>
/// The terrain heightmap or visibility map editing action that records before and after states to swap between unmodified and modified terrain data.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
abstract class EditTerrainMapAction : IUndoAction
{
/// <summary>
/// The compact data for the terrain patch modification.
/// </summary>
[Serializable]
protected struct PatchData
{
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
/// <summary>
/// The data before (allocated on heap memory or null).
/// </summary>
public IntPtr Before;
/// <summary>
/// The data after (allocated on heap memory or null).
/// </summary>
public IntPtr After;
/// <summary>
/// The custom tag.
/// </summary>
public object Tag;
}
/// <summary>
/// The terrain (actor Id).
/// </summary>
[Serialize]
protected readonly Guid _terrain;
/// <summary>
/// The heightmap length (vertex count).
/// </summary>
[Serialize]
protected readonly int _heightmapLength;
/// <summary>
/// The heightmap data size (in bytes).
/// </summary>
[Serialize]
protected readonly int _heightmapDataSize;
/// <summary>
/// The terrain patches
/// </summary>
[Serialize]
protected readonly List<PatchData> _patches;
/// <summary>
/// Gets a value indicating whether this action has any modification to the terrain (recorded patches changes).
/// </summary>
[NoSerialize]
public bool HasAnyModification => _patches.Count > 0;
/// <summary>
/// Gets the terrain.
/// </summary>
[NoSerialize]
public FlaxEngine.Terrain Terrain
{
get
{
var terrainId = _terrain;
return FlaxEngine.Object.Find<FlaxEngine.Terrain>(ref terrainId);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="EditTerrainMapAction"/> class.
/// </summary>
/// <remarks>Use <see cref="AddPatch"/> to mark new patches to record and <see cref="OnEditingEnd"/> to finalize patches data after editing action.</remarks>
/// <param name="terrain">The terrain.</param>
/// <param name="stride">The data stride (eg. sizeof(float)).</param>
protected EditTerrainMapAction(FlaxEngine.Terrain terrain, int stride)
{
_terrain = terrain.ID;
_patches = new List<PatchData>(4);
var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
_heightmapLength = heightmapSize * heightmapSize;
_heightmapDataSize = _heightmapLength * stride;
}
/// <summary>
/// Checks if the patch at the given coordinates has been already added.
/// </summary>
/// <param name="patchCoord">The patch coordinates.</param>
/// <returns>True if patch has been added, otherwise false.</returns>
public bool HashPatch(ref Int2 patchCoord)
{
for (int i = 0; i < _patches.Count; i++)
{
if (_patches[i].PatchCoord == patchCoord)
return true;
}
return false;
}
/// <summary>
/// Adds the patch to the action and records its current state.
/// </summary>
/// <param name="patchCoord">The patch coordinates.</param>
/// <param name="tag">The custom argument (per patch).</param>
public void AddPatch(ref Int2 patchCoord, object tag = null)
{
var data = Marshal.AllocHGlobal(_heightmapDataSize);
var source = GetData(ref patchCoord, tag);
Utils.MemoryCopy(source, data, _heightmapDataSize);
_patches.Add(new PatchData
{
PatchCoord = patchCoord,
Before = data,
After = IntPtr.Zero,
Tag = tag,
});
}
/// <summary>
/// Called when terrain action editing ends. Record the `after` state of the patches.
/// </summary>
public void OnEditingEnd()
{
if (_patches.Count == 0)
return;
for (int i = 0; i < _patches.Count; i++)
{
var patch = _patches[i];
if (patch.After != IntPtr.Zero)
throw new InvalidOperationException("Invalid terrain edit undo action usage.");
var data = Marshal.AllocHGlobal(_heightmapDataSize);
var source = GetData(ref patch.PatchCoord, patch.Tag);
Utils.MemoryCopy(source, data, _heightmapDataSize);
patch.After = data;
_patches[i] = patch;
}
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);
}
/// <inheritdoc />
public abstract string ActionString { get; }
/// <inheritdoc />
public void Do()
{
Set(x => x.After);
}
/// <inheritdoc />
public void Undo()
{
Set(x => x.Before);
}
/// <inheritdoc />
public void Dispose()
{
// Ensure to release memory
for (int i = 0; i < _patches.Count; i++)
{
Marshal.FreeHGlobal(_patches[i].Before);
Marshal.FreeHGlobal(_patches[i].After);
}
_patches.Clear();
}
private void Set(Func<PatchData, IntPtr> dataGetter)
{
for (int i = 0; i < _patches.Count; i++)
{
var patch = _patches[i];
var data = dataGetter(patch);
SetData(ref patch.PatchCoord, data, patch.Tag);
}
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);
}
/// <summary>
/// Gets the patch data.
/// </summary>
/// <param name="patchCoord">The patch coordinates.</param>
/// <param name="tag">The custom argument (per patch).</param>
/// <returns>The data buffer (pointer to unmanaged memory).</returns>
protected abstract IntPtr GetData(ref Int2 patchCoord, object tag);
/// <summary>
/// Sets the patch data.
/// </summary>
/// <param name="patchCoord">The patch coordinates.</param>
/// <param name="data">The patch data.</param>
/// <param name="tag">The custom argument (per patch).</param>
protected abstract void SetData(ref Int2 patchCoord, IntPtr data, object tag);
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Undo
{
/// <summary>
/// The terrain splatmap editing action that records before and after states to swap between unmodified and modified terrain data.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
/// <seealso cref="EditTerrainMapAction" />
[Serializable]
unsafe class EditTerrainSplatMapAction : EditTerrainMapAction
{
/// <summary>
/// Initializes a new instance of the <see cref="EditTerrainSplatMapAction"/> class.
/// </summary>
/// <param name="terrain">The terrain.</param>
public EditTerrainSplatMapAction(FlaxEngine.Terrain terrain)
: base(terrain, Color32.SizeInBytes)
{
}
/// <inheritdoc />
public override string ActionString => "Edit terrain splatmap";
/// <inheritdoc />
protected override IntPtr GetData(ref Int2 patchCoord, object tag)
{
return new IntPtr(TerrainTools.GetSplatMapData(Terrain, ref patchCoord, (int)tag));
}
/// <inheritdoc />
protected override void SetData(ref Int2 patchCoord, IntPtr data, object tag)
{
var offset = Int2.Zero;
var size = new Int2((int)Mathf.Sqrt(_heightmapLength));
if (TerrainTools.ModifySplatMap(Terrain, ref patchCoord, (int)tag, (Color32*)data, ref offset, ref size))
throw new FlaxException("Failed to modify the splatmap.");
}
}
}