You're breathtaking!

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

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Foliage painting brush.
/// </summary>
public 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 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 effect.")]
public float Size = 800.0f;
/// <summary>
/// If checked, brush will apply only once on painting start instead of continuous painting.
/// </summary>
[EditorOrder(10), Tooltip("If checked, brush will apply only once on painting start instead of continuous painting.")]
public bool SingleClick = false;
/// <summary>
/// The additional scale for foliage density when painting. Can be used to increase or decrease foliage density during painting.
/// </summary>
[EditorOrder(20), Limit(0.0f, 1000.0f, 0.01f), Tooltip("The additional scale for foliage density when painting. Can be used to increase or decrease foliage density during painting.")]
public float DensityScale = 1.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>
/// <param name="sceneDepth">The scene depth buffer (used for manual brush pixels clipping with rendered scene).</param>
/// <returns>The ready to render material for terrain chunks overlay on top of the terrain.</returns>
public MaterialInstance GetBrushMaterial(ref Vector3 position, ref Color color, GPUTexture sceneDepth)
{
if (!_material)
{
var material = FlaxEngine.Content.LoadAsyncInternal<Material>(EditorAssets.FoliageBrushMaterial);
material.WaitForLoaded();
_material = material.CreateVirtualInstance();
}
if (_material)
{
_material.SetParameterValue("Color", color);
_material.SetParameterValue("DepthBuffer", sceneDepth);
}
return _material;
}
}
}

View File

@@ -0,0 +1,243 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Gizmo;
using FlaxEditor.Tools.Foliage.Undo;
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Gizmo for editing foliage instances. Managed by the <see cref="EditFoliageGizmoMode"/>.
/// </summary>
/// <seealso cref="FlaxEditor.Gizmo.TransformGizmoBase" />
public sealed class EditFoliageGizmo : TransformGizmoBase
{
private MaterialBase _highlightMaterial;
private bool _needSync = true;
private EditInstanceAction _action;
/// <summary>
/// The parent mode.
/// </summary>
public readonly EditFoliageGizmoMode GizmoMode;
/// <summary>
/// Initializes a new instance of the <see cref="EditFoliageGizmo"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="mode">The mode.</param>
public EditFoliageGizmo(IGizmoOwner owner, EditFoliageGizmoMode mode)
: base(owner)
{
GizmoMode = mode;
}
/// <inheritdoc />
protected override int SelectionCount
{
get
{
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
return 0;
var instanceIndex = GizmoMode.SelectedInstanceIndex;
if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount)
return 0;
return 1;
}
}
/// <inheritdoc />
protected override Transform GetSelectedObject(int index)
{
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
throw new InvalidOperationException("No foliage selected.");
var instanceIndex = GizmoMode.SelectedInstanceIndex;
if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount)
throw new InvalidOperationException("No foliage instance selected.");
var instance = foliage.GetInstance(instanceIndex);
return foliage.Transform.LocalToWorld(instance.Transform);
}
/// <inheritdoc />
protected override void GetSelectedObjectsBounds(out BoundingBox bounds, out bool navigationDirty)
{
bounds = BoundingBox.Empty;
navigationDirty = false;
}
/// <inheritdoc />
protected override void OnStartTransforming()
{
base.OnStartTransforming();
// Start undo
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
throw new InvalidOperationException("No foliage selected.");
_action = new EditInstanceAction(foliage, GizmoMode.SelectedInstanceIndex);
}
/// <inheritdoc />
protected override void OnApplyTransformation(ref Vector3 translationDelta, ref Quaternion rotationDelta, ref Vector3 scaleDelta)
{
base.OnApplyTransformation(ref translationDelta, ref rotationDelta, ref scaleDelta);
bool applyRotation = !rotationDelta.IsIdentity;
bool useObjCenter = ActivePivot == PivotType.ObjectCenter;
Vector3 gizmoPosition = Position;
// Get instance transform
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
throw new InvalidOperationException("No foliage selected.");
var instanceIndex = GizmoMode.SelectedInstanceIndex;
if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount)
throw new InvalidOperationException("No foliage instance selected.");
var instance = foliage.GetInstance(instanceIndex);
var trans = foliage.Transform.LocalToWorld(instance.Transform);
// Apply rotation
if (applyRotation)
{
Vector3 pivotOffset = trans.Translation - gizmoPosition;
if (useObjCenter || pivotOffset.IsZero)
{
trans.Orientation *= Quaternion.Invert(trans.Orientation) * rotationDelta * trans.Orientation;
}
else
{
Matrix.RotationQuaternion(ref trans.Orientation, out var transWorld);
Matrix.RotationQuaternion(ref rotationDelta, out var deltaWorld);
Matrix world = transWorld * Matrix.Translation(pivotOffset) * deltaWorld * Matrix.Translation(-pivotOffset);
trans.SetRotation(ref world);
trans.Translation += world.TranslationVector;
}
}
// Apply scale
const float scaleLimit = 99_999_999.0f;
trans.Scale = Vector3.Clamp(trans.Scale + scaleDelta, new Vector3(-scaleLimit), new Vector3(scaleLimit));
// Apply translation
trans.Translation += translationDelta;
// Transform foliage instance
instance.Transform = foliage.Transform.WorldToLocal(trans);
foliage.SetInstanceTransform(instanceIndex, ref instance.Transform);
foliage.RebuildClusters();
}
/// <inheritdoc />
protected override void OnEndTransforming()
{
base.OnEndTransforming();
// End undo
_action.RecordEnd();
Owner.Undo?.AddAction(_action);
_action = null;
}
/// <inheritdoc />
protected override void OnDuplicate()
{
base.OnDuplicate();
// Get selected instance
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
throw new InvalidOperationException("No foliage selected.");
var instanceIndex = GizmoMode.SelectedInstanceIndex;
if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount)
throw new InvalidOperationException("No foliage instance selected.");
var instance = foliage.GetInstance(instanceIndex);
var action = new EditFoliageAction(foliage);
// Duplicate instance and select it
var newIndex = foliage.InstancesCount;
foliage.AddInstance(ref instance);
action.RecordEnd();
Owner.Undo?.AddAction(new MultiUndoAction(action, new EditSelectedInstanceIndexAction(GizmoMode.SelectedInstanceIndex, newIndex)));
GizmoMode.SelectedInstanceIndex = newIndex;
}
/// <inheritdoc />
public override void Draw(ref RenderContext renderContext)
{
base.Draw(ref renderContext);
if (!IsActive || !_highlightMaterial)
return;
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
return;
var instanceIndex = GizmoMode.SelectedInstanceIndex;
if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount)
return;
var instance = foliage.GetInstance(instanceIndex);
var model = foliage.GetFoliageType(instance.Type).Model;
if (model)
{
foliage.GetLocalToWorldMatrix(out var world);
instance.Transform.GetWorld(out var matrix);
Matrix.Multiply(ref matrix, ref world, out var instanceWorld);
model.Draw(ref renderContext, _highlightMaterial, ref instanceWorld, StaticFlags.None, false);
}
}
/// <inheritdoc />
public override void Pick()
{
// Ensure player is not moving objects
if (ActiveAxis != Axis.None)
return;
// Get mouse ray and try to hit foliage instance
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
return;
var ray = Owner.MouseRay;
foliage.Intersects(ref ray, out _, out _, out var instanceIndex);
// Change the selection (with undo)
if (GizmoMode.SelectedInstanceIndex == instanceIndex)
return;
var action = new EditSelectedInstanceIndexAction(GizmoMode.SelectedInstanceIndex, instanceIndex);
action.Do();
Owner.Undo?.AddAction(action);
}
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
_highlightMaterial = EditorAssets.Cache.HighlightMaterialInstance;
if (_needSync)
{
_needSync = false;
// Sync with main transform gizmo
var mainTransformGizmo = Editor.Instance.MainTransformGizmo;
ActiveMode = mainTransformGizmo.ActiveMode;
ActiveTransformSpace = mainTransformGizmo.ActiveTransformSpace;
mainTransformGizmo.ModeChanged += () => ActiveMode = mainTransformGizmo.ActiveMode;
mainTransformGizmo.TransformSpaceChanged += () => ActiveTransformSpace = mainTransformGizmo.ActiveTransformSpace;
}
}
/// <inheritdoc />
public override void OnDeactivated()
{
_highlightMaterial = null;
base.OnDeactivated();
}
}
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Foliage instances editing mode.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
public class EditFoliageGizmoMode : EditorGizmoMode
{
private int _selectedInstanceIndex = -1;
/// <summary>
/// The foliage painting gizmo.
/// </summary>
public EditFoliageGizmo Gizmo;
/// <summary>
/// The foliage editing selection outline.
/// </summary>
public EditFoliageSelectionOutline SelectionOutline;
/// <summary>
/// Gets the selected foliage actor (see <see cref="Modules.SceneEditingModule"/>).
/// </summary>
public FlaxEngine.Foliage SelectedFoliage
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var foliageNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as FoliageNode : null;
return (FlaxEngine.Foliage)foliageNode?.Actor;
}
}
/// <summary>
/// The selected foliage instance index.
/// </summary>
public int SelectedInstanceIndex
{
get => _selectedInstanceIndex;
set
{
if (_selectedInstanceIndex == value)
return;
_selectedInstanceIndex = value;
SelectedInstanceIndexChanged?.Invoke();
}
}
/// <summary>
/// Occurs when selected instance index gets changed.
/// </summary>
public event Action SelectedInstanceIndexChanged;
/// <inheritdoc />
public override void Init(MainEditorGizmoViewport viewport)
{
base.Init(viewport);
Gizmo = new EditFoliageGizmo(viewport, this);
SelectionOutline = FlaxEngine.Object.New<EditFoliageSelectionOutline>();
SelectionOutline.GizmoMode = this;
}
/// <inheritdoc />
public override void Dispose()
{
FlaxEngine.Object.Destroy(ref SelectionOutline);
base.Dispose();
}
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
Viewport.Gizmos.Active = Gizmo;
Viewport.OverrideSelectionOutline(SelectionOutline);
SelectedInstanceIndex = -1;
}
/// <inheritdoc />
public override void OnDeactivated()
{
Viewport.OverrideSelectionOutline(null);
base.OnDeactivated();
}
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Gizmo;
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// The custom outline for drawing the selected foliage instances outlines.
/// </summary>
/// <seealso cref="FlaxEditor.Gizmo.SelectionOutline" />
[HideInEditor]
public class EditFoliageSelectionOutline : SelectionOutline
{
private StaticModel _staticModel;
/// <summary>
/// The parent mode.
/// </summary>
public EditFoliageGizmoMode GizmoMode;
/// <inheritdoc />
public override bool CanRender
{
get
{
if (!HasDataReady)
return false;
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
return false;
var instanceIndex = GizmoMode.SelectedInstanceIndex;
if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount)
return false;
return true;
}
}
/// <inheritdoc />
protected override void DrawSelectionDepth(GPUContext context, SceneRenderTask task, GPUTexture customDepth)
{
var foliage = GizmoMode.SelectedFoliage;
if (!foliage)
return;
var instanceIndex = GizmoMode.SelectedInstanceIndex;
if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount)
return;
// Draw single instance
var instance = foliage.GetInstance(instanceIndex);
var model = foliage.GetFoliageType(instance.Type).Model;
if (model)
{
Transform instanceWorld = foliage.Transform.LocalToWorld(instance.Transform);
if (!_staticModel)
{
_staticModel = new StaticModel();
_staticModel.StaticFlags = StaticFlags.None;
}
_staticModel.Model = model;
_staticModel.Transform = instanceWorld;
_actors.Add(_staticModel);
Renderer.DrawSceneDepth(context, task, customDepth, _actors);
}
}
}
}

View File

