You're breathtaking!
This commit is contained in:
60
Source/Editor/Tools/Foliage/Brush.cs
Normal file
60
Source/Editor/Tools/Foliage/Brush.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
243
Source/Editor/Tools/Foliage/EditFoliageGizmo.cs
Normal file
243
Source/Editor/Tools/Foliage/EditFoliageGizmo.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Source/Editor/Tools/Foliage/EditFoliageGizmoMode.cs
Normal file
98
Source/Editor/Tools/Foliage/EditFoliageGizmoMode.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Source/Editor/Tools/Foliage/EditFoliageSelectionOutline.cs
Normal file
71
Source/Editor/Tools/Foliage/EditFoliageSelectionOutline.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
244
Source/Editor/Tools/Foliage/EditTab.cs
Normal file
244
Source/Editor/Tools/Foliage/EditTab.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
252
Source/Editor/Tools/Foliage/FoliageTab.cs
Normal file
252
Source/Editor/Tools/Foliage/FoliageTab.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
396
Source/Editor/Tools/Foliage/FoliageTools.cpp
Normal file
396
Source/Editor/Tools/Foliage/FoliageTools.cpp
Normal 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();
|
||||
}
|
||||
46
Source/Editor/Tools/Foliage/FoliageTools.h
Normal file
46
Source/Editor/Tools/Foliage/FoliageTools.h
Normal 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);
|
||||
};
|
||||
535
Source/Editor/Tools/Foliage/FoliageTypesTab.cs
Normal file
535
Source/Editor/Tools/Foliage/FoliageTypesTab.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
223
Source/Editor/Tools/Foliage/PaintFoliageGizmo.cs
Normal file
223
Source/Editor/Tools/Foliage/PaintFoliageGizmo.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
Source/Editor/Tools/Foliage/PaintFoliageGizmoMode.cs
Normal file
108
Source/Editor/Tools/Foliage/PaintFoliageGizmoMode.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
337
Source/Editor/Tools/Foliage/PaintTab.cs
Normal file
337
Source/Editor/Tools/Foliage/PaintTab.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Source/Editor/Tools/Foliage/Undo/DeleteInstanceAction.cs
Normal file
69
Source/Editor/Tools/Foliage/Undo/DeleteInstanceAction.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Source/Editor/Tools/Foliage/Undo/EditFoliageAction.cs
Normal file
80
Source/Editor/Tools/Foliage/Undo/EditFoliageAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
Source/Editor/Tools/Foliage/Undo/EditInstanceAction.cs
Normal file
78
Source/Editor/Tools/Foliage/Undo/EditInstanceAction.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Source/Editor/Tools/Terrain/Brushes/Brush.cs
Normal file
56
Source/Editor/Tools/Terrain/Brushes/Brush.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Brushes
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain sculpture or paint brush logic descriptor.
|
||||
/// </summary>
|
||||
[HideInEditor]
|
||||
public abstract class Brush
|
||||
{
|
||||
/// <summary>
|
||||
/// The cached material instance for the brush usage.
|
||||
/// </summary>
|
||||
protected MaterialInstance _material;
|
||||
|
||||
/// <summary>
|
||||
/// The brush size (in world units). Within this area, the brush will have at least some effect.
|
||||
/// </summary>
|
||||
[EditorOrder(0), Limit(0.0f, 1000000.0f, 10.0f), Tooltip("The brush size (in world units). Within this area, the brush will have at least some effect.")]
|
||||
public float Size = 4000.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush material for the terrain chunk rendering. It must have domain set to Terrain. Setup material parameters within this call.
|
||||
/// </summary>
|
||||
/// <param name="position">The world-space brush position.</param>
|
||||
/// <param name="color">The brush position.</param>
|
||||
/// <returns>The ready to render material for terrain chunks overlay on top of the terrain.</returns>
|
||||
public abstract MaterialInstance GetBrushMaterial(ref Vector3 position, ref Color color);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the brush material from the internal location. It's later cached by the object and reused.
|
||||
/// </summary>
|
||||
/// <param name="internalPath">The brush material path (for in-build editor brushes).</param>
|
||||
/// <returns>The brush material instance or null if cannot load or missing.</returns>
|
||||
protected MaterialInstance CacheMaterial(string internalPath)
|
||||
{
|
||||
if (!_material)
|
||||
{
|
||||
var material = FlaxEngine.Content.LoadAsyncInternal<Material>(internalPath);
|
||||
material.WaitForLoaded();
|
||||
_material = material.CreateVirtualInstance();
|
||||
}
|
||||
return _material;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Samples the brush intensity at the specified position.
|
||||
/// </summary>
|
||||
/// <param name="brushPosition">The brush center position (world-space).</param>
|
||||
/// <param name="samplePosition">The sample position (world-space).</param>
|
||||
/// <returns>The sampled value. Normalized to range 0-1 as an intensity to apply.</returns>
|
||||
public abstract float Sample(ref Vector3 brushPosition, ref Vector3 samplePosition);
|
||||
}
|
||||
}
|
||||
135
Source/Editor/Tools/Terrain/Brushes/CircleBrush.cs
Normal file
135
Source/Editor/Tools/Terrain/Brushes/CircleBrush.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Brushes
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain brush that has circle shape and uses radial falloff.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Tools.Terrain.Brushes.Brush" />
|
||||
[HideInEditor]
|
||||
public class CircleBrush : Brush
|
||||
{
|
||||
/// <summary>
|
||||
/// Circle brush falloff types.
|
||||
/// </summary>
|
||||
public enum FalloffTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// A linear falloff that has been smoothed to round off the sharp edges where the falloff begins and ends.
|
||||
/// </summary>
|
||||
Smooth = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A sharp linear falloff, without rounded edges.
|
||||
/// </summary>
|
||||
Linear = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A half-ellipsoid-shaped falloff that begins smoothly and ends sharply.
|
||||
/// </summary>
|
||||
Spherical = 2,
|
||||
|
||||
/// <summary>
|
||||
/// A falloff with an abrupt start and a smooth ellipsoidal end. The opposite of the Sphere falloff.
|
||||
/// </summary>
|
||||
Tip = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The brush falloff that defines the percentage from the brush's extents where the falloff should begin. Essentially, this describes how hard the brush's edges are. A falloff of 0 means the brush will have full effect throughout with hard edges. A falloff of 1 means the brush will only have full effect at its center, and the effect will be reduced throughout its entire area to the edge.
|
||||
/// </summary>
|
||||
[EditorOrder(10), Limit(0, 1, 0.01f), Tooltip("The brush falloff that defines the percentage from the brush's extents where the falloff should begin.")]
|
||||
public float Falloff = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The brush falloff type. Defines circle brush falloff mode.
|
||||
/// </summary>
|
||||
[EditorOrder(20), Tooltip("The brush falloff type. Defines circle brush falloff mode.")]
|
||||
public FalloffTypes FalloffType = FalloffTypes.Smooth;
|
||||
|
||||
private delegate float CalculateFalloffDelegate(float distance, float radius, float falloff);
|
||||
|
||||
private float CalculateFalloff_Smooth(float distance, float radius, float falloff)
|
||||
{
|
||||
// Smooth-step linear falloff
|
||||
float alpha = CalculateFalloff_Linear(distance, radius, falloff);
|
||||
return alpha * alpha * (3 - 2 * alpha);
|
||||
}
|
||||
|
||||
private float CalculateFalloff_Linear(float distance, float radius, float falloff)
|
||||
{
|
||||
return distance < radius ? 1.0f : falloff > 0.0f ? Mathf.Max(0.0f, 1.0f - (distance - radius) / falloff) : 0.0f;
|
||||
}
|
||||
|
||||
private float CalculateFalloff_Spherical(float distance, float radius, float falloff)
|
||||
{
|
||||
if (distance <= radius)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
if (distance > radius + falloff)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Elliptical falloff
|
||||
return Mathf.Sqrt(1.0f - Mathf.Square((distance - radius) / falloff));
|
||||
}
|
||||
|
||||
private float CalculateFalloff_Tip(float distance, float radius, float falloff)
|
||||
{
|
||||
if (distance <= radius)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
if (distance > radius + falloff)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Inverse elliptical falloff
|
||||
return 1.0f - Mathf.Sqrt(1.0f - Mathf.Square((falloff + radius - distance) / falloff));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override MaterialInstance GetBrushMaterial(ref Vector3 position, ref Color color)
|
||||
{
|
||||
var material = CacheMaterial(EditorAssets.TerrainCircleBrushMaterial);
|
||||
if (material)
|
||||
{
|
||||
// Data 0: XYZ: position, W: radius
|
||||
// Data 1: X: falloff, Y: type
|
||||
float halfSize = Size * 0.5f;
|
||||
float falloff = halfSize * Falloff;
|
||||
float radius = halfSize - falloff;
|
||||
material.SetParameterValue("Color", color);
|
||||
material.SetParameterValue("BrushData0", new Vector4(position, radius));
|
||||
material.SetParameterValue("BrushData1", new Vector4(falloff, (float)FalloffType, 0, 0));
|
||||
}
|
||||
return material;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override float Sample(ref Vector3 brushPosition, ref Vector3 samplePosition)
|
||||
{
|
||||
Vector3.DistanceXZ(ref brushPosition, ref samplePosition, out var distanceXZ);
|
||||
float halfSize = Size * 0.5f;
|
||||
float falloff = halfSize * Falloff;
|
||||
float radius = halfSize - falloff;
|
||||
|
||||
switch (FalloffType)
|
||||
{
|
||||
case FalloffTypes.Smooth: return CalculateFalloff_Smooth(distanceXZ, radius, falloff);
|
||||
case FalloffTypes.Linear: return CalculateFalloff_Linear(distanceXZ, radius, falloff);
|
||||
case FalloffTypes.Spherical: return CalculateFalloff_Spherical(distanceXZ, radius, falloff);
|
||||
case FalloffTypes.Tip: return CalculateFalloff_Tip(distanceXZ, radius, falloff);
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
187
Source/Editor/Tools/Terrain/CarveTab.cs
Normal file
187
Source/Editor/Tools/Terrain/CarveTab.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEditor.GUI.Tabs;
|
||||
using FlaxEditor.Modules;
|
||||
using FlaxEditor.SceneGraph.Actors;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain carving tab. Supports different modes for terrain editing including: carving, painting and managing tools.
|
||||
/// </summary>
|
||||
/// <seealso cref="Tab" />
|
||||
[HideInEditor]
|
||||
public class CarveTab : Tab
|
||||
{
|
||||
private readonly Tabs _modes;
|
||||
private readonly ContainerControl _noTerrainPanel;
|
||||
|
||||
/// <summary>
|
||||
/// The editor instance.
|
||||
/// </summary>
|
||||
public readonly Editor Editor;
|
||||
|
||||
/// <summary>
|
||||
/// The cached selected terrain. It's synchronized with <see cref="SceneEditingModule.Selection"/>.
|
||||
/// </summary>
|
||||
public FlaxEngine.Terrain SelectedTerrain;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when selected terrain gets changed (to a different value).
|
||||
/// </summary>
|
||||
public event Action SelectedTerrainChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The sculpt tab;
|
||||
/// </summary>
|
||||
public SculptTab Sculpt;
|
||||
|
||||
/// <summary>
|
||||
/// The paint tab;
|
||||
/// </summary>
|
||||
public PaintTab Paint;
|
||||
|
||||
/// <summary>
|
||||
/// The edit tab;
|
||||
/// </summary>
|
||||
public EditTab Edit;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CarveTab"/> class.
|
||||
/// </summary>
|
||||
/// <param name="icon">The icon.</param>
|
||||
/// <param name="editor">The editor instance.</param>
|
||||
public CarveTab(SpriteHandle icon, Editor editor)
|
||||
: base(string.Empty, icon)
|
||||
{
|
||||
Editor = editor;
|
||||
Editor.SceneEditing.SelectionChanged += OnSelectionChanged;
|
||||
|
||||
Selected += OnSelected;
|
||||
|
||||
_modes = new Tabs
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
UseScroll = true,
|
||||
Offsets = Margin.Zero,
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
TabsSize = new Vector2(50, 32),
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Init tool modes
|
||||
InitSculptMode();
|
||||
InitPaintMode();
|
||||
InitEditMode();
|
||||
|
||||
_modes.SelectedTabIndex = 0;
|
||||
|
||||
_noTerrainPanel = new ContainerControl
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
BackgroundColor = Style.Current.Background,
|
||||
Parent = this
|
||||
};
|
||||
var noTerrainLabel = new Label
|
||||
{
|
||||
Text = "Select terrain to edit\nor\n\n\n\n",
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = _noTerrainPanel
|
||||
};
|
||||
var noTerrainButton = new Button
|
||||
{
|
||||
Text = "Create new terrain",
|
||||
AnchorPreset = AnchorPresets.MiddleCenter,
|
||||
Offsets = new Margin(-60, 120, -12, 24),
|
||||
Parent = _noTerrainPanel
|
||||
};
|
||||
noTerrainButton.Clicked += OnCreateNewTerrainClicked;
|
||||
}
|
||||
|
||||
private void OnSelected(Tab tab)
|
||||
{
|
||||
// Auto select first terrain actor to make usage easier
|
||||
if (Editor.SceneEditing.SelectionCount == 1 && Editor.SceneEditing.Selection[0] is SceneGraph.ActorNode actorNode && actorNode.Actor is FlaxEngine.Terrain)
|
||||
return;
|
||||
var actor = Level.FindActor<FlaxEngine.Terrain>();
|
||||
if (actor)
|
||||
{
|
||||
Editor.SceneEditing.Select(actor);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCreateNewTerrainClicked()
|
||||
{
|
||||
Editor.UI.CreateTerrain();
|
||||
}
|
||||
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
var terrainNode = Editor.SceneEditing.SelectionCount > 0 ? Editor.SceneEditing.Selection[0] as TerrainNode : null;
|
||||
var terrain = terrainNode?.Actor as FlaxEngine.Terrain;
|
||||
if (terrain != SelectedTerrain)
|
||||
{
|
||||
SelectedTerrain = terrain;
|
||||
SelectedTerrainChanged?.Invoke();
|
||||
}
|
||||
|
||||
_noTerrainPanel.Visible = terrain == null;
|
||||
}
|
||||
|
||||
private void InitSculptMode()
|
||||
{
|
||||
var tab = _modes.AddTab(Sculpt = new SculptTab(this, Editor.Windows.EditWin.Viewport.SculptTerrainGizmo));
|
||||
tab.Selected += OnTabSelected;
|
||||
}
|
||||
|
||||
private void InitPaintMode()
|
||||
{
|
||||
var tab = _modes.AddTab(Paint = new PaintTab(this, Editor.Windows.EditWin.Viewport.PaintTerrainGizmo));
|
||||
tab.Selected += OnTabSelected;
|
||||
}
|
||||
|
||||
private void InitEditMode()
|
||||
{
|
||||
var tab = _modes.AddTab(Edit = new EditTab(this, Editor.Windows.EditWin.Viewport.EditTerrainGizmo));
|
||||
tab.Selected += OnTabSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSelected()
|
||||
{
|
||||
base.OnSelected();
|
||||
|
||||
UpdateGizmoMode();
|
||||
}
|
||||
|
||||
private void OnTabSelected(Tab tab)
|
||||
{
|
||||
UpdateGizmoMode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the active viewport gizmo mode based on the current mode.
|
||||
/// </summary>
|
||||
private void UpdateGizmoMode()
|
||||
{
|
||||
switch (_modes.SelectedTabIndex)
|
||||
{
|
||||
case 0:
|
||||
Editor.Windows.EditWin.Viewport.SetActiveMode<SculptTerrainGizmoMode>();
|
||||
break;
|
||||
case 1:
|
||||
Editor.Windows.EditWin.Viewport.SetActiveMode<PaintTerrainGizmoMode>();
|
||||
break;
|
||||
case 2:
|
||||
Editor.Windows.EditWin.Viewport.SetActiveMode<EditTerrainGizmoMode>();
|
||||
break;
|
||||
default: throw new IndexOutOfRangeException("Invalid carve tab mode.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
254
Source/Editor/Tools/Terrain/CreateTerrainDialog.cs
Normal file
254
Source/Editor/Tools/Terrain/CreateTerrainDialog.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.GUI.Dialogs;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
// ReSharper disable UnusedMember.Local
|
||||
// ReSharper disable UnusedMember.Global
|
||||
#pragma warning disable 649
|
||||
#pragma warning disable 414
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain creator dialog. Allows user to specify initial terrain properties perform proper setup.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.GUI.Dialogs.Dialog" />
|
||||
sealed class CreateTerrainDialog : Dialog
|
||||
{
|
||||
private enum ChunkSizes
|
||||
{
|
||||
_31 = 31,
|
||||
_63 = 63,
|
||||
_127 = 127,
|
||||
_255 = 255,
|
||||
}
|
||||
|
||||
private class Options
|
||||
{
|
||||
[EditorOrder(100), EditorDisplay("Layout", "Number Of Patches"), DefaultValue(typeof(Int2), "1,1"), Limit(0, 512), Tooltip("Amount of terrain patches in each direction (X and Z). Each terrain patch contains a grid of 16 chunks. Patches can be later added or removed from terrain using a terrain editor tool.")]
|
||||
public Int2 NumberOfPatches = new Int2(1, 1);
|
||||
|
||||
[EditorOrder(110), EditorDisplay("Layout"), DefaultValue(ChunkSizes._127), Tooltip("The size of the chunk (amount of quads per edge for the highest LOD). Must be power of two minus one (eg. 63).")]
|
||||
public ChunkSizes ChunkSize = ChunkSizes._127;
|
||||
|
||||
[EditorOrder(120), EditorDisplay("Layout", "LOD Count"), DefaultValue(6), Limit(1, FlaxEngine.Terrain.MaxLODs), Tooltip("The maximum Level Of Details count. The actual amount of LODs may be lower due to provided chunk size (each LOD has 4 times less quads).")]
|
||||
public int LODCount = 6;
|
||||
|
||||
[EditorOrder(130), EditorDisplay("Layout"), DefaultValue(null), Tooltip("The default material used for terrain rendering (chunks can override this). It must have Domain set to terrain.")]
|
||||
public MaterialBase Material;
|
||||
|
||||
[EditorOrder(200), EditorDisplay("Collision"), DefaultValue(null), AssetReference(typeof(PhysicalMaterial), true), Tooltip("Terrain default physical material used to define the collider physical properties.")]
|
||||
public JsonAsset PhysicalMaterial;
|
||||
|
||||
[EditorOrder(210), EditorDisplay("Collision", "Collision LOD"), DefaultValue(-1), Limit(-1, 100, 0.1f), Tooltip("Terrain geometry LOD index used for collision.")]
|
||||
public int CollisionLOD = -1;
|
||||
|
||||
[EditorOrder(300), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom heightmap texture to import. Used as a source for height field values (from channel Red).")]
|
||||
public Texture Heightmap;
|
||||
|
||||
[EditorOrder(310), EditorDisplay("Import Data"), DefaultValue(5000.0f), Tooltip("Custom heightmap texture values scale. Applied to adjust the normalized heightmap values into the world units.")]
|
||||
public float HeightmapScale = 5000.0f;
|
||||
|
||||
[EditorOrder(320), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers compositing.")]
|
||||
public Texture Splatmap1;
|
||||
|
||||
[EditorOrder(330), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers compositing.")]
|
||||
public Texture Splatmap2;
|
||||
|
||||
[EditorOrder(400), EditorDisplay("Transform", "Position"), DefaultValue(typeof(Vector3), "0,0,0"), Tooltip("Position of the terrain (importer offset it on the Y axis.)")]
|
||||
public Vector3 Position = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
|
||||
[EditorOrder(410), EditorDisplay("Transform", "Rotation"), DefaultValue(typeof(Quaternion), "0,0,0,1"), Tooltip("Orientation of the terrain")]
|
||||
public Quaternion Orientation = Quaternion.Identity;
|
||||
|
||||
[EditorOrder(420), EditorDisplay("Transform", "Scale"), DefaultValue(typeof(Vector3), "1,1,1"), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("Scale of the terrain")]
|
||||
public Vector3 Scale = Vector3.One;
|
||||
}
|
||||
|
||||
private readonly Options _options = new Options();
|
||||
private bool _isDone;
|
||||
private bool _isWorking;
|
||||
private FlaxEngine.Terrain _terrain;
|
||||
private CustomEditorPresenter _editor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CreateTerrainDialog"/> class.
|
||||
/// </summary>
|
||||
public CreateTerrainDialog()
|
||||
: base("Create terrain")
|
||||
{
|
||||
const float TotalWidth = 450;
|
||||
const float EditorHeight = 600;
|
||||
Width = TotalWidth;
|
||||
|
||||
// Header and help description
|
||||
var headerLabel = new Label
|
||||
{
|
||||
Text = "New Terrain",
|
||||
AnchorPreset = AnchorPresets.HorizontalStretchTop,
|
||||
Offsets = new Margin(0, 0, 0, 40),
|
||||
Parent = this,
|
||||
Font = new FontReference(Style.Current.FontTitle)
|
||||
};
|
||||
var infoLabel = new Label
|
||||
{
|
||||
Text = "Specify options for new terrain.\nIt will be added to the first opened scene.\nMany of the following settings can be adjusted later.\nYou can also create terrain at runtime from code.",
|
||||
HorizontalAlignment = TextAlignment.Near,
|
||||
Margin = new Margin(7),
|
||||
AnchorPreset = AnchorPresets.HorizontalStretchTop,
|
||||
Offsets = new Margin(10, -20, 45, 70),
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Buttons
|
||||
const float ButtonsWidth = 60;
|
||||
const float ButtonsHeight = 24;
|
||||
const float ButtonsMargin = 8;
|
||||
var importButton = new Button
|
||||
{
|
||||
Text = "Create",
|
||||
AnchorPreset = AnchorPresets.BottomRight,
|
||||
Offsets = new Margin(-ButtonsWidth - ButtonsMargin, ButtonsWidth, -ButtonsHeight - ButtonsMargin, ButtonsHeight),
|
||||
Parent = this
|
||||
};
|
||||
importButton.Clicked += OnCreate;
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
AnchorPreset = AnchorPresets.BottomRight,
|
||||
Offsets = new Margin(-ButtonsWidth - ButtonsMargin - ButtonsWidth - ButtonsMargin, ButtonsWidth, -ButtonsHeight - ButtonsMargin, ButtonsHeight),
|
||||
Parent = this
|
||||
};
|
||||
cancelButton.Clicked += OnCancel;
|
||||
|
||||
// Settings editor
|
||||
var settingsEditor = new CustomEditorPresenter(null);
|
||||
settingsEditor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
|
||||
settingsEditor.Panel.Offsets = new Margin(2, 2, infoLabel.Bottom + 2, EditorHeight);
|
||||
settingsEditor.Panel.Parent = this;
|
||||
_editor = settingsEditor;
|
||||
|
||||
_dialogSize = new Vector2(TotalWidth, settingsEditor.Panel.Bottom);
|
||||
|
||||
settingsEditor.Select(_options);
|
||||
}
|
||||
|
||||
private void OnCreate()
|
||||
{
|
||||
if (_isWorking)
|
||||
return;
|
||||
|
||||
var scene = Level.GetScene(0);
|
||||
if (scene == null)
|
||||
throw new InvalidOperationException("No scene found to add terrain to it!");
|
||||
|
||||
// Create terrain object and setup some options
|
||||
var terrain = new FlaxEngine.Terrain();
|
||||
terrain.Setup(_options.LODCount, (int)_options.ChunkSize);
|
||||
terrain.Transform = new Transform(_options.Position, _options.Orientation, _options.Scale);
|
||||
terrain.Material = _options.Material;
|
||||
terrain.PhysicalMaterial = _options.PhysicalMaterial;
|
||||
terrain.CollisionLOD = _options.CollisionLOD;
|
||||
if (_options.Heightmap)
|
||||
terrain.Position -= new Vector3(0, _options.HeightmapScale * 0.5f, 0);
|
||||
|
||||
// Add to scene (even if generation fails user gets a terrain in the scene)
|
||||
terrain.Parent = scene;
|
||||
Editor.Instance.Scene.MarkSceneEdited(scene);
|
||||
|
||||
// Show loading label
|
||||
var label = new Label
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Text = "Generating terrain...",
|
||||
BackgroundColor = Style.Current.ForegroundDisabled,
|
||||
Parent = this,
|
||||
};
|
||||
|
||||
// Lock UI
|
||||
_editor.Panel.Enabled = false;
|
||||
_isWorking = true;
|
||||
_isDone = false;
|
||||
|
||||
// Start async work
|
||||
_terrain = terrain;
|
||||
var thread = new System.Threading.Thread(Generate);
|
||||
thread.Name = "Terrain Generator";
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private void Generate()
|
||||
{
|
||||
_isWorking = true;
|
||||
_isDone = false;
|
||||
|
||||
// Call tool to generate the terrain patches from the input data
|
||||
if (TerrainTools.GenerateTerrain(_terrain, ref _options.NumberOfPatches, _options.Heightmap, _options.HeightmapScale, _options.Splatmap1, _options.Splatmap2))
|
||||
{
|
||||
Editor.LogError("Failed to generate terrain. See log for more info.");
|
||||
}
|
||||
|
||||
_isWorking = false;
|
||||
_isDone = true;
|
||||
}
|
||||
|
||||
private void OnCancel()
|
||||
{
|
||||
if (_isWorking)
|
||||
return;
|
||||
|
||||
Close(DialogResult.Cancel);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (_isDone)
|
||||
{
|
||||
Editor.Instance.SceneEditing.Select(_terrain);
|
||||
|
||||
_terrain = null;
|
||||
_isDone = false;
|
||||
Close(DialogResult.OK);
|
||||
return;
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool CanCloseWindow(ClosingReason reason)
|
||||
{
|
||||
if (_isWorking && reason == ClosingReason.User)
|
||||
return false;
|
||||
|
||||
return base.CanCloseWindow(reason);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnKeyDown(KeyboardKeys key)
|
||||
{
|
||||
if (_isWorking)
|
||||
return true;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case KeyboardKeys.Escape:
|
||||
OnCancel();
|
||||
return true;
|
||||
case KeyboardKeys.Return:
|
||||
OnCreate();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
405
Source/Editor/Tools/Terrain/EditTab.cs
Normal file
405
Source/Editor/Tools/Terrain/EditTab.cs
Normal file
@@ -0,0 +1,405 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.GUI.Tabs;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
using Object = FlaxEngine.Object;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Carve tab related to terrain editing. Allows to pick a terrain patch and remove it or add new patches. Can be used to modify selected chunk properties.
|
||||
/// </summary>
|
||||
/// <seealso cref="Tab" />
|
||||
[HideInEditor]
|
||||
public class EditTab : Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// The parent carve tab.
|
||||
/// </summary>
|
||||
public readonly CarveTab CarveTab;
|
||||
|
||||
/// <summary>
|
||||
/// The related edit terrain gizmo.
|
||||
/// </summary>
|
||||
public readonly EditTerrainGizmoMode Gizmo;
|
||||
|
||||
private readonly ComboBox _modeComboBox;
|
||||
private readonly Label _selectionInfoLabel;
|
||||
private readonly Button _deletePatchButton;
|
||||
private readonly Button _exportTerrainButton;
|
||||
private readonly ContainerControl _chunkProperties;
|
||||
private readonly AssetPicker _chunkOverrideMaterial;
|
||||
private bool _isUpdatingUI;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EditTab"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tab">The parent tab.</param>
|
||||
/// <param name="gizmo">The related gizmo.</param>
|
||||
public EditTab(CarveTab tab, EditTerrainGizmoMode gizmo)
|
||||
: base("Edit")
|
||||
{
|
||||
CarveTab = tab;
|
||||
Gizmo = gizmo;
|
||||
CarveTab.SelectedTerrainChanged += OnSelectionChanged;
|
||||
Gizmo.SelectedChunkCoordChanged += OnSelectionChanged;
|
||||
|
||||
// Main panel
|
||||
var panel = new Panel(ScrollBars.Both)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Mode
|
||||
var modeLabel = new Label(4, 4, 40, 18)
|
||||
{
|
||||
HorizontalAlignment = TextAlignment.Near,
|
||||
Text = "Mode:",
|
||||
Parent = panel,
|
||||
};
|
||||
_modeComboBox = new ComboBox(modeLabel.Right + 4, 4, 110)
|
||||
{
|
||||
Parent = panel,
|
||||
};
|
||||
_modeComboBox.AddItem("Edit Chunk");
|
||||
_modeComboBox.AddItem("Add Patch");
|
||||
_modeComboBox.AddItem("Remove Patch");
|
||||
_modeComboBox.AddItem("Export terrain");
|
||||
_modeComboBox.SelectedIndex = 0;
|
||||
_modeComboBox.SelectedIndexChanged += (combobox) => Gizmo.EditMode = (EditTerrainGizmoMode.Modes)combobox.SelectedIndex;
|
||||
Gizmo.ModeChanged += OnGizmoModeChanged;
|
||||
|
||||
// Info
|
||||
_selectionInfoLabel = new Label(modeLabel.X, modeLabel.Bottom + 4, 40, 18 * 3)
|
||||
{
|
||||
VerticalAlignment = TextAlignment.Near,
|
||||
HorizontalAlignment = TextAlignment.Near,
|
||||
Parent = panel,
|
||||
};
|
||||
|
||||
// Chunk Properties
|
||||
_chunkProperties = new Panel(ScrollBars.None)
|
||||
{
|
||||
Location = new Vector2(_selectionInfoLabel.X, _selectionInfoLabel.Bottom + 4),
|
||||
Parent = panel,
|
||||
};
|
||||
var chunkOverrideMaterialLabel = new Label(0, 0, 90, 64)
|
||||
{
|
||||
HorizontalAlignment = TextAlignment.Near,
|
||||
Text = "Override Material",
|
||||
Parent = _chunkProperties,
|
||||
};
|
||||
_chunkOverrideMaterial = new AssetPicker(new ScriptType(typeof(MaterialBase)), new Vector2(chunkOverrideMaterialLabel.Right + 4, 0))
|
||||
{
|
||||
Width = 300.0f,
|
||||
Parent = _chunkProperties,
|
||||
};
|
||||
_chunkOverrideMaterial.SelectedItemChanged += OnSelectedChunkOverrideMaterialChanged;
|
||||
_chunkProperties.Size = new Vector2(_chunkOverrideMaterial.Right + 4, _chunkOverrideMaterial.Bottom + 4);
|
||||
|
||||
// Delete patch
|
||||
_deletePatchButton = new Button(_selectionInfoLabel.X, _selectionInfoLabel.Bottom + 4)
|
||||
{
|
||||
Text = "Delete Patch",
|
||||
Parent = panel,
|
||||
};
|
||||
_deletePatchButton.Clicked += OnDeletePatchButtonClicked;
|
||||
|
||||
// Export terrain
|
||||
_exportTerrainButton = new Button(_selectionInfoLabel.X, _selectionInfoLabel.Bottom + 4)
|
||||
{
|
||||
Text = "Export terrain",
|
||||
Parent = panel,
|
||||
};
|
||||
_exportTerrainButton.Clicked += OnExportTerrainButtonClicked;
|
||||
|
||||
// Update UI to match the current state
|
||||
OnSelectionChanged();
|
||||
OnGizmoModeChanged();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class DeletePatchAction : IUndoAction
|
||||
{
|
||||
[Serialize]
|
||||
private Guid _terrainId;
|
||||
|
||||
[Serialize]
|
||||
private Int2 _patchCoord;
|
||||
|
||||
[Serialize]
|
||||
private string _data;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ActionString => "Delete terrain patch";
|
||||
|
||||
public DeletePatchAction(FlaxEngine.Terrain terrain, ref Int2 patchCoord)
|
||||
{
|
||||
if (terrain == null)
|
||||
throw new ArgumentException(nameof(terrain));
|
||||
|
||||
_terrainId = terrain.ID;
|
||||
_patchCoord = patchCoord;
|
||||
_data = TerrainTools.SerializePatch(terrain, ref patchCoord);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Do()
|
||||
{
|
||||
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
|
||||
if (terrain == null)
|
||||
{
|
||||
Editor.LogError("Missing terrain actor.");
|
||||
return;
|
||||
}
|
||||
|
||||
terrain.GetPatchBounds(terrain.GetPatchIndex(ref _patchCoord), out var patchBounds);
|
||||
terrain.RemovePatch(ref _patchCoord);
|
||||
OnPatchEdit(terrain, ref patchBounds);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
|
||||
if (terrain == null)
|
||||
{
|
||||
Editor.LogError("Missing terrain actor.");
|
||||
return;
|
||||
}
|
||||
|
||||
terrain.AddPatch(ref _patchCoord);
|
||||
TerrainTools.DeserializePatch(terrain, ref _patchCoord, _data);
|
||||
terrain.GetPatchBounds(terrain.GetPatchIndex(ref _patchCoord), out var patchBounds);
|
||||
OnPatchEdit(terrain, ref patchBounds);
|
||||
}
|
||||
|
||||
private void OnPatchEdit(FlaxEngine.Terrain terrain, ref BoundingBox patchBounds)
|
||||
{
|
||||
Editor.Instance.Scene.MarkSceneEdited(terrain.Scene);
|
||||
|
||||
var editorOptions = Editor.Instance.Options.Options;
|
||||
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
|
||||
|
||||
// Auto NavMesh rebuild
|
||||
if (!isPlayMode && editorOptions.General.AutoRebuildNavMesh)
|
||||
{
|
||||
if (terrain.Scene && (terrain.StaticFlags & StaticFlags.Navigation) == StaticFlags.Navigation)
|
||||
{
|
||||
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_data = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeletePatchButtonClicked()
|
||||
{
|
||||
if (_isUpdatingUI)
|
||||
return;
|
||||
|
||||
var patchCoord = Gizmo.SelectedPatchCoord;
|
||||
if (!CarveTab.SelectedTerrain.HasPatch(ref patchCoord))
|
||||
return;
|
||||
|
||||
var action = new DeletePatchAction(CarveTab.SelectedTerrain, ref patchCoord);
|
||||
action.Do();
|
||||
CarveTab.Editor.Undo.AddAction(action);
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class EditChunkMaterialAction : IUndoAction
|
||||
{
|
||||
[Serialize]
|
||||
private Guid _terrainId;
|
||||
|
||||
[Serialize]
|
||||
private Int2 _patchCoord;
|
||||
|
||||
[Serialize]
|
||||
private Int2 _chunkCoord;
|
||||
|
||||
[Serialize]
|
||||
private Guid _beforeMaterial;
|
||||
|
||||
[Serialize]
|
||||
private Guid _afterMaterial;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ActionString => "Edit terrain chunk material";
|
||||
|
||||
public EditChunkMaterialAction(FlaxEngine.Terrain terrain, ref Int2 patchCoord, ref Int2 chunkCoord, MaterialBase toSet)
|
||||
{
|
||||
if (terrain == null)
|
||||
throw new ArgumentException(nameof(terrain));
|
||||
|
||||
_terrainId = terrain.ID;
|
||||
_patchCoord = patchCoord;
|
||||
_chunkCoord = chunkCoord;
|
||||
_beforeMaterial = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord)?.ID ?? Guid.Empty;
|
||||
_afterMaterial = toSet?.ID ?? Guid.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Do()
|
||||
{
|
||||
Set(ref _afterMaterial);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
Set(ref _beforeMaterial);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private void Set(ref Guid id)
|
||||
{
|
||||
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
|
||||
if (terrain == null)
|
||||
{
|
||||
Editor.LogError("Missing terrain actor.");
|
||||
return;
|
||||
}
|
||||
|
||||
terrain.SetChunkOverrideMaterial(ref _patchCoord, ref _chunkCoord, FlaxEngine.Content.LoadAsync<MaterialBase>(id));
|
||||
|
||||
Editor.Instance.Scene.MarkSceneEdited(terrain.Scene);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectedChunkOverrideMaterialChanged()
|
||||
{
|
||||
if (_isUpdatingUI)
|
||||
return;
|
||||
|
||||
var patchCoord = Gizmo.SelectedPatchCoord;
|
||||
var chunkCoord = Gizmo.SelectedChunkCoord;
|
||||
var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.SelectedAsset as MaterialBase);
|
||||
action.Do();
|
||||
CarveTab.Editor.Undo.AddAction(action);
|
||||
}
|
||||
|
||||
private void OnExportTerrainButtonClicked()
|
||||
{
|
||||
if (_isUpdatingUI)
|
||||
return;
|
||||
|
||||
if (FileSystem.ShowBrowseFolderDialog(null, null, "Select the output folder", out var outputFolder))
|
||||
return;
|
||||
TerrainTools.ExportTerrain(CarveTab.SelectedTerrain, outputFolder);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
var terrain = CarveTab.SelectedTerrain;
|
||||
if (terrain == null)
|
||||
{
|
||||
_selectionInfoLabel.Text = "Select a terrain to modify its properties.";
|
||||
_chunkProperties.Visible = false;
|
||||
_deletePatchButton.Visible = false;
|
||||
_exportTerrainButton.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var patchCoord = Gizmo.SelectedPatchCoord;
|
||||
switch (Gizmo.EditMode)
|
||||
{
|
||||
case EditTerrainGizmoMode.Modes.Edit:
|
||||
{
|
||||
var chunkCoord = Gizmo.SelectedChunkCoord;
|
||||
_selectionInfoLabel.Text = string.Format(
|
||||
"Selected terrain: {0}\nPatch: {1}x{2}\nChunk: {3}x{4}",
|
||||
terrain.Name,
|
||||
patchCoord.X, patchCoord.Y,
|
||||
chunkCoord.X, chunkCoord.Y
|
||||
);
|
||||
_chunkProperties.Visible = true;
|
||||
_deletePatchButton.Visible = false;
|
||||
_exportTerrainButton.Visible = false;
|
||||
|
||||
_isUpdatingUI = true;
|
||||
if (terrain.HasPatch(ref patchCoord))
|
||||
{
|
||||
_chunkOverrideMaterial.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord);
|
||||
_chunkOverrideMaterial.Enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_chunkOverrideMaterial.SelectedAsset = null;
|
||||
_chunkOverrideMaterial.Enabled = false;
|
||||
}
|
||||
_isUpdatingUI = false;
|
||||
break;
|
||||
}
|
||||
case EditTerrainGizmoMode.Modes.Add:
|
||||
{
|
||||
if (terrain.HasPatch(ref patchCoord))
|
||||
{
|
||||
_selectionInfoLabel.Text = string.Format(
|
||||
"Selected terrain: {0}\nMove mouse cursor at location without a patch.",
|
||||
terrain.Name
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectionInfoLabel.Text = string.Format(
|
||||
"Selected terrain: {0}\nPatch to add: {1}x{2}\nTo add a new patch press the left mouse button.",
|
||||
terrain.Name,
|
||||
patchCoord.X, patchCoord.Y
|
||||
);
|
||||
}
|
||||
_chunkProperties.Visible = false;
|
||||
_deletePatchButton.Visible = false;
|
||||
_exportTerrainButton.Visible = false;
|
||||
break;
|
||||
}
|
||||
case EditTerrainGizmoMode.Modes.Remove:
|
||||
{
|
||||
_selectionInfoLabel.Text = string.Format(
|
||||
"Selected terrain: {0}\nPatch: {1}x{2}",
|
||||
terrain.Name,
|
||||
patchCoord.X, patchCoord.Y
|
||||
);
|
||||
_chunkProperties.Visible = false;
|
||||
_deletePatchButton.Visible = true;
|
||||
_exportTerrainButton.Visible = false;
|
||||
break;
|
||||
}
|
||||
case EditTerrainGizmoMode.Modes.Export:
|
||||
{
|
||||
_selectionInfoLabel.Text = string.Format(
|
||||
"Selected terrain: {0}",
|
||||
terrain.Name
|
||||
);
|
||||
_chunkProperties.Visible = false;
|
||||
_deletePatchButton.Visible = false;
|
||||
_exportTerrainButton.Visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGizmoModeChanged()
|
||||
{
|
||||
_modeComboBox.SelectedIndex = (int)Gizmo.EditMode;
|
||||
OnSelectionChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
277
Source/Editor/Tools/Terrain/EditTerrainGizmo.cs
Normal file
277
Source/Editor/Tools/Terrain/EditTerrainGizmo.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEditor.Gizmo;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEditor.SceneGraph.Actors;
|
||||
using FlaxEngine;
|
||||
using Object = FlaxEngine.Object;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Gizmo for picking terrain chunks and patches. Managed by the <see cref="EditTerrainGizmoMode"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Gizmo.GizmoBase" />
|
||||
[HideInEditor]
|
||||
public sealed class EditTerrainGizmo : GizmoBase
|
||||
{
|
||||
private Model _planeModel;
|
||||
private MaterialBase _highlightMaterial;
|
||||
private MaterialBase _highlightTerrainMaterial;
|
||||
|
||||
/// <summary>
|
||||
/// The parent mode.
|
||||
/// </summary>
|
||||
public readonly EditTerrainGizmoMode Mode;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EditTerrainGizmo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <param name="mode">The mode.</param>
|
||||
public EditTerrainGizmo(IGizmoOwner owner, EditTerrainGizmoMode mode)
|
||||
: base(owner)
|
||||
{
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
private void GetAssets()
|
||||
{
|
||||
if (_planeModel)
|
||||
return;
|
||||
|
||||
_planeModel = FlaxEngine.Content.LoadAsyncInternal<Model>("Editor/Primitives/Plane");
|
||||
_highlightTerrainMaterial = FlaxEngine.Content.LoadAsyncInternal<MaterialBase>(EditorAssets.HighlightTerrainMaterial);
|
||||
_highlightMaterial = EditorAssets.Cache.HighlightMaterialInstance;
|
||||
}
|
||||
|
||||
private FlaxEngine.Terrain SelectedTerrain
|
||||
{
|
||||
get
|
||||
{
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
|
||||
return (FlaxEngine.Terrain)terrainNode?.Actor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw(ref RenderContext renderContext)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
var terrain = SelectedTerrain;
|
||||
if (!terrain)
|
||||
return;
|
||||
|
||||
GetAssets();
|
||||
|
||||
switch (Mode.EditMode)
|
||||
{
|
||||
case EditTerrainGizmoMode.Modes.Edit:
|
||||
{
|
||||
// Highlight selected chunk
|
||||
var patchCoord = Mode.SelectedPatchCoord;
|
||||
if (terrain.HasPatch(ref patchCoord))
|
||||
{
|
||||
var chunkCoord = Mode.SelectedChunkCoord;
|
||||
terrain.DrawChunk(ref renderContext, ref patchCoord, ref chunkCoord, _highlightTerrainMaterial);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EditTerrainGizmoMode.Modes.Add:
|
||||
{
|
||||
// Highlight patch to add location
|
||||
var patchCoord = Mode.SelectedPatchCoord;
|
||||
if (!terrain.HasPatch(ref patchCoord) && _planeModel)
|
||||
{
|
||||
var planeSize = 256.0f;
|
||||
var patchSize = terrain.ChunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
|
||||
Matrix world = Matrix.RotationZ(-Mathf.PiOverTwo) *
|
||||
Matrix.Scaling(patchSize / planeSize) *
|
||||
Matrix.Translation(patchSize * (0.5f + patchCoord.X), 0, patchSize * (0.5f + patchCoord.Y)) *
|
||||
Matrix.Scaling(terrain.Scale) *
|
||||
Matrix.RotationQuaternion(terrain.Orientation) *
|
||||
Matrix.Translation(terrain.Position);
|
||||
_planeModel.Draw(ref renderContext, _highlightMaterial, ref world);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EditTerrainGizmoMode.Modes.Remove:
|
||||
{
|
||||
// Highlight selected patch
|
||||
var patchCoord = Mode.SelectedPatchCoord;
|
||||
if (terrain.HasPatch(ref patchCoord))
|
||||
{
|
||||
terrain.DrawPatch(ref renderContext, ref patchCoord, _highlightTerrainMaterial);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float dt)
|
||||
{
|
||||
base.Update(dt);
|
||||
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
if (Mode.EditMode == EditTerrainGizmoMode.Modes.Add)
|
||||
{
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
|
||||
if (terrainNode != null)
|
||||
{
|
||||
// Check if mouse ray hits any of the terrain patches sides to add a new patch there
|
||||
var mouseRay = Owner.MouseRay;
|
||||
var terrain = (FlaxEngine.Terrain)terrainNode.Actor;
|
||||
var chunkCoord = Int2.Zero;
|
||||
if (!TerrainTools.TryGetPatchCoordToAdd(terrain, mouseRay, out var patchCoord))
|
||||
{
|
||||
// If terrain has no patches TryGetPatchCoordToAdd will always return the default patch to add, otherwise fallback to already used location
|
||||
terrain.GetPatchCoord(0, out patchCoord);
|
||||
}
|
||||
Mode.SetSelectedChunk(ref patchCoord, ref chunkCoord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class AddPatchAction : IUndoAction
|
||||
{
|
||||
[Serialize]
|
||||
private Guid _terrainId;
|
||||
|
||||
[Serialize]
|
||||
private Int2 _patchCoord;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ActionString => "Add terrain patch";
|
||||
|
||||
public AddPatchAction(FlaxEngine.Terrain terrain, ref Int2 patchCoord)
|
||||
{
|
||||
if (terrain == null)
|
||||
throw new ArgumentException(nameof(terrain));
|
||||
|
||||
_terrainId = terrain.ID;
|
||||
_patchCoord = patchCoord;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Do()
|
||||
{
|
||||
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
|
||||
if (terrain == null)
|
||||
{
|
||||
Editor.LogError("Missing terrain actor.");
|
||||
return;
|
||||
}
|
||||
|
||||
terrain.AddPatch(ref _patchCoord);
|
||||
if (TerrainTools.InitializePatch(terrain, ref _patchCoord))
|
||||
{
|
||||
Editor.LogError("Failed to initialize terrain patch.");
|
||||
}
|
||||
terrain.GetPatchBounds(terrain.GetPatchIndex(ref _patchCoord), out var patchBounds);
|
||||
OnPatchEdit(terrain, ref patchBounds);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
var terrain = Object.Find<FlaxEngine.Terrain>(ref _terrainId);
|
||||
if (terrain == null)
|
||||
{
|
||||
Editor.LogError("Missing terrain actor.");
|
||||
return;
|
||||
}
|
||||
|
||||
terrain.GetPatchBounds(terrain.GetPatchIndex(ref _patchCoord), out var patchBounds);
|
||||
terrain.RemovePatch(ref _patchCoord);
|
||||
OnPatchEdit(terrain, ref patchBounds);
|
||||
}
|
||||
|
||||
private void OnPatchEdit(FlaxEngine.Terrain terrain, ref BoundingBox patchBounds)
|
||||
{
|
||||
Editor.Instance.Scene.MarkSceneEdited(terrain.Scene);
|
||||
|
||||
var editorOptions = Editor.Instance.Options.Options;
|
||||
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
|
||||
|
||||
// Auto NavMesh rebuild
|
||||
if (!isPlayMode && editorOptions.General.AutoRebuildNavMesh)
|
||||
{
|
||||
if (terrain.Scene && (terrain.StaticFlags & StaticFlags.Navigation) == StaticFlags.Navigation)
|
||||
{
|
||||
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryAddPatch()
|
||||
{
|
||||
var terrain = SelectedTerrain;
|
||||
if (terrain)
|
||||
{
|
||||
var patchCoord = Mode.SelectedPatchCoord;
|
||||
if (!terrain.HasPatch(ref patchCoord))
|
||||
{
|
||||
// Add a new patch (with undo)
|
||||
var action = new AddPatchAction(terrain, ref patchCoord);
|
||||
action.Do();
|
||||
Editor.Instance.Undo.AddAction(action);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Pick()
|
||||
{
|
||||
if (Mode.EditMode == EditTerrainGizmoMode.Modes.Add && TryAddPatch())
|
||||
{
|
||||
// Patch added!
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get mouse ray and try to hit terrain
|
||||
var ray = Owner.MouseRay;
|
||||
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
|
||||
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders;
|
||||
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags) as TerrainNode;
|
||||
|
||||
// Update selection
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
if (hit != null)
|
||||
{
|
||||
if (Mode.EditMode != EditTerrainGizmoMode.Modes.Add)
|
||||
{
|
||||
// Perform detailed tracing
|
||||
var terrain = (FlaxEngine.Terrain)hit.Actor;
|
||||
terrain.RayCast(ray, out _, out var patchCoord, out var chunkCoord);
|
||||
Mode.SetSelectedChunk(ref patchCoord, ref chunkCoord);
|
||||
}
|
||||
|
||||
sceneEditing.Select(hit);
|
||||
}
|
||||
else
|
||||
{
|
||||
sceneEditing.Deselect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Source/Editor/Tools/Terrain/EditTerrainGizmoMode.cs
Normal file
123
Source/Editor/Tools/Terrain/EditTerrainGizmoMode.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEditor.Viewport;
|
||||
using FlaxEditor.Viewport.Modes;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain management and editing tool.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
|
||||
[HideInEditor]
|
||||
public class EditTerrainGizmoMode : EditorGizmoMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The terrain properties editing modes.
|
||||
/// </summary>
|
||||
public enum Modes
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain chunks editing mode.
|
||||
/// </summary>
|
||||
Edit,
|
||||
|
||||
/// <summary>
|
||||
/// Terrain patches adding mode.
|
||||
/// </summary>
|
||||
Add,
|
||||
|
||||
/// <summary>
|
||||
/// Terrain patches removing mode.
|
||||
/// </summary>
|
||||
Remove,
|
||||
|
||||
/// <summary>
|
||||
/// Terrain exporting mode.
|
||||
/// </summary>
|
||||
Export,
|
||||
}
|
||||
|
||||
private Modes _mode;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain editing gizmo.
|
||||
/// </summary>
|
||||
public EditTerrainGizmo Gizmo;
|
||||
|
||||
/// <summary>
|
||||
/// The patch coordinates of the last picked patch.
|
||||
/// </summary>
|
||||
public Int2 SelectedPatchCoord { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The chunk coordinates (relative to the patch) of the last picked chunk.
|
||||
/// </summary>
|
||||
public Int2 SelectedChunkCoord { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when mode gets changed.
|
||||
/// </summary>
|
||||
public event Action ModeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The active edit mode.
|
||||
/// </summary>
|
||||
public Modes EditMode
|
||||
{
|
||||
get => _mode;
|
||||
set
|
||||
{
|
||||
if (_mode != value)
|
||||
{
|
||||
_mode = value;
|
||||
ModeChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when selected patch or/and chunk coord gets changed (after picking by user).
|
||||
/// </summary>
|
||||
public event Action SelectedChunkCoordChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Init(MainEditorGizmoViewport viewport)
|
||||
{
|
||||
base.Init(viewport);
|
||||
|
||||
EditMode = Modes.Edit;
|
||||
Gizmo = new EditTerrainGizmo(viewport, this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnActivated()
|
||||
{
|
||||
base.OnActivated();
|
||||
|
||||
Viewport.Gizmos.Active = Gizmo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the selected chunk coordinates.
|
||||
/// </summary>
|
||||
/// <param name="patchCoord">The patch coord.</param>
|
||||
/// <param name="chunkCoord">The chunk coord.</param>
|
||||
public void SetSelectedChunk(ref Int2 patchCoord, ref Int2 chunkCoord)
|
||||
{
|
||||
if (SelectedPatchCoord != patchCoord || SelectedChunkCoord != chunkCoord)
|
||||
{
|
||||
SelectedPatchCoord = patchCoord;
|
||||
SelectedChunkCoord = chunkCoord;
|
||||
OnSelectedTerrainChunkChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectedTerrainChunkChanged()
|
||||
{
|
||||
SelectedChunkCoordChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
234
Source/Editor/Tools/Terrain/Paint/Mode.cs
Normal file
234
Source/Editor/Tools/Terrain/Paint/Mode.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Tools.Terrain.Brushes;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Paint
|
||||
{
|
||||
/// <summary>
|
||||
/// The base class for terran paint tool modes.
|
||||
/// </summary>
|
||||
[HideInEditor]
|
||||
public abstract class Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// The options container for the terrain editing apply.
|
||||
/// </summary>
|
||||
public struct Options
|
||||
{
|
||||
/// <summary>
|
||||
/// If checked, modification apply method should be inverted.
|
||||
/// </summary>
|
||||
public bool Invert;
|
||||
|
||||
/// <summary>
|
||||
/// The master strength parameter to apply when editing the terrain.
|
||||
/// </summary>
|
||||
public float Strength;
|
||||
|
||||
/// <summary>
|
||||
/// The delta time (in seconds) for the terrain modification apply. Used to scale the strength. Adjusted to handle low-FPS.
|
||||
/// </summary>
|
||||
public float DeltaTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tool strength (normalized to range 0-1). Defines the intensity of the paint operation to make it stronger or mre subtle.
|
||||
/// </summary>
|
||||
[EditorOrder(0), Limit(0, 10, 0.01f), Tooltip("The tool strength (normalized to range 0-1). Defines the intensity of the paint operation to make it stronger or more subtle.")]
|
||||
public float Strength = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this mode supports negative apply for terrain modification.
|
||||
/// </summary>
|
||||
public virtual bool SupportsNegativeApply => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the active splatmap texture to modify by the tool. It must be equal or higher than zero bu less than <see cref="FlaxEngine.Terrain.MaxSplatmapsCount"/>.
|
||||
/// </summary>
|
||||
public abstract int ActiveSplatmapIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the modification to the terrain.
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="gizmo">The gizmo.</param>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
public unsafe void Apply(Brush brush, ref Options options, PaintTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
|
||||
{
|
||||
// Combine final apply strength
|
||||
float strength = Strength * options.Strength * options.DeltaTime * 10.0f;
|
||||
if (strength <= 0.0f)
|
||||
return;
|
||||
if (options.Invert && SupportsNegativeApply)
|
||||
strength *= -1;
|
||||
|
||||
// Prepare
|
||||
var splatmapIndex = ActiveSplatmapIndex;
|
||||
var chunkSize = terrain.ChunkSize;
|
||||
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
|
||||
var heightmapLength = heightmapSize * heightmapSize;
|
||||
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
|
||||
var tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes).ToPointer();
|
||||
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
|
||||
ApplyParams p = new ApplyParams
|
||||
{
|
||||
Terrain = terrain,
|
||||
Brush = brush,
|
||||
Gizmo = gizmo,
|
||||
Options = options,
|
||||
Strength = strength,
|
||||
SplatmapIndex = splatmapIndex,
|
||||
HeightmapSize = heightmapSize,
|
||||
TempBuffer = tempBuffer,
|
||||
};
|
||||
|
||||
// Get brush bounds in terrain local space
|
||||
var brushBounds = gizmo.CursorBrushBounds;
|
||||
terrain.GetLocalToWorldMatrix(out p.TerrainWorld);
|
||||
terrain.GetWorldToLocalMatrix(out var terrainInvWorld);
|
||||
BoundingBox.Transform(ref brushBounds, ref terrainInvWorld, out var brushBoundsLocal);
|
||||
|
||||
// TODO: try caching brush weights before apply to reduce complexity and batch brush sampling
|
||||
|
||||
// Process all the patches under the cursor
|
||||
for (int patchIndex = 0; patchIndex < gizmo.PatchesUnderCursor.Count; patchIndex++)
|
||||
{
|
||||
var patch = gizmo.PatchesUnderCursor[patchIndex];
|
||||
var patchPositionLocal = new Vector3(patch.PatchCoord.X * patchSize, 0, patch.PatchCoord.Y * patchSize);
|
||||
|
||||
// Transform brush bounds from local terrain space into local patch vertex space
|
||||
var brushBoundsPatchLocalMin = (brushBoundsLocal.Minimum - patchPositionLocal) * unitsPerVertexInv;
|
||||
var brushBoundsPatchLocalMax = (brushBoundsLocal.Maximum - patchPositionLocal) * unitsPerVertexInv;
|
||||
|
||||
// Calculate patch heightmap area to modify by brush
|
||||
var brushPatchMin = new Int2(Mathf.FloorToInt(brushBoundsPatchLocalMin.X), Mathf.FloorToInt(brushBoundsPatchLocalMin.Z));
|
||||
var brushPatchMax = new Int2(Mathf.CeilToInt(brushBoundsPatchLocalMax.X), Mathf.FloorToInt(brushBoundsPatchLocalMax.Z));
|
||||
var modifiedOffset = brushPatchMin;
|
||||
var modifiedSize = brushPatchMax - brushPatchMin;
|
||||
|
||||
// Clamp to prevent overflows
|
||||
if (modifiedOffset.X < 0)
|
||||
{
|
||||
modifiedSize.X += modifiedOffset.X;
|
||||
modifiedOffset.X = 0;
|
||||
}
|
||||
if (modifiedOffset.Y < 0)
|
||||
{
|
||||
modifiedSize.Y += modifiedOffset.Y;
|
||||
modifiedOffset.Y = 0;
|
||||
}
|
||||
modifiedSize.X = Mathf.Min(modifiedSize.X, heightmapSize - modifiedOffset.X);
|
||||
modifiedSize.Y = Mathf.Min(modifiedSize.Y, heightmapSize - modifiedOffset.Y);
|
||||
|
||||
// Skip patch won't be modified at all
|
||||
if (modifiedSize.X <= 0 || modifiedSize.Y <= 0)
|
||||
continue;
|
||||
|
||||
// Get the patch data (cached internally by the c++ core in editor)
|
||||
var sourceData = TerrainTools.GetSplatMapData(terrain, ref patch.PatchCoord, splatmapIndex);
|
||||
if (sourceData == null)
|
||||
{
|
||||
throw new FlaxException("Cannot modify terrain. Loading splatmap failed. See log for more info.");
|
||||
}
|
||||
|
||||
// Record patch data before editing it
|
||||
if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
|
||||
{
|
||||
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord, splatmapIndex);
|
||||
}
|
||||
|
||||
// Apply modification
|
||||
p.ModifiedOffset = modifiedOffset;
|
||||
p.ModifiedSize = modifiedSize;
|
||||
p.PatchCoord = patch.PatchCoord;
|
||||
p.PatchPositionLocal = patchPositionLocal;
|
||||
p.SourceData = sourceData;
|
||||
Apply(ref p);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The mode apply parameters.
|
||||
/// </summary>
|
||||
public unsafe struct ApplyParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The brush.
|
||||
/// </summary>
|
||||
public Brush Brush;
|
||||
|
||||
/// <summary>
|
||||
/// The options.
|
||||
/// </summary>
|
||||
public Options Options;
|
||||
|
||||
/// <summary>
|
||||
/// The gizmo.
|
||||
/// </summary>
|
||||
public PaintTerrainGizmoMode Gizmo;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain.
|
||||
/// </summary>
|
||||
public FlaxEngine.Terrain Terrain;
|
||||
|
||||
/// <summary>
|
||||
/// The patch coordinates.
|
||||
/// </summary>
|
||||
public Int2 PatchCoord;
|
||||
|
||||
/// <summary>
|
||||
/// The modified offset.
|
||||
/// </summary>
|
||||
public Int2 ModifiedOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The modified size.
|
||||
/// </summary>
|
||||
public Int2 ModifiedSize;
|
||||
|
||||
/// <summary>
|
||||
/// The final calculated strength of the effect to apply (can be negative for inverted terrain modification if <see cref="SupportsNegativeApply"/> is set).
|
||||
/// </summary>
|
||||
public float Strength;
|
||||
|
||||
/// <summary>
|
||||
/// The splatmap texture index.
|
||||
/// </summary>
|
||||
public int SplatmapIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The temporary data buffer (for modified data).
|
||||
/// </summary>
|
||||
public Color32* TempBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// The source data buffer.
|
||||
/// </summary>
|
||||
public Color32* SourceData;
|
||||
|
||||
/// <summary>
|
||||
/// The heightmap size (edge).
|
||||
/// </summary>
|
||||
public int HeightmapSize;
|
||||
|
||||
/// <summary>
|
||||
/// The patch position in terrain local-space.
|
||||
/// </summary>
|
||||
public Vector3 PatchPositionLocal;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain local-to-world matrix.
|
||||
/// </summary>
|
||||
public Matrix TerrainWorld;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the modification to the terrain.
|
||||
/// </summary>
|
||||
/// <param name="p">The parameters to use.</param>
|
||||
public abstract void Apply(ref ApplyParams p);
|
||||
}
|
||||
}
|
||||
125
Source/Editor/Tools/Terrain/Paint/SingleLayerMode.cs
Normal file
125
Source/Editor/Tools/Terrain/Paint/SingleLayerMode.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Paint
|
||||
{
|
||||
/// <summary>
|
||||
/// Paint tool mode. Edits terrain splatmap by painting with the single layer on top of the others.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Tools.Terrain.Paint.Mode" />
|
||||
[HideInEditor]
|
||||
public sealed class SingleLayerMode : Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// The paint layers.
|
||||
/// </summary>
|
||||
public enum Layers
|
||||
{
|
||||
/// <summary>
|
||||
/// The layer 0.
|
||||
/// </summary>
|
||||
Layer0,
|
||||
|
||||
/// <summary>
|
||||
/// The layer 0.
|
||||
/// </summary>
|
||||
Layer1,
|
||||
|
||||
/// <summary>
|
||||
/// The layer 2.
|
||||
/// </summary>
|
||||
Layer2,
|
||||
|
||||
/// <summary>
|
||||
/// The layer 3.
|
||||
/// </summary>
|
||||
Layer3,
|
||||
|
||||
/// <summary>
|
||||
/// The layer 4.
|
||||
/// </summary>
|
||||
Layer4,
|
||||
|
||||
/// <summary>
|
||||
/// The layer 5.
|
||||
/// </summary>
|
||||
Layer5,
|
||||
|
||||
/// <summary>
|
||||
/// The layer 6.
|
||||
/// </summary>
|
||||
Layer6,
|
||||
|
||||
/// <summary>
|
||||
/// The layer 7.
|
||||
/// </summary>
|
||||
Layer7,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The layer to paint with it.
|
||||
/// </summary>
|
||||
[EditorOrder(10), Tooltip("The layer to paint with it. Terrain material can access per-layer blend weight to perform materials or textures blending.")]
|
||||
public Layers Layer = Layers.Layer0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int ActiveSplatmapIndex => (int)Layer < 4 ? 0 : 1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override unsafe void Apply(ref ApplyParams p)
|
||||
{
|
||||
var strength = p.Strength;
|
||||
var layer = (int)Layer;
|
||||
var brushPosition = p.Gizmo.CursorPosition;
|
||||
var layerComponent = layer % 4;
|
||||
|
||||
// Apply brush modification
|
||||
Profiler.BeginEvent("Apply Brush");
|
||||
for (int z = 0; z < p.ModifiedSize.Y; z++)
|
||||
{
|
||||
var zz = z + p.ModifiedOffset.Y;
|
||||
for (int x = 0; x < p.ModifiedSize.X; x++)
|
||||
{
|
||||
var xx = x + p.ModifiedOffset.X;
|
||||
var src = p.SourceData[zz * p.HeightmapSize + xx];
|
||||
|
||||
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, 0, zz * FlaxEngine.Terrain.UnitsPerVertex);
|
||||
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
|
||||
|
||||
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
|
||||
|
||||
// Extract layer weight
|
||||
byte* srcPtr = &src.R;
|
||||
var srcWeight = *(srcPtr + layerComponent) / 255.0f;
|
||||
|
||||
// Accumulate weight
|
||||
float dstWeight = srcWeight + paintAmount;
|
||||
|
||||
// Check for solid layer case
|
||||
if (dstWeight >= 1.0f)
|
||||
{
|
||||
// Erase other layers
|
||||
// TODO: maybe erase only the higher layers?
|
||||
// TODO: need to erase also weights form the other splatmaps
|
||||
src = Color32.Transparent;
|
||||
|
||||
// Use limit value
|
||||
dstWeight = 1.0f;
|
||||
}
|
||||
|
||||
// Modify packed weight
|
||||
*(srcPtr + layerComponent) = (byte)(dstWeight * 255.0f);
|
||||
|
||||
// Write back
|
||||
p.TempBuffer[z * p.ModifiedSize.X + x] = src;
|
||||
}
|
||||
}
|
||||
Profiler.EndEvent();
|
||||
|
||||
// Update terrain patch
|
||||
TerrainTools.ModifySplatMap(p.Terrain, ref p.PatchCoord, p.SplatmapIndex, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Source/Editor/Tools/Terrain/PaintTab.cs
Normal file
124
Source/Editor/Tools/Terrain/PaintTab.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.GUI.Tabs;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Carve tab related to terrain painting. Allows to modify terrain splatmap using brush.
|
||||
/// </summary>
|
||||
/// <seealso cref="Tab" />
|
||||
[HideInEditor]
|
||||
public class PaintTab : Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// The object for paint mode settings adjusting via Custom Editor.
|
||||
/// </summary>
|
||||
private sealed class ProxyObject
|
||||
{
|
||||
private readonly PaintTerrainGizmoMode _mode;
|
||||
private object _currentMode, _currentBrush;
|
||||
|
||||
public ProxyObject(PaintTerrainGizmoMode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
SyncData();
|
||||
}
|
||||
|
||||
public void SyncData()
|
||||
{
|
||||
_currentMode = _mode.CurrentMode;
|
||||
_currentBrush = _mode.CurrentBrush;
|
||||
}
|
||||
|
||||
[EditorOrder(0), EditorDisplay("Tool"), Tooltip("Paint tool mode to use.")]
|
||||
public PaintTerrainGizmoMode.ModeTypes ToolMode
|
||||
{
|
||||
get => _mode.ToolModeType;
|
||||
set => _mode.ToolModeType = value;
|
||||
}
|
||||
|
||||
[EditorOrder(100), EditorDisplay("Tool", EditorDisplayAttribute.InlineStyle)]
|
||||
public object Mode
|
||||
{
|
||||
get => _currentMode;
|
||||
set { }
|
||||
}
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Brush"), Tooltip("Paint brush type to use.")]
|
||||
public PaintTerrainGizmoMode.BrushTypes BrushTypeType
|
||||
{
|
||||
get => _mode.ToolBrushType;
|
||||
set => _mode.ToolBrushType = value;
|
||||
}
|
||||
|
||||
[EditorOrder(1100), EditorDisplay("Brush", EditorDisplayAttribute.InlineStyle)]
|
||||
public object Brush
|
||||
{
|
||||
get => _currentBrush;
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ProxyObject _proxy;
|
||||
private readonly CustomEditorPresenter _presenter;
|
||||
|
||||
/// <summary>
|
||||
/// The parent carve tab.
|
||||
/// </summary>
|
||||
public readonly CarveTab CarveTab;
|
||||
|
||||
/// <summary>
|
||||
/// The related sculp terrain gizmo.
|
||||
/// </summary>
|
||||
public readonly PaintTerrainGizmoMode Gizmo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PaintTab"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tab">The parent tab.</param>
|
||||
/// <param name="gizmo">The related gizmo.</param>
|
||||
public PaintTab(CarveTab tab, PaintTerrainGizmoMode gizmo)
|
||||
: base("Paint")
|
||||
{
|
||||
CarveTab = tab;
|
||||
Gizmo = gizmo;
|
||||
Gizmo.ToolModeChanged += OnToolModeChanged;
|
||||
_proxy = new ProxyObject(gizmo);
|
||||
|
||||
// Main panel
|
||||
var panel = new Panel(ScrollBars.Both)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Options editor
|
||||
// TODO: use editor undo for changing brush options
|
||||
var editor = new CustomEditorPresenter(null);
|
||||
editor.Panel.Parent = panel;
|
||||
editor.Select(_proxy);
|
||||
_presenter = editor;
|
||||
}
|
||||
|
||||
private void OnToolModeChanged()
|
||||
{
|
||||
_presenter.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (_presenter.BuildOnUpdate)
|
||||
{
|
||||
_proxy.SyncData();
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
206
Source/Editor/Tools/Terrain/PaintTerrainGizmo.cs
Normal file
206
Source/Editor/Tools/Terrain/PaintTerrainGizmo.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEditor.Gizmo;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEditor.SceneGraph.Actors;
|
||||
using FlaxEditor.Tools.Terrain.Paint;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Gizmo for painting terrain. Managed by the <see cref="PaintTerrainGizmoMode"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Gizmo.GizmoBase" />
|
||||
[HideInEditor]
|
||||
public sealed class PaintTerrainGizmo : GizmoBase
|
||||
{
|
||||
private FlaxEngine.Terrain _paintTerrain;
|
||||
private Ray _prevRay;
|
||||
|
||||
/// <summary>
|
||||
/// The parent mode.
|
||||
/// </summary>
|
||||
public readonly PaintTerrainGizmoMode Mode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether gizmo tool is painting the terrain splatmap.
|
||||
/// </summary>
|
||||
public bool IsPainting => _paintTerrain != null;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when terrain paint has been started.
|
||||
/// </summary>
|
||||
public event Action PaintStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when terrain paint has been ended.
|
||||
/// </summary>
|
||||
public event Action PaintEnded;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PaintTerrainGizmo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <param name="mode">The mode.</param>
|
||||
public PaintTerrainGizmo(IGizmoOwner owner, PaintTerrainGizmoMode mode)
|
||||
: base(owner)
|
||||
{
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
private FlaxEngine.Terrain SelectedTerrain
|
||||
{
|
||||
get
|
||||
{
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
|
||||
return (FlaxEngine.Terrain)terrainNode?.Actor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw(ref RenderContext renderContext)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
var terrain = SelectedTerrain;
|
||||
if (!terrain)
|
||||
return;
|
||||
|
||||
if (Mode.HasValidHit)
|
||||
{
|
||||
var brushPosition = Mode.CursorPosition;
|
||||
var brushColor = new Color(1.0f, 0.85f, 0.0f); // TODO: expose to editor options
|
||||
var brushMaterial = Mode.CurrentBrush.GetBrushMaterial(ref brushPosition, ref brushColor);
|
||||
if (!brushMaterial)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < Mode.ChunksUnderCursor.Count; i++)
|
||||
{
|
||||
var chunk = Mode.ChunksUnderCursor[i];
|
||||
terrain.DrawChunk(ref renderContext, ref chunk.PatchCoord, ref chunk.ChunkCoord, brushMaterial, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to start terrain painting
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
private void PaintStart(FlaxEngine.Terrain terrain)
|
||||
{
|
||||
// Skip if already is painting
|
||||
if (IsPainting)
|
||||
return;
|
||||
|
||||
_paintTerrain = terrain;
|
||||
PaintStarted?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to update terrain painting logic.
|
||||
/// </summary>
|
||||
/// <param name="dt">The delta time (in seconds).</param>
|
||||
private void PaintUpdate(float dt)
|
||||
{
|
||||
// Skip if is not painting
|
||||
if (!IsPainting)
|
||||
return;
|
||||
|
||||
// Edit the terrain
|
||||
Profiler.BeginEvent("Paint Terrain");
|
||||
var options = new Mode.Options
|
||||
{
|
||||
Strength = 1.0f,
|
||||
DeltaTime = dt,
|
||||
Invert = Owner.IsControlDown
|
||||
};
|
||||
Mode.CurrentMode.Apply(Mode.CurrentBrush, ref options, Mode, _paintTerrain);
|
||||
Profiler.EndEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to end terrain painting.
|
||||
/// </summary>
|
||||
private void PaintEnd()
|
||||
{
|
||||
// Skip if nothing was painted
|
||||
if (!IsPainting)
|
||||
return;
|
||||
|
||||
_paintTerrain = null;
|
||||
PaintEnded?.Invoke();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float dt)
|
||||
{
|
||||
base.Update(dt);
|
||||
|
||||
// Check if gizmo is not active
|
||||
if (!IsActive)
|
||||
{
|
||||
PaintEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if no terrain is selected
|
||||
var terrain = SelectedTerrain;
|
||||
if (!terrain)
|
||||
{
|
||||
PaintEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if selected terrain was changed during painting
|
||||
if (terrain != _paintTerrain && IsPainting)
|
||||
{
|
||||
PaintEnd();
|
||||
}
|
||||
|
||||
// Special case if user is sculpting terrain and mouse is not moving then freeze the brush location to help painting vertical tip objects
|
||||
var mouseRay = Owner.MouseRay;
|
||||
if (IsPainting && _prevRay == mouseRay)
|
||||
{
|
||||
// Freeze cursor
|
||||
}
|
||||
// Perform detailed tracing to find cursor location on the terrain
|
||||
else if (terrain.RayCast(mouseRay, out var closest, out var patchCoord, out var chunkCoord))
|
||||
{
|
||||
var hitLocation = mouseRay.GetPoint(closest);
|
||||
Mode.SetCursor(ref hitLocation);
|
||||
}
|
||||
// No hit
|
||||
else
|
||||
{
|
||||
Mode.ClearCursor();
|
||||
}
|
||||
_prevRay = mouseRay;
|
||||
|
||||
// Handle painting
|
||||
if (Owner.IsLeftMouseButtonDown)
|
||||
PaintStart(terrain);
|
||||
else
|
||||
PaintEnd();
|
||||
PaintUpdate(dt);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Pick()
|
||||
{
|
||||
// Get mouse ray and try to hit terrain
|
||||
var ray = Owner.MouseRay;
|
||||
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
|
||||
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders;
|
||||
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags);
|
||||
|
||||
// Update selection
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
if (hit is TerrainNode)
|
||||
sceneEditing.Select(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
357
Source/Editor/Tools/Terrain/PaintTerrainGizmoMode.cs
Normal file
357
Source/Editor/Tools/Terrain/PaintTerrainGizmoMode.cs
Normal file
@@ -0,0 +1,357 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using FlaxEditor.SceneGraph.Actors;
|
||||
using FlaxEditor.Tools.Terrain.Brushes;
|
||||
using FlaxEditor.Tools.Terrain.Paint;
|
||||
using FlaxEditor.Tools.Terrain.Undo;
|
||||
using FlaxEditor.Viewport;
|
||||
using FlaxEditor.Viewport.Modes;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain painting tool mode.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
|
||||
[HideInEditor]
|
||||
public class PaintTerrainGizmoMode : EditorGizmoMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The terrain layer names.
|
||||
/// </summary>
|
||||
public static readonly string[] TerrainLayerNames =
|
||||
{
|
||||
"Layer 0",
|
||||
"Layer 1",
|
||||
"Layer 2",
|
||||
"Layer 3",
|
||||
"Layer 4",
|
||||
"Layer 5",
|
||||
"Layer 6",
|
||||
"Layer 7",
|
||||
};
|
||||
|
||||
private IntPtr _cachedSplatmapData;
|
||||
private int _cachedSplatmapDataSize;
|
||||
private EditTerrainMapAction _activeAction;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain painting gizmo.
|
||||
/// </summary>
|
||||
public PaintTerrainGizmo Gizmo;
|
||||
|
||||
/// <summary>
|
||||
/// The tool modes.
|
||||
/// </summary>
|
||||
public enum ModeTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// The single layer mode.
|
||||
/// </summary>
|
||||
SingleLayer,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The brush types.
|
||||
/// </summary>
|
||||
public enum BrushTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// The circle brush.
|
||||
/// </summary>
|
||||
CircleBrush,
|
||||
}
|
||||
|
||||
private readonly Mode[] _modes =
|
||||
{
|
||||
new SingleLayerMode(),
|
||||
};
|
||||
|
||||
private readonly Brush[] _brushes =
|
||||
{
|
||||
new CircleBrush(),
|
||||
};
|
||||
|
||||
private ModeTypes _modeType = ModeTypes.SingleLayer;
|
||||
private BrushTypes _brushType = BrushTypes.CircleBrush;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when tool mode gets changed.
|
||||
/// </summary>
|
||||
public event Action ToolModeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current tool mode (enum).
|
||||
/// </summary>
|
||||
public ModeTypes ToolModeType
|
||||
{
|
||||
get => _modeType;
|
||||
set
|
||||
{
|
||||
if (_modeType != value)
|
||||
{
|
||||
if (_activeAction != null)
|
||||
throw new InvalidOperationException("Cannot change paint tool mode during terrain editing.");
|
||||
|
||||
_modeType = value;
|
||||
ToolModeChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current tool mode.
|
||||
/// </summary>
|
||||
public Mode CurrentMode => _modes[(int)_modeType];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the single layer mode instance.
|
||||
/// </summary>
|
||||
public SingleLayerMode SingleLayerMode => _modes[(int)ModeTypes.SingleLayer] as SingleLayerMode;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when tool brush gets changed.
|
||||
/// </summary>
|
||||
public event Action ToolBrushChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current tool brush (enum).
|
||||
/// </summary>
|
||||
public BrushTypes ToolBrushType
|
||||
{
|
||||
get => _brushType;
|
||||
set
|
||||
{
|
||||
if (_brushType != value)
|
||||
{
|
||||
if (_activeAction != null)
|
||||
throw new InvalidOperationException("Cannot change sculpt tool brush type during terrain editing.");
|
||||
|
||||
_brushType = value;
|
||||
ToolBrushChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current brush.
|
||||
/// </summary>
|
||||
public Brush CurrentBrush => _brushes[(int)_brushType];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the circle brush instance.
|
||||
/// </summary>
|
||||
public CircleBrush CircleBrush => _brushes[(int)BrushTypes.CircleBrush] as CircleBrush;
|
||||
|
||||
/// <summary>
|
||||
/// The last valid cursor position of the brush (in world space).
|
||||
/// </summary>
|
||||
public Vector3 CursorPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag used to indicate whenever last cursor position of the brush is valid.
|
||||
/// </summary>
|
||||
public bool HasValidHit { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Describes the terrain patch link.
|
||||
/// </summary>
|
||||
public struct PatchLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// The patch coordinates.
|
||||
/// </summary>
|
||||
public Int2 PatchCoord;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The selected terrain patches collection that are under cursor (affected by the brush).
|
||||
/// </summary>
|
||||
public readonly List<PatchLocation> PatchesUnderCursor = new List<PatchLocation>();
|
||||
|
||||
/// <summary>
|
||||
/// Describes the terrain chunk link.
|
||||
/// </summary>
|
||||
public struct ChunkLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// The patch coordinates.
|
||||
/// </summary>
|
||||
public Int2 PatchCoord;
|
||||
|
||||
/// <summary>
|
||||
/// The chunk coordinates.
|
||||
/// </summary>
|
||||
public Int2 ChunkCoord;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The selected terrain chunk collection that are under cursor (affected by the brush).
|
||||
/// </summary>
|
||||
public readonly List<ChunkLocation> ChunksUnderCursor = new List<ChunkLocation>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selected terrain actor (see <see cref="Modules.SceneEditingModule"/>).
|
||||
/// </summary>
|
||||
public FlaxEngine.Terrain SelectedTerrain
|
||||
{
|
||||
get
|
||||
{
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
|
||||
return (FlaxEngine.Terrain)terrainNode?.Actor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the world bounds of the brush located at the current cursor position (defined by <see cref="CursorPosition"/>). Valid only if <see cref="HasValidHit"/> is set to true.
|
||||
/// </summary>
|
||||
public BoundingBox CursorBrushBounds
|
||||
{
|
||||
get
|
||||
{
|
||||
const float brushExtentY = 10000.0f;
|
||||
float brushSizeHalf = CurrentBrush.Size * 0.5f;
|
||||
Vector3 center = CursorPosition;
|
||||
|
||||
BoundingBox box;
|
||||
box.Minimum = new Vector3(center.X - brushSizeHalf, center.Y - brushSizeHalf - brushExtentY, center.Z - brushSizeHalf);
|
||||
box.Maximum = new Vector3(center.X + brushSizeHalf, center.Y + brushSizeHalf + brushExtentY, center.Z + brushSizeHalf);
|
||||
return box;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the splatmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
|
||||
/// </summary>
|
||||
/// <param name="size">The minimum buffer size (in bytes).</param>
|
||||
/// <returns>The allocated memory using <see cref="Marshal"/> interface.</returns>
|
||||
public IntPtr GetSplatmapTempBuffer(int size)
|
||||
{
|
||||
if (_cachedSplatmapDataSize < size)
|
||||
{
|
||||
if (_cachedSplatmapData != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(_cachedSplatmapData);
|
||||
}
|
||||
_cachedSplatmapData = Marshal.AllocHGlobal(size);
|
||||
_cachedSplatmapDataSize = size;
|
||||
}
|
||||
|
||||
return _cachedSplatmapData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current edit terrain undo system action. Use it to record the data for the undo restoring after terrain editing.
|
||||
/// </summary>
|
||||
internal EditTerrainMapAction CurrentEditUndoAction => _activeAction;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Init(MainEditorGizmoViewport viewport)
|
||||
{
|
||||
base.Init(viewport);
|
||||
|
||||
Gizmo = new PaintTerrainGizmo(viewport, this);
|
||||
Gizmo.PaintStarted += OnPaintStarted;
|
||||
Gizmo.PaintEnded += OnPaintEnded;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnActivated()
|
||||
{
|
||||
base.OnActivated();
|
||||
|
||||
Viewport.Gizmos.Active = Gizmo;
|
||||
ClearCursor();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDeactivated()
|
||||
{
|
||||
base.OnDeactivated();
|
||||
|
||||
// Free temporary memory buffer
|
||||
if (_cachedSplatmapData != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(_cachedSplatmapData);
|
||||
_cachedSplatmapData = IntPtr.Zero;
|
||||
_cachedSplatmapDataSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cursor location information cached within the gizmo mode.
|
||||
/// </summary>
|
||||
public void ClearCursor()
|
||||
{
|
||||
HasValidHit = false;
|
||||
PatchesUnderCursor.Clear();
|
||||
ChunksUnderCursor.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cursor location in the world space. Updates the brush location and cached affected chunks.
|
||||
/// </summary>
|
||||
/// <param name="hitPosition">The cursor hit location on the selected terrain.</param>
|
||||
public void SetCursor(ref Vector3 hitPosition)
|
||||
{
|
||||
HasValidHit = true;
|
||||
CursorPosition = hitPosition;
|
||||
PatchesUnderCursor.Clear();
|
||||
ChunksUnderCursor.Clear();
|
||||
|
||||
// Find patches and chunks affected by the brush
|
||||
var terrain = SelectedTerrain;
|
||||
if (terrain == null)
|
||||
throw new InvalidOperationException("Cannot set cursor then no terrain is selected.");
|
||||
var brushBounds = CursorBrushBounds;
|
||||
var patchesCount = terrain.PatchesCount;
|
||||
BoundingBox tmp;
|
||||
for (int patchIndex = 0; patchIndex < patchesCount; patchIndex++)
|
||||
{
|
||||
terrain.GetPatchBounds(patchIndex, out tmp);
|
||||
if (!tmp.Intersects(ref brushBounds))
|
||||
continue;
|
||||
|
||||
terrain.GetPatchCoord(patchIndex, out var patchCoord);
|
||||
PatchesUnderCursor.Add(new PatchLocation() { PatchCoord = patchCoord });
|
||||
|
||||
for (int chunkIndex = 0; chunkIndex < FlaxEngine.Terrain.PatchChunksCount; chunkIndex++)
|
||||
{
|
||||
terrain.GetChunkBounds(patchIndex, chunkIndex, out tmp);
|
||||
if (!tmp.Intersects(ref brushBounds))
|
||||
continue;
|
||||
|
||||
var chunkCoord = new Int2(chunkIndex % FlaxEngine.Terrain.PatchEdgeChunksCount, chunkIndex / FlaxEngine.Terrain.PatchEdgeChunksCount);
|
||||
ChunksUnderCursor.Add(new ChunkLocation() { PatchCoord = patchCoord, ChunkCoord = chunkCoord });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPaintStarted()
|
||||
{
|
||||
if (_activeAction != null)
|
||||
throw new InvalidOperationException("Terrain paint start/end resynchronization.");
|
||||
|
||||
var terrain = SelectedTerrain;
|
||||
_activeAction = new EditTerrainSplatMapAction(terrain);
|
||||
}
|
||||
|
||||
private void OnPaintEnded()
|
||||
{
|
||||
if (_activeAction != null)
|
||||
{
|
||||
if (_activeAction.HasAnyModification)
|
||||
{
|
||||
_activeAction.OnEditingEnd();
|
||||
Editor.Instance.Undo.AddAction(_activeAction);
|
||||
}
|
||||
_activeAction = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs
Normal file
61
Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Sculpt
|
||||
{
|
||||
/// <summary>
|
||||
/// Sculpt tool mode that flattens the terrain heightmap area affected by brush to the target value.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
|
||||
[HideInEditor]
|
||||
public sealed class FlattenMode : Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// The target terrain height to blend to.
|
||||
/// </summary>
|
||||
[EditorOrder(10), Tooltip("The target terrain height to blend to.")]
|
||||
public float TargetHeight = 0.0f;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override unsafe void Apply(ref ApplyParams p)
|
||||
{
|
||||
// If used with invert mode pick the target height level
|
||||
if (p.Options.Invert)
|
||||
{
|
||||
var center = p.ModifiedOffset + p.ModifiedSize / 2;
|
||||
TargetHeight = p.SourceHeightMap[center.Y * p.HeightmapSize + center.X];
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare
|
||||
var brushPosition = p.Gizmo.CursorPosition;
|
||||
var targetHeight = TargetHeight;
|
||||
var strength = Mathf.Saturate(p.Strength);
|
||||
|
||||
// Apply brush modification
|
||||
Profiler.BeginEvent("Apply Brush");
|
||||
for (int z = 0; z < p.ModifiedSize.Y; z++)
|
||||
{
|
||||
var zz = z + p.ModifiedOffset.Y;
|
||||
for (int x = 0; x < p.ModifiedSize.X; x++)
|
||||
{
|
||||
var xx = x + p.ModifiedOffset.X;
|
||||
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
|
||||
|
||||
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex);
|
||||
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
|
||||
|
||||
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
|
||||
|
||||
// Blend between the height and the target value
|
||||
p.TempBuffer[z * p.ModifiedSize.X + x] = Mathf.Lerp(sourceHeight, targetHeight, paintAmount);
|
||||
}
|
||||
}
|
||||
Profiler.EndEvent();
|
||||
|
||||
// Update terrain patch
|
||||
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs
Normal file
60
Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Sculpt
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain holes creating tool mode edits terrain holes mask by changing area affected by brush.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
|
||||
[HideInEditor]
|
||||
public sealed class HolesMode : Mode
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool SupportsNegativeApply => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool EditHoles => true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HolesMode"/> class.
|
||||
/// </summary>
|
||||
public HolesMode()
|
||||
{
|
||||
Strength = 6.0f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override unsafe void Apply(ref ApplyParams p)
|
||||
{
|
||||
var strength = p.Strength * -10.0f;
|
||||
var brushPosition = p.Gizmo.CursorPosition;
|
||||
var tempBuffer = (byte*)p.TempBuffer;
|
||||
|
||||
// Apply brush modification
|
||||
Profiler.BeginEvent("Apply Brush");
|
||||
for (int z = 0; z < p.ModifiedSize.Y; z++)
|
||||
{
|
||||
var zz = z + p.ModifiedOffset.Y;
|
||||
for (int x = 0; x < p.ModifiedSize.X; x++)
|
||||
{
|
||||
var xx = x + p.ModifiedOffset.X;
|
||||
var sourceMask = p.SourceHolesMask[zz * p.HeightmapSize + xx] != 0 ? 1.0f : 0.0f;
|
||||
|
||||
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, 0, zz * FlaxEngine.Terrain.UnitsPerVertex);
|
||||
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
|
||||
samplePositionWorld.Y = brushPosition.Y;
|
||||
|
||||
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld);
|
||||
|
||||
tempBuffer[z * p.ModifiedSize.X + x] = (byte)((sourceMask + paintAmount * strength) < 0.8f ? 0 : 255);
|
||||
}
|
||||
}
|
||||
Profiler.EndEvent();
|
||||
|
||||
// Update terrain patch
|
||||
TerrainTools.ModifyHolesMask(p.Terrain, ref p.PatchCoord, tempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
246
Source/Editor/Tools/Terrain/Sculpt/Mode.cs
Normal file
246
Source/Editor/Tools/Terrain/Sculpt/Mode.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Tools.Terrain.Brushes;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Sculpt
|
||||
{
|
||||
/// <summary>
|
||||
/// The base class for terran sculpt tool modes.
|
||||
/// </summary>
|
||||
[HideInEditor]
|
||||
public abstract class Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// The options container for the terrain editing apply.
|
||||
/// </summary>
|
||||
public struct Options
|
||||
{
|
||||
/// <summary>
|
||||
/// If checked, modification apply method should be inverted.
|
||||
/// </summary>
|
||||
public bool Invert;
|
||||
|
||||
/// <summary>
|
||||
/// The master strength parameter to apply when editing the terrain.
|
||||
/// </summary>
|
||||
public float Strength;
|
||||
|
||||
/// <summary>
|
||||
/// The delta time (in seconds) for the terrain modification apply. Used to scale the strength. Adjusted to handle low-FPS.
|
||||
/// </summary>
|
||||
public float DeltaTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tool strength (normalized to range 0-1). Defines the intensity of the sculpt operation to make it stronger or mre subtle.
|
||||
/// </summary>
|
||||
[EditorOrder(0), Limit(0, 6, 0.01f), Tooltip("The tool strength (normalized to range 0-1). Defines the intensity of the sculpt operation to make it stronger or more subtle.")]
|
||||
public float Strength = 1.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this mode supports negative apply for terrain modification.
|
||||
/// </summary>
|
||||
public virtual bool SupportsNegativeApply => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this mode modifies the terrain holes mask rather than heightmap.
|
||||
/// </summary>
|
||||
public virtual bool EditHoles => false;
|
||||
|
||||
/// <summary>
|
||||
/// Applies the modification to the terrain.
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="gizmo">The gizmo.</param>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
public unsafe void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
|
||||
{
|
||||
// Combine final apply strength
|
||||
float strength = Strength * options.Strength * options.DeltaTime;
|
||||
if (strength <= 0.0f)
|
||||
return;
|
||||
if (options.Invert && SupportsNegativeApply)
|
||||
strength *= -1;
|
||||
|
||||
// Prepare
|
||||
var chunkSize = terrain.ChunkSize;
|
||||
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
|
||||
var heightmapLength = heightmapSize * heightmapSize;
|
||||
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
|
||||
var tempBuffer = (float*)gizmo.GetHeightmapTempBuffer(heightmapLength * sizeof(float)).ToPointer();
|
||||
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
|
||||
ApplyParams p = new ApplyParams
|
||||
{
|
||||
Terrain = terrain,
|
||||
Brush = brush,
|
||||
Gizmo = gizmo,
|
||||
Options = options,
|
||||
Strength = strength,
|
||||
HeightmapSize = heightmapSize,
|
||||
TempBuffer = tempBuffer,
|
||||
};
|
||||
|
||||
// Get brush bounds in terrain local space
|
||||
var brushBounds = gizmo.CursorBrushBounds;
|
||||
terrain.GetLocalToWorldMatrix(out p.TerrainWorld);
|
||||
terrain.GetWorldToLocalMatrix(out var terrainInvWorld);
|
||||
BoundingBox.Transform(ref brushBounds, ref terrainInvWorld, out var brushBoundsLocal);
|
||||
|
||||
// TODO: try caching brush weights before apply to reduce complexity and batch brush sampling
|
||||
|
||||
// Process all the patches under the cursor
|
||||
for (int patchIndex = 0; patchIndex < gizmo.PatchesUnderCursor.Count; patchIndex++)
|
||||
{
|
||||
var patch = gizmo.PatchesUnderCursor[patchIndex];
|
||||
var patchPositionLocal = new Vector3(patch.PatchCoord.X * patchSize, 0, patch.PatchCoord.Y * patchSize);
|
||||
|
||||
// Transform brush bounds from local terrain space into local patch vertex space
|
||||
var brushBoundsPatchLocalMin = (brushBoundsLocal.Minimum - patchPositionLocal) * unitsPerVertexInv;
|
||||
var brushBoundsPatchLocalMax = (brushBoundsLocal.Maximum - patchPositionLocal) * unitsPerVertexInv;
|
||||
|
||||
// Calculate patch heightmap area to modify by brush
|
||||
var brushPatchMin = new Int2(Mathf.FloorToInt(brushBoundsPatchLocalMin.X), Mathf.FloorToInt(brushBoundsPatchLocalMin.Z));
|
||||
var brushPatchMax = new Int2(Mathf.CeilToInt(brushBoundsPatchLocalMax.X), Mathf.FloorToInt(brushBoundsPatchLocalMax.Z));
|
||||
var modifiedOffset = brushPatchMin;
|
||||
var modifiedSize = brushPatchMax - brushPatchMin;
|
||||
|
||||
// Expand the modification area by one vertex in each direction to ensure normal vectors are updated for edge cases, also clamp to prevent overflows
|
||||
if (modifiedOffset.X < 0)
|
||||
{
|
||||
modifiedSize.X += modifiedOffset.X;
|
||||
modifiedOffset.X = 0;
|
||||
}
|
||||
if (modifiedOffset.Y < 0)
|
||||
{
|
||||
modifiedSize.Y += modifiedOffset.Y;
|
||||
modifiedOffset.Y = 0;
|
||||
}
|
||||
modifiedSize.X = Mathf.Min(modifiedSize.X + 2, heightmapSize - modifiedOffset.X);
|
||||
modifiedSize.Y = Mathf.Min(modifiedSize.Y + 2, heightmapSize - modifiedOffset.Y);
|
||||
|
||||
// Skip patch won't be modified at all
|
||||
if (modifiedSize.X <= 0 || modifiedSize.Y <= 0)
|
||||
continue;
|
||||
|
||||
// Get the patch data (cached internally by the c++ core in editor)
|
||||
float* sourceHeights = EditHoles ? null : TerrainTools.GetHeightmapData(terrain, ref patch.PatchCoord);
|
||||
byte* sourceHoles = EditHoles ? TerrainTools.GetHolesMaskData(terrain, ref patch.PatchCoord) : null;
|
||||
if (sourceHeights == null && sourceHoles == null)
|
||||
{
|
||||
throw new FlaxException("Cannot modify terrain. Loading heightmap failed. See log for more info.");
|
||||
}
|
||||
|
||||
// Record patch data before editing it
|
||||
if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
|
||||
{
|
||||
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord);
|
||||
}
|
||||
|
||||
// Apply modification
|
||||
p.ModifiedOffset = modifiedOffset;
|
||||
p.ModifiedSize = modifiedSize;
|
||||
p.PatchCoord = patch.PatchCoord;
|
||||
p.PatchPositionLocal = patchPositionLocal;
|
||||
p.SourceHeightMap = sourceHeights;
|
||||
p.SourceHolesMask = sourceHoles;
|
||||
Apply(ref p);
|
||||
}
|
||||
|
||||
var editorOptions = Editor.Instance.Options.Options;
|
||||
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
|
||||
|
||||
// Auto NavMesh rebuild
|
||||
if (!isPlayMode && editorOptions.General.AutoRebuildNavMesh)
|
||||
{
|
||||
if (terrain.Scene && (terrain.StaticFlags & StaticFlags.Navigation) == StaticFlags.Navigation)
|
||||
{
|
||||
Navigation.BuildNavMesh(terrain.Scene, brushBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The mode apply parameters.
|
||||
/// </summary>
|
||||
public unsafe struct ApplyParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The brush.
|
||||
/// </summary>
|
||||
public Brush Brush;
|
||||
|
||||
/// <summary>
|
||||
/// The options.
|
||||
/// </summary>
|
||||
public Options Options;
|
||||
|
||||
/// <summary>
|
||||
/// The gizmo.
|
||||
/// </summary>
|
||||
public SculptTerrainGizmoMode Gizmo;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain.
|
||||
/// </summary>
|
||||
public FlaxEngine.Terrain Terrain;
|
||||
|
||||
/// <summary>
|
||||
/// The patch coordinates.
|
||||
/// </summary>
|
||||
public Int2 PatchCoord;
|
||||
|
||||
/// <summary>
|
||||
/// The modified offset.
|
||||
/// </summary>
|
||||
public Int2 ModifiedOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The modified size.
|
||||
/// </summary>
|
||||
public Int2 ModifiedSize;
|
||||
|
||||
/// <summary>
|
||||
/// The final calculated strength of the effect to apply (can be negative for inverted terrain modification if <see cref="SupportsNegativeApply"/> is set).
|
||||
/// </summary>
|
||||
public float Strength;
|
||||
|
||||
/// <summary>
|
||||
/// The temporary data buffer (for modified data). Has size of array of floats that has size of heightmap length.
|
||||
/// </summary>
|
||||
public float* TempBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// The source heightmap data buffer. May be null if modified is holes mask.
|
||||
/// </summary>
|
||||
public float* SourceHeightMap;
|
||||
|
||||
/// <summary>
|
||||
/// The source holes mask data buffer. May be null if modified is.
|
||||
/// </summary>
|
||||
public byte* SourceHolesMask;
|
||||
|
||||
/// <summary>
|
||||
/// The heightmap size (edge).
|
||||
/// </summary>
|
||||
public int HeightmapSize;
|
||||
|
||||
/// <summary>
|
||||
/// The patch position in terrain local-space.
|
||||
/// </summary>
|
||||
public Vector3 PatchPositionLocal;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain local-to-world matrix.
|
||||
/// </summary>
|
||||
public Matrix TerrainWorld;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the modification to the terrain.
|
||||
/// </summary>
|
||||
/// <param name="p">The parameters to use.</param>
|
||||
public abstract void Apply(ref ApplyParams p);
|
||||
}
|
||||
}
|
||||
65
Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs
Normal file
65
Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.Utilities;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Sculpt
|
||||
{
|
||||
/// <summary>
|
||||
/// Sculpt tool mode that applies the noise to the terrain heightmap area affected by brush.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
|
||||
[HideInEditor]
|
||||
public sealed class NoiseMode : Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// The tool noise scale. Adjusts the noise pattern scale.
|
||||
/// </summary>
|
||||
[EditorOrder(10), Limit(0, 10000), Tooltip("The tool noise scale. Adjusts the noise pattern scale.")]
|
||||
public float NoiseScale = 128.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The tool noise amount scale. Adjusts the noise amplitude scale.
|
||||
/// </summary>
|
||||
[EditorOrder(10), Limit(0, 10000000), Tooltip("The tool noise amount scale. Adjusts the noise amplitude scale.")]
|
||||
public float NoiseAmount = 10000.0f;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool SupportsNegativeApply => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override unsafe void Apply(ref ApplyParams p)
|
||||
{
|
||||
// Prepare
|
||||
var brushPosition = p.Gizmo.CursorPosition;
|
||||
var noise = new PerlinNoise(0, NoiseScale, p.Strength * NoiseAmount);
|
||||
var chunkSize = p.Terrain.ChunkSize;
|
||||
var patchSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount;
|
||||
var patchOffset = p.PatchCoord * patchSize;
|
||||
|
||||
// Apply brush modification
|
||||
Profiler.BeginEvent("Apply Brush");
|
||||
for (int z = 0; z < p.ModifiedSize.Y; z++)
|
||||
{
|
||||
var zz = z + p.ModifiedOffset.Y;
|
||||
for (int x = 0; x < p.ModifiedSize.X; x++)
|
||||
{
|
||||
var xx = x + p.ModifiedOffset.X;
|
||||
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
|
||||
|
||||
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex);
|
||||
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
|
||||
|
||||
var noiseSample = noise.Sample(xx + patchOffset.X, zz + patchOffset.Y);
|
||||
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld);
|
||||
|
||||
p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight + noiseSample * paintAmount;
|
||||
}
|
||||
}
|
||||
Profiler.EndEvent();
|
||||
|
||||
// Update terrain patch
|
||||
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs
Normal file
47
Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Sculpt
|
||||
{
|
||||
/// <summary>
|
||||
/// Sculpt tool mode. Edits terrain heightmap by moving area affected by brush up or down.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
|
||||
[HideInEditor]
|
||||
public sealed class SculptMode : Mode
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool SupportsNegativeApply => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override unsafe void Apply(ref ApplyParams p)
|
||||
{
|
||||
var strength = p.Strength * 1000.0f;
|
||||
var brushPosition = p.Gizmo.CursorPosition;
|
||||
|
||||
// Apply brush modification
|
||||
Profiler.BeginEvent("Apply Brush");
|
||||
for (int z = 0; z < p.ModifiedSize.Y; z++)
|
||||
{
|
||||
var zz = z + p.ModifiedOffset.Y;
|
||||
for (int x = 0; x < p.ModifiedSize.X; x++)
|
||||
{
|
||||
var xx = x + p.ModifiedOffset.X;
|
||||
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
|
||||
|
||||
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex);
|
||||
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
|
||||
|
||||
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld);
|
||||
|
||||
p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight + paintAmount * strength;
|
||||
}
|
||||
}
|
||||
Profiler.EndEvent();
|
||||
|
||||
// Update terrain patch
|
||||
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs
Normal file
82
Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Sculpt
|
||||
{
|
||||
/// <summary>
|
||||
/// Sculpt tool mode that smooths the terrain heightmap area affected by brush.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Tools.Terrain.Sculpt.Mode" />
|
||||
[HideInEditor]
|
||||
public sealed class SmoothMode : Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// The tool smoothing radius. Defines the size of smoothing kernel, the higher value the more nearby samples is included into normalized sum. Scaled by the brush size.
|
||||
/// </summary>
|
||||
[EditorOrder(10), Limit(0, 1, 0.01f), Tooltip("The tool smoothing radius. Defines the size of smoothing kernel, the higher value the more nearby samples is included into normalized sum. Scaled by the brush size.")]
|
||||
public float FilterRadius = 0.4f;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override unsafe void Apply(ref ApplyParams p)
|
||||
{
|
||||
// Prepare
|
||||
var brushPosition = p.Gizmo.CursorPosition;
|
||||
var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * p.Brush.Size), 2);
|
||||
var max = p.HeightmapSize - 1;
|
||||
var strength = Mathf.Saturate(p.Strength);
|
||||
|
||||
// Apply brush modification
|
||||
Profiler.BeginEvent("Apply Brush");
|
||||
for (int z = 0; z < p.ModifiedSize.Y; z++)
|
||||
{
|
||||
var zz = z + p.ModifiedOffset.Y;
|
||||
for (int x = 0; x < p.ModifiedSize.X; x++)
|
||||
{
|
||||
var xx = x + p.ModifiedOffset.X;
|
||||
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
|
||||
|
||||
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex);
|
||||
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
|
||||
|
||||
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
|
||||
|
||||
if (paintAmount > 0)
|
||||
{
|
||||
// Sum the nearby values
|
||||
float smoothValue = 0;
|
||||
int smoothValueSamples = 0;
|
||||
int minX = Math.Max(x - radius + p.ModifiedOffset.X, 0);
|
||||
int minZ = Math.Max(z - radius + p.ModifiedOffset.Y, 0);
|
||||
int maxX = Math.Min(x + radius + p.ModifiedOffset.X, max);
|
||||
int maxZ = Math.Min(z + radius + p.ModifiedOffset.Y, max);
|
||||
for (int dz = minZ; dz <= maxZ; dz++)
|
||||
{
|
||||
for (int dx = minX; dx <= maxX; dx++)
|
||||
{
|
||||
var height = p.SourceHeightMap[dz * p.HeightmapSize + dx];
|
||||
smoothValue += height;
|
||||
smoothValueSamples++;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize
|
||||
smoothValue /= smoothValueSamples;
|
||||
|
||||
// Blend between the height and smooth value
|
||||
p.TempBuffer[z * p.ModifiedSize.X + x] = Mathf.Lerp(sourceHeight, smoothValue, paintAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
Profiler.EndEvent();
|
||||
|
||||
// Update terrain patch
|
||||
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Source/Editor/Tools/Terrain/SculptTab.cs
Normal file
124
Source/Editor/Tools/Terrain/SculptTab.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.GUI.Tabs;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Carve tab related to terrain carving. Allows modifying terrain height and visibility using a brush.
|
||||
/// </summary>
|
||||
/// <seealso cref="Tab" />
|
||||
[HideInEditor]
|
||||
public class SculptTab : Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// The object for sculp mode settings adjusting via Custom Editor.
|
||||
/// </summary>
|
||||
private sealed class ProxyObject
|
||||
{
|
||||
private readonly SculptTerrainGizmoMode _mode;
|
||||
private object _currentMode, _currentBrush;
|
||||
|
||||
public ProxyObject(SculptTerrainGizmoMode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
SyncData();
|
||||
}
|
||||
|
||||
public void SyncData()
|
||||
{
|
||||
_currentMode = _mode.CurrentMode;
|
||||
_currentBrush = _mode.CurrentBrush;
|
||||
}
|
||||
|
||||
[EditorOrder(0), EditorDisplay("Tool"), Tooltip("Sculpt tool mode to use.")]
|
||||
public SculptTerrainGizmoMode.ModeTypes ToolMode
|
||||
{
|
||||
get => _mode.ToolModeType;
|
||||
set => _mode.ToolModeType = value;
|
||||
}
|
||||
|
||||
[EditorOrder(100), EditorDisplay("Tool", EditorDisplayAttribute.InlineStyle)]
|
||||
public object Mode
|
||||
{
|
||||
get => _currentMode;
|
||||
set { }
|
||||
}
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Brush"), Tooltip("Sculpt brush type to use.")]
|
||||
public SculptTerrainGizmoMode.BrushTypes BrushTypeType
|
||||
{
|
||||
get => _mode.ToolBrushType;
|
||||
set => _mode.ToolBrushType = value;
|
||||
}
|
||||
|
||||
[EditorOrder(1100), EditorDisplay("Brush", EditorDisplayAttribute.InlineStyle)]
|
||||
public object Brush
|
||||
{
|
||||
get => _currentBrush;
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ProxyObject _proxy;
|
||||
private readonly CustomEditorPresenter _presenter;
|
||||
|
||||
/// <summary>
|
||||
/// The parent carve tab.
|
||||
/// </summary>
|
||||
public readonly CarveTab CarveTab;
|
||||
|
||||
/// <summary>
|
||||
/// The related sculp terrain gizmo.
|
||||
/// </summary>
|
||||
public readonly SculptTerrainGizmoMode Gizmo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SculptTab"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tab">The parent tab.</param>
|
||||
/// <param name="gizmo">The related gizmo.</param>
|
||||
public SculptTab(CarveTab tab, SculptTerrainGizmoMode gizmo)
|
||||
: base("Sculpt")
|
||||
{
|
||||
CarveTab = tab;
|
||||
Gizmo = gizmo;
|
||||
Gizmo.ToolModeChanged += OnToolModeChanged;
|
||||
_proxy = new ProxyObject(gizmo);
|
||||
|
||||
// Main panel
|
||||
var panel = new Panel(ScrollBars.Both)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Options editor
|
||||
// TODO: use editor undo for changing brush options
|
||||
var editor = new CustomEditorPresenter(null);
|
||||
editor.Panel.Parent = panel;
|
||||
editor.Select(_proxy);
|
||||
_presenter = editor;
|
||||
}
|
||||
|
||||
private void OnToolModeChanged()
|
||||
{
|
||||
_presenter.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (_presenter.BuildOnUpdate)
|
||||
{
|
||||
_proxy.SyncData();
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
206
Source/Editor/Tools/Terrain/SculptTerrainGizmo.cs
Normal file
206
Source/Editor/Tools/Terrain/SculptTerrainGizmo.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEditor.Gizmo;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEditor.SceneGraph.Actors;
|
||||
using FlaxEditor.Tools.Terrain.Sculpt;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Gizmo for carving terrain. Managed by the <see cref="SculptTerrainGizmoMode"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Gizmo.GizmoBase" />
|
||||
[HideInEditor]
|
||||
public sealed class SculptTerrainGizmo : GizmoBase
|
||||
{
|
||||
private FlaxEngine.Terrain _paintTerrain;
|
||||
private Ray _prevRay;
|
||||
|
||||
/// <summary>
|
||||
/// The parent mode.
|
||||
/// </summary>
|
||||
public readonly SculptTerrainGizmoMode Mode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether gizmo tool is painting the terrain heightmap.
|
||||
/// </summary>
|
||||
public bool IsPainting => _paintTerrain != null;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when terrain paint has been started.
|
||||
/// </summary>
|
||||
public event Action PaintStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when terrain paint has been ended.
|
||||
/// </summary>
|
||||
public event Action PaintEnded;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SculptTerrainGizmo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <param name="mode">The mode.</param>
|
||||
public SculptTerrainGizmo(IGizmoOwner owner, SculptTerrainGizmoMode mode)
|
||||
: base(owner)
|
||||
{
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
private FlaxEngine.Terrain SelectedTerrain
|
||||
{
|
||||
get
|
||||
{
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
|
||||
return (FlaxEngine.Terrain)terrainNode?.Actor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw(ref RenderContext renderContext)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
var terrain = SelectedTerrain;
|
||||
if (!terrain)
|
||||
return;
|
||||
|
||||
if (Mode.HasValidHit)
|
||||
{
|
||||
var brushPosition = Mode.CursorPosition;
|
||||
var brushColor = new Color(1.0f, 0.85f, 0.0f); // TODO: expose to editor options
|
||||
var brushMaterial = Mode.CurrentBrush.GetBrushMaterial(ref brushPosition, ref brushColor);
|
||||
if (!brushMaterial)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < Mode.ChunksUnderCursor.Count; i++)
|
||||
{
|
||||
var chunk = Mode.ChunksUnderCursor[i];
|
||||
terrain.DrawChunk(ref renderContext, ref chunk.PatchCoord, ref chunk.ChunkCoord, brushMaterial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to start terrain painting
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
private void PaintStart(FlaxEngine.Terrain terrain)
|
||||
{
|
||||
// Skip if already is painting
|
||||
if (IsPainting)
|
||||
return;
|
||||
|
||||
_paintTerrain = terrain;
|
||||
PaintStarted?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to update terrain painting logic.
|
||||
/// </summary>
|
||||
/// <param name="dt">The delta time (in seconds).</param>
|
||||
private void PaintUpdate(float dt)
|
||||
{
|
||||
// Skip if is not painting
|
||||
if (!IsPainting)
|
||||
return;
|
||||
|
||||
// Edit the terrain
|
||||
Profiler.BeginEvent("Edit Terrain");
|
||||
var options = new Mode.Options
|
||||
{
|
||||
Strength = 1.0f,
|
||||
DeltaTime = dt,
|
||||
Invert = Owner.IsControlDown
|
||||
};
|
||||
Mode.CurrentMode.Apply(Mode.CurrentBrush, ref options, Mode, _paintTerrain);
|
||||
Profiler.EndEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to end terrain painting.
|
||||
/// </summary>
|
||||
private void PaintEnd()
|
||||
{
|
||||
// Skip if nothing was painted
|
||||
if (!IsPainting)
|
||||
return;
|
||||
|
||||
_paintTerrain = null;
|
||||
PaintEnded?.Invoke();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float dt)
|
||||
{
|
||||
base.Update(dt);
|
||||
|
||||
// Check if gizmo is not active
|
||||
if (!IsActive)
|
||||
{
|
||||
PaintEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if no terrain is selected
|
||||
var terrain = SelectedTerrain;
|
||||
if (!terrain)
|
||||
{
|
||||
PaintEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if selected terrain was changed during painting
|
||||
if (terrain != _paintTerrain && IsPainting)
|
||||
{
|
||||
PaintEnd();
|
||||
}
|
||||
|
||||
// Special case if user is sculpting terrain and mouse is not moving then freeze the brush location to help painting vertical tip objects
|
||||
var mouseRay = Owner.MouseRay;
|
||||
if (IsPainting && _prevRay == mouseRay)
|
||||
{
|
||||
// Freeze cursor
|
||||
}
|
||||
// Perform detailed tracing to find cursor location on the terrain
|
||||
else if (terrain.RayCast(mouseRay, out var closest, out var patchCoord, out var chunkCoord))
|
||||
{
|
||||
var hitLocation = mouseRay.GetPoint(closest);
|
||||
Mode.SetCursor(ref hitLocation);
|
||||
}
|
||||
// No hit
|
||||
else
|
||||
{
|
||||
Mode.ClearCursor();
|
||||
}
|
||||
_prevRay = mouseRay;
|
||||
|
||||
// Handle painting
|
||||
if (Owner.IsLeftMouseButtonDown)
|
||||
PaintStart(terrain);
|
||||
else
|
||||
PaintEnd();
|
||||
PaintUpdate(dt);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Pick()
|
||||
{
|
||||
// Get mouse ray and try to hit terrain
|
||||
var ray = Owner.MouseRay;
|
||||
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
|
||||
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders;
|
||||
var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags);
|
||||
|
||||
// Update selection
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
if (hit is TerrainNode)
|
||||
sceneEditing.Select(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
379
Source/Editor/Tools/Terrain/SculptTerrainGizmoMode.cs
Normal file
379
Source/Editor/Tools/Terrain/SculptTerrainGizmoMode.cs
Normal file
@@ -0,0 +1,379 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using FlaxEditor.SceneGraph.Actors;
|
||||
using FlaxEditor.Tools.Terrain.Brushes;
|
||||
using FlaxEditor.Tools.Terrain.Sculpt;
|
||||
using FlaxEditor.Tools.Terrain.Undo;
|
||||
using FlaxEditor.Viewport;
|
||||
using FlaxEditor.Viewport.Modes;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain carving tool mode.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
|
||||
[HideInEditor]
|
||||
public class SculptTerrainGizmoMode : EditorGizmoMode
|
||||
{
|
||||
private IntPtr _cachedHeightmapData;
|
||||
private int _cachedHeightmapDataSize;
|
||||
private EditTerrainMapAction _activeAction;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain carving gizmo.
|
||||
/// </summary>
|
||||
public SculptTerrainGizmo Gizmo;
|
||||
|
||||
/// <summary>
|
||||
/// The tool modes.
|
||||
/// </summary>
|
||||
public enum ModeTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// The sculpt mode.
|
||||
/// </summary>
|
||||
[Tooltip("Sculpt tool mode. Edits terrain heightmap by moving area affected by brush up or down.")]
|
||||
Sculpt,
|
||||
|
||||
/// <summary>
|
||||
/// The smooth mode.
|
||||
/// </summary>
|
||||
[Tooltip("Sculpt tool mode that smooths the terrain heightmap area affected by brush.")]
|
||||
Smooth,
|
||||
|
||||
/// <summary>
|
||||
/// The flatten mode.
|
||||
/// </summary>
|
||||
[Tooltip("Sculpt tool mode that flattens the terrain heightmap area affected by brush to the target value.")]
|
||||
Flatten,
|
||||
|
||||
/// <summary>
|
||||
/// The noise mode.
|
||||
/// </summary>
|
||||
[Tooltip("Sculpt tool mode that applies the noise to the terrain heightmap area affected by brush.")]
|
||||
Noise,
|
||||
|
||||
/// <summary>
|
||||
/// The holes mode.
|
||||
/// </summary>
|
||||
[Tooltip("Terrain holes creating tool mode edits terrain holes mask by changing area affected by brush.")]
|
||||
Holes,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The brush types.
|
||||
/// </summary>
|
||||
public enum BrushTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// The circle brush.
|
||||
/// </summary>
|
||||
CircleBrush,
|
||||
}
|
||||
|
||||
private readonly Mode[] _modes =
|
||||
{
|
||||
new SculptMode(),
|
||||
new SmoothMode(),
|
||||
new FlattenMode(),
|
||||
new NoiseMode(),
|
||||
new HolesMode(),
|
||||
};
|
||||
|
||||
private readonly Brush[] _brushes =
|
||||
{
|
||||
new CircleBrush(),
|
||||
};
|
||||
|
||||
private ModeTypes _modeType = ModeTypes.Sculpt;
|
||||
private BrushTypes _brushType = BrushTypes.CircleBrush;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when tool mode gets changed.
|
||||
/// </summary>
|
||||
public event Action ToolModeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current tool mode (enum).
|
||||
/// </summary>
|
||||
public ModeTypes ToolModeType
|
||||
{
|
||||
get => _modeType;
|
||||
set
|
||||
{
|
||||
if (_modeType != value)
|
||||
{
|
||||
if (_activeAction != null)
|
||||
throw new InvalidOperationException("Cannot change sculpt tool mode during terrain editing.");
|
||||
|
||||
_modeType = value;
|
||||
ToolModeChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current tool mode.
|
||||
/// </summary>
|
||||
public Mode CurrentMode => _modes[(int)_modeType];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sculpt mode instance.
|
||||
/// </summary>
|
||||
public SculptMode SculptMode => _modes[(int)ModeTypes.Sculpt] as SculptMode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the smooth mode instance.
|
||||
/// </summary>
|
||||
public SmoothMode SmoothMode => _modes[(int)ModeTypes.Smooth] as SmoothMode;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when tool brush gets changed.
|
||||
/// </summary>
|
||||
public event Action ToolBrushChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current tool brush (enum).
|
||||
/// </summary>
|
||||
public BrushTypes ToolBrushType
|
||||
{
|
||||
get => _brushType;
|
||||
set
|
||||
{
|
||||
if (_brushType != value)
|
||||
{
|
||||
if (_activeAction != null)
|
||||
throw new InvalidOperationException("Cannot change sculpt tool brush type during terrain editing.");
|
||||
|
||||
_brushType = value;
|
||||
ToolBrushChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current brush.
|
||||
/// </summary>
|
||||
public Brush CurrentBrush => _brushes[(int)_brushType];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the circle brush instance.
|
||||
/// </summary>
|
||||
public CircleBrush CircleBrush => _brushes[(int)BrushTypes.CircleBrush] as CircleBrush;
|
||||
|
||||
/// <summary>
|
||||
/// The last valid cursor position of the brush (in world space).
|
||||
/// </summary>
|
||||
public Vector3 CursorPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag used to indicate whenever last cursor position of the brush is valid.
|
||||
/// </summary>
|
||||
public bool HasValidHit { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Describes the terrain patch link.
|
||||
/// </summary>
|
||||
public struct PatchLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// The patch coordinates.
|
||||
/// </summary>
|
||||
public Int2 PatchCoord;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The selected terrain patches collection that are under cursor (affected by the brush).
|
||||
/// </summary>
|
||||
public readonly List<PatchLocation> PatchesUnderCursor = new List<PatchLocation>();
|
||||
|
||||
/// <summary>
|
||||
/// Describes the terrain chunk link.
|
||||
/// </summary>
|
||||
public struct ChunkLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// The patch coordinates.
|
||||
/// </summary>
|
||||
public Int2 PatchCoord;
|
||||
|
||||
/// <summary>
|
||||
/// The chunk coordinates.
|
||||
/// </summary>
|
||||
public Int2 ChunkCoord;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The selected terrain chunk collection that are under cursor (affected by the brush).
|
||||
/// </summary>
|
||||
public readonly List<ChunkLocation> ChunksUnderCursor = new List<ChunkLocation>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selected terrain actor (see <see cref="Modules.SceneEditingModule"/>).
|
||||
/// </summary>
|
||||
public FlaxEngine.Terrain SelectedTerrain
|
||||
{
|
||||
get
|
||||
{
|
||||
var sceneEditing = Editor.Instance.SceneEditing;
|
||||
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
|
||||
return (FlaxEngine.Terrain)terrainNode?.Actor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the world bounds of the brush located at the current cursor position (defined by <see cref="CursorPosition"/>). Valid only if <see cref="HasValidHit"/> is set to true.
|
||||
/// </summary>
|
||||
public BoundingBox CursorBrushBounds
|
||||
{
|
||||
get
|
||||
{
|
||||
const float brushExtentY = 10000.0f;
|
||||
float brushSizeHalf = CurrentBrush.Size * 0.5f;
|
||||
Vector3 center = CursorPosition;
|
||||
|
||||
BoundingBox box;
|
||||
box.Minimum = new Vector3(center.X - brushSizeHalf, center.Y - brushSizeHalf - brushExtentY, center.Z - brushSizeHalf);
|
||||
box.Maximum = new Vector3(center.X + brushSizeHalf, center.Y + brushSizeHalf + brushExtentY, center.Z + brushSizeHalf);
|
||||
return box;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the heightmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
|
||||
/// </summary>
|
||||
/// <param name="size">The minimum buffer size (in bytes).</param>
|
||||
/// <returns>The allocated memory using <see cref="Marshal"/> interface.</returns>
|
||||
public IntPtr GetHeightmapTempBuffer(int size)
|
||||
{
|
||||
if (_cachedHeightmapDataSize < size)
|
||||
{
|
||||
if (_cachedHeightmapData != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(_cachedHeightmapData);
|
||||
}
|
||||
_cachedHeightmapData = Marshal.AllocHGlobal(size);
|
||||
_cachedHeightmapDataSize = size;
|
||||
}
|
||||
|
||||
return _cachedHeightmapData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current edit terrain undo system action. Use it to record the data for the undo restoring after terrain editing.
|
||||
/// </summary>
|
||||
internal EditTerrainMapAction CurrentEditUndoAction => _activeAction;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Init(MainEditorGizmoViewport viewport)
|
||||
{
|
||||
base.Init(viewport);
|
||||
|
||||
Gizmo = new SculptTerrainGizmo(viewport, this);
|
||||
Gizmo.PaintStarted += OnPaintStarted;
|
||||
Gizmo.PaintEnded += OnPaintEnded;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnActivated()
|
||||
{
|
||||
base.OnActivated();
|
||||
|
||||
Viewport.Gizmos.Active = Gizmo;
|
||||
ClearCursor();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDeactivated()
|
||||
{
|
||||
base.OnDeactivated();
|
||||
|
||||
// Free temporary memory buffer
|
||||
if (_cachedHeightmapData != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(_cachedHeightmapData);
|
||||
_cachedHeightmapData = IntPtr.Zero;
|
||||
_cachedHeightmapDataSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cursor location information cached within the gizmo mode.
|
||||
/// </summary>
|
||||
public void ClearCursor()
|
||||
{
|
||||
HasValidHit = false;
|
||||
PatchesUnderCursor.Clear();
|
||||
ChunksUnderCursor.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cursor location in the world space. Updates the brush location and cached affected chunks.
|
||||
/// </summary>
|
||||
/// <param name="hitPosition">The cursor hit location on the selected terrain.</param>
|
||||
public void SetCursor(ref Vector3 hitPosition)
|
||||
{
|
||||
HasValidHit = true;
|
||||
CursorPosition = hitPosition;
|
||||
PatchesUnderCursor.Clear();
|
||||
ChunksUnderCursor.Clear();
|
||||
|
||||
// Find patches and chunks affected by the brush
|
||||
var terrain = SelectedTerrain;
|
||||
if (terrain == null)
|
||||
throw new InvalidOperationException("Cannot set cursor then no terrain is selected.");
|
||||
var brushBounds = CursorBrushBounds;
|
||||
var patchesCount = terrain.PatchesCount;
|
||||
BoundingBox tmp;
|
||||
for (int patchIndex = 0; patchIndex < patchesCount; patchIndex++)
|
||||
{
|
||||
terrain.GetPatchBounds(patchIndex, out tmp);
|
||||
if (!tmp.Intersects(ref brushBounds))
|
||||
continue;
|
||||
|
||||
terrain.GetPatchCoord(patchIndex, out var patchCoord);
|
||||
PatchesUnderCursor.Add(new PatchLocation { PatchCoord = patchCoord });
|
||||
|
||||
for (int chunkIndex = 0; chunkIndex < FlaxEngine.Terrain.PatchChunksCount; chunkIndex++)
|
||||
{
|
||||
terrain.GetChunkBounds(patchIndex, chunkIndex, out tmp);
|
||||
if (!tmp.Intersects(ref brushBounds))
|
||||
continue;
|
||||
|
||||
var chunkCoord = new Int2(chunkIndex % FlaxEngine.Terrain.PatchEdgeChunksCount, chunkIndex / FlaxEngine.Terrain.PatchEdgeChunksCount);
|
||||
ChunksUnderCursor.Add(new ChunkLocation { PatchCoord = patchCoord, ChunkCoord = chunkCoord });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPaintStarted()
|
||||
{
|
||||
if (_activeAction != null)
|
||||
throw new InvalidOperationException("Terrain paint start/end resynchronization.");
|
||||
|
||||
var terrain = SelectedTerrain;
|
||||
if (CurrentMode.EditHoles)
|
||||
_activeAction = new EditTerrainHolesMapAction(terrain);
|
||||
else
|
||||
_activeAction = new EditTerrainHeightMapAction(terrain);
|
||||
}
|
||||
|
||||
private void OnPaintEnded()
|
||||
{
|
||||
if (_activeAction != null)
|
||||
{
|
||||
if (_activeAction.HasAnyModification)
|
||||
{
|
||||
_activeAction.OnEditingEnd();
|
||||
Editor.Instance.Undo.AddAction(_activeAction);
|
||||
}
|
||||
_activeAction = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
479
Source/Editor/Tools/Terrain/TerrainTools.cpp
Normal file
479
Source/Editor/Tools/Terrain/TerrainTools.cpp
Normal file
@@ -0,0 +1,479 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "TerrainTools.h"
|
||||
#include "Engine/Core/Cache.h"
|
||||
#include "Engine/Core/Math/VectorInt.h"
|
||||
#include "Engine/Core/Math/Color32.h"
|
||||
#include "Engine/Core/Collections/CollectionPoolCache.h"
|
||||
#include "Engine/Terrain/TerrainPatch.h"
|
||||
#include "Engine/Terrain/Terrain.h"
|
||||
#include "Engine/Profiler/ProfilerCPU.h"
|
||||
#include "Engine/Graphics/PixelFormatExtensions.h"
|
||||
#include "Engine/Tools/TextureTool/TextureTool.h"
|
||||
#include "Engine/Graphics/Textures/TextureData.h"
|
||||
#include "Engine/Serialization/JsonWriters.h"
|
||||
#include "Engine/Scripting/Scripting.h"
|
||||
#include "Engine/Platform/FileSystem.h"
|
||||
#include "FlaxEngine.Gen.h"
|
||||
|
||||
bool TerrainTools::TryGetPatchCoordToAdd(Terrain* terrain, const Ray& ray, Int2& result)
|
||||
{
|
||||
CHECK_RETURN(terrain, true);
|
||||
result = Int2::Zero;
|
||||
const float patchSize = terrain->GetChunkSize() * TERRAIN_UNITS_PER_VERTEX * TerrainPatch::CHUNKS_COUNT_EDGE;
|
||||
|
||||
// Try to pick any of the patch edges
|
||||
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
|
||||
{
|
||||
const auto patch = terrain->GetPatch(patchIndex);
|
||||
const auto x = patch->GetX();
|
||||
const auto z = patch->GetZ();
|
||||
const auto bounds = patch->GetBounds();
|
||||
|
||||
// TODO: use chunk neighbors to reduce algorithm complexity
|
||||
|
||||
#define CHECK_EDGE(dx, dz) \
|
||||
if (terrain->GetPatch(x + dx, z + dz) == nullptr) \
|
||||
{ \
|
||||
if (bounds.MakeOffsetted(Vector3(patchSize * dx, 0, patchSize * dz)).Intersects(ray)) \
|
||||
{ \
|
||||
result = Int2(x + dx, z + dz); \
|
||||
return true; \
|
||||
} \
|
||||
}
|
||||
|
||||
CHECK_EDGE(1, 0);
|
||||
CHECK_EDGE(-1, 0);
|
||||
CHECK_EDGE(0, 1);
|
||||
CHECK_EDGE(0, -1);
|
||||
CHECK_EDGE(1, 1);
|
||||
CHECK_EDGE(1, -1);
|
||||
CHECK_EDGE(1, 1);
|
||||
CHECK_EDGE(-1, -1);
|
||||
CHECK_EDGE(-1, 1);
|
||||
|
||||
#undef CHECK_EDGE
|
||||
}
|
||||
|
||||
// Use the default patch if none added
|
||||
if (terrain->GetPatchesCount() == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
struct TextureDataResult
|
||||
{
|
||||
FlaxStorage::LockData Lock;
|
||||
BytesContainer Mip0Data;
|
||||
TextureData Tmp;
|
||||
uint32 RowPitch, SlicePitch;
|
||||
PixelFormat Format;
|
||||
Int2 Mip0Size;
|
||||
BytesContainer* Mip0DataPtr;
|
||||
|
||||
TextureDataResult()
|
||||
: Lock(FlaxStorage::LockData::Invalid)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
bool GetTextureDataForSampling(Texture* texture, TextureDataResult& data)
|
||||
{
|
||||
// Lock asset chunks (if not virtual)
|
||||
data.Lock = texture->LockData();
|
||||
|
||||
// Get the highest mip
|
||||
{
|
||||
PROFILE_CPU_NAMED("GetMipData");
|
||||
|
||||
texture->GetMipDataWithLoading(0, data.Mip0Data);
|
||||
if (data.Mip0Data.IsInvalid())
|
||||
{
|
||||
LOG(Warning, "Failed to get texture data.");
|
||||
return true;
|
||||
}
|
||||
if (!texture->GetMipDataCustomPitch(0, data.RowPitch, data.SlicePitch))
|
||||
texture->GetTexture()->ComputePitch(0, data.RowPitch, data.SlicePitch);
|
||||
}
|
||||
data.Mip0Size = Int2(texture->GetTexture()->Size());
|
||||
data.Format = texture->GetTexture()->Format();
|
||||
|
||||
// Decompress or convert data if need to
|
||||
data.Mip0DataPtr = &data.Mip0Data;
|
||||
if (PixelFormatExtensions::IsCompressed(data.Format))
|
||||
{
|
||||
PROFILE_CPU_NAMED("Decompress");
|
||||
|
||||
// Prepare source data descriptor (no data copy, just link the mip data)
|
||||
TextureData src;
|
||||
src.Width = data.Mip0Size.X;
|
||||
src.Height = data.Mip0Size.Y;
|
||||
src.Depth = 1;
|
||||
src.Format = data.Format;
|
||||
src.Items.Resize(1);
|
||||
src.Items[0].Mips.Resize(1);
|
||||
auto& srcMip = src.Items[0].Mips[0];
|
||||
srcMip.Data.Link(data.Mip0Data);
|
||||
srcMip.DepthPitch = data.SlicePitch;
|
||||
srcMip.RowPitch = data.RowPitch;
|
||||
srcMip.Lines = src.Height;
|
||||
|
||||
// Decompress texture
|
||||
if (TextureTool::Convert(data.Tmp, src, PixelFormat::R8G8B8A8_UNorm))
|
||||
{
|
||||
LOG(Warning, "Failed to decompress data.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Override source data and format
|
||||
data.Format = data.Tmp.Format;
|
||||
data.RowPitch = data.Tmp.Items[0].Mips[0].RowPitch;
|
||||
data.SlicePitch = data.Tmp.Items[0].Mips[0].DepthPitch;
|
||||
data.Mip0DataPtr = &data.Tmp.Items[0].Mips[0].Data;
|
||||
}
|
||||
// TODO: convert to RGBA from other formats that cannot be sampled?
|
||||
|
||||
// Check if can even sample the given format
|
||||
const auto sampler = TextureTool::GetSampler(data.Format);
|
||||
if (sampler == nullptr)
|
||||
{
|
||||
LOG(Warning, "Texture format {0} cannot be sampled.", (int32)data.Format);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches, Texture* heightmap, float heightmapScale, Texture* splatmap1, Texture* splatmap2)
|
||||
{
|
||||
CHECK_RETURN(terrain && terrain->GetChunkSize() != 0, true);
|
||||
if (numberOfPatches.X < 1 || numberOfPatches.Y < 1)
|
||||
{
|
||||
LOG(Warning, "Cannot setup terain with no patches.");
|
||||
return false;
|
||||
}
|
||||
|
||||
PROFILE_CPU_NAMED("Terrain.GenerateTerrain");
|
||||
|
||||
// Wait for assets to be loaded
|
||||
if (heightmap && heightmap->WaitForLoaded())
|
||||
{
|
||||
LOG(Warning, "Loading heightmap texture failed.");
|
||||
return true;
|
||||
}
|
||||
if (splatmap1 && splatmap1->WaitForLoaded())
|
||||
{
|
||||
LOG(Warning, "Loading splatmap texture failed.");
|
||||
return true;
|
||||
}
|
||||
if (splatmap2 && splatmap2->WaitForLoaded())
|
||||
{
|
||||
LOG(Warning, "Loading splatmap texture failed.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Spawn patches
|
||||
terrain->AddPatches(numberOfPatches);
|
||||
|
||||
// Prepare data
|
||||
const auto heightmapSize = terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1;
|
||||
Array<float> heightmapData;
|
||||
heightmapData.Resize(heightmapSize * heightmapSize);
|
||||
|
||||
// Get heightmap data
|
||||
if (heightmap && !Math::IsZero(heightmapScale))
|
||||
{
|
||||
// Get data
|
||||
TextureDataResult dataHeightmap;
|
||||
if (GetTextureDataForSampling(heightmap, dataHeightmap))
|
||||
return true;
|
||||
const auto sampler = TextureTool::GetSampler(dataHeightmap.Format);
|
||||
|
||||
// Initialize with sub-range of the input heightmap
|
||||
const Vector2 uvPerPatch = Vector2::One / Vector2(numberOfPatches);
|
||||
const float heightmapSizeInv = 1.0f / (heightmapSize - 1);
|
||||
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchIndex);
|
||||
|
||||
const Vector2 uvStart = Vector2((float)patch->GetX(), (float)patch->GetZ()) * uvPerPatch;
|
||||
|
||||
// Sample heightmap pixels with interpolation to get actual heightmap vertices locations
|
||||
for (int32 z = 0; z < heightmapSize; z++)
|
||||
{
|
||||
for (int32 x = 0; x < heightmapSize; x++)
|
||||
{
|
||||
const Vector2 uv = uvStart + Vector2(x * heightmapSizeInv, z * heightmapSizeInv) * uvPerPatch;
|
||||
const Color color = TextureTool::SampleLinear(sampler, uv, dataHeightmap.Mip0DataPtr->Get(), dataHeightmap.Mip0Size, dataHeightmap.RowPitch);
|
||||
heightmapData[z * heightmapSize + x] = color.R * heightmapScale;
|
||||
}
|
||||
}
|
||||
|
||||
if (patch->SetupHeightMap(heightmapData.Count(), heightmapData.Get(), nullptr))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Initialize flat heightmap data
|
||||
heightmapData.SetAll(0.0f);
|
||||
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchIndex);
|
||||
if (patch->SetupHeightMap(heightmapData.Count(), heightmapData.Get(), nullptr))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize terrain layers weights
|
||||
Texture* splatmaps[2] = { splatmap1, splatmap2 };
|
||||
Array<Color32> splatmapData;
|
||||
TextureDataResult data1;
|
||||
const Vector2 uvPerPatch = Vector2::One / Vector2(numberOfPatches);
|
||||
const float heightmapSizeInv = 1.0f / (heightmapSize - 1);
|
||||
for (int32 index = 0; index < ARRAY_COUNT(splatmaps); index++)
|
||||
{
|
||||
const auto splatmap = splatmaps[index];
|
||||
if (!splatmap)
|
||||
continue;
|
||||
|
||||
// Prepare data
|
||||
if (splatmapData.IsEmpty())
|
||||
splatmapData.Resize(heightmapSize * heightmapSize);
|
||||
|
||||
// Get splatmap data
|
||||
if (GetTextureDataForSampling(splatmap, data1))
|
||||
return true;
|
||||
const auto sampler = TextureTool::GetSampler(data1.Format);
|
||||
|
||||
// Modify heightmap splatmaps with sub-range of the input splatmaps
|
||||
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchIndex);
|
||||
|
||||
const Vector2 uvStart = Vector2((float)patch->GetX(), (float)patch->GetZ()) * uvPerPatch;
|
||||
|
||||
// Sample splatmap pixels with interpolation to get actual splatmap values
|
||||
for (int32 z = 0; z < heightmapSize; z++)
|
||||
{
|
||||
for (int32 x = 0; x < heightmapSize; x++)
|
||||
{
|
||||
const Vector2 uv = uvStart + Vector2(x * heightmapSizeInv, z * heightmapSizeInv) * uvPerPatch;
|
||||
|
||||
const Color color = TextureTool::SampleLinear(sampler, uv, data1.Mip0DataPtr->Get(), data1.Mip0Size, data1.RowPitch);
|
||||
|
||||
Color32 layers;
|
||||
layers.R = (byte)(Math::Min(1.0f, color.R) * 255.0f);
|
||||
layers.G = (byte)(Math::Min(1.0f, color.G) * 255.0f);
|
||||
layers.B = (byte)(Math::Min(1.0f, color.B) * 255.0f);
|
||||
layers.A = (byte)(Math::Min(1.0f, color.A) * 255.0f);
|
||||
|
||||
splatmapData[z * heightmapSize + x] = layers;
|
||||
}
|
||||
}
|
||||
|
||||
if (patch->ModifySplatMap(index, splatmapData.Get(), Int2::Zero, Int2(heightmapSize)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
StringAnsi TerrainTools::SerializePatch(Terrain* terrain, const Int2& patchCoord)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK_RETURN(patch, StringAnsi::Empty);
|
||||
|
||||
rapidjson_flax::StringBuffer buffer;
|
||||
CompactJsonWriter writerObj(buffer);
|
||||
JsonWriter& writer = writerObj;
|
||||
writer.StartObject();
|
||||
patch->Serialize(writer, nullptr);
|
||||
writer.EndObject();
|
||||
|
||||
return StringAnsi(buffer.GetString());
|
||||
}
|
||||
|
||||
void TerrainTools::DeserializePatch(Terrain* terrain, const Int2& patchCoord, const StringAnsiView& value)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK(patch);
|
||||
|
||||
rapidjson_flax::Document document;
|
||||
document.Parse(value.Get(), value.Length());
|
||||
CHECK(!document.HasParseError());
|
||||
|
||||
auto modifier = Cache::ISerializeModifier.Get();
|
||||
modifier->EngineBuild = FLAXENGINE_VERSION_BUILD;
|
||||
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
|
||||
|
||||
patch->Deserialize(document, modifier.Value);
|
||||
patch->UpdatePostManualDeserialization();
|
||||
}
|
||||
|
||||
bool TerrainTools::InitializePatch(Terrain* terrain, const Int2& patchCoord)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK_RETURN(patch, true);
|
||||
return patch->InitializeHeightMap();
|
||||
}
|
||||
|
||||
bool TerrainTools::ModifyHeightMap(Terrain* terrain, const Int2& patchCoord, float* samples, const Int2& offset, const Int2& size)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK_RETURN(patch, true);
|
||||
return patch->ModifyHeightMap(samples, offset, size);
|
||||
}
|
||||
|
||||
bool TerrainTools::ModifyHolesMask(Terrain* terrain, const Int2& patchCoord, byte* samples, const Int2& offset, const Int2& size)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK_RETURN(patch, true);
|
||||
return patch->ModifyHolesMask(samples, offset, size);
|
||||
}
|
||||
|
||||
bool TerrainTools::ModifySplatMap(Terrain* terrain, const Int2& patchCoord, int32 index, Color32* samples, const Int2& offset, const Int2& size)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK_RETURN(patch, true);
|
||||
CHECK_RETURN(index >= 0 && index < TERRAIN_MAX_SPLATMAPS_COUNT, true);
|
||||
return patch->ModifySplatMap(index, samples, offset, size);
|
||||
}
|
||||
|
||||
float* TerrainTools::GetHeightmapData(Terrain* terrain, const Int2& patchCoord)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK_RETURN(patch, nullptr);
|
||||
return patch->GetHeightmapData();
|
||||
}
|
||||
|
||||
byte* TerrainTools::GetHolesMaskData(Terrain* terrain, const Int2& patchCoord)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK_RETURN(patch, nullptr);
|
||||
return patch->GetHolesMaskData();
|
||||
}
|
||||
|
||||
Color32* TerrainTools::GetSplatMapData(Terrain* terrain, const Int2& patchCoord, int32 index)
|
||||
{
|
||||
auto patch = terrain->GetPatch(patchCoord);
|
||||
CHECK_RETURN(patch, nullptr);
|
||||
return patch->GetSplatMapData(index);
|
||||
}
|
||||
|
||||
bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
|
||||
{
|
||||
CHECK_RETURN(terrain && terrain->GetPatchesCount() != 0, true);
|
||||
const auto firstPatch = terrain->GetPatch(0);
|
||||
|
||||
// Calculate texture size
|
||||
const int32 patchEdgeVertexCount = terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1;
|
||||
const int32 patchVertexCount = patchEdgeVertexCount * patchEdgeVertexCount;
|
||||
|
||||
// Find size of heightmap in patches
|
||||
Int2 start(firstPatch->GetX(), firstPatch->GetZ());
|
||||
Int2 end(start);
|
||||
for (int32 i = 0; i < terrain->GetPatchesCount(); i++)
|
||||
{
|
||||
const int32 x = terrain->GetPatch(i)->GetX();
|
||||
const int32 y = terrain->GetPatch(i)->GetZ();
|
||||
|
||||
if (x < start.X)
|
||||
start.X = x;
|
||||
if (y < start.Y)
|
||||
start.Y = y;
|
||||
if (x > end.X)
|
||||
end.X = x;
|
||||
if (y > end.Y)
|
||||
end.Y = y;
|
||||
}
|
||||
const Int2 size = (end + 1) - start;
|
||||
|
||||
// Allocate - with space for non-existent patches
|
||||
Array<float> heightmap;
|
||||
heightmap.Resize(patchVertexCount * size.X * size.Y);
|
||||
|
||||
// Set to any element, where: min < elem < max
|
||||
heightmap.SetAll(firstPatch->GetHeightmapData()[0]);
|
||||
|
||||
const int32 heightmapWidth = patchEdgeVertexCount * size.X;
|
||||
|
||||
// Fill heightmap with data
|
||||
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
|
||||
{
|
||||
// Pick a patch
|
||||
const auto patch = terrain->GetPatch(patchIndex);
|
||||
const float* data = patch->GetHeightmapData();
|
||||
|
||||
// Beginning of patch
|
||||
int32 dstIndex = (patch->GetX() - start.X) * patchEdgeVertexCount +
|
||||
(patch->GetZ() - start.Y) * size.Y * patchVertexCount;
|
||||
|
||||
// Iterate over lines in patch
|
||||
for (int32 z = 0; z < patchEdgeVertexCount; z++)
|
||||
{
|
||||
// Iterate over vertices in line
|
||||
for (int32 x = 0; x < patchEdgeVertexCount; x++)
|
||||
{
|
||||
heightmap[dstIndex + x] = data[z * patchEdgeVertexCount + x];
|
||||
}
|
||||
|
||||
dstIndex += heightmapWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Interpolate to 16-bit int
|
||||
float maxHeight, minHeight;
|
||||
maxHeight = minHeight = heightmap[0];
|
||||
for (int32 i = 1; i < heightmap.Count(); i++)
|
||||
{
|
||||
float h = heightmap[i];
|
||||
if (maxHeight < h)
|
||||
maxHeight = h;
|
||||
else if (minHeight > h)
|
||||
minHeight = h;
|
||||
}
|
||||
|
||||
const float maxValue = 65535.0f;
|
||||
const float alpha = maxValue / (maxHeight - minHeight);
|
||||
|
||||
// Storage for pixel data
|
||||
Array<uint16> byteHeightmap(heightmap.Capacity());
|
||||
|
||||
for (auto& elem : heightmap)
|
||||
{
|
||||
byteHeightmap.Add(static_cast<uint16>(alpha * (elem - minHeight)));
|
||||
}
|
||||
|
||||
// Create texture
|
||||
TextureData textureData;
|
||||
textureData.Height = textureData.Width = heightmapWidth;
|
||||
textureData.Depth = 1;
|
||||
textureData.Format = PixelFormat::R16_UNorm;
|
||||
textureData.Items.Resize(1);
|
||||
textureData.Items[0].Mips.Resize(1);
|
||||
|
||||
// Fill mip data
|
||||
TextureMipData* srcMip = textureData.GetData(0, 0);
|
||||
srcMip->Data.Link(byteHeightmap.Get());
|
||||
srcMip->Lines = textureData.Height;
|
||||
srcMip->RowPitch = textureData.Width * 2; // 2 bytes per pixel for format R16
|
||||
srcMip->DepthPitch = srcMip->Lines * srcMip->RowPitch;
|
||||
|
||||
// Find next non-existing file heightmap file
|
||||
FileSystem::NormalizePath(outputFolder);
|
||||
const String baseFileName(TEXT("heightmap"));
|
||||
String outputPath;
|
||||
for (int32 i = 0; i < MAX_int32; i++)
|
||||
{
|
||||
outputPath = outputFolder / baseFileName + StringUtils::ToString(i) + TEXT(".png");
|
||||
if (!FileSystem::FileExists(outputPath))
|
||||
break;
|
||||
}
|
||||
|
||||
return TextureTool::ExportTexture(outputPath, textureData);
|
||||
}
|
||||
128
Source/Editor/Tools/Terrain/TerrainTools.h
Normal file
128
Source/Editor/Tools/Terrain/TerrainTools.h
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Scripting/ScriptingType.h"
|
||||
|
||||
class Terrain;
|
||||
class Texture;
|
||||
|
||||
/// <summary>
|
||||
/// Terrain* tools for editor. Allows to create and modify terrain.
|
||||
/// </summary>
|
||||
API_CLASS(Static, Namespace="FlaxEditor") class TerrainTools
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_NO_SPAWN(TerrainTools);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given ray hits any of the terrain patches sides to add a new patch there.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="ray">The ray to use for tracing (eg. mouse ray in world space).</param>
|
||||
/// <param name="result">The result patch coordinates (x and z). Valid only when method returns true.</param>
|
||||
/// <returns>True if result is valid, otherwise nothing to add there.</returns>
|
||||
API_FUNCTION() static bool TryGetPatchCoordToAdd(Terrain* terrain, const Ray& ray, API_PARAM(Out) Int2& result);
|
||||
|
||||
/// <summary>
|
||||
/// Generates the terrain from the input heightmap and splat maps.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="numberOfPatches">The number of patches (X and Z axis).</param>
|
||||
/// <param name="heightmap">The heightmap texture.</param>
|
||||
/// <param name="heightmapScale">The heightmap scale. Applied to adjust the normalized heightmap values into the world units.</param>
|
||||
/// <param name="splatmap1">The custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers composting. It's optional.</param>
|
||||
/// <param name="splatmap2">The custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers composting. It's optional.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() static bool GenerateTerrain(Terrain* terrain, API_PARAM(Ref) const Int2& numberOfPatches, Texture* heightmap, float heightmapScale, Texture* splatmap1, Texture* splatmap2);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the terrain chunk data to JSON string.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z).</param>
|
||||
/// <returns>The serialized chunk data.</returns>
|
||||
API_FUNCTION() static StringAnsi SerializePatch(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the terrain chunk data from the JSON string.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z).</param>
|
||||
/// <param name="value">The JSON string with serialized patch data.</param>
|
||||
API_FUNCTION() static void DeserializePatch(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, const StringAnsiView& value);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the patch heightmap and collision to the default flat level.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z) to initialize it.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() static bool InitializePatch(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord);
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the terrain patch heightmap with the given samples.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z) to modify it.</param>
|
||||
/// <param name="samples">The samples. The array length is size.X*size.Y. It has to be type of float.</param>
|
||||
/// <param name="offset">The offset from the first row and column of the heightmap data (offset destination x and z start position).</param>
|
||||
/// <param name="size">The size of the heightmap to modify (x and z). Amount of samples in each direction.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() static bool ModifyHeightMap(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, float* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size);
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the terrain patch holes mask with the given samples.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z) to modify it.</param>
|
||||
/// <param name="samples">The samples. The array length is size.X*size.Y. It has to be type of byte.</param>
|
||||
/// <param name="offset">The offset from the first row and column of the mask data (offset destination x and z start position).</param>
|
||||
/// <param name="size">The size of the mask to modify (x and z). Amount of samples in each direction.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() static bool ModifyHolesMask(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, byte* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size);
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the terrain patch splat map (layers mask) with the given samples.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z) to modify it.</param>
|
||||
/// <param name="index">The zero-based splatmap texture index.</param>
|
||||
/// <param name="samples">The samples. The array length is size.X*size.Y. It has to be type of <see cref="Color32"/>.</param>
|
||||
/// <param name="offset">The offset from the first row and column of the splatmap data (offset destination x and z start position).</param>
|
||||
/// <param name="size">The size of the splatmap to modify (x and z). Amount of samples in each direction.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() static bool ModifySplatMap(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, int32 index, Color32* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw pointer to the heightmap data (cached internally by the c++ core in editor).
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z) to gather it.</param>
|
||||
/// <returns>The pointer to the array of floats with terrain patch heights data. Null if failed to gather the data.</returns>
|
||||
API_FUNCTION() static float* GetHeightmapData(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw pointer to the holes mask data (cached internally by the c++ core in editor).
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z) to gather it.</param>
|
||||
/// <returns>The pointer to the array of bytes with terrain patch holes mask data. Null if failed to gather the data.</returns>
|
||||
API_FUNCTION() static byte* GetHolesMaskData(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw pointer to the splatmap data (cached internally by the c++ core in editor).
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="patchCoord">The patch coordinates (x and z) to gather it.</param>
|
||||
/// <param name="index">The zero-based splatmap texture index.</param>
|
||||
/// <returns>The pointer to the array of Color32 with terrain patch packed splatmap data. Null if failed to gather the data.</returns>
|
||||
API_FUNCTION() static Color32* GetSplatMapData(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, int32 index);
|
||||
|
||||
/// <summary>
|
||||
/// Export terrain's heightmap as a texture.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="outputFolder">The output folder path</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() static bool ExportTerrain(Terrain* terrain, String outputFolder);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Undo
|
||||
{
|
||||
/// <summary>
|
||||
/// The terrain heightmap editing action that records before and after states to swap between unmodified and modified terrain data.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.IUndoAction" />
|
||||
/// <seealso cref="EditTerrainMapAction" />
|
||||
[Serializable]
|
||||
unsafe class EditTerrainHeightMapAction : EditTerrainMapAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EditTerrainHeightMapAction"/> class.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
public EditTerrainHeightMapAction(FlaxEngine.Terrain terrain)
|
||||
: base(terrain, sizeof(float))
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ActionString => "Edit terrain heightmap";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IntPtr GetData(ref Int2 patchCoord, object tag)
|
||||
{
|
||||
return new IntPtr(TerrainTools.GetHeightmapData(Terrain, ref patchCoord));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetData(ref Int2 patchCoord, IntPtr data, object tag)
|
||||
{
|
||||
var offset = Int2.Zero;
|
||||
var size = new Int2((int)Mathf.Sqrt(_heightmapLength));
|
||||
if (TerrainTools.ModifyHeightMap(Terrain, ref patchCoord, (float*)data, ref offset, ref size))
|
||||
throw new FlaxException("Failed to modify the heightmap.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Undo
|
||||
{
|
||||
/// <summary>
|
||||
/// The terrain holes mask editing action that records before and after states to swap between unmodified and modified terrain data.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.IUndoAction" />
|
||||
/// <seealso cref="EditTerrainMapAction" />
|
||||
[Serializable]
|
||||
unsafe class EditTerrainHolesMapAction : EditTerrainMapAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EditTerrainHolesMapAction"/> class.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
public EditTerrainHolesMapAction(FlaxEngine.Terrain terrain)
|
||||
: base(terrain, sizeof(byte))
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ActionString => "Edit terrain holes";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IntPtr GetData(ref Int2 patchCoord, object tag)
|
||||
{
|
||||
return new IntPtr(TerrainTools.GetHolesMaskData(Terrain, ref patchCoord));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetData(ref Int2 patchCoord, IntPtr data, object tag)
|
||||
{
|
||||
var offset = Int2.Zero;
|
||||
var size = new Int2((int)Mathf.Sqrt(_heightmapLength));
|
||||
if (TerrainTools.ModifyHolesMask(Terrain, ref patchCoord, (byte*)data, ref offset, ref size))
|
||||
throw new FlaxException("Failed to modify the terrain holes.");
|
||||
}
|
||||
}
|
||||
}
|
||||
216
Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs
Normal file
216
Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Undo
|
||||
{
|
||||
/// <summary>
|
||||
/// The terrain heightmap or visibility map editing action that records before and after states to swap between unmodified and modified terrain data.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.IUndoAction" />
|
||||
[Serializable]
|
||||
abstract class EditTerrainMapAction : IUndoAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The compact data for the terrain patch modification.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
protected struct PatchData
|
||||
{
|
||||
/// <summary>
|
||||
/// The patch coordinates.
|
||||
/// </summary>
|
||||
public Int2 PatchCoord;
|
||||
|
||||
/// <summary>
|
||||
/// The data before (allocated on heap memory or null).
|
||||
/// </summary>
|
||||
public IntPtr Before;
|
||||
|
||||
/// <summary>
|
||||
/// The data after (allocated on heap memory or null).
|
||||
/// </summary>
|
||||
public IntPtr After;
|
||||
|
||||
/// <summary>
|
||||
/// The custom tag.
|
||||
/// </summary>
|
||||
public object Tag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The terrain (actor Id).
|
||||
/// </summary>
|
||||
[Serialize]
|
||||
protected readonly Guid _terrain;
|
||||
|
||||
/// <summary>
|
||||
/// The heightmap length (vertex count).
|
||||
/// </summary>
|
||||
[Serialize]
|
||||
protected readonly int _heightmapLength;
|
||||
|
||||
/// <summary>
|
||||
/// The heightmap data size (in bytes).
|
||||
/// </summary>
|
||||
[Serialize]
|
||||
protected readonly int _heightmapDataSize;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain patches
|
||||
/// </summary>
|
||||
[Serialize]
|
||||
protected readonly List<PatchData> _patches;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this action has any modification to the terrain (recorded patches changes).
|
||||
/// </summary>
|
||||
[NoSerialize]
|
||||
public bool HasAnyModification => _patches.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terrain.
|
||||
/// </summary>
|
||||
[NoSerialize]
|
||||
public FlaxEngine.Terrain Terrain
|
||||
{
|
||||
get
|
||||
{
|
||||
var terrainId = _terrain;
|
||||
return FlaxEngine.Object.Find<FlaxEngine.Terrain>(ref terrainId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EditTerrainMapAction"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>Use <see cref="AddPatch"/> to mark new patches to record and <see cref="OnEditingEnd"/> to finalize patches data after editing action.</remarks>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
/// <param name="stride">The data stride (eg. sizeof(float)).</param>
|
||||
protected EditTerrainMapAction(FlaxEngine.Terrain terrain, int stride)
|
||||
{
|
||||
_terrain = terrain.ID;
|
||||
_patches = new List<PatchData>(4);
|
||||
var chunkSize = terrain.ChunkSize;
|
||||
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
|
||||
_heightmapLength = heightmapSize * heightmapSize;
|
||||
_heightmapDataSize = _heightmapLength * stride;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the patch at the given coordinates has been already added.
|
||||
/// </summary>
|
||||
/// <param name="patchCoord">The patch coordinates.</param>
|
||||
/// <returns>True if patch has been added, otherwise false.</returns>
|
||||
public bool HashPatch(ref Int2 patchCoord)
|
||||
{
|
||||
for (int i = 0; i < _patches.Count; i++)
|
||||
{
|
||||
if (_patches[i].PatchCoord == patchCoord)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the patch to the action and records its current state.
|
||||
/// </summary>
|
||||
/// <param name="patchCoord">The patch coordinates.</param>
|
||||
/// <param name="tag">The custom argument (per patch).</param>
|
||||
public void AddPatch(ref Int2 patchCoord, object tag = null)
|
||||
{
|
||||
var data = Marshal.AllocHGlobal(_heightmapDataSize);
|
||||
var source = GetData(ref patchCoord, tag);
|
||||
Utils.MemoryCopy(source, data, _heightmapDataSize);
|
||||
_patches.Add(new PatchData
|
||||
{
|
||||
PatchCoord = patchCoord,
|
||||
Before = data,
|
||||
After = IntPtr.Zero,
|
||||
Tag = tag,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when terrain action editing ends. Record the `after` state of the patches.
|
||||
/// </summary>
|
||||
public void OnEditingEnd()
|
||||
{
|
||||
if (_patches.Count == 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < _patches.Count; i++)
|
||||
{
|
||||
var patch = _patches[i];
|
||||
if (patch.After != IntPtr.Zero)
|
||||
throw new InvalidOperationException("Invalid terrain edit undo action usage.");
|
||||
|
||||
var data = Marshal.AllocHGlobal(_heightmapDataSize);
|
||||
var source = GetData(ref patch.PatchCoord, patch.Tag);
|
||||
Utils.MemoryCopy(source, data, _heightmapDataSize);
|
||||
patch.After = data;
|
||||
_patches[i] = patch;
|
||||
}
|
||||
|
||||
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string ActionString { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Do()
|
||||
{
|
||||
Set(x => x.After);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
Set(x => x.Before);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// Ensure to release memory
|
||||
for (int i = 0; i < _patches.Count; i++)
|
||||
{
|
||||
Marshal.FreeHGlobal(_patches[i].Before);
|
||||
Marshal.FreeHGlobal(_patches[i].After);
|
||||
}
|
||||
_patches.Clear();
|
||||
}
|
||||
|
||||
private void Set(Func<PatchData, IntPtr> dataGetter)
|
||||
{
|
||||
for (int i = 0; i < _patches.Count; i++)
|
||||
{
|
||||
var patch = _patches[i];
|
||||
var data = dataGetter(patch);
|
||||
SetData(ref patch.PatchCoord, data, patch.Tag);
|
||||
}
|
||||
|
||||
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the patch data.
|
||||
/// </summary>
|
||||
/// <param name="patchCoord">The patch coordinates.</param>
|
||||
/// <param name="tag">The custom argument (per patch).</param>
|
||||
/// <returns>The data buffer (pointer to unmanaged memory).</returns>
|
||||
protected abstract IntPtr GetData(ref Int2 patchCoord, object tag);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the patch data.
|
||||
/// </summary>
|
||||
/// <param name="patchCoord">The patch coordinates.</param>
|
||||
/// <param name="data">The patch data.</param>
|
||||
/// <param name="tag">The custom argument (per patch).</param>
|
||||
protected abstract void SetData(ref Int2 patchCoord, IntPtr data, object tag);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Undo
|
||||
{
|
||||
/// <summary>
|
||||
/// The terrain splatmap editing action that records before and after states to swap between unmodified and modified terrain data.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.IUndoAction" />
|
||||
/// <seealso cref="EditTerrainMapAction" />
|
||||
[Serializable]
|
||||
unsafe class EditTerrainSplatMapAction : EditTerrainMapAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EditTerrainSplatMapAction"/> class.
|
||||
/// </summary>
|
||||
/// <param name="terrain">The terrain.</param>
|
||||
public EditTerrainSplatMapAction(FlaxEngine.Terrain terrain)
|
||||
: base(terrain, Color32.SizeInBytes)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ActionString => "Edit terrain splatmap";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IntPtr GetData(ref Int2 patchCoord, object tag)
|
||||
{
|
||||
return new IntPtr(TerrainTools.GetSplatMapData(Terrain, ref patchCoord, (int)tag));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetData(ref Int2 patchCoord, IntPtr data, object tag)
|
||||
{
|
||||
var offset = Int2.Zero;
|
||||
var size = new Int2((int)Mathf.Sqrt(_heightmapLength));
|
||||
if (TerrainTools.ModifySplatMap(Terrain, ref patchCoord, (int)tag, (Color32*)data, ref offset, ref size))
|
||||
throw new FlaxException("Failed to modify the splatmap.");
|
||||
}
|
||||
}
|
||||
}
|
||||
800
Source/Editor/Tools/VertexPainting.cs
Normal file
800
Source/Editor/Tools/VertexPainting.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user