Add cloth painting tools to Editor
This commit is contained in:
77
Source/Editor/CustomEditors/Dedicated/ClothEditor.cs
Normal file
77
Source/Editor/CustomEditors/Dedicated/ClothEditor.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Gizmo;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
using FlaxEngine.Tools;
|
||||
|
||||
namespace FlaxEditor.CustomEditors.Dedicated
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom editor for <see cref="Cloth"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="ActorEditor" />
|
||||
[CustomEditor(typeof(Cloth)), DefaultEditor]
|
||||
class ClothEditor : ActorEditor
|
||||
{
|
||||
private ClothPaintingGizmoMode _gizmoMode;
|
||||
private Viewport.Modes.EditorGizmoMode _prevMode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
base.Initialize(layout);
|
||||
|
||||
if (Values.Count != 1)
|
||||
return;
|
||||
|
||||
// Add gizmo painting mode to the viewport
|
||||
var owner = Presenter.Owner;
|
||||
if (owner == null)
|
||||
return;
|
||||
var gizmoOwner = owner as IGizmoOwner ?? owner.PresenterViewport as IGizmoOwner;
|
||||
if (gizmoOwner == null)
|
||||
return;
|
||||
var gizmos = gizmoOwner.Gizmos;
|
||||
_gizmoMode = new ClothPaintingGizmoMode();
|
||||
gizmos.AddMode(_gizmoMode);
|
||||
_prevMode = gizmos.ActiveMode;
|
||||
gizmos.ActiveMode = _gizmoMode;
|
||||
_gizmoMode.Gizmo.SetPaintCloth((Cloth)Values[0]);
|
||||
|
||||
// Insert gizmo mode options to properties editing
|
||||
var paintGroup = layout.Group("Cloth Painting");
|
||||
var paintValue = new ReadOnlyValueContainer(new ScriptType(typeof(ClothPaintingGizmoMode)), _gizmoMode);
|
||||
paintGroup.Object(paintValue);
|
||||
{
|
||||
var grid = paintGroup.CustomContainer<UniformGridPanel>();
|
||||
var gridControl = grid.CustomControl;
|
||||
gridControl.ClipChildren = false;
|
||||
gridControl.Height = Button.DefaultHeight;
|
||||
gridControl.SlotsHorizontally = 2;
|
||||
gridControl.SlotsVertically = 1;
|
||||
grid.Button("Fill", "Fills the cloth particles with given paint value.").Button.Clicked += _gizmoMode.Gizmo.Fill;
|
||||
grid.Button("Reset", "Clears the cloth particles paint.").Button.Clicked += _gizmoMode.Gizmo.Reset;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Deinitialize()
|
||||
{
|
||||
// Cleanup gizmos
|
||||
if (_gizmoMode != null)
|
||||
{
|
||||
var gizmos = _gizmoMode.Owner.Gizmos;
|
||||
if (gizmos.ActiveMode == _gizmoMode)
|
||||
gizmos.ActiveMode = _prevMode;
|
||||
gizmos.RemoveMode(_gizmoMode);
|
||||
_gizmoMode.Dispose();
|
||||
_gizmoMode = null;
|
||||
}
|
||||
_prevMode = null;
|
||||
|
||||
base.Deinitialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,10 @@ namespace FlaxEditor.Gizmo
|
||||
/// </summary>
|
||||
public event Action<EditorGizmoMode> ActiveModeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Init.
|
||||
/// </summary>
|
||||
/// <param name="owner">The gizmos owner interface.</param>
|
||||
public GizmosCollection(IGizmoOwner owner)
|
||||
{
|
||||
_owner = owner;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Gizmo
|
||||
@@ -94,5 +95,11 @@ namespace FlaxEditor.Gizmo
|
||||
/// Gets the root tree node for the scene graph.
|
||||
/// </summary>
|
||||
SceneGraph.RootNode SceneGraphRoot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Selects the scene objects.
|
||||
/// </summary>
|
||||
/// <param name="nodes">The nodes to select</param>
|
||||
void Select(List<SceneGraph.SceneGraphNode> nodes);
|
||||
}
|
||||
}
|
||||
|
||||
359
Source/Editor/Tools/ClothPainting.cs
Normal file
359
Source/Editor/Tools/ClothPainting.cs
Normal file
@@ -0,0 +1,359 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FlaxEditor;
|
||||
using FlaxEditor.Gizmo;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEditor.Viewport.Modes;
|
||||
|
||||
namespace FlaxEngine.Tools
|
||||
{
|
||||
sealed class ClothPaintingGizmoMode : EditorGizmoMode
|
||||
{
|
||||
[HideInEditor]
|
||||
public ClothPaintingGizmo Gizmo;
|
||||
|
||||
#pragma warning disable CS0649
|
||||
/// <summary>
|
||||
/// Brush radius (world-space).
|
||||
/// </summary>
|
||||
public float BrushSize = 50.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Brush paint intensity.
|
||||
/// </summary>
|
||||
public float BrushStrength = 2.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Brush paint falloff. Hardens or softens painting.
|
||||
/// </summary>
|
||||
public float BrushFalloff = 1.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Value to paint with. Hold Ctrl hey to paint with inverse value (1 - value).
|
||||
/// </summary>
|
||||
public float PaintValue = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Enables continuous painting, otherwise single paint on click.
|
||||
/// </summary>
|
||||
public bool ContinuousPaint;
|
||||
#pragma warning restore CS0649
|
||||
|
||||
public override void Init(IGizmoOwner owner)
|
||||
{
|
||||
base.Init(owner);
|
||||
|
||||
Gizmo = new ClothPaintingGizmo(owner, this);
|
||||
}
|
||||
|
||||
public override void OnActivated()
|
||||
{
|
||||
base.OnActivated();
|
||||
|
||||
Owner.Gizmos.Active = Gizmo;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ClothPaintingGizmo : GizmoBase
|
||||
{
|
||||
private Model _brushModel;
|
||||
private MaterialInstance _brushMaterial;
|
||||
private ClothPaintingGizmoMode _gizmoMode;
|
||||
private bool _isPainting;
|
||||
private int _paintUpdateCount;
|
||||
private bool _hasHit;
|
||||
private Vector3 _hitLocation;
|
||||
private Vector3 _hitNormal;
|
||||
private Cloth _cloth;
|
||||
private Float3[] _clothParticles;
|
||||
private float[] _clothPaint;
|
||||
private EditClothPaintAction _undoAction;
|
||||
|
||||
public bool IsPainting => _isPainting;
|
||||
|
||||
public ClothPaintingGizmo(IGizmoOwner owner, ClothPaintingGizmoMode mode)
|
||||
: base(owner)
|
||||
{
|
||||
_gizmoMode = mode;
|
||||
}
|
||||
|
||||
public void SetPaintCloth(Cloth cloth)
|
||||
{
|
||||
if (_cloth == cloth)
|
||||
return;
|
||||
PaintEnd();
|
||||
_cloth = cloth;
|
||||
_clothParticles = cloth?.GetParticles();
|
||||
_clothPaint = null;
|
||||
_hasHit = false;
|
||||
}
|
||||
|
||||
public void Fill()
|
||||
{
|
||||
PaintEnd();
|
||||
PaintStart();
|
||||
var clothPaint = _clothPaint;
|
||||
var paintValue = Mathf.Saturate(_gizmoMode.PaintValue);
|
||||
for (int i = 0; i < clothPaint.Length; i++)
|
||||
clothPaint[i] = paintValue;
|
||||
_cloth.SetPaint(clothPaint);
|
||||
PaintEnd();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
PaintEnd();
|
||||
PaintStart();
|
||||
var clothPaint = _clothPaint;
|
||||
for (int i = 0; i < clothPaint.Length; i++)
|
||||
clothPaint[i] = 1.0f;
|
||||
_cloth.SetPaint(clothPaint);
|
||||
PaintEnd();
|
||||
}
|
||||
|
||||
private void PaintStart()
|
||||
{
|
||||
if (IsPainting)
|
||||
return;
|
||||
|
||||
if (Editor.Instance.Undo.Enabled)
|
||||
_undoAction = new EditClothPaintAction(_cloth);
|
||||
_isPainting = true;
|
||||
_paintUpdateCount = 0;
|
||||
|
||||
// Get initial cloth paint state
|
||||
var clothParticles = _clothParticles;
|
||||
var clothPaint = _cloth.GetPaint();
|
||||
if (clothPaint == null || clothPaint.Length != clothParticles.Length)
|
||||
{
|
||||
_clothPaint = clothPaint = new float[clothParticles.Length];
|
||||
for (int i = 0; i < clothPaint.Length; i++)
|
||||
clothPaint[i] = 1.0f;
|
||||
}
|
||||
_clothPaint = clothPaint;
|
||||
}
|
||||
|
||||
private void PaintUpdate()
|
||||
{
|
||||
if (!_gizmoMode.ContinuousPaint && _paintUpdateCount > 0)
|
||||
return;
|
||||
Profiler.BeginEvent("Cloth Paint");
|
||||
|
||||
// Edit the cloth paint
|
||||
var clothParticles = _clothParticles;
|
||||
var clothPaint = _clothPaint;
|
||||
if (clothParticles == null || clothPaint == null)
|
||||
throw new Exception();
|
||||
var instanceTransform = _cloth.Transform;
|
||||
var brushSphere = new BoundingSphere(_hitLocation, _gizmoMode.BrushSize);
|
||||
var paintValue = Mathf.Saturate(_gizmoMode.PaintValue);
|
||||
if (Owner.IsControlDown)
|
||||
paintValue = 1.0f - paintValue;
|
||||
var modified = false;
|
||||
for (int i = 0; i < clothParticles.Length; i++)
|
||||
{
|
||||
var pos = instanceTransform.LocalToWorld(clothParticles[i]);
|
||||
var dst = Vector3.Distance(ref pos, ref brushSphere.Center);
|
||||
if (dst > brushSphere.Radius)
|
||||
continue;
|
||||
float strength = _gizmoMode.BrushStrength * Mathf.Lerp(1.0f, 1.0f - (float)dst / (float)brushSphere.Radius, _gizmoMode.BrushFalloff);
|
||||
if (strength > Mathf.Epsilon)
|
||||
{
|
||||
// Paint the particle
|
||||
ref var paint = ref clothPaint[i];
|
||||
paint = Mathf.Saturate(Mathf.Lerp(paint, paintValue, Mathf.Saturate(strength)));
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
_paintUpdateCount++;
|
||||
if (modified)
|
||||
{
|
||||
// Update cloth particles state
|
||||
_cloth.SetPaint(clothPaint);
|
||||
}
|
||||
|
||||
Profiler.EndEvent();
|
||||
}
|
||||
|
||||
private void PaintEnd()
|
||||
{
|
||||
if (!IsPainting)
|
||||
return;
|
||||
|
||||
if (_undoAction != null)
|
||||
{
|
||||
_undoAction.RecordEnd();
|
||||
Editor.Instance.Undo.AddAction(_undoAction);
|
||||
_undoAction = null;
|
||||
}
|
||||
_isPainting = false;
|
||||
_paintUpdateCount = 0;
|
||||
_clothPaint = null;
|
||||
}
|
||||
|
||||
public override bool IsControllingMouse => IsPainting;
|
||||
|
||||
public override BoundingSphere FocusBounds => _cloth?.Sphere ?? base.FocusBounds;
|
||||
|
||||
public override void Update(float dt)
|
||||
{
|
||||
_hasHit = false;
|
||||
if (!IsActive)
|
||||
{
|
||||
SetPaintCloth(null);
|
||||
return;
|
||||
}
|
||||
var cloth = _cloth;
|
||||
if (cloth == null)
|
||||
return;
|
||||
|
||||
// Perform detailed tracing to find cursor location for the brush
|
||||
var ray = Owner.MouseRay;
|
||||
if (cloth.IntersectsItself(ray, out var closest, out var hitNormal))
|
||||
{
|
||||
// Cursor hit cloth
|
||||
_hasHit = true;
|
||||
_hitLocation = ray.GetPoint(closest);
|
||||
_hitNormal = hitNormal;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cursor hit other object or nothing
|
||||
PaintEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle painting
|
||||
if (Owner.IsLeftMouseButtonDown)
|
||||
PaintStart();
|
||||
else
|
||||
PaintEnd();
|
||||
if (IsPainting)
|
||||
PaintUpdate();
|
||||
}
|
||||
|
||||
public override void Pick()
|
||||
{
|
||||
var ray = Owner.MouseRay;
|
||||
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
|
||||
var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives;
|
||||
var hit = Owner.SceneGraphRoot.RayCast(ref ray, ref view, out _, rayCastFlags);
|
||||
if (hit != null && hit is ActorNode)
|
||||
Owner.Select(new List<SceneGraphNode> { hit });
|
||||
}
|
||||
|
||||
public override void Draw(ref RenderContext renderContext)
|
||||
{
|
||||
if (!IsActive || !_cloth)
|
||||
return;
|
||||
|
||||
base.Draw(ref renderContext);
|
||||
|
||||
// TODO: impl this
|
||||
if (_hasHit)
|
||||
{
|
||||
var viewOrigin = renderContext.View.Origin;
|
||||
|
||||
// Draw paint brush
|
||||
if (!_brushModel)
|
||||
_brushModel = Content.LoadAsyncInternal<Model>("Editor/Primitives/Sphere");
|
||||
if (!_brushMaterial)
|
||||
_brushMaterial = Content.LoadAsyncInternal<Material>(EditorAssets.FoliageBrushMaterial)?.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 = RootNode.RaycastNormalRotation(ref _hitNormal);
|
||||
Matrix transform = Matrix.Scaling(_gizmoMode.BrushSize * 0.01f) * Matrix.RotationQuaternion(rotation) * Matrix.Translation(_hitLocation - viewOrigin);
|
||||
_brushModel.Draw(ref renderContext, _brushMaterial, ref transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnActivated()
|
||||
{
|
||||
base.OnActivated();
|
||||
|
||||
_hasHit = false;
|
||||
}
|
||||
|
||||
public override void OnDeactivated()
|
||||
{
|
||||
base.OnDeactivated();
|
||||
|
||||
PaintEnd();
|
||||
SetPaintCloth(null);
|
||||
Object.Destroy(ref _brushMaterial);
|
||||
_brushModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class EditClothPaintAction : IUndoAction
|
||||
{
|
||||
private Guid _actorId;
|
||||
private string _before, _after;
|
||||
|
||||
public EditClothPaintAction(Cloth cloth)
|
||||
{
|
||||
_actorId = cloth.ID;
|
||||
_before = GetState(cloth);
|
||||
}
|
||||
|
||||
public static bool IsValidState(string state)
|
||||
{
|
||||
return state != null && state.Contains("\"Paint\":");
|
||||
}
|
||||
|
||||
public static string GetState(Cloth cloth)
|
||||
{
|
||||
var json = cloth.ToJson();
|
||||
var start = json.IndexOf("\"Paint\":");
|
||||
if (start == -1)
|
||||
return null;
|
||||
var end = json.IndexOf('\"', json.IndexOf('\"', start + 8) + 1);
|
||||
json = "{" + json.Substring(start, end - start) + "\"}";
|
||||
return json;
|
||||
}
|
||||
|
||||
public static void SetState(Cloth cloth, string state)
|
||||
{
|
||||
if (state == null)
|
||||
cloth.SetPaint(null);
|
||||
else
|
||||
Editor.Internal_DeserializeSceneObject(Object.GetUnmanagedPtr(cloth), state);
|
||||
}
|
||||
|
||||
public void RecordEnd()
|
||||
{
|
||||
var cloth = Object.Find<Cloth>(ref _actorId);
|
||||
_after = GetState(cloth);
|
||||
Editor.Instance.Scene.MarkSceneEdited(cloth.Scene);
|
||||
}
|
||||
|
||||
private void Set(string state)
|
||||
{
|
||||
var cloth = Object.Find<Cloth>(ref _actorId);
|
||||
SetState(cloth, state);
|
||||
Editor.Instance.Scene.MarkSceneEdited(cloth.Scene);
|
||||
}
|
||||
|
||||
public string ActionString => "Edit Cloth Paint";
|
||||
|
||||
public void Do()
|
||||
{
|
||||
Set(_after);
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
Set(_before);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_before = _after = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlaxEditor.CustomEditors;
|
||||
@@ -596,18 +597,12 @@ namespace FlaxEditor.Tools
|
||||
/// <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;
|
||||
var hit = Owner.SceneGraphRoot.RayCast(ref ray, ref view, out _, rayCastFlags);
|
||||
if (hit != null && hit is ActorNode actorNode && actorNode.Actor is StaticModel model)
|
||||
{
|
||||
sceneEditing.Select(hit);
|
||||
}
|
||||
Owner.Select(new List<SceneGraphNode> { hit });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using FlaxEditor.Gizmo;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEditor.Viewport.Cameras;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
@@ -12,7 +14,7 @@ namespace FlaxEditor.Viewport
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Viewport.EditorViewport" />
|
||||
/// <seealso cref="IGizmoOwner" />
|
||||
public class EditorGizmoViewport : EditorViewport, IGizmoOwner
|
||||
public abstract class EditorGizmoViewport : EditorViewport, IGizmoOwner
|
||||
{
|
||||
private UpdateDelegate _update;
|
||||
|
||||
@@ -79,6 +81,9 @@ namespace FlaxEditor.Viewport
|
||||
/// <inheritdoc />
|
||||
public SceneGraph.RootNode SceneGraphRoot { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Select(List<SceneGraphNode> nodes);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false;
|
||||
|
||||
|
||||
@@ -1150,6 +1150,12 @@ namespace FlaxEditor.Viewport
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Select(List<SceneGraphNode> nodes)
|
||||
{
|
||||
_editor.SceneEditing.Select(nodes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
|
||||
@@ -301,7 +301,7 @@ namespace FlaxEditor.Viewport
|
||||
/// </summary>
|
||||
public void ShowSelectedActors()
|
||||
{
|
||||
var orient = Viewport.ViewOrientation;
|
||||
var orient = ViewOrientation;
|
||||
((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orient);
|
||||
}
|
||||
|
||||
@@ -345,7 +345,10 @@ namespace FlaxEditor.Viewport
|
||||
public RootNode SceneGraphRoot => _window.Graph.Root;
|
||||
|
||||
/// <inheritdoc />
|
||||
public EditorViewport Viewport => this;
|
||||
public void Select(List<SceneGraphNode> nodes)
|
||||
{
|
||||
_window.Select(nodes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false;
|
||||
|
||||
Reference in New Issue
Block a user