You're breathtaking!
This commit is contained in:
56
Source/Editor/Tools/Terrain/Brushes/Brush.cs
Normal file
56
Source/Editor/Tools/Terrain/Brushes/Brush.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
135
Source/Editor/Tools/Terrain/Brushes/CircleBrush.cs
Normal file
135
Source/Editor/Tools/Terrain/Brushes/CircleBrush.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
187
Source/Editor/Tools/Terrain/CarveTab.cs
Normal file
187
Source/Editor/Tools/Terrain/CarveTab.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
254
Source/Editor/Tools/Terrain/CreateTerrainDialog.cs
Normal file
254
Source/Editor/Tools/Terrain/CreateTerrainDialog.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
405
Source/Editor/Tools/Terrain/EditTab.cs
Normal file
405
Source/Editor/Tools/Terrain/EditTab.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
277
Source/Editor/Tools/Terrain/EditTerrainGizmo.cs
Normal file
277
Source/Editor/Tools/Terrain/EditTerrainGizmo.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Source/Editor/Tools/Terrain/EditTerrainGizmoMode.cs
Normal file
123
Source/Editor/Tools/Terrain/EditTerrainGizmoMode.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
234
Source/Editor/Tools/Terrain/Paint/Mode.cs
Normal file
234
Source/Editor/Tools/Terrain/Paint/Mode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
125
Source/Editor/Tools/Terrain/Paint/SingleLayerMode.cs
Normal file
125
Source/Editor/Tools/Terrain/Paint/SingleLayerMode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Source/Editor/Tools/Terrain/PaintTab.cs
Normal file
124
Source/Editor/Tools/Terrain/PaintTab.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
206
Source/Editor/Tools/Terrain/PaintTerrainGizmo.cs
Normal file
206
Source/Editor/Tools/Terrain/PaintTerrainGizmo.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
357
Source/Editor/Tools/Terrain/PaintTerrainGizmoMode.cs
Normal file
357
Source/Editor/Tools/Terrain/PaintTerrainGizmoMode.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs
Normal file
61
Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs
Normal file
60
Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
246
Source/Editor/Tools/Terrain/Sculpt/Mode.cs
Normal file
246
Source/Editor/Tools/Terrain/Sculpt/Mode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
65
Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs
Normal file
65
Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs
Normal file
47
Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs
Normal file
82
Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Source/Editor/Tools/Terrain/SculptTab.cs
Normal file
124
Source/Editor/Tools/Terrain/SculptTab.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
206
Source/Editor/Tools/Terrain/SculptTerrainGizmo.cs
Normal file
206
Source/Editor/Tools/Terrain/SculptTerrainGizmo.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
379
Source/Editor/Tools/Terrain/SculptTerrainGizmoMode.cs
Normal file
379
Source/Editor/Tools/Terrain/SculptTerrainGizmoMode.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
479
Source/Editor/Tools/Terrain/TerrainTools.cpp
Normal file
479
Source/Editor/Tools/Terrain/TerrainTools.cpp
Normal 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);
|
||||
}
|
||||
128
Source/Editor/Tools/Terrain/TerrainTools.h
Normal file
128
Source/Editor/Tools/Terrain/TerrainTools.h
Normal 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);
|
||||
};
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
216
Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs
Normal file
216
Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user