@@ -0,0 +1,244 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.GUI.Tabs;
using FlaxEditor.Tools.Foliage.Undo;
using FlaxEngine;
using FlaxEngine.GUI;
// ReSharper disable UnusedMember.Local
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Foliage instances editor tab. Allows to pick and edit a single foliage instance properties.
/// </summary>
/// <seealso cref="GUI.Tabs.Tab" />
public class EditTab : Tab
{
/// <summary>
/// The object for foliage settings adjusting via Custom Editor.
/// </summary>
[CustomEditor(typeof(ProxyObjectEditor))]
private sealed class ProxyObject
{
/// <summary>
/// The gizmo mode.
/// </summary>
[HideInEditor]
public readonly EditFoliageGizmoMode Mode;
/// <summary>
/// The selected foliage actor.
/// </summary>
[HideInEditor]
public FlaxEngine.Foliage Foliage;
/// <summary>
/// The selected foliage instance index.
/// </summary>
[HideInEditor]
public int InstanceIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ProxyObject"/> class.
/// </summary>
/// <param name="mode">The foliage editing gizmo mode.</param>
public ProxyObject(EditFoliageGizmoMode mode)
{
Mode = mode;
InstanceIndex = -1;
}
private FoliageInstance _instance;
public void SyncOptions()
{
if (Foliage != null && InstanceIndex > -1 && InstanceIndex < Foliage.InstancesCount)
{
_instance = Foliage.GetInstance(InstanceIndex);
}
}
public void SetOptions()
{
if (Foliage != null && InstanceIndex > -1 && InstanceIndex < Foliage.InstancesCount)
{
Foliage.SetInstanceTransform(InstanceIndex, ref _instance.Transform);
Foliage.RebuildClusters();
}
}
//
[EditorOrder(-10), EditorDisplay("Instance"), ReadOnly, Tooltip("The foliage instance zero-based index (read-only).")]
public int Index
{
get => InstanceIndex;
set => throw new Exception();
}
[EditorOrder(0), EditorDisplay("Instance"), ReadOnly, Tooltip("The foliage instance model (read-only).")]
public Model Model
{
get => _instance.Type >= 0 && _instance.Type < Foliage.InstancesCount ? Foliage.GetFoliageType(_instance.Type).Model : null;
set => throw new Exception();
}
[EditorOrder(10), EditorDisplay("Instance"), Tooltip("The local-space position of the mesh relative to the foliage actor.")]
public Vector3 Position
{
get => _instance.Transform.Translation;
set
{
_instance.Transform.Translation = value;
SetOptions();
}
}
[EditorOrder(20), EditorDisplay("Instance"), Tooltip("The local-space rotation of the mesh relative to the foliage actor.")]
public Quaternion Rotation
{
get => _instance.Transform.Orientation;
set
{
_instance.Transform.Orientation = value;
SetOptions();
}
}
[EditorOrder(30), EditorDisplay("Instance"), Tooltip("The local-space scale of the mesh relative to the foliage actor.")]
public Vector3 Scale
{
get => _instance.Transform.Scale;
set
{
_instance.Transform.Scale = value;
SetOptions();
}
}
}
/// <summary>
/// The custom editor for <see cref="ProxyObject"/>.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.GenericEditor" />
private sealed class ProxyObjectEditor : GenericEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
var proxyObject = (ProxyObject)Values[0];
if (proxyObject.InstanceIndex != -1)
{
var area = layout.Space(32);
var button = new Button(4, 6, 100)
{
Text = "Delete",
TooltipText = "Removes the selected foliage instance",
Parent = area.ContainerControl
};
button.Clicked += OnDeleteButtonClicked;
}
}
private void OnDeleteButtonClicked()
{
var proxyObject = (ProxyObject)Values[0];
var mode = proxyObject.Mode;
var foliage = mode.SelectedFoliage;
if (!foliage)
throw new InvalidOperationException("No foliage selected.");
var instanceIndex = mode.SelectedInstanceIndex;
if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount)
throw new InvalidOperationException("No foliage instance selected.");
// Delete instance and deselect it
var action = new DeleteInstanceAction(foliage, instanceIndex);
action.Do();
Editor.Instance.Undo?.AddAction(new MultiUndoAction(action, new EditSelectedInstanceIndexAction(instanceIndex, -1)));
mode.SelectedInstanceIndex = -1;
}
/// <inheritdoc />
public override void Refresh()
{
// Sync selected foliage options once before update to prevent too many data copies when fetching data from UI properties accessors
var proxyObject = (ProxyObject)Values[0];
proxyObject.SyncOptions();
base.Refresh();
}
}
private readonly ProxyObject _proxy;
private readonly CustomEditorPresenter _presenter;
/// <summary>
/// The parent foliage tab.
/// </summary>
public readonly FoliageTab Tab;
/// <summary>
/// The related gizmo mode.
/// </summary>
public readonly EditFoliageGizmoMode Mode;
/// <summary>
/// Initializes a new instance of the <see cref="EditTab"/> class.
/// </summary>
/// <param name="tab">The parent tab.</param>
/// <param name="mode">The related gizmo mode.</param>
public EditTab(FoliageTab tab, EditFoliageGizmoMode mode)
: base("Edit")
{
Mode = mode;
Tab = tab;
Tab.SelectedFoliageChanged += OnSelectedFoliageChanged;
mode.SelectedInstanceIndexChanged += OnSelectedInstanceIndexChanged;
_proxy = new ProxyObject(mode);
// Main panel
var panel = new Panel(ScrollBars.Vertical)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
// Options editor
var editor = new CustomEditorPresenter(tab.Editor.Undo, "No foliage instance selected");
editor.Panel.Parent = panel;
editor.Modified += OnModified;
_presenter = editor;
}
private void OnSelectedInstanceIndexChanged()
{
_proxy.InstanceIndex = Mode.SelectedInstanceIndex;
if (_proxy.InstanceIndex == -1)
{
_presenter.Deselect();
}
else
{
_presenter.Select(_proxy);
}
}
private void OnModified()
{
Editor.Instance.Scene.MarkSceneEdited(_proxy.Foliage?.Scene);
}
private void OnSelectedFoliageChanged()
{
Mode.SelectedInstanceIndex = -1;
_proxy.Foliage = Tab.SelectedFoliage;
}
}
}

View File

@@ -0,0 +1,252 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.GUI.Tabs;
using FlaxEditor.Modules;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Foliage editing tab. Supports different modes for foliage editing including spawning, removing, and managing tools.
/// </summary>
/// <seealso cref="Tab" />
[HideInEditor]
public class FoliageTab : Tab
{
private readonly Tabs _modes;
private readonly ContainerControl _noFoliagePanel;
private int _selectedFoliageTypeIndex = -1;
/// <summary>
/// The editor instance.
/// </summary>
public readonly Editor Editor;
/// <summary>
/// The cached selected foliage. It's synchronized with <see cref="SceneEditingModule.Selection"/>.
/// </summary>
public FlaxEngine.Foliage SelectedFoliage;
/// <summary>
/// Occurs when selected foliage gets changed (to a different value).
/// </summary>
public event Action SelectedFoliageChanged;
/// <summary>
/// Delegate signature for selected foliage index change.
/// </summary>
/// <param name="previousIndex">The index of the previous foliage type.</param>
/// <param name="currentIndex">The index of the current foliage type.</param>
public delegate void SelectedFoliageTypeIndexChangedDelegate(int previousIndex, int currentIndex);
/// <summary>
/// Occurs when selected foliage type index gets changed.
/// </summary>
public event SelectedFoliageTypeIndexChangedDelegate SelectedFoliageTypeIndexChanged;
/// <summary>
/// Occurs when selected foliage actors gets modification for foliage types collection (item added or removed). UI uses it to update the layout without manually tracking the collection.
/// </summary>
public event Action SelectedFoliageTypesChanged;
/// <summary>
/// Gets or sets the index of the selected foliage type.
/// </summary>
public int SelectedFoliageTypeIndex
{
get => _selectedFoliageTypeIndex;
set
{
var prev = _selectedFoliageTypeIndex;
if (value == prev)
return;
_selectedFoliageTypeIndex = value;
SelectedFoliageTypeIndexChanged?.Invoke(prev, value);
}
}
/// <summary>
/// The foliage types tab;
/// </summary>
public FoliageTypesTab FoliageTypes;
/// <summary>
/// The paint tab;
/// </summary>
public PaintTab Paint;
/// <summary>
/// The edit tab;
/// </summary>
public EditTab Edit;
/// <summary>
/// The foliage type model asset IDs checked to paint with them by default.
/// </summary>
public readonly Dictionary<Guid, bool> FoliageTypeModelIdsToPaint = new Dictionary<Guid, bool>();
/// <summary>
/// Initializes a new instance of the <see cref="FoliageTab"/> class.
/// </summary>
/// <param name="icon">The icon.</param>
/// <param name="editor">The editor instance.</param>
public FoliageTab(SpriteHandle icon, Editor editor)
: base(string.Empty, icon)
{
Editor = editor;
Editor.SceneEditing.SelectionChanged += OnSelectionChanged;
Selected += OnSelected;
_modes = new Tabs
{
Orientation = Orientation.Vertical,
UseScroll = true,
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
TabsSize = new Vector2(50, 32),
Parent = this
};
// Init tool modes
InitSculptMode();
InitPaintMode();
InitEditMode();
_modes.SelectedTabIndex = 0;
_noFoliagePanel = new ContainerControl
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
BackgroundColor = Style.Current.Background,
Parent = this
};
var noFoliageLabel = new Label
{
Text = "Select foliage to edit\nor\n\n\n\n",
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = _noFoliagePanel
};
var noFoliageButton = new Button
{
Text = "Create new foliage",
AnchorPreset = AnchorPresets.MiddleCenter,
Offsets = new Margin(-60, 120, -12, 24),
Parent = _noFoliagePanel,
};
noFoliageButton.Clicked += OnCreateNewFoliageClicked;
}
private void OnSelected(Tab tab)
{
// Auto select first foliage actor to make usage easier
if (Editor.SceneEditing.SelectionCount == 1 && Editor.SceneEditing.Selection[0] is SceneGraph.ActorNode actorNode && actorNode.Actor is FlaxEngine.Foliage)
return;
var actor = Level.FindActor<FlaxEngine.Foliage>();
if (actor)
{
Editor.SceneEditing.Select(actor);
}
}
private void OnCreateNewFoliageClicked()
{
// Create
var actor = new FlaxEngine.Foliage();
actor.StaticFlags = StaticFlags.FullyStatic;
actor.Name = "Foliage";
// Spawn
Editor.SceneEditing.Spawn(actor);
// Select
Editor.SceneEditing.Select(actor);
}
private void OnSelectionChanged()
{
var node = Editor.SceneEditing.SelectionCount > 0 ? Editor.SceneEditing.Selection[0] as FoliageNode : null;
var foliage = node?.Actor as FlaxEngine.Foliage;
if (foliage != SelectedFoliage)
{
SelectedFoliageTypeIndex = -1;
SelectedFoliage = foliage;
SelectedFoliageChanged?.Invoke();
}
_noFoliagePanel.Visible = foliage == null;
}
private void InitSculptMode()
{
var tab = _modes.AddTab(FoliageTypes = new FoliageTypesTab(this));
tab.Selected += OnTabSelected;
}
private void InitPaintMode()
{
var tab = _modes.AddTab(Paint = new PaintTab(this, Editor.Windows.EditWin.Viewport.PaintFoliageGizmo));
tab.Selected += OnTabSelected;
}
private void InitEditMode()
{
var tab = _modes.AddTab(Edit = new EditTab(this, Editor.Windows.EditWin.Viewport.EditFoliageGizmo));
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<NoGizmoMode>();
break;
case 1:
Editor.Windows.EditWin.Viewport.SetActiveMode<PaintFoliageGizmoMode>();
break;
case 2:
Editor.Windows.EditWin.Viewport.SetActiveMode<EditFoliageGizmoMode>();
break;
default: throw new IndexOutOfRangeException("Invalid foliage tab mode.");
}
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
FoliageTypes.CheckFoliageTypesCount();
base.Update(deltaTime);
}
internal void OnSelectedFoliageTypesChanged()
{
SelectedFoliageTypesChanged?.Invoke();
}
}
}

View File

@@ -0,0 +1,396 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "FoliageTools.h"
#include "Engine/Core/Math/BoundingSphere.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/SceneQuery.h"
#include "Engine/Terrain/Terrain.h"
#include "Engine/Terrain/TerrainPatch.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Foliage/Foliage.h"
#include "Engine/UI/TextRender.h"
#include "Engine/Core/Random.h"
struct GeometryTriangle
{
Vector3 Vertex;
Vector3 Vector1;
Vector3 Vector2;
Vector3 Normal;
float Area;
GeometryTriangle(const bool isDeterminantPositive, const Vector3& v0, const Vector3& v1, const Vector3& v2)
{
Vertex = v0;
Vector1 = v1 - Vertex;
Vector2 = v2 - Vertex;
Normal = isDeterminantPositive ? Vector1 ^ Vector2 : Vector2 ^ Vector1;
const float normalLength = Normal.Length();
Area = normalLength * 0.5f;
if (normalLength > ZeroTolerance)
{
Normal /= normalLength;
}
}
void GetRandomPoint(Vector3& result) const
{
// Sample parallelogram
float x = Random::Rand();
float y = Random::Rand();
// Flip if we're outside the triangle
if (x + y > 1.0f)
{
x = 1.0f - x;
y = 1.0f - y;
}
result = Vertex + x * Vector1 + y * Vector2;
}
};
template<>
struct TIsPODType<GeometryTriangle>
{
enum { Value = true };
};
struct GeometryLookup
{
BoundingSphere Brush;
Array<GeometryTriangle> Triangles;
Array<Vector3> TerrainCache;
GeometryLookup(const Vector3& brushPosition, float brushRadius)
: Brush(brushPosition, brushRadius)
{
}
static bool Search(Actor* actor, GeometryLookup& lookup)
{
// Early out if object is not intersecting with the foliage brush bounds
if (!actor->GetBox().Intersects(lookup.Brush))
return true;
const auto brush = lookup.Brush;
if (const auto* staticModel = dynamic_cast<StaticModel*>(actor))
{
// Skip if model is not loaded
if (staticModel->Model == nullptr || staticModel->Model->WaitForLoaded() || staticModel->Model->GetLoadedLODs() == 0)
return true;
PROFILE_CPU_NAMED("StaticModel");
// Check model meshes
Matrix world;
staticModel->GetWorld(&world);
const bool isDeterminantPositive = staticModel->GetTransform().GetDeterminant() >= 0.0f;
auto& lod = staticModel->Model->LODs[0];
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
{
auto& mesh = lod.Meshes[meshIndex];
auto& proxy = mesh.GetCollisionProxy();
// Check every triangle
for (int32 triangleIndex = 0; triangleIndex < proxy.Triangles.Count(); triangleIndex++)
{
auto t = proxy.Triangles[triangleIndex];
// Transform triangle vertices from mesh space to world space
Vector3 t0, t1, t2;
Vector3::Transform(t.V0, world, t0);
Vector3::Transform(t.V1, world, t1);
Vector3::Transform(t.V2, world, t2);
// Check if triangles intersects with the brush
if (CollisionsHelper::SphereIntersectsTriangle(brush, t0, t1, t2))
{
lookup.Triangles.Add(GeometryTriangle(isDeterminantPositive, t0, t1, t2));
}
}
}
}
else if (const auto* terrain = dynamic_cast<Terrain*>(actor))
{
const bool isDeterminantPositive = terrain->GetTransform().GetDeterminant() >= 0.0f;
PROFILE_CPU_NAMED("Terrain");
// Check every patch
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
auto patch = terrain->GetPatch(patchIndex);
auto& triangles = lookup.TerrainCache;
patch->GetCollisionTriangles(brush, triangles);
for (int32 vertexIndex = 0; vertexIndex < triangles.Count();)
{
const Vector3 t0 = triangles[vertexIndex++];
const Vector3 t1 = triangles[vertexIndex++];
const Vector3 t2 = triangles[vertexIndex++];
lookup.Triangles.Add(GeometryTriangle(isDeterminantPositive, t0, t1, t2));
}
}
}
else if (const auto* textRender = dynamic_cast<TextRender*>(actor))
{
PROFILE_CPU_NAMED("TextRender");
// Skip if text is not ready
if (textRender->GetCollisionProxy().Triangles.IsEmpty())
return true;
auto& proxy = textRender->GetCollisionProxy();
// Check model meshes
Matrix world;
textRender->GetLocalToWorldMatrix(world);
const bool isDeterminantPositive = textRender->GetTransform().GetDeterminant() >= 0.0f;
// Check every triangle
for (int32 triangleIndex = 0; triangleIndex < proxy.Triangles.Count(); triangleIndex++)
{
auto t = proxy.Triangles[triangleIndex];
// Transform triangle vertices from mesh space to world space
Vector3 t0, t1, t2;
Vector3::Transform(t.V0, world, t0);
Vector3::Transform(t.V1, world, t1);
Vector3::Transform(t.V2, world, t2);
// Check if triangles intersects with the brush
if (CollisionsHelper::SphereIntersectsTriangle(brush, t0, t1, t2))
{
lookup.Triangles.Add(GeometryTriangle(isDeterminantPositive, t0, t1, t2));
}
}
}
return true;
}
};
struct FoliagePlacement
{
int32 FoliageTypeIndex;
Vector3 Location; // In world space
Vector3 Normal; // In world space
};
void FoliageTools::Paint(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius, bool additive, float densityScale)
{
if (additive)
Paint(foliage, foliageTypesIndices, brushPosition, brushRadius, densityScale);
else
Remove(foliage, foliageTypesIndices, brushPosition, brushRadius);
}
void FoliageTools::Paint(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius, float densityScale)
{
if (foliageTypesIndices.Length() <= 0)
return;
PROFILE_CPU();
// Prepare
GeometryLookup geometry(brushPosition, brushRadius);
// Find geometry actors to place foliage on top of them
{
PROFILE_CPU_NAMED("Search Geometry");
Function<bool(Actor*, GeometryLookup&)> treeWalkFunction(GeometryLookup::Search);
SceneQuery::TreeExecute<GeometryLookup&>(treeWalkFunction, geometry);
}
// For each selected foliage instance type try to place something
Array<FoliagePlacement> placements;
{
PROFILE_CPU_NAMED("Find Placements");
for (int32 i1 = 0; i1 < foliageTypesIndices.Length(); i1++)
{
const int32 foliageTypeIndex = foliageTypesIndices[i1];
ASSERT(foliageTypeIndex >= 0 && foliageTypeIndex < foliage->FoliageTypes.Count());
const FoliageType& foliageType = foliage->FoliageTypes[foliageTypeIndex];
// Prepare
const float minNormalAngle = Math::Cos(foliageType.PaintGroundSlopeAngleMin * DegreesToRadians);
const float maxNormalAngle = Math::Cos(foliageType.PaintGroundSlopeAngleMax * DegreesToRadians);
const bool usePaintRadius = foliageType.PaintRadius > 0.0f;
const float paintRadiusSqr = foliageType.PaintRadius * foliageType.PaintRadius;
// Check every area
for (int32 triangleIndex = 0; triangleIndex < geometry.Triangles.Count(); triangleIndex++)
{
const auto& triangle = geometry.Triangles[triangleIndex];
// Check if can reject triangle based on its normal
if ((maxNormalAngle > (triangle.Normal.Y + ZeroTolerance) || minNormalAngle < (triangle.Normal.Y - ZeroTolerance)))
{
continue;
}
// This is the total set of instances disregarding parameters like slope, height or layer
const float desiredInstanceCountFloat = triangle.Area * foliageType.PaintDensity * densityScale / (1000.0f * 1000.0f);
// Allow a single instance with a random chance, if the brush is smaller than the density
const int32 desiredInstanceCount = desiredInstanceCountFloat > 1.0f ? Math::RoundToInt(desiredInstanceCountFloat) : Random::Rand() < desiredInstanceCountFloat ? 1 : 0;
// Try to new instances
FoliagePlacement placement;
for (int32 j = 0; j < desiredInstanceCount; j++)
{
triangle.GetRandomPoint(placement.Location);
// Reject locations outside the brush
if (CollisionsHelper::SphereContainsPoint(geometry.Brush, placement.Location) == ContainmentType::Disjoint)
continue;
// Check if it's too close to any other instances
if (usePaintRadius)
{
// Skip if any places instance is close that placement location
bool isInvalid = false;
// TODO: use quad tree to boost this logic
for (auto i = foliage->Instances.Begin(); i.IsNotEnd(); ++i)
{
const auto& instance = *i;
if (Vector3::DistanceSquared(instance.World.GetTranslation(), placement.Location) <= paintRadiusSqr)
{
isInvalid = true;
break;
}
}
if (isInvalid)
continue;
// Skip if any places instance is close that placement location
isInvalid = false;
for (int32 i = 0; i < placements.Count(); i++)
{
if (Vector3::DistanceSquared(placements[i].Location, placement.Location) <= paintRadiusSqr)
{
isInvalid = true;
break;
}
}
if (isInvalid)
continue;
}
placement.FoliageTypeIndex = foliageTypeIndex;
placement.Normal = triangle.Normal;
placements.Add(placement);
}
}
}
}
// Place foliage instances
if (placements.HasItems())
{
PROFILE_CPU_NAMED("Place Instances");
Matrix matrix;
FoliageInstance instance;
Quaternion tmp;
Matrix world;
foliage->GetLocalToWorldMatrix(world);
for (int32 i = 0; i < placements.Count(); i++)
{
const auto& placement = placements[i];
const FoliageType& foliageType = foliage->FoliageTypes[placement.FoliageTypeIndex];
const Vector3 normal = foliageType.PlacementAlignToNormal ? placement.Normal : Vector3::Up;
if (normal == Vector3::Down)
instance.Transform.Orientation = Quaternion(0.0f, 0.0f, Math::Sin(PI_OVER_2), Math::Cos(PI_OVER_2));
else
instance.Transform.Orientation = Quaternion::LookRotation(Vector3::Cross(Vector3::Cross(normal, Vector3::Forward), normal), normal);
if (foliageType.PlacementRandomYaw)
{
Quaternion::RotationAxis(Vector3::UnitY, Random::Rand() * TWO_PI, tmp);
instance.Transform.Orientation *= tmp;
}
if (!Math::IsZero(foliageType.PlacementRandomRollAngle))
{
Quaternion::RotationAxis(Vector3::UnitZ, Random::Rand() * DegreesToRadians * foliageType.PlacementRandomRollAngle, tmp);
instance.Transform.Orientation *= tmp;
}
if (!Math::IsZero(foliageType.PlacementRandomPitchAngle))
{
Quaternion::RotationAxis(Vector3::UnitX, Random::Rand() * DegreesToRadians * foliageType.PlacementRandomPitchAngle, tmp);
instance.Transform.Orientation *= tmp;
}
instance.Type = placement.FoliageTypeIndex;
instance.Random = Random::Rand();
instance.Transform.Translation = placement.Location;
if (!foliageType.PlacementOffsetY.IsZero())
{
float offsetY = Math::Lerp(foliageType.PlacementOffsetY.X, foliageType.PlacementOffsetY.Y, Random::Rand());
instance.Transform.Translation += (instance.Transform.Orientation * Vector3::Up) * offsetY;
}
instance.Transform.Scale = foliageType.GetRandomScale();
instance.Transform.Orientation.Normalize();
// Convert instance transformation into the local-space of the foliage actor
foliage->GetTransform().WorldToLocal(instance.Transform, instance.Transform);
// Calculate foliage instance geometry transformation matrix
instance.Transform.GetWorld(matrix);
Matrix::Multiply(matrix, world, instance.World);
instance.DrawState.PrevWorld = instance.World;
// Add foliage instance
foliage->AddInstance(instance);
}
foliage->RebuildClusters();
}
}
void FoliageTools::Remove(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius)
{
if (foliageTypesIndices.Length() <= 0)
return;
PROFILE_CPU();
// For each selected foliage instance type try to remove something
const BoundingSphere brush(brushPosition, brushRadius);
for (auto i = foliage->Instances.Begin(); i.IsNotEnd(); ++i)
{
auto& instance = *i;
// Skip instances outside the brush
if (CollisionsHelper::SphereContainsPoint(brush, instance.World.GetTranslation()) == ContainmentType::Disjoint)
continue;
// Skip instances not existing in a filter
bool skip = true;
for (int32 i1 = 0; i1 < foliageTypesIndices.Length(); i1++)
{
if (foliageTypesIndices[i1] == instance.Type)
{
skip = false;
break;
}
}
if (skip)
continue;
// Remove instance
foliage->RemoveInstance(i);
--i;
}
foliage->RebuildClusters();
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Types/Span.h"
#include "Engine/Scripting/ScriptingType.h"
class Foliage;
/// <summary>
/// Foliage tools for editor. Allows to spawn and modify foliage instances.
/// </summary>
API_CLASS(Static, Namespace="FlaxEditor") class FoliageTools
{
DECLARE_SCRIPTING_TYPE_NO_SPAWN(FoliageTools);
/// <summary>
/// Paints the foliage instances using the given foliage types selection and the brush location.
/// </summary>
/// <param name="foliage">The foliage actor.</param>
/// <param name="foliageTypesIndices">The foliage types indices to use for painting.</param>
/// <param name="brushPosition">The brush position.</param>
/// <param name="brushRadius">The brush radius.</param>
/// <param name="additive">True if paint using additive mode, false if remove foliage instances.</param>
/// <param name="densityScale">The additional scale for foliage density when painting. Can be used to increase or decrease foliage density during painting.</param>
API_FUNCTION() static void Paint(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius, bool additive, float densityScale = 1.0f);
/// <summary>
/// Paints the foliage instances using the given foliage types selection and the brush location.
/// </summary>
/// <param name="foliage">The foliage actor.</param>
/// <param name="foliageTypesIndices">The foliage types indices to use for painting.</param>
/// <param name="brushPosition">The brush position.</param>
/// <param name="brushRadius">The brush radius.</param>
/// <param name="densityScale">The additional scale for foliage density when painting. Can be used to increase or decrease foliage density during painting.</param>
API_FUNCTION() static void Paint(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius, float densityScale = 1.0f);
/// <summary>
/// Removes the foliage instances using the given foliage types selection and the brush location.
/// </summary>
/// <param name="foliage">The foliage actor.</param>
/// <param name="foliageTypesIndices">The foliage types indices to use for painting.</param>
/// <param name="brushPosition">The brush position.</param>
/// <param name="brushRadius">The brush radius.</param>
API_FUNCTION() static void Remove(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius);
};

View File

@@ -0,0 +1,535 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.ComponentModel;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Foliage types editor tab. Allows to add, remove or modify foliage instance types defined for the current foliage object.
/// </summary>
/// <seealso cref="GUI.Tabs.Tab" />
public class FoliageTypesTab : Tab
{
/// <summary>
/// The object for foliage type settings adjusting via Custom Editor.
/// </summary>
[CustomEditor(typeof(ProxyObjectEditor))]
private sealed class ProxyObject
{
/// <summary>
/// The tab.
/// </summary>
[HideInEditor]
public readonly FoliageTypesTab Tab;
/// <summary>
/// The foliage actor.
/// </summary>
[HideInEditor]
public FlaxEngine.Foliage Foliage;
/// <summary>
/// The selected foliage instance type index.
/// </summary>
[HideInEditor]
public int SelectedFoliageTypeIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ProxyObject"/> class.
/// </summary>
/// <param name="tab">The tab.</param>
public ProxyObject(FoliageTypesTab tab)
{
Tab = tab;
SelectedFoliageTypeIndex = -1;
}
private FoliageType _type;
public void SyncOptions()
{
_type = Foliage.GetFoliageType(SelectedFoliageTypeIndex);
}
//
[EditorOrder(10), EditorDisplay("Model"), Tooltip("Model to draw by all the foliage instances of this type. It must be unique within the foliage actor and cannot be null.")]
public Model Model
{
get => _type.Model;
set
{
if (_type.Model == value)
return;
if (Foliage.FoliageTypes.Any(x => x.Model == value))
{
MessageBox.Show("The given model is already used by the other foliage type.", "Invalid model", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
_type.Model = value;
Tab.UpdateFoliageTypesList();
// TODO: support undo for editing foliage type properties
}
}
[EditorOrder(20), EditorDisplay("Model"), Collection(ReadOnly = true), Tooltip("Model materials override collection. Can be used to change a specific material of the mesh to the custom one without editing the asset.")]
public MaterialBase[] Materials
{
get
{
if (Model.WaitForLoaded())
throw new Exception("Failed to load foliage model.");
return _type.Materials;
}
set
{
if (Model.WaitForLoaded())
throw new Exception("Failed to load foliage model.");
_type.Materials = value;
}
}
//
[EditorOrder(100), EditorDisplay("Instance Options"), Limit(0.0f), Tooltip("The per-instance cull distance.")]
public float CullDistance
{
get => _type.CullDistance;
set
{
if (Mathf.NearEqual(_type.CullDistance, value))
return;
_type.CullDistance = value;
Foliage.UpdateCullDistance();
}
}
[EditorOrder(110), EditorDisplay("Instance Options"), Limit(0.0f), Tooltip("The per-instance cull distance randomization range (randomized per instance and added to master CullDistance value).")]
public float CullDistanceRandomRange
{
get => _type.CullDistanceRandomRange;
set
{
if (Mathf.NearEqual(_type.CullDistanceRandomRange, value))
return;
_type.CullDistanceRandomRange = value;
Foliage.UpdateCullDistance();
}
}
[EditorOrder(120), DefaultValue(DrawPass.Default), EditorDisplay("Instance Options"), Tooltip("The draw passes to use for rendering this foliage type.")]
public DrawPass DrawModes
{
get => _type.DrawModes;
set => _type.DrawModes = value;
}
[EditorOrder(130), DefaultValue(ShadowsCastingMode.All), EditorDisplay("Instance Options"), Tooltip("The shadows casting mode.")]
public ShadowsCastingMode ShadowsMode
{
get => _type.ShadowsMode;
set => _type.ShadowsMode = value;
}
[EditorOrder(140), EditorDisplay("Instance Options"), Tooltip("Determines whenever this meshes can receive decals.")]
public bool ReceiveDecals
{
get => _type.ReceiveDecals;
set => _type.ReceiveDecals = value;
}
[EditorOrder(145), EditorDisplay("Instance Options"), Limit(0.0f), Tooltip("The scale in lightmap (for instances of this foliage type). Can be used to adjust static lighting quality for the foliage instances.")]
public float ScaleInLightmap
{
get => _type.ScaleInLightmap;
set => _type.ScaleInLightmap = value;
}
[EditorOrder(150), EditorDisplay("Instance Options"), Tooltip("Flag used to determinate whenever use global foliage density scaling for instances of this foliage type.")]
public bool UseDensityScaling
{
get => _type.UseDensityScaling;
set
{
if (_type.UseDensityScaling == value)
return;
_type.UseDensityScaling = value;
Foliage.RebuildClusters();
}
}
[EditorOrder(160), VisibleIf("UseDensityScaling"), Limit(0, 10, 0.001f), EditorDisplay("Instance Options"), Tooltip("The density scaling scale applied to the global scale for the foliage instances of this type. Can be used to boost or reduce density scaling effect on this foliage type.")]
public float DensityScalingScale
{
get => _type.DensityScalingScale;
set
{
if (Mathf.NearEqual(_type.DensityScalingScale, value))
return;
_type.DensityScalingScale = value;
Foliage.RebuildClusters();
}
}
//
[EditorOrder(200), EditorDisplay("Painting"), Limit(0.0f), Tooltip("The foliage instances density defined in instances count per 1000x1000 units area.")]
public float Density
{
get => _type.PaintDensity;
set => _type.PaintDensity = value;
}
[EditorOrder(210), EditorDisplay("Painting"), Limit(0.0f), Tooltip("The minimum radius between foliage instances.")]
public float Radius
{
get => _type.PaintRadius;
set => _type.PaintRadius = value;
}
[EditorOrder(215), EditorDisplay("Painting"), Limit(0.0f, 360.0f), Tooltip("The minimum and maximum ground slope angle to paint foliage on it (in degrees).")]
public Vector2 PaintGroundSlopeAngleRange
{
get => new Vector2(_type.PaintGroundSlopeAngleMin, _type.PaintGroundSlopeAngleMax);
set
{
_type.PaintGroundSlopeAngleMin = value.X;
_type.PaintGroundSlopeAngleMax = value.Y;
}
}
[EditorOrder(220), EditorDisplay("Painting"), Tooltip("The scaling mode.")]
public FoliageScalingModes Scaling
{
get => _type.PaintScaling;
set => _type.PaintScaling = value;
}
[EditorOrder(230), EditorDisplay("Painting"), Limit(0.0f), CustomEditor(typeof(ActorTransformEditor.PositionScaleEditor)), Tooltip("The scale minimum values per axis.")]
public Vector3 ScaleMin
{
get => _type.PaintScaleMin;
set => _type.PaintScaleMin = value;
}
[EditorOrder(240), EditorDisplay("Painting"), Limit(0.0f), CustomEditor(typeof(ActorTransformEditor.PositionScaleEditor)), Tooltip("The scale maximum values per axis.")]
public Vector3 ScaleMax
{
get => _type.PaintScaleMax;
set => _type.PaintScaleMax = value;
}
//
[EditorOrder(300), EditorDisplay("Placement", "Offset Y"), Tooltip("The per-instance random offset range on axis Y (min-max).")]
public Vector2 OffsetY
{
get => _type.PlacementOffsetY;
set => _type.PlacementOffsetY = value;
}
[EditorOrder(310), EditorDisplay("Placement"), Limit(0.0f), Tooltip("The random pitch angle range (uniform in both ways around normal vector).")]
public float RandomPitchAngle
{
get => _type.PlacementRandomPitchAngle;
set => _type.PlacementRandomPitchAngle = value;
}
[EditorOrder(320), EditorDisplay("Placement"), Limit(0.0f), Tooltip("The random roll angle range (uniform in both ways around normal vector).")]
public float RandomRollAngle
{
get => _type.PlacementRandomRollAngle;
set => _type.PlacementRandomRollAngle = value;
}
[EditorOrder(330), EditorDisplay("Placement", "Align To Normal"), Tooltip("If checked, instances will be aligned to normal of the placed surface.")]
public bool AlignToNormal
{
get => _type.PlacementAlignToNormal;
set => _type.PlacementAlignToNormal = value;
}
[EditorOrder(340), EditorDisplay("Placement"), Tooltip("If checked, instances will use randomized yaw when placed. Random yaw uses will rotation range over the Y axis.")]
public bool RandomYaw
{
get => _type.PlacementRandomYaw;
set => _type.PlacementRandomYaw = value;
}
}
/// <summary>
/// The custom editor for <see cref="ProxyObject"/>.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.GenericEditor" />
private sealed class ProxyObjectEditor : GenericEditor
{
private Label _info;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
var space = layout.Space(22);
var removeButton = new Button(2, 2.0f, 80.0f, 18.0f)
{
Text = "Remove",
TooltipText = "Removes the selected foliage type and all foliage instances using this type",
Parent = space.Spacer
};
removeButton.Clicked += OnRemoveButtonClicked;
_info = new Label(removeButton.Right + 6, 2, 200, 18.0f)
{
HorizontalAlignment = TextAlignment.Near,
Parent = space.Spacer
};
}
/// <inheritdoc />
public override void Refresh()
{
// Sync selected foliage options once before update to prevent too many data copies when fetching data from UI properties accessors
var proxyObject = (ProxyObject)Values[0];
proxyObject.SyncOptions();
_info.Text = string.Format("Instances: {0}, Total: {1}", proxyObject.Foliage.GetFoliageTypeInstancesCount(proxyObject.SelectedFoliageTypeIndex), proxyObject.Foliage.InstancesCount);
base.Refresh();
}
private void OnRemoveButtonClicked()
{
var proxyObject = (ProxyObject)Values[0];
proxyObject.Tab.RemoveFoliageType(proxyObject.SelectedFoliageTypeIndex);
}
}
private readonly ProxyObject _proxy;
private readonly VerticalPanel _items;
private readonly Button _addFoliageTypeButton;
private readonly CustomEditorPresenter _presenter;
private int _foliageTypesCount;
/// <summary>
/// The parent foliage tab.
/// </summary>
public readonly FoliageTab Tab;
/// <summary>
/// Initializes a new instance of the <see cref="FoliageTypesTab"/> class.
/// </summary>
/// <param name="tab">The parent tab.</param>
public FoliageTypesTab(FoliageTab tab)
: base("Foliage Types")
{
Tab = tab;
Tab.SelectedFoliageChanged += OnSelectedFoliageChanged;
Tab.SelectedFoliageTypeIndexChanged += OnSelectedFoliageTypeIndexChanged;
Tab.SelectedFoliageTypesChanged += UpdateFoliageTypesList;
_proxy = new ProxyObject(this);
// Main panel
var splitPanel = new SplitPanel(Orientation.Vertical, ScrollBars.Vertical, ScrollBars.Vertical)
{
SplitterValue = 0.2f,
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
// Foliage types list
_items = new VerticalPanel
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = new Margin(4, 4, 4, 0),
IsScrollable = true,
Parent = splitPanel.Panel1
};
// Foliage add button
_addFoliageTypeButton = new Button
{
Text = "Add Foliage Type",
TooltipText = "Add new model to use it as a new foliage type for instancing and spawning in the level",
Parent = splitPanel.Panel1
};
_addFoliageTypeButton.Clicked += OnAddFoliageTypeButtonClicked;
// Options editor
// TODO: use editor undo for changing foliage type options
var editor = new CustomEditorPresenter(null, "No foliage type selected");
editor.Panel.Parent = splitPanel.Panel2;
editor.Modified += OnModified;
_presenter = editor;
}
private void OnModified()
{
Editor.Instance.Scene.MarkSceneEdited(_proxy.Foliage?.Scene);
}
private void OnSelectedFoliageChanged()
{
_proxy.SelectedFoliageTypeIndex = -1;
_proxy.Foliage = Tab.SelectedFoliage;
_presenter.Deselect();
UpdateFoliageTypesList();
}
private void OnSelectedFoliageTypeIndexChanged(int previousIndex, int currentIndex)
{
if (previousIndex != -1)
{
_items.Children[previousIndex].BackgroundColor = Color.Transparent;
}
_proxy.SelectedFoliageTypeIndex = currentIndex;
if (currentIndex != -1)
{
_items.Children[currentIndex].BackgroundColor = Style.Current.BackgroundSelected;
_presenter.Select(_proxy);
_presenter.BuildLayoutOnUpdate();
}
else
{
_presenter.Deselect();
}
}
private void RemoveFoliageType(int index)
{
// Deselect if selected
if (Tab.SelectedFoliageTypeIndex == index)
Tab.SelectedFoliageTypeIndex = -1;
var foliage = Tab.SelectedFoliage;
var action = new Undo.EditFoliageAction(foliage);
foliage.RemoveFoliageType(index);
action.RecordEnd();
Tab.Editor.Undo.AddAction(action);
Tab.OnSelectedFoliageTypesChanged();
}
private void OnAddFoliageTypeButtonClicked()
{
// Show model picker
AssetSearchPopup.Show(_addFoliageTypeButton, new Vector2(_addFoliageTypeButton.Width * 0.5f, _addFoliageTypeButton.Height), IsItemValidForFoliageModel, OnItemSelectedForFoliageModel);
}
private void OnItemSelectedForFoliageModel(AssetItem item)
{
var foliage = Tab.SelectedFoliage;
var model = FlaxEngine.Content.LoadAsync<Model>(item.ID);
var action = new Undo.EditFoliageAction(foliage);
foliage.AddFoliageType(model);
Editor.Instance.Scene.MarkSceneEdited(foliage.Scene);
action.RecordEnd();
Tab.Editor.Undo.AddAction(action);
Tab.OnSelectedFoliageTypesChanged();
Tab.SelectedFoliageTypeIndex = foliage.FoliageTypesCount - 1;
RootWindow.Focus();
Focus();
}
private bool IsItemValidForFoliageModel(AssetItem item)
{
return item is BinaryAssetItem binaryItem && binaryItem.Type == typeof(Model);
}
private void UpdateFoliageTypesList()
{
var foliage = Tab.SelectedFoliage;
// Cleanup previous items
_items.DisposeChildren();
// Add new ones
float y = 0;
if (foliage != null)
{
int typesCount = foliage.FoliageTypesCount;
_foliageTypesCount = typesCount;
for (int i = 0; i < typesCount; i++)
{
var model = foliage.GetFoliageType(i).Model;
var asset = Tab.Editor.ContentDatabase.FindAsset(model.ID);
var itemView = new AssetSearchPopup.AssetItemView(asset)
{
TooltipText = asset.NamePath,
Tag = i,
Parent = _items,
};
itemView.Clicked += OnFoliageTypeListItemClicked;
y += itemView.Height + _items.Spacing;
}
y += _items.Margin.Height;
}
else
{
_foliageTypesCount = 0;
}
_items.Height = y;
var selectedFoliageTypeIndex = Tab.SelectedFoliageTypeIndex;
if (selectedFoliageTypeIndex != -1)
{
_items.Children[selectedFoliageTypeIndex].BackgroundColor = Style.Current.BackgroundSelected;
}
ArrangeAddFoliageButton();
}
private void OnFoliageTypeListItemClicked(ItemsListContextMenu.Item item)
{
Tab.SelectedFoliageTypeIndex = (int)item.Tag;
}
private void ArrangeAddFoliageButton()
{
_addFoliageTypeButton.Location = new Vector2((_addFoliageTypeButton.Parent.Width - _addFoliageTypeButton.Width) * 0.5f, _items.Bottom + 4);
}
internal void CheckFoliageTypesCount()
{
var foliage = Tab.SelectedFoliage;
var count = foliage ? foliage.FoliageTypesCount : 0;
if (foliage != null && count != _foliageTypesCount)
{
if (Tab.SelectedFoliageTypeIndex >= count)
Tab.SelectedFoliageTypeIndex = -1;
Tab.OnSelectedFoliageTypesChanged();
}
}
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
base.PerformLayoutBeforeChildren();
ArrangeAddFoliageButton();
}
}
}

View File

@@ -0,0 +1,223 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Tools.Foliage.Undo;
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Gizmo for painting with foliage. Managed by the <see cref="PaintFoliageGizmoMode"/>.
/// </summary>
/// <seealso cref="FlaxEditor.Gizmo.GizmoBase" />
public sealed class PaintFoliageGizmo : GizmoBase
{
private FlaxEngine.Foliage _paintFoliage;
private Model _brushModel;
private List<int> _foliageTypesIndices;
private EditFoliageAction _undoAction;
private int _paintUpdateCount;
/// <summary>
/// The parent mode.
/// </summary>
public readonly PaintFoliageGizmoMode Mode;
/// <summary>
/// Gets a value indicating whether gizmo tool is painting the foliage.
/// </summary>
public bool IsPainting => _paintFoliage != null;
/// <summary>
/// Occurs when foliage paint has been started.
/// </summary>
public event Action PaintStarted;
/// <summary>
/// Occurs when foliage paint has been ended.
/// </summary>
public event Action PaintEnded;
/// <summary>
/// Initializes a new instance of the <see cref="PaintFoliageGizmo"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="mode">The mode.</param>
public PaintFoliageGizmo(IGizmoOwner owner, PaintFoliageGizmoMode mode)
: base(owner)
{
Mode = mode;
}
private FlaxEngine.Foliage SelectedFoliage
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var foliageNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as FoliageNode : null;
return (FlaxEngine.Foliage)foliageNode?.Actor;
}
}
/// <inheritdoc />
public override void Draw(ref RenderContext renderContext)
{
if (!IsActive)
return;
var foliage = SelectedFoliage;
if (!foliage)
return;
if (Mode.HasValidHit)
{
var brushPosition = Mode.CursorPosition;
var brushNormal = Mode.CursorNormal;
var brushColor = new Color(1.0f, 0.85f, 0.0f); // TODO: expose to editor options
var sceneDepth = Owner.RenderTask.Buffers.DepthBuffer;
var brushMaterial = Mode.CurrentBrush.GetBrushMaterial(ref brushPosition, ref brushColor, sceneDepth);
if (!_brushModel)
{
_brushModel = FlaxEngine.Content.LoadAsyncInternal<Model>("Editor/Primitives/Sphere");
}
// Draw paint brush
if (_brushModel && brushMaterial)
{
Quaternion rotation;
if (brushNormal == Vector3.Down)
rotation = Quaternion.RotationZ(Mathf.Pi);
else
rotation = Quaternion.LookRotation(Vector3.Cross(Vector3.Cross(brushNormal, Vector3.Forward), brushNormal), brushNormal);
Matrix transform = Matrix.Scaling(Mode.CurrentBrush.Size * 0.01f) * Matrix.RotationQuaternion(rotation) * Matrix.Translation(brushPosition);
_brushModel.Draw(ref renderContext, brushMaterial, ref transform);
}
}
}
/// <summary>
/// Called to start foliage painting
/// </summary>
/// <param name="foliage">The foliage.</param>
private void PaintStart(FlaxEngine.Foliage foliage)
{
// Skip if already is painting
if (IsPainting)
return;
if (Editor.Instance.Undo.Enabled)
_undoAction = new EditFoliageAction(foliage);
_paintFoliage = foliage;
_paintUpdateCount = 0;
PaintStarted?.Invoke();
}
/// <summary>
/// Called to update foliage 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;
if (Mode.CurrentBrush.SingleClick && _paintUpdateCount > 0)
return;
// Edit the foliage
var foliage = SelectedFoliage;
int foliageTypesCount = foliage.FoliageTypesCount;
var foliageTypeModelIdsToPaint = Editor.Instance.Windows.ToolboxWin.Foliage.FoliageTypeModelIdsToPaint;
if (_foliageTypesIndices == null)
_foliageTypesIndices = new List<int>(foliageTypesCount);
else
_foliageTypesIndices.Clear();
for (int index = 0; index < foliageTypesCount; index++)
{
var model = foliage.GetFoliageType(index).Model;
if (model && (!foliageTypeModelIdsToPaint.TryGetValue(model.ID, out var selected) || selected))
{
_foliageTypesIndices.Add(index);
}
}
// TODO: don't call _foliageTypesIndices.ToArray() but reuse allocation
FoliageTools.Paint(foliage, _foliageTypesIndices.ToArray(), Mode.CursorPosition, Mode.CurrentBrush.Size * 0.5f, !Owner.IsControlDown, Mode.CurrentBrush.DensityScale);
_paintUpdateCount++;
}
/// <summary>
/// Called to end foliage painting.
/// </summary>
private void PaintEnd()
{
// Skip if nothing was painted
if (!IsPainting)
return;
if (_undoAction != null)
{
_undoAction.RecordEnd();
Editor.Instance.Undo.AddAction(_undoAction);
_undoAction = null;
}
_paintFoliage = null;
_paintUpdateCount = 0;
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 foliage is selected
var foliage = SelectedFoliage;
if (!foliage)
{
PaintEnd();
return;
}
// Check if selected foliage was changed during painting
if (foliage != _paintFoliage && IsPainting)
{
PaintEnd();
}
// Perform detailed tracing to find cursor location for the foliage placement
var mouseRay = Owner.MouseRay;
var ray = Owner.MouseRay;
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives | SceneGraphNode.RayCastData.FlagTypes.SkipColliders;
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out var closest, out var hitNormal, rayCastFlags);
if (hit != null)
{
var hitLocation = mouseRay.GetPoint(closest);
Mode.SetCursor(ref hitLocation, ref hitNormal);
}
// No hit
else
{
Mode.ClearCursor();
}
// Handle painting
if (Owner.IsLeftMouseButtonDown)
PaintStart(foliage);
else
PaintEnd();
PaintUpdate(dt);
}
}
}

View File

@@ -0,0 +1,108 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Foliage painting tool mode.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
public class PaintFoliageGizmoMode : EditorGizmoMode
{
/// <summary>
/// The foliage painting gizmo.
/// </summary>
public PaintFoliageGizmo Gizmo;
/// <summary>
/// Gets the current brush.
/// </summary>
public readonly Brush CurrentBrush = new Brush();
/// <summary>
/// The last valid cursor position of the brush (in world space).
/// </summary>
public Vector3 CursorPosition { get; private set; }
/// <summary>
/// The last valid cursor hit point normal vector of the brush (in world space).
/// </summary>
public Vector3 CursorNormal { 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>
/// Gets the selected foliage actor (see <see cref="Modules.SceneEditingModule"/>).
/// </summary>
public FlaxEngine.Foliage SelectedFoliage
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var foliageNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as FoliageNode : null;
return (FlaxEngine.Foliage)foliageNode?.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
{
float brushSizeHalf = CurrentBrush.Size * 0.5f;
Vector3 center = CursorPosition;
BoundingBox box;
box.Minimum = new Vector3(center.X - brushSizeHalf, center.Y - brushSizeHalf - brushSizeHalf, center.Z - brushSizeHalf);
box.Maximum = new Vector3(center.X + brushSizeHalf, center.Y + brushSizeHalf + brushSizeHalf, center.Z + brushSizeHalf);
return box;
}
}
/// <inheritdoc />
public override void Init(MainEditorGizmoViewport viewport)
{
base.Init(viewport);
Gizmo = new PaintFoliageGizmo(viewport, this);
}
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
Viewport.Gizmos.Active = Gizmo;
ClearCursor();
}
/// <summary>
/// Clears the cursor location information cached within the gizmo mode.
/// </summary>
public void ClearCursor()
{
HasValidHit = false;
}
/// <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 foliage.</param>
/// <param name="hitNormal">The cursor hit location normal vector fot he surface.</param>
public void SetCursor(ref Vector3 hitPosition, ref Vector3 hitNormal)
{
HasValidHit = true;
CursorPosition = hitPosition;
CursorNormal = hitNormal;
}
}
}

View File

@@ -0,0 +1,337 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Foliage painting tab. Allows to add or remove foliage instances defined for the current foliage object.
/// </summary>
/// <seealso cref="GUI.Tabs.Tab" />
public class PaintTab : Tab
{
/// <summary>
/// The object for foliage painting settings adjusting via Custom Editor.
/// </summary>
[CustomEditor(typeof(ProxyObjectEditor))]
private sealed class ProxyObject
{
private readonly PaintFoliageGizmoMode _mode;
private readonly PaintTab _tab;
/// <summary>
/// The foliage actor.
/// </summary>
[HideInEditor]
public FlaxEngine.Foliage Foliage;
/// <summary>
/// The selected foliage instance type index.
/// </summary>
[HideInEditor]
public int SelectedFoliageTypeIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ProxyObject"/> class.
/// </summary>
/// <param name="tab">The tab.</param>
/// <param name="mode">The mode.</param>
public ProxyObject(PaintTab tab, PaintFoliageGizmoMode mode)
{
_mode = mode;
_tab = tab;
SelectedFoliageTypeIndex = -1;
}
private FlaxEngine.FoliageType _type;
public void SyncOptions()
{
_type = Foliage.GetFoliageType(SelectedFoliageTypeIndex);
}
//
[EditorOrder(100), EditorDisplay("Brush", EditorDisplayAttribute.InlineStyle)]
public Brush Brush
{
get => _mode.CurrentBrush;
set { }
}
//
[EditorOrder(200), EditorDisplay("Painting"), Limit(0.0f), Tooltip("The foliage instances density defined in instances count per 1000x1000 units area.")]
public float Density
{
get => _type.PaintDensity;
set => _type.PaintDensity = value;
}
[EditorOrder(210), EditorDisplay("Painting"), Limit(0.0f), Tooltip("The minimum radius between foliage instances.")]
public float Radius
{
get => _type.PaintRadius;
set => _type.PaintRadius = value;
}
[EditorOrder(215), EditorDisplay("Painting"), Limit(0.0f, 360.0f), Tooltip("The minimum and maximum ground slope angle to paint foliage on it (in degrees).")]
public Vector2 PaintGroundSlopeAngleRange
{
get => new Vector2(_type.PaintGroundSlopeAngleMin, _type.PaintGroundSlopeAngleMax);
set
{
_type.PaintGroundSlopeAngleMin = value.X;
_type.PaintGroundSlopeAngleMax = value.Y;
}
}
[EditorOrder(220), EditorDisplay("Painting"), Tooltip("The scaling mode.")]
public FoliageScalingModes Scaling
{
get => _type.PaintScaling;
set => _type.PaintScaling = value;
}
[EditorOrder(230), EditorDisplay("Painting"), Limit(0.0f), CustomEditor(typeof(ActorTransformEditor.PositionScaleEditor)), Tooltip("The scale minimum values per axis.")]
public Vector3 ScaleMin
{
get => _type.PaintScaleMin;
set => _type.PaintScaleMin = value;
}
[EditorOrder(240), EditorDisplay("Painting"), Limit(0.0f), CustomEditor(typeof(ActorTransformEditor.PositionScaleEditor)), Tooltip("The scale maximum values per axis.")]
public Vector3 ScaleMax
{
get => _type.PaintScaleMax;
set => _type.PaintScaleMax = value;
}
//
[EditorOrder(300), EditorDisplay("Placement", "Offset Y"), Tooltip("The per-instance random offset range on axis Y (min-max).")]
public Vector2 OffsetY
{
get => _type.PlacementOffsetY;
set => _type.PlacementOffsetY = value;
}
[EditorOrder(310), EditorDisplay("Placement"), Limit(0.0f), Tooltip("The random pitch angle range (uniform in both ways around normal vector).")]
public float RandomPitchAngle
{
get => _type.PlacementRandomPitchAngle;
set => _type.PlacementRandomPitchAngle = value;
}
[EditorOrder(320), EditorDisplay("Placement"), Limit(0.0f), Tooltip("The random roll angle range (uniform in both ways around normal vector).")]
public float RandomRollAngle
{
get => _type.PlacementRandomRollAngle;
set => _type.PlacementRandomRollAngle = value;
}
[EditorOrder(330), EditorDisplay("Placement", "Align To Normal"), Tooltip("If checked, instances will be aligned to normal of the placed surface.")]
public bool AlignToNormal
{
get => _type.PlacementAlignToNormal;
set => _type.PlacementAlignToNormal = value;
}
[EditorOrder(340), EditorDisplay("Placement"), Tooltip("If checked, instances will use randomized yaw when placed. Random yaw uses will rotation range over the Y axis.")]
public bool RandomYaw
{
get => _type.PlacementRandomYaw;
set => _type.PlacementRandomYaw = value;
}
}
/// <summary>
/// The custom editor for <see cref="ProxyObject"/>.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.GenericEditor" />
private sealed class ProxyObjectEditor : GenericEditor
{
/// <inheritdoc />
public override void Refresh()
{
// Sync selected foliage options once before update to prevent too many data copies when fetching data from UI properties accessors
var proxyObject = (ProxyObject)Values[0];
proxyObject.SyncOptions();
base.Refresh();
}
}
private readonly ProxyObject _proxy;
private readonly VerticalPanel _items;
private readonly CustomEditorPresenter _presenter;
/// <summary>
/// The parent foliage tab.
/// </summary>
public readonly FoliageTab Tab;
/// <summary>
/// Initializes a new instance of the <see cref="PaintTab"/> class.
/// </summary>
/// <param name="tab">The parent tab.</param>
/// <param name="mode">The gizmo mode.</param>
public PaintTab(FoliageTab tab, PaintFoliageGizmoMode mode)
: base("Paint")
{
Tab = tab;
Tab.SelectedFoliageChanged += OnSelectedFoliageChanged;
Tab.SelectedFoliageTypeIndexChanged += OnSelectedFoliageTypeIndexChanged;
Tab.SelectedFoliageTypesChanged += UpdateFoliageTypesList;
_proxy = new ProxyObject(this, mode);
// Main panel
var splitPanel = new SplitPanel(Orientation.Vertical, ScrollBars.Vertical, ScrollBars.Vertical)
{
SplitterValue = 0.2f,
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
// Foliage types list
_items = new VerticalPanel
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = new Margin(4, 4, 4, 0),
IsScrollable = true,
Parent = splitPanel.Panel1
};
// Options editor
// TODO: use editor undo for changing foliage type options
var editor = new CustomEditorPresenter(null, "No foliage type selected");
editor.Panel.Parent = splitPanel.Panel2;
editor.Modified += OnModified;
_presenter = editor;
}
private void OnModified()
{
Editor.Instance.Scene.MarkSceneEdited(_proxy.Foliage?.Scene);
}
private void OnSelectedFoliageChanged()
{
_proxy.SelectedFoliageTypeIndex = -1;
_proxy.Foliage = Tab.SelectedFoliage;
_presenter.Deselect();
UpdateFoliageTypesList();
}
private void OnSelectedFoliageTypeIndexChanged(int previousIndex, int currentIndex)
{
if (previousIndex != -1)
{
((ContainerControl)_items.Children[previousIndex]).Children[1].BackgroundColor = Color.Transparent;
}
_proxy.SelectedFoliageTypeIndex = currentIndex;
if (currentIndex != -1)
{
((ContainerControl)_items.Children[currentIndex]).Children[1].BackgroundColor = Style.Current.BackgroundSelected;
_presenter.Select(_proxy);
_presenter.BuildLayoutOnUpdate();
}
else
{
_presenter.Deselect();
}
}
private void UpdateFoliageTypesList()
{
var foliage = Tab.SelectedFoliage;
// Cleanup previous items
_items.DisposeChildren();
// Add new ones
float y = 0;
if (foliage != null)
{
int typesCount = foliage.FoliageTypesCount;
for (int i = 0; i < typesCount; i++)
{
var model = foliage.GetFoliageType(i).Model;
var asset = Tab.Editor.ContentDatabase.FindAsset(model.ID);
var itemPanel = new ContainerControl();
var itemCheck = new CheckBox
{
AnchorPreset = AnchorPresets.VerticalStretchLeft,
Offsets = new Margin(0, 18, 0, 0),
TooltipText = "If checked, enables painting with this foliage type.",
Tag = i,
Parent = itemPanel,
};
// Try restore painting with the given model ID
bool itemChecked;
if (!Tab.FoliageTypeModelIdsToPaint.TryGetValue(model.ID, out itemChecked))
{
// Enable by default
itemChecked = true;
Tab.FoliageTypeModelIdsToPaint[model.ID] = itemChecked;
}
itemCheck.Checked = itemChecked;
itemCheck.StateChanged += OnItemCheckStateChanged;
var itemView = new AssetSearchPopup.AssetItemView(asset)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = new Margin(itemCheck.Width + 2, 0, 1, 1),
TooltipText = asset.NamePath,
Tag = i,
Parent = itemPanel,
};
itemView.Clicked += OnFoliageTypeListItemClicked;
itemPanel.Height = 34;
itemPanel.Parent = _items;
itemPanel.UnlockChildrenRecursive();
itemPanel.PerformLayout();
y += itemPanel.Height + _items.Spacing;
}
y += _items.Margin.Height;
}
_items.Height = y;
var selectedFoliageTypeIndex = Tab.SelectedFoliageTypeIndex;
if (selectedFoliageTypeIndex != -1)
{
((ContainerControl)_items.Children[selectedFoliageTypeIndex]).Children[1].BackgroundColor = Style.Current.BackgroundSelected;
}
}
private void OnItemCheckStateChanged(CheckBox item)
{
var index = (int)item.Tag;
var foliage = Tab.SelectedFoliage;
var model = foliage.GetFoliageType(index).Model;
Tab.FoliageTypeModelIdsToPaint[model.ID] = item.Checked;
}
private void OnFoliageTypeListItemClicked(ItemsListContextMenu.Item item)
{
Tab.SelectedFoliageTypeIndex = (int)item.Tag;
}
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage.Undo
{
/// <summary>
/// The foliage instance delete action that can restore it.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
sealed class DeleteInstanceAction : IUndoAction
{
[Serialize]
private readonly Guid _foliageId;
[Serialize]
private int _index;
[Serialize]
private FoliageInstance _instance;
/// <summary>
/// Initializes a new instance of the <see cref="DeleteInstanceAction"/> class.
/// </summary>
/// <param name="foliage">The foliage.</param>
/// <param name="index">The instance index.</param>
public DeleteInstanceAction(FlaxEngine.Foliage foliage, int index)
{
_foliageId = foliage.ID;
_index = index;
}
/// <inheritdoc />
public string ActionString => "Delete foliage instance";
/// <inheritdoc />
public void Do()
{
var foliageId = _foliageId;
var foliage = FlaxEngine.Object.Find<FlaxEngine.Foliage>(ref foliageId);
_instance = foliage.GetInstance(_index);
foliage.RemoveInstance(_index);
foliage.RebuildClusters();
Editor.Instance.Scene.MarkSceneEdited(foliage.Scene);
}
/// <inheritdoc />
public void Undo()
{
var foliageId = _foliageId;
var foliage = FlaxEngine.Object.Find<FlaxEngine.Foliage>(ref foliageId);
_index = foliage.InstancesCount;
foliage.AddInstance(ref _instance);
foliage.RebuildClusters();
Editor.Instance.Scene.MarkSceneEdited(foliage.Scene);
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage.Undo
{
/// <summary>
/// The foliage editing action that records before and after states to swap between unmodified and modified foliage data.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
public sealed class EditFoliageAction : IUndoAction
{
[Serialize]
private readonly Guid _foliageId;
[Serialize]
private string _before;
[Serialize]
private string _after;
/// <summary>
/// Initializes a new instance of the <see cref="EditFoliageAction"/> class.
/// </summary>
/// <remarks>Use <see cref="RecordEnd"/> to finalize foliage data after editing action.</remarks>
/// <param name="foliage">The foliage.</param>
public EditFoliageAction(FlaxEngine.Foliage foliage)
{
_foliageId = foliage.ID;
_before = foliage.ToJson();
}
/// <summary>
/// Called when foliage editing ends. Records the `after` state of the actor. Marks foliage actor parent scene edited.
/// </summary>
public void RecordEnd()
{
var foliageId = _foliageId;
var foliage = FlaxEngine.Object.Find<FlaxEngine.Foliage>(ref foliageId);
_after = foliage.ToJson();
Editor.Instance.Scene.MarkSceneEdited(foliage.Scene);
}
/// <inheritdoc />
public string ActionString => "Edit foliage";
/// <inheritdoc />
public void Do()
{
Set(_after);
}
/// <inheritdoc />
public void Undo()
{
Set(_before);
}
/// <inheritdoc />
public void Dispose()
{
_before = null;
_after = null;
}
private void Set(string data)
{
var foliageId = _foliageId;
var foliage = FlaxEngine.Object.Find<FlaxEngine.Foliage>(ref foliageId);
foliage.FromJson(data);
Editor.Instance.Scene.MarkSceneEdited(foliage.Scene);
}
}
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage.Undo
{
/// <summary>
/// The foliage instance edit action.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
sealed class EditInstanceAction : IUndoAction
{
[Serialize]
private readonly Guid _foliageId;
[Serialize]
private int _index;
[Serialize]
private Transform _before;
[Serialize]
private Transform _after;
/// <summary>
/// Initializes a new instance of the <see cref="EditInstanceAction"/> class.
/// </summary>
/// <param name="foliage">The foliage.</param>
/// <param name="index">The instance index.</param>
public EditInstanceAction(FlaxEngine.Foliage foliage, int index)
{
_foliageId = foliage.ID;
_index = index;
_before = foliage.GetInstance(_index).Transform;
}
/// <summary>
/// Called when foliage editing ends. Records the `after` state of the instance. Marks foliage actor parent scene edited.
/// </summary>
public void RecordEnd()
{
var foliageId = _foliageId;
var foliage = FlaxEngine.Object.Find<FlaxEngine.Foliage>(ref foliageId);
_after = foliage.GetInstance(_index).Transform;
Editor.Instance.Scene.MarkSceneEdited(foliage.Scene);
}
/// <inheritdoc />
public string ActionString => "Edit foliage instance";
/// <inheritdoc />
public void Do()
{
var foliageId = _foliageId;
var foliage = FlaxEngine.Object.Find<FlaxEngine.Foliage>(ref foliageId);
foliage.SetInstanceTransform(_index, ref _after);
foliage.RebuildClusters();
Editor.Instance.Scene.MarkSceneEdited(foliage.Scene);
}
/// <inheritdoc />
public void Undo()
{
var foliageId = _foliageId;
var foliage = FlaxEngine.Object.Find<FlaxEngine.Foliage>(ref foliageId);
foliage.SetInstanceTransform(_index, ref _before);
foliage.RebuildClusters();
Editor.Instance.Scene.MarkSceneEdited(foliage.Scene);
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Foliage.Undo
{
/// <summary>
/// The foliage editing action that handles changing selected foliage actor instance index.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
public sealed class EditSelectedInstanceIndexAction : IUndoAction
{
[Serialize]
private int _before;
[Serialize]
private int _after;
/// <summary>
/// Initializes a new instance of the <see cref="EditSelectedInstanceIndexAction"/> class.
/// </summary>
/// <param name="before">The selected index before.</param>
/// <param name="after">The selected index after.</param>
public EditSelectedInstanceIndexAction(int before, int after)
{
_before = before;
_after = after;
}
/// <inheritdoc />
public string ActionString => "Edit selected foliage instance index";
/// <inheritdoc />
public void Dispose()
{
}
/// <inheritdoc />
public void Do()
{
Editor.Instance.Windows.ToolboxWin.Foliage.Edit.Mode.SelectedInstanceIndex = _after;
}
/// <inheritdoc />
public void Undo()
{
Editor.Instance.Windows.ToolboxWin.Foliage.Edit.Mode.SelectedInstanceIndex = _before;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,234 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Paint
{
/// <summary>
/// The base class for terran paint tool modes.
/// </summary>
[HideInEditor]
public abstract class Mode
{
/// <summary>
/// The options container for the terrain editing apply.
/// </summary>
public struct Options
{
/// <summary>
/// If checked, modification apply method should be inverted.
/// </summary>
public bool Invert;
/// <summary>
/// The master strength parameter to apply when editing the terrain.
/// </summary>
public float Strength;
/// <summary>
/// The delta time (in seconds) for the terrain modification apply. Used to scale the strength. Adjusted to handle low-FPS.
/// </summary>
public float DeltaTime;
}
/// <summary>
/// The tool strength (normalized to range 0-1). Defines the intensity of the paint operation to make it stronger or mre subtle.
/// </summary>
[EditorOrder(0), Limit(0, 10, 0.01f), Tooltip("The tool strength (normalized to range 0-1). Defines the intensity of the paint operation to make it stronger or more subtle.")]
public float Strength = 1.0f;
/// <summary>
/// Gets a value indicating whether this mode supports negative apply for terrain modification.
/// </summary>
public virtual bool SupportsNegativeApply => false;
/// <summary>
/// Gets the index of the active splatmap texture to modify by the tool. It must be equal or higher than zero bu less than <see cref="FlaxEngine.Terrain.MaxSplatmapsCount"/>.
/// </summary>
public abstract int ActiveSplatmapIndex { get; }
/// <summary>
/// Applies the modification to the terrain.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="options">The options.</param>
/// <param name="gizmo">The gizmo.</param>
/// <param name="terrain">The terrain.</param>
public unsafe void Apply(Brush brush, ref Options options, PaintTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
{
// Combine final apply strength
float strength = Strength * options.Strength * options.DeltaTime * 10.0f;
if (strength <= 0.0f)
return;
if (options.Invert && SupportsNegativeApply)
strength *= -1;
// Prepare
var splatmapIndex = ActiveSplatmapIndex;
var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
var heightmapLength = heightmapSize * heightmapSize;
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
var tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes).ToPointer();
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
ApplyParams p = new ApplyParams
{
Terrain = terrain,
Brush = brush,
Gizmo = gizmo,
Options = options,
Strength = strength,
SplatmapIndex = splatmapIndex,
HeightmapSize = heightmapSize,
TempBuffer = tempBuffer,
};
// Get brush bounds in terrain local space
var brushBounds = gizmo.CursorBrushBounds;
terrain.GetLocalToWorldMatrix(out p.TerrainWorld);
terrain.GetWorldToLocalMatrix(out var terrainInvWorld);
BoundingBox.Transform(ref brushBounds, ref terrainInvWorld, out var brushBoundsLocal);
// TODO: try caching brush weights before apply to reduce complexity and batch brush sampling
// Process all the patches under the cursor
for (int patchIndex = 0; patchIndex < gizmo.PatchesUnderCursor.Count; patchIndex++)
{
var patch = gizmo.PatchesUnderCursor[patchIndex];
var patchPositionLocal = new Vector3(patch.PatchCoord.X * patchSize, 0, patch.PatchCoord.Y * patchSize);
// Transform brush bounds from local terrain space into local patch vertex space
var brushBoundsPatchLocalMin = (brushBoundsLocal.Minimum - patchPositionLocal) * unitsPerVertexInv;
var brushBoundsPatchLocalMax = (brushBoundsLocal.Maximum - patchPositionLocal) * unitsPerVertexInv;
// Calculate patch heightmap area to modify by brush
var brushPatchMin = new Int2(Mathf.FloorToInt(brushBoundsPatchLocalMin.X), Mathf.FloorToInt(brushBoundsPatchLocalMin.Z));
var brushPatchMax = new Int2(Mathf.CeilToInt(brushBoundsPatchLocalMax.X), Mathf.FloorToInt(brushBoundsPatchLocalMax.Z));
var modifiedOffset = brushPatchMin;
var modifiedSize = brushPatchMax - brushPatchMin;
// Clamp to prevent overflows
if (modifiedOffset.X < 0)
{
modifiedSize.X += modifiedOffset.X;
modifiedOffset.X = 0;
}
if (modifiedOffset.Y < 0)
{
modifiedSize.Y += modifiedOffset.Y;
modifiedOffset.Y = 0;
}
modifiedSize.X = Mathf.Min(modifiedSize.X, heightmapSize - modifiedOffset.X);
modifiedSize.Y = Mathf.Min(modifiedSize.Y, heightmapSize - modifiedOffset.Y);
// Skip patch won't be modified at all
if (modifiedSize.X <= 0 || modifiedSize.Y <= 0)
continue;
// Get the patch data (cached internally by the c++ core in editor)
var sourceData = TerrainTools.GetSplatMapData(terrain, ref patch.PatchCoord, splatmapIndex);
if (sourceData == null)
{
throw new FlaxException("Cannot modify terrain. Loading splatmap failed. See log for more info.");
}
// Record patch data before editing it
if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
{
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord, splatmapIndex);
}
// Apply modification
p.ModifiedOffset = modifiedOffset;
p.ModifiedSize = modifiedSize;
p.PatchCoord = patch.PatchCoord;
p.PatchPositionLocal = patchPositionLocal;
p.SourceData = sourceData;
Apply(ref p);
}
}
/// <summary>
/// The mode apply parameters.
/// </summary>
public unsafe struct ApplyParams
{
/// <summary>
/// The brush.
/// </summary>
public Brush Brush;
/// <summary>
/// The options.
/// </summary>
public Options Options;
/// <summary>
/// The gizmo.
/// </summary>
public PaintTerrainGizmoMode Gizmo;
/// <summary>
/// The terrain.
/// </summary>
public FlaxEngine.Terrain Terrain;
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
/// <summary>
/// The modified offset.
/// </summary>
public Int2 ModifiedOffset;
/// <summary>
/// The modified size.
/// </summary>
public Int2 ModifiedSize;
/// <summary>
/// The final calculated strength of the effect to apply (can be negative for inverted terrain modification if <see cref="SupportsNegativeApply"/> is set).
/// </summary>
public float Strength;
/// <summary>
/// The splatmap texture index.
/// </summary>
public int SplatmapIndex;
/// <summary>
/// The temporary data buffer (for modified data).
/// </summary>
public Color32* TempBuffer;
/// <summary>
/// The source data buffer.
/// </summary>
public Color32* SourceData;
/// <summary>
/// The heightmap size (edge).
/// </summary>
public int HeightmapSize;
/// <summary>
/// The patch position in terrain local-space.
/// </summary>
public Vector3 PatchPositionLocal;
/// <summary>
/// The terrain local-to-world matrix.
/// </summary>
public Matrix TerrainWorld;
}
/// <summary>
/// Applies the modification to the terrain.
/// </summary>
/// <param name="p">The parameters to use.</param>
public abstract void Apply(ref ApplyParams p);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
/// <summary>
/// Sculpt tool mode that smooths the terrain heightmap area affected by brush.
/// </summary>
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
[HideInEditor]
public sealed class SmoothMode : Mode
{
/// <summary>
/// The tool smoothing radius. Defines the size of smoothing kernel, the higher value the more nearby samples is included into normalized sum. Scaled by the brush size.
/// </summary>
[EditorOrder(10), Limit(0, 1, 0.01f), Tooltip("The tool smoothing radius. Defines the size of smoothing kernel, the higher value the more nearby samples is included into normalized sum. Scaled by the brush size.")]
public float FilterRadius = 0.4f;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
{
// Prepare
var brushPosition = p.Gizmo.CursorPosition;
var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * p.Brush.Size), 2);
var max = p.HeightmapSize - 1;
var strength = Mathf.Saturate(p.Strength);
// Apply brush modification
Profiler.BeginEvent("Apply Brush");
for (int z = 0; z < p.ModifiedSize.Y; z++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
{
var xx = x + p.ModifiedOffset.X;
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex);
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
if (paintAmount > 0)
{
// Sum the nearby values
float smoothValue = 0;
int smoothValueSamples = 0;
int minX = Math.Max(x - radius + p.ModifiedOffset.X, 0);
int minZ = Math.Max(z - radius + p.ModifiedOffset.Y, 0);
int maxX = Math.Min(x + radius + p.ModifiedOffset.X, max);
int maxZ = Math.Min(z + radius + p.ModifiedOffset.Y, max);
for (int dz = minZ; dz <= maxZ; dz++)
{
for (int dx = minX; dx <= maxX; dx++)
{
var height = p.SourceHeightMap[dz * p.HeightmapSize + dx];
smoothValue += height;
smoothValueSamples++;
}
}
// Normalize
smoothValue /= smoothValueSamples;
// Blend between the height and smooth value
p.TempBuffer[z * p.ModifiedSize.X + x] = Mathf.Lerp(sourceHeight, smoothValue, paintAmount);
}
else
{
p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight;
}
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,800 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.Tabs;
using FlaxEditor.Modules;
using FlaxEditor.SceneGraph;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Tools
{
/// <summary>
/// Vertex painting tab. Allows to modify paint the models vertex colors.
/// </summary>
/// <seealso cref="Tab" />
[HideInEditor]
public class VertexPaintingTab : Tab
{
[CustomEditor(typeof(ProxyEditor))]
sealed class ProxyObject
{
[HideInEditor, NoSerialize]
public readonly VertexPaintingTab Tab;
public ProxyObject(VertexPaintingTab tab)
{
Tab = tab;
}
[EditorOrder(0), EditorDisplay("Brush"), Limit(0.0001f, 100000.0f, 0.1f), Tooltip("The size of the paint brush (sphere radius in world-space).")]
public float BrushSize
{
get => Tab._gizmoMode.BrushSize;
set => Tab._gizmoMode.BrushSize = value;
}
[EditorOrder(10), EditorDisplay("Brush"), Limit(0.0f, 1.0f, 0.01f), Tooltip("The intensity of the brush when painting. Use lower values to make painting smoother and softer.")]
public float BrushStrength
{
get => Tab._gizmoMode.BrushStrength;
set => Tab._gizmoMode.BrushStrength = value;
}
[EditorOrder(20), EditorDisplay("Brush"), Limit(0.0f, 1.0f, 0.01f), Tooltip("The falloff parameter fo the brush. Adjusts the paint strength for the vertices that are far from the brush center. Use lower values to make painting smoother and softer.")]
public float BrushFalloff
{
get => Tab._gizmoMode.BrushFalloff;
set => Tab._gizmoMode.BrushFalloff = value;
}
[EditorOrder(30), EditorDisplay("Brush", "Model LOD"), Limit(-1, Model.MaxLODs, 0.001f), Tooltip("The index of the model LOD to paint over it. To paint over all LODs use value -1.")]
public int ModelLOD
{
get => Tab._gizmoMode.ModelLOD;
set => Tab._gizmoMode.ModelLOD = value;
}
[EditorOrder(100), EditorDisplay("Paint"), Tooltip("The paint color (32-bit RGBA).")]
public Color PaintColor
{
get => Tab._gizmoMode.PaintColor;
set => Tab._gizmoMode.PaintColor = value;
}
[EditorOrder(110), EditorDisplay("Paint"), Tooltip("The paint color mask. Can be used to not exclude certain color channels from painting and preserve their contents.")]
public VertexPaintingGizmoMode.VertexColorsMask PaintMask
{
get => Tab._gizmoMode.PaintMask;
set => Tab._gizmoMode.PaintMask = value;
}
[EditorOrder(120), EditorDisplay("Paint"), Tooltip("If checked, the painting will be continuous action while you move the brush over the model. Otherwise it will use single-click paint action form ore precise painting.")]
public bool ContinuousPaint
{
get => Tab._gizmoMode.ContinuousPaint;
set => Tab._gizmoMode.ContinuousPaint = value;
}
[EditorOrder(200), EditorDisplay("Preview"), Tooltip("The preview mode to use for the selected model visualization.")]
public VertexPaintingGizmoMode.VertexColorsPreviewMode PreviewMode
{
get => Tab._gizmoMode.PreviewMode;
set => Tab._gizmoMode.PreviewMode = value;
}
[EditorOrder(210), EditorDisplay("Preview"), Limit(0, 100.0f, 0.1f), Tooltip("The size of the vertices for the painted vertices visualization.")]
public float PreviewVertexSize
{
get => Tab._gizmoMode.PreviewVertexSize;
set => Tab._gizmoMode.PreviewVertexSize = value;
}
}
sealed class ProxyEditor : GenericEditor
{
private Button _removeVertexColorsButton;
private Button _copyVertexColorsButton;
private Button _pasteVertexColorsButton;
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
layout.Space(10.0f);
_removeVertexColorsButton = layout.Button("Remove vertex colors").Button;
_removeVertexColorsButton.TooltipText = "Removes the painted vertex colors data from the model instance.";
_removeVertexColorsButton.Clicked += OnRemoveVertexColorsButtonClicked;
_copyVertexColorsButton = layout.Button("Copy vertex colors").Button;
_copyVertexColorsButton.TooltipText = "Copies the painted vertex colors data from the model instance to the system clipboard.";
_copyVertexColorsButton.Clicked += OnCopyVertexColorsButtonClicked;
_pasteVertexColorsButton = layout.Button("Paste vertex colors").Button;
_pasteVertexColorsButton.TooltipText = "Pastes the copied vertex colors data from the system clipboard to the model instance.";
_pasteVertexColorsButton.Clicked += OnPasteVertexColorsButtonClicked;
}
private void OnRemoveVertexColorsButtonClicked()
{
var proxy = (ProxyObject)Values[0];
var undoAction = new EditModelVertexColorsAction(proxy.Tab.SelectedModel);
proxy.Tab.SelectedModel.RemoveVertexColors();
undoAction.RecordEnd();
Editor.Instance.Undo.AddAction(undoAction);
}
private void OnCopyVertexColorsButtonClicked()
{
var proxy = (ProxyObject)Values[0];
Clipboard.Text = EditModelVertexColorsAction.GetState(proxy.Tab.SelectedModel);
}
private void OnPasteVertexColorsButtonClicked()
{
var proxy = (ProxyObject)Values[0];
var undoAction = new EditModelVertexColorsAction(proxy.Tab.SelectedModel);
EditModelVertexColorsAction.SetState(proxy.Tab.SelectedModel, Clipboard.Text);
undoAction.RecordEnd();
Editor.Instance.Undo.AddAction(undoAction);
}
public override void Refresh()
{
var proxy = (ProxyObject)Values[0];
_removeVertexColorsButton.Enabled = _copyVertexColorsButton.Enabled = proxy.Tab.SelectedModel?.HasVertexColors ?? false;
_pasteVertexColorsButton.Enabled = EditModelVertexColorsAction.IsValidState(Clipboard.Text);
base.Refresh();
}
}
private readonly ProxyObject _proxy;
private readonly CustomEditorPresenter _presenter;
private VertexPaintingGizmoMode _gizmoMode;
internal MaterialBase[] _cachedMaterials;
private readonly Editor _editor;
/// <summary>
/// The cached selected model. It's synchronized with <see cref="SceneEditingModule.Selection"/>.
/// </summary>
public StaticModel SelectedModel;
/// <summary>
/// Occurs when selected model gets changed (to a different value).
/// </summary>
public event Action SelectedModelChanged;
/// <summary>
/// Initializes a new instance of the <see cref="VertexPaintingTab"/> class.
/// </summary>
/// <param name="icon">The icon.</param>
/// <param name="editor">The editor instance.</param>
public VertexPaintingTab(SpriteHandle icon, Editor editor)
: base(string.Empty, icon)
{
_editor = editor;
_proxy = new ProxyObject(this);
var panel = new Panel(ScrollBars.Vertical)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
var presenter = new CustomEditorPresenter(null, "No model selected");
presenter.Panel.Parent = panel;
_presenter = presenter;
}
private void OnSelectionChanged()
{
var node = _editor.SceneEditing.SelectionCount > 0 ? _editor.SceneEditing.Selection[0] as ActorNode : null;
var model = node?.Actor as StaticModel;
if (model != SelectedModel)
{
if (SelectedModel != null)
{
Level.SceneSaving -= OnSceneSaving;
if (_cachedMaterials != null)
{
for (int i = 0; i < _cachedMaterials.Length; i++)
SelectedModel.SetMaterial(i, _cachedMaterials[i]);
_cachedMaterials = null;
}
}
_presenter.Select(model ? _proxy : null);
SelectedModel = model;
if (SelectedModel != null)
{
var entries = SelectedModel.Entries;
_cachedMaterials = new MaterialBase[entries.Length];
for (int i = 0; i < _cachedMaterials.Length; i++)
_cachedMaterials[i] = entries[i].Material;
Level.SceneSaving += OnSceneSaving;
}
SelectedModelChanged?.Invoke();
}
}
private void OnSceneSaving(Scene scene, Guid id)
{
// Ensure that selected model has own materials during saving
if (_cachedMaterials != null && SelectedModel != null)
{
for (int i = 0; i < _cachedMaterials.Length; i++)
SelectedModel.SetMaterial(i, _cachedMaterials[i]);
}
}
private void UpdateGizmoMode()
{
if (_gizmoMode == null)
{
_gizmoMode = new VertexPaintingGizmoMode
{
Tab = this,
};
_editor.Windows.EditWin.Viewport.AddMode(_gizmoMode);
}
_editor.Windows.EditWin.Viewport.SetActiveMode(_gizmoMode);
}
/// <inheritdoc />
public override void OnSelected()
{
base.OnSelected();
UpdateGizmoMode();
OnSelectionChanged();
_editor.SceneEditing.SelectionChanged += OnSelectionChanged;
}
/// <inheritdoc />
public override void OnDeselected()
{
if (SelectedModel)
{
Level.SceneSaving -= OnSceneSaving;
if (_cachedMaterials != null)
{
for (int i = 0; i < _cachedMaterials.Length; i++)
SelectedModel.SetMaterial(i, _cachedMaterials[i]);
_cachedMaterials = null;
}
_presenter.Deselect();
SelectedModel = null;
SelectedModelChanged?.Invoke();
}
_editor.SceneEditing.SelectionChanged -= OnSelectionChanged;
base.OnDeselected();
}
}
class VertexPaintingGizmoMode : EditorGizmoMode
{
public enum VertexColorsPreviewMode
{
None,
RGB,
Red,
Green,
Blue,
Alpha,
}
[Flags]
public enum VertexColorsMask
{
Red = 1,
Green = 2,
Blue = 4,
Alpha = 8,
RGB = Red | Green | Blue,
RGBA = Red | Green | Blue | Alpha,
}
public VertexPaintingTab Tab;
public VertexPaintingGizmo Gizmo;
public VertexColorsPreviewMode PreviewMode = VertexColorsPreviewMode.RGB;
public float PreviewVertexSize = 6.0f;
public float BrushSize = 100.0f;
public float BrushStrength = 1.0f;
public float BrushFalloff = 1.0f;
public bool ContinuousPaint;
public int ModelLOD = -1;
public Color PaintColor = Color.White;
public VertexColorsMask PaintMask = VertexColorsMask.RGB;
public override void Init(MainEditorGizmoViewport viewport)
{
base.Init(viewport);
Gizmo = new VertexPaintingGizmo(viewport, this);
}
public override void OnActivated()
{
base.OnActivated();
Viewport.Gizmos.Active = Gizmo;
}
}
sealed class VertexPaintingGizmo : GizmoBase
{
private struct MeshData
{
public Mesh.Vertex[] VertexBuffer;
}
private MaterialInstance _vertexColorsPreviewMaterial;
private Model _brushModel;
private MaterialInstance _brushMaterial;
private MaterialBase _verticesPreviewMaterial;
private VertexPaintingGizmoMode _gizmoMode;
private bool _isPainting;
private int _paintUpdateCount;
private bool _hasHit;
private Vector3 _hitLocation;
private Vector3 _hitNormal;
private StaticModel _selectedModel;
private MeshData[][] _meshDatas;
private bool _meshDatasInProgress;
private bool _meshDatasCancel;
private EditModelVertexColorsAction _undoAction;
public bool IsPainting => _isPainting;
public VertexPaintingGizmo(IGizmoOwner owner, VertexPaintingGizmoMode mode)
: base(owner)
{
_gizmoMode = mode;
}
private void SetPaintModel(StaticModel model)
{
if (_selectedModel == model)
return;
PaintEnd();
WaitForMeshDataRequestEnd();
_selectedModel = model;
_meshDatas = null;
_meshDatasInProgress = false;
_meshDatasCancel = false;
_hasHit = false;
RequestMeshData();
}
private void RequestMeshData()
{
if (_meshDatasInProgress)
return;
if (_meshDatas != null)
return;
_meshDatasInProgress = true;
_meshDatasCancel = false;
Task.Run(DownloadMeshData);
}
private void WaitForMeshDataRequestEnd()
{
if (_meshDatasInProgress)
{
_meshDatasCancel = true;
for (int i = 0; i < 500 && _meshDatasInProgress; i++)
Thread.Sleep(10);
}
}
private void DownloadMeshData()
{
try
{
if (!_selectedModel)
{
_meshDatasInProgress = false;
return;
}
var model = _selectedModel.Model;
var lods = model.LODs;
_meshDatas = new MeshData[lods.Length][];
for (int lodIndex = 0; lodIndex < lods.Length && !_meshDatasCancel; lodIndex++)
{
var lod = lods[lodIndex];
var meshes = lod.Meshes;
_meshDatas[lodIndex] = new MeshData[meshes.Length];
for (int meshIndex = 0; meshIndex < meshes.Length && !_meshDatasCancel; meshIndex++)
{
var mesh = meshes[meshIndex];
_meshDatas[lodIndex][meshIndex] = new MeshData
{
VertexBuffer = mesh.DownloadVertexBuffer()
};
}
}
}
catch (Exception ex)
{
Editor.LogWarning("Failed to get mesh data. " + ex.Message);
Editor.LogWarning(ex);
}
finally
{
_meshDatasInProgress = false;
}
}
private void PaintStart()
{
if (IsPainting)
return;
if (Editor.Instance.Undo.Enabled)
_undoAction = new EditModelVertexColorsAction(_selectedModel);
_isPainting = true;
_paintUpdateCount = 0;
}
private void PaintUpdate()
{
if (!IsPainting || _gizmoMode.PaintMask == 0)
return;
if (!_gizmoMode.ContinuousPaint && _paintUpdateCount > 0)
return;
Profiler.BeginEvent("Vertex Paint");
// Ensure to have vertex data ready
WaitForMeshDataRequestEnd();
// Edit the model vertex colors
var meshDatas = _meshDatas;
if (meshDatas == null)
throw new Exception("Missing mesh data of the model to paint.");
var instanceTransform = _selectedModel.Transform;
var brushSphere = new BoundingSphere(_hitLocation, _gizmoMode.BrushSize);
if (_paintUpdateCount == 0 && !_selectedModel.HasVertexColors)
{
// Initialize the instance vertex colors with originals from the asset
for (int lodIndex = 0; lodIndex < meshDatas.Length; lodIndex++)
{
if (_gizmoMode.ModelLOD != -1 && _gizmoMode.ModelLOD != lodIndex)
continue;
var lodData = meshDatas[lodIndex];
for (int meshIndex = 0; meshIndex < lodData.Length; meshIndex++)
{
var meshData = lodData[meshIndex];
for (int vertexIndex = 0; vertexIndex < meshData.VertexBuffer.Length; vertexIndex++)
{
ref var v = ref meshData.VertexBuffer[vertexIndex];
_selectedModel.SetVertexColor(lodIndex, meshIndex, vertexIndex, v.Color);
}
}
}
}
for (int lodIndex = 0; lodIndex < meshDatas.Length; lodIndex++)
{
if (_gizmoMode.ModelLOD != -1 && _gizmoMode.ModelLOD != lodIndex)
continue;
var lodData = meshDatas[lodIndex];
for (int meshIndex = 0; meshIndex < lodData.Length; meshIndex++)
{
var meshData = lodData[meshIndex];
for (int vertexIndex = 0; vertexIndex < meshData.VertexBuffer.Length; vertexIndex++)
{
ref var v = ref meshData.VertexBuffer[vertexIndex];
var pos = instanceTransform.LocalToWorld(v.Position);
var dst = Vector3.Distance(ref pos, ref brushSphere.Center);
if (dst > brushSphere.Radius)
continue;
float strength = _gizmoMode.BrushStrength * Mathf.Lerp(1.0f, 1.0f - dst / brushSphere.Radius, _gizmoMode.BrushFalloff);
if (strength > Mathf.Epsilon)
{
// Paint the vertex
var color = (Color)_selectedModel.GetVertexColor(lodIndex, meshIndex, vertexIndex);
var paintColor = _gizmoMode.PaintColor;
var paintMask = _gizmoMode.PaintMask;
color = new Color
(
Mathf.Lerp(color.R, paintColor.R, ((paintMask & VertexPaintingGizmoMode.VertexColorsMask.Red) == VertexPaintingGizmoMode.VertexColorsMask.Red ? strength : 0.0f)),
Mathf.Lerp(color.G, paintColor.G, ((paintMask & VertexPaintingGizmoMode.VertexColorsMask.Green) == VertexPaintingGizmoMode.VertexColorsMask.Green ? strength : 0.0f)),
Mathf.Lerp(color.B, paintColor.B, ((paintMask & VertexPaintingGizmoMode.VertexColorsMask.Blue) == VertexPaintingGizmoMode.VertexColorsMask.Blue ? strength : 0.0f)),
Mathf.Lerp(color.A, paintColor.A, ((paintMask & VertexPaintingGizmoMode.VertexColorsMask.Alpha) == VertexPaintingGizmoMode.VertexColorsMask.Alpha ? strength : 0.0f))
);
_selectedModel.SetVertexColor(lodIndex, meshIndex, vertexIndex, color);
}
}
}
}
_paintUpdateCount++;
Profiler.EndEvent();
}
private void PaintEnd()
{
if (!IsPainting)
return;
if (_undoAction != null)
{
_undoAction.RecordEnd();
Editor.Instance.Undo.AddAction(_undoAction);
_undoAction = null;
}
_isPainting = false;
_paintUpdateCount = 0;
}
/// <inheritdoc />
public override void Update(float dt)
{
_hasHit = false;
base.Update(dt);
if (!IsActive)
{
SetPaintModel(null);
return;
}
// Select model
var model = _gizmoMode.Tab.SelectedModel;
SetPaintModel(model);
if (!model)
{
return;
}
// Perform detailed tracing to find cursor location for the brush
var ray = Owner.MouseRay;
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives;
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out var closest, out var hitNormal, rayCastFlags);
if (hit != null && hit is ActorNode actorNode && actorNode.Actor as StaticModel != model)
{
// Cursor hit other model
PaintEnd();
return;
}
if (hit != null)
{
_hasHit = true;
_hitLocation = ray.GetPoint(closest);
_hitNormal = hitNormal;
}
// Handle painting
if (Owner.IsLeftMouseButtonDown)
PaintStart();
else
PaintEnd();
PaintUpdate();
}
/// <inheritdoc />
public override void Pick()
{
// Get mouse ray and try to hit any object
var ray = Owner.MouseRay;
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives;
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags);
// Update selection
var sceneEditing = Editor.Instance.SceneEditing;
if (hit != null && hit is ActorNode actorNode && actorNode.Actor is StaticModel model)
{
sceneEditing.Select(hit);
}
}
/// <inheritdoc />
public override void Draw(ref RenderContext renderContext)
{
if (!IsActive || !_selectedModel)
return;
base.Draw(ref renderContext);
if (_hasHit)
{
// Draw paint brush
if (!_brushModel)
{
_brushModel = FlaxEngine.Content.LoadAsyncInternal<Model>("Editor/Primitives/Sphere");
}
if (!_brushMaterial)
{
var material = FlaxEngine.Content.LoadAsyncInternal<Material>(EditorAssets.FoliageBrushMaterial);
_brushMaterial = material.CreateVirtualInstance();
}
if (_brushModel && _brushMaterial)
{
_brushMaterial.SetParameterValue("Color", new Color(1.0f, 0.85f, 0.0f)); // TODO: expose to editor options
_brushMaterial.SetParameterValue("DepthBuffer", Owner.RenderTask.Buffers.DepthBuffer);
Quaternion rotation;
if (_hitNormal == Vector3.Down)
rotation = Quaternion.RotationZ(Mathf.Pi);
else
rotation = Quaternion.LookRotation(Vector3.Cross(Vector3.Cross(_hitNormal, Vector3.Forward), _hitNormal), _hitNormal);
Matrix transform = Matrix.Scaling(_gizmoMode.BrushSize * 0.01f) * Matrix.RotationQuaternion(rotation) * Matrix.Translation(_hitLocation);
_brushModel.Draw(ref renderContext, _brushMaterial, ref transform);
}
// Draw intersecting vertices
var meshDatas = _meshDatas;
if (meshDatas != null && _gizmoMode.PreviewVertexSize > Mathf.Epsilon)
{
if (!_verticesPreviewMaterial)
{
_verticesPreviewMaterial = FlaxEngine.Content.LoadAsyncInternal<MaterialBase>(EditorAssets.WiresDebugMaterial);
}
var instanceTransform = _selectedModel.Transform;
var modelScaleMatrix = Matrix.Scaling(_gizmoMode.PreviewVertexSize * 0.01f);
var brushSphere = new BoundingSphere(_hitLocation, _gizmoMode.BrushSize);
var lodIndex = _gizmoMode.ModelLOD == -1 ? RenderTools.ComputeModelLOD(_selectedModel.Model, ref renderContext.View.Position, _selectedModel.Sphere.Radius, ref renderContext) : _gizmoMode.ModelLOD;
lodIndex = Mathf.Clamp(lodIndex, 0, meshDatas.Length - 1);
var lodData = meshDatas[lodIndex];
if (lodData != null)
{
for (int meshIndex = 0; meshIndex < lodData.Length; meshIndex++)
{
var meshData = lodData[meshIndex];
if (meshData.VertexBuffer == null)
continue;
for (int vertexIndex = 0; vertexIndex < meshData.VertexBuffer.Length; vertexIndex++)
{
ref var v = ref meshData.VertexBuffer[vertexIndex];
var pos = instanceTransform.LocalToWorld(v.Position);
if (brushSphere.Contains(ref pos) == ContainmentType.Disjoint)
continue;
Matrix transform = modelScaleMatrix * Matrix.Translation(pos);
_brushModel.Draw(ref renderContext, _verticesPreviewMaterial, ref transform);
}
}
}
}
}
// Update vertex colors preview
var cachedMaterials = _gizmoMode.Tab._cachedMaterials;
if (cachedMaterials == null)
return;
var previewMode = _gizmoMode.PreviewMode;
if (previewMode == VertexPaintingGizmoMode.VertexColorsPreviewMode.None)
{
for (int i = 0; i < cachedMaterials.Length; i++)
_selectedModel.SetMaterial(i, cachedMaterials[i]);
return;
}
if (!_vertexColorsPreviewMaterial)
_vertexColorsPreviewMaterial = FlaxEngine.Content.LoadAsyncInternal<MaterialBase>(EditorAssets.VertexColorsPreviewMaterial).CreateVirtualInstance();
if (!_vertexColorsPreviewMaterial)
return;
var channelMask = new Vector4();
switch (previewMode)
{
case VertexPaintingGizmoMode.VertexColorsPreviewMode.RGB:
channelMask = new Vector4(1, 1, 1, 0);
break;
case VertexPaintingGizmoMode.VertexColorsPreviewMode.Red:
channelMask.X = 1.0f;
break;
case VertexPaintingGizmoMode.VertexColorsPreviewMode.Green:
channelMask.Y = 1.0f;
break;
case VertexPaintingGizmoMode.VertexColorsPreviewMode.Blue:
channelMask.Z = 1.0f;
break;
case VertexPaintingGizmoMode.VertexColorsPreviewMode.Alpha:
channelMask.W = 1.0f;
break;
}
_vertexColorsPreviewMaterial.SetParameterValue("ChannelMask", channelMask);
for (int i = 0; i < cachedMaterials.Length; i++)
_selectedModel.SetMaterial(i, _vertexColorsPreviewMaterial);
}
public override void OnActivated()
{
base.OnActivated();
_hasHit = false;
}
public override void OnDeactivated()
{
base.OnDeactivated();
PaintEnd();
SetPaintModel(null);
Object.Destroy(ref _vertexColorsPreviewMaterial);
Object.Destroy(ref _brushMaterial);
_brushModel = null;
_verticesPreviewMaterial = null;
}
}
sealed class EditModelVertexColorsAction : IUndoAction
{
private Guid _actorId;
private string _before, _after;
public EditModelVertexColorsAction(StaticModel model)
{
_actorId = model.ID;
_before = GetState(model);
}
public static bool IsValidState(string state)
{
return state != null && state.Contains("\"VertexColors\":");
}
public static string GetState(StaticModel model)
{
var json = model.ToJson();
var start = json.IndexOf("\"VertexColors\":");
if (start == -1)
return null;
var end = json.IndexOf(']', start);
json = "{" + json.Substring(start, end - start) + "]}";
return json;
}
public static void SetState(StaticModel model, string state)
{
if (state == null)
model.RemoveVertexColors();
else
Editor.Internal_DeserializeSceneObject(Object.GetUnmanagedPtr(model), state);
}
public void RecordEnd()
{
var model = Object.Find<StaticModel>(ref _actorId);
_after = GetState(model);
Editor.Instance.Scene.MarkSceneEdited(model.Scene);
}
private void Set(string state)
{
var model = Object.Find<StaticModel>(ref _actorId);
SetState(model, state);
Editor.Instance.Scene.MarkSceneEdited(model.Scene);
}
public string ActionString => "Edit Vertex Colors";
public void Do()
{
Set(_after);
}
public void Undo()
{
Set(_before);
}
public void Dispose()
{
_before = _after = null;
}
}
}