Merge remote-tracking branch 'upstream/master' into VISjectIsNowMoreVISuallyAppealingAndLegible
This commit is contained in:
3
.github/workflows/build_linux.yml
vendored
3
.github/workflows/build_linux.yml
vendored
@@ -16,7 +16,8 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --fix-missing libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
|
||||
- name: Setup Vulkan
|
||||
uses: ./.github/actions/vulkan
|
||||
- name: Setup .NET
|
||||
|
||||
6
.github/workflows/cd.yml
vendored
6
.github/workflows/cd.yml
vendored
@@ -87,7 +87,8 @@ jobs:
|
||||
git ${{ env.GIT_LFS_PULL_OPTIONS }} lfs pull
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --fix-missing libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
|
||||
- name: Setup Vulkan
|
||||
uses: ./.github/actions/vulkan
|
||||
- name: Setup .NET
|
||||
@@ -118,7 +119,8 @@ jobs:
|
||||
git ${{ env.GIT_LFS_PULL_OPTIONS }} lfs pull
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --fix-missing libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
|
||||
- name: Setup Vulkan
|
||||
uses: ./.github/actions/vulkan
|
||||
- name: Setup .NET
|
||||
|
||||
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -28,7 +28,8 @@ jobs:
|
||||
git lfs pull
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --fix-missing libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
|
||||
- name: Build
|
||||
run: |
|
||||
./GenerateProjectFiles.sh -vs2022 -log -verbose -printSDKs -dotnet=8
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -28,6 +28,7 @@ TextureCube SkyLightTexture : register(t__SRV__);
|
||||
Buffer<float4> ShadowsBuffer : register(t__SRV__);
|
||||
Texture2D<float> ShadowMap : register(t__SRV__);
|
||||
Texture3D VolumetricFogTexture : register(t__SRV__);
|
||||
Texture2D PreIntegratedGF : register(t__SRV__);
|
||||
@4// Forward Shading: Utilities
|
||||
// Public accessors for lighting data, use them as data binding might change but those methods will remain.
|
||||
LightData GetDirectionalLight() { return DirectionalLight; }
|
||||
@@ -108,7 +109,8 @@ void PS_Forward(
|
||||
|
||||
// Calculate reflections
|
||||
#if USE_REFLECTIONS
|
||||
float3 reflections = SampleReflectionProbe(ViewPos, EnvProbe, EnvironmentProbe, gBuffer.WorldPos, gBuffer.Normal, gBuffer.Roughness).rgb;
|
||||
float4 reflections = SampleReflectionProbe(ViewPos, EnvProbe, EnvironmentProbe, gBuffer.WorldPos, gBuffer.Normal, gBuffer.Roughness);
|
||||
reflections.rgb *= reflections.a;
|
||||
|
||||
#if MATERIAL_REFLECTIONS == MATERIAL_REFLECTIONS_SSR
|
||||
// Screen Space Reflections
|
||||
@@ -124,7 +126,7 @@ void PS_Forward(
|
||||
if (hit.z > 0)
|
||||
{
|
||||
float3 screenColor = sceneColorTexture.SampleLevel(SamplerPointClamp, hit.xy, 0).rgb;
|
||||
reflections = lerp(reflections, screenColor, hit.z);
|
||||
reflections.rgb = lerp(reflections.rgb, screenColor, hit.z);
|
||||
}
|
||||
|
||||
// Fallback to software tracing if possible
|
||||
@@ -136,17 +138,17 @@ void PS_Forward(
|
||||
if (TraceSDFSoftwareReflections(gBuffer, reflectWS, surfaceAtlas))
|
||||
{
|
||||
float3 screenColor = sceneColorTexture.SampleLevel(SamplerPointClamp, hit.xy, 0).rgb;
|
||||
reflections = lerp(surfaceAtlas, float4(screenColor, 1), hit.z);
|
||||
reflections.rgb = lerp(surfaceAtlas, float4(screenColor, 1), hit.z);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
light.rgb += reflections * GetReflectionSpecularLighting(ViewPos, gBuffer) * light.a;
|
||||
light.rgb += reflections.rgb * GetReflectionSpecularLighting(PreIntegratedGF, ViewPos, gBuffer);
|
||||
#endif
|
||||
|
||||
// Add lighting (apply ambient occlusion)
|
||||
output.rgb += light.rgb * gBuffer.AO;
|
||||
// Add lighting
|
||||
output.rgb += light.rgb;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -158,7 +160,8 @@ void PS_Forward(
|
||||
#else
|
||||
float fogSceneDistance = gBuffer.ViewPos.z;
|
||||
#endif
|
||||
float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, 0, fogSceneDistance);
|
||||
float fogSkipDistance = max(ExponentialHeightFog.VolumetricFogMaxDistance - 100, 0);
|
||||
float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, fogSkipDistance, fogSceneDistance);
|
||||
|
||||
if (ExponentialHeightFog.VolumetricFogMaxDistance > 0)
|
||||
{
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Shaders/GI/DDGI.flax
LFS
BIN
Content/Shaders/GI/DDGI.flax
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,10 +4,10 @@
|
||||
"Major": 1,
|
||||
"Minor": 11,
|
||||
"Revision": 0,
|
||||
"Build": 6805
|
||||
"Build": 6807
|
||||
},
|
||||
"Company": "Flax",
|
||||
"Copyright": "Copyright (c) 2012-2025 Wojciech Figat. All rights reserved.",
|
||||
"Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.",
|
||||
"GameTarget": "FlaxGame",
|
||||
"EditorTarget": "FlaxEditor",
|
||||
"Configuration": {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.CustomEditors.Dedicated
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom editor for <see cref="NavMeshBoundsVolume"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="ActorEditor" />
|
||||
[CustomEditor(typeof(NavMeshBoundsVolume)), DefaultEditor]
|
||||
internal class NavMeshBoundsVolumeEditor : ActorEditor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
base.Initialize(layout);
|
||||
|
||||
if (Values.HasDifferentTypes == false)
|
||||
{
|
||||
var button = layout.Button("Build");
|
||||
button.Button.Clicked += OnBuildClicked;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBuildClicked()
|
||||
{
|
||||
foreach (var value in Values)
|
||||
{
|
||||
if (value is NavMeshBoundsVolume volume)
|
||||
{
|
||||
Navigation.BuildNavMesh(volume.Box, volume.Scene);
|
||||
Editor.Instance.Scene.MarkSceneEdited(volume.Scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection.Emit;
|
||||
using FlaxEditor.CustomEditors.GUI;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
@@ -909,7 +909,8 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
settingsButton.Tag = script;
|
||||
settingsButton.Clicked += OnSettingsButtonClicked;
|
||||
|
||||
group.Panel.HeaderTextMargin = new Margin(scriptDrag.Right - 12, 15, 2, 2);
|
||||
// Adjust margin to not overlap with other ui elements in the header
|
||||
group.Panel.HeaderTextMargin = group.Panel.HeaderTextMargin with { Left = scriptDrag.Right - 12, Right = settingsButton.Width + Utilities.Constants.UIMargin };
|
||||
group.Object(values, editor);
|
||||
// Remove drop down arrows and containment lines if no objects in the group
|
||||
if (group.Children.Count == 0)
|
||||
|
||||
@@ -450,6 +450,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
protected bool NotNullItems;
|
||||
|
||||
private IntValueBox _sizeBox;
|
||||
private Label _label;
|
||||
private Color _background;
|
||||
private int _elementsCount, _minCount, _maxCount;
|
||||
private bool _readOnly;
|
||||
@@ -566,7 +567,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
Parent = dropPanel,
|
||||
};
|
||||
|
||||
var label = new Label
|
||||
_label = new Label
|
||||
{
|
||||
Text = "Size",
|
||||
AnchorPreset = AnchorPresets.TopRight,
|
||||
@@ -592,11 +593,12 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
panel.Panel.Offsets = new Margin(7, 7, 0, 0);
|
||||
panel.Panel.BackgroundColor = _background;
|
||||
var elementType = ElementType;
|
||||
var bindingAttr = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public;
|
||||
bool single = elementType.IsPrimitive ||
|
||||
elementType.Equals(new ScriptType(typeof(string))) ||
|
||||
elementType.IsEnum ||
|
||||
(elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) ||
|
||||
(elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) ||
|
||||
(elementType.GetFields(bindingAttr).Length == 1 && elementType.GetProperties(bindingAttr).Length == 0) ||
|
||||
(elementType.GetProperties(bindingAttr).Length == 1 && elementType.GetFields(bindingAttr).Length == 0) ||
|
||||
elementType.Equals(new ScriptType(typeof(JsonAsset))) ||
|
||||
elementType.Equals(new ScriptType(typeof(SettingsBase)));
|
||||
if (_cachedDropPanels == null)
|
||||
@@ -672,8 +674,10 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
Resize(Count + 1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Layout.ContainerControl.SizeChanged += OnLayoutSizeChanged;
|
||||
}
|
||||
|
||||
private void OnSetupContextMenu(ContextMenu menu, DropPanel panel)
|
||||
{
|
||||
if (menu.Items.Any(x => x is ContextMenuButton b && b.Text.Equals("Open All", StringComparison.Ordinal)))
|
||||
@@ -696,10 +700,24 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
});
|
||||
}
|
||||
|
||||
private void OnLayoutSizeChanged(Control control)
|
||||
{
|
||||
if (Layout.ContainerControl is DropPanel dropPanel)
|
||||
{
|
||||
// Hide "Size" text when array editor title overlaps
|
||||
var headerTextSize = dropPanel.HeaderTextFont.GetFont().MeasureText(dropPanel.HeaderText);
|
||||
if (headerTextSize.X + DropPanel.DropDownIconSize >= _label.Left)
|
||||
_label.TextColor = _label.TextColorHighlighted = Color.Transparent;
|
||||
else
|
||||
_label.TextColor = _label.TextColorHighlighted = FlaxEngine.GUI.Style.Current.Foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Deinitialize()
|
||||
{
|
||||
_sizeBox = null;
|
||||
Layout.ContainerControl.SizeChanged -= OnLayoutSizeChanged;
|
||||
|
||||
base.Deinitialize();
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ namespace FlaxEditor.CustomEditors.Elements
|
||||
{
|
||||
var style = Style.Current;
|
||||
var settingsButtonSize = Panel.HeaderHeight;
|
||||
return new Image
|
||||
Panel.HeaderTextMargin = Panel.HeaderTextMargin with { Right = settingsButtonSize + Utilities.Constants.UIMargin };
|
||||
; return new Image
|
||||
{
|
||||
TooltipText = "Settings",
|
||||
AutoFocus = true,
|
||||
|
||||
@@ -23,6 +23,7 @@ using FlaxEngine.Assertions;
|
||||
using FlaxEngine.GUI;
|
||||
using FlaxEngine.Interop;
|
||||
using FlaxEngine.Json;
|
||||
using FlaxEngine.Utilities;
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
@@ -1370,7 +1371,7 @@ namespace FlaxEditor
|
||||
public void BuildCSG()
|
||||
{
|
||||
var scenes = Level.Scenes;
|
||||
scenes.ToList().ForEach(x => x.BuildCSG(0));
|
||||
scenes.ForEach(x => x.BuildCSG(0));
|
||||
Scene.MarkSceneEdited(scenes);
|
||||
}
|
||||
|
||||
@@ -1380,7 +1381,7 @@ namespace FlaxEditor
|
||||
public void BuildNavMesh()
|
||||
{
|
||||
var scenes = Level.Scenes;
|
||||
scenes.ToList().ForEach(x => Navigation.BuildNavMesh(x, 0));
|
||||
Navigation.BuildNavMesh();
|
||||
Scene.MarkSceneEdited(scenes);
|
||||
}
|
||||
|
||||
|
||||
@@ -502,6 +502,7 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
if (base.OnKeyDown(key))
|
||||
return true;
|
||||
|
||||
// Keyboard navigation around the menu
|
||||
switch (key)
|
||||
{
|
||||
case KeyboardKeys.ArrowDown:
|
||||
@@ -526,6 +527,20 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
}
|
||||
}
|
||||
break;
|
||||
case KeyboardKeys.ArrowRight:
|
||||
for (int i = 0; i < _panel.Children.Count; i++)
|
||||
{
|
||||
if (_panel.Children[i] is ContextMenuChildMenu item && item.Visible && item.IsFocused && !item.ContextMenu.IsOpened)
|
||||
{
|
||||
item.ShowChild(this);
|
||||
item.ContextMenu._panel.Children.FirstOrDefault(x => x is ContextMenuButton && x.Visible)?.Focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case KeyboardKeys.ArrowLeft:
|
||||
ParentCM?.RootWindow.Focus();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -72,6 +72,11 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
/// </summary>
|
||||
public bool HasChildCMOpened => _childCM != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent context menu (if exists).
|
||||
/// </summary>
|
||||
public ContextMenuBase ParentCM => _parentCM;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the topmost context menu.
|
||||
/// </summary>
|
||||
@@ -81,9 +86,7 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
{
|
||||
var cm = this;
|
||||
while (cm._parentCM != null && cm._isSubMenu)
|
||||
{
|
||||
cm = cm._parentCM;
|
||||
}
|
||||
return cm;
|
||||
}
|
||||
}
|
||||
@@ -108,6 +111,11 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
/// </summary>
|
||||
public bool UseInput = true;
|
||||
|
||||
/// <summary>
|
||||
/// Optional flag that can disable UI navigation (tab/enter).
|
||||
/// </summary>
|
||||
public bool UseNavigation = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContextMenuBase"/> class.
|
||||
/// </summary>
|
||||
@@ -594,6 +602,21 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
case KeyboardKeys.Escape:
|
||||
Hide();
|
||||
return true;
|
||||
case KeyboardKeys.Return:
|
||||
if (UseNavigation && Root?.FocusedControl != null)
|
||||
{
|
||||
Root.SubmitFocused();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case KeyboardKeys.Tab:
|
||||
if (UseNavigation && Root != null)
|
||||
{
|
||||
bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
|
||||
Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
CloseMenuOnClick = false;
|
||||
}
|
||||
|
||||
private void ShowChild(ContextMenu parentContextMenu)
|
||||
internal void ShowChild(ContextMenu parentContextMenu)
|
||||
{
|
||||
// Hide parent CM popups and set itself as child
|
||||
var vAlign = parentContextMenu.ItemsAreaMargin.Top;
|
||||
|
||||
@@ -522,6 +522,16 @@ namespace FlaxEditor.GUI
|
||||
cm.AddButton("Show whole curve", _editor.ShowWholeCurve);
|
||||
cm.AddButton("Reset view", _editor.ResetView);
|
||||
}
|
||||
cm.AddSeparator();
|
||||
var presetCm = cm.AddChildMenu("Apply preset");
|
||||
foreach (var value in Enum.GetValues(typeof(CurvePreset)))
|
||||
{
|
||||
CurvePreset preset = (CurvePreset)value;
|
||||
string name = Utilities.Utils.GetPropertyNameUI(preset.ToString());
|
||||
var b = presetCm.ContextMenu.AddButton(name, () => _editor.ApplyPreset(preset));
|
||||
b.Enabled = !(_editor is LinearCurveEditor<T> && (preset != CurvePreset.Constant && preset != CurvePreset.Linear));
|
||||
}
|
||||
|
||||
_editor.OnShowContextMenu(cm, selectionCount);
|
||||
cm.Show(this, location);
|
||||
}
|
||||
@@ -619,6 +629,33 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of avaliable curve presets for the <see cref="CurveEditor{T}"/>.
|
||||
/// </summary>
|
||||
public enum CurvePreset
|
||||
{
|
||||
/// <summary>
|
||||
/// A curve where every point has the same value.
|
||||
/// </summary>
|
||||
Constant,
|
||||
/// <summary>
|
||||
/// A curve linear curve.
|
||||
/// </summary>
|
||||
Linear,
|
||||
/// <summary>
|
||||
/// A curve that starts a slowly and then accelerates until the end.
|
||||
/// </summary>
|
||||
EaseIn,
|
||||
/// <summary>
|
||||
/// A curve that starts a steep and then flattens until the end.
|
||||
/// </summary>
|
||||
EaseOut,
|
||||
/// <summary>
|
||||
/// A combination of the <see cref="CurvePreset.EaseIn"/> and <see cref="CurvePreset.EaseOut"/> preset.
|
||||
/// </summary>
|
||||
Smoothstep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnKeyframesDeselect(IKeyframesEditor editor)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,48 @@ namespace FlaxEditor.GUI
|
||||
/// <seealso cref="CurveEditorBase" />
|
||||
public abstract partial class CurveEditor<T> : CurveEditorBase where T : new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single point in a <see cref="CurveEditorPreset"/>.
|
||||
/// </summary>
|
||||
protected struct CurvePresetPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The time.
|
||||
/// </summary>
|
||||
public float Time;
|
||||
|
||||
/// <summary>
|
||||
/// The value.
|
||||
/// </summary>
|
||||
public float Value;
|
||||
|
||||
/// <summary>
|
||||
/// The in tangent. Will be ignored in <see cref="LinearCurveEditor{T}"/>
|
||||
/// </summary>
|
||||
public float TangentIn;
|
||||
|
||||
/// <summary>
|
||||
/// The out tangent. Will be ignored in <see cref="LinearCurveEditor{T}"/>
|
||||
/// </summary>
|
||||
public float TangentOut;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A curve preset.
|
||||
/// </summary>
|
||||
protected struct CurveEditorPreset()
|
||||
{
|
||||
/// <summary>
|
||||
/// If the tangents will be linear or smooth.
|
||||
/// </summary>
|
||||
public bool LinearTangents;
|
||||
|
||||
/// <summary>
|
||||
/// The points of the preset.
|
||||
/// </summary>
|
||||
public List<CurvePresetPoint> Points;
|
||||
}
|
||||
|
||||
private class Popup : ContextMenuBase
|
||||
{
|
||||
private CustomEditorPresenter _presenter;
|
||||
@@ -26,11 +68,12 @@ namespace FlaxEditor.GUI
|
||||
private List<int> _keyframeIndices;
|
||||
private bool _isDirty;
|
||||
|
||||
public Popup(CurveEditor<T> editor, object[] selection, List<int> keyframeIndices = null, float height = 140.0f)
|
||||
: this(editor, height)
|
||||
public Popup(CurveEditor<T> editor, object[] selection, List<int> keyframeIndices = null, float maxHeight = 140.0f)
|
||||
: this(editor, maxHeight)
|
||||
{
|
||||
_presenter.Select(selection);
|
||||
_presenter.OpenAllGroups();
|
||||
Size = new Float2(Size.X, Mathf.Min(_presenter.ContainerControl.Size.Y, maxHeight));
|
||||
_keyframeIndices = keyframeIndices;
|
||||
if (keyframeIndices != null && selection.Length != keyframeIndices.Count)
|
||||
throw new Exception();
|
||||
@@ -169,7 +212,7 @@ namespace FlaxEditor.GUI
|
||||
if (IsSelected)
|
||||
color = Editor.ContainsFocus ? style.SelectionBorder : Color.Lerp(style.ForegroundDisabled, style.SelectionBorder, 0.4f);
|
||||
if (IsMouseOver)
|
||||
color *= 1.1f;
|
||||
color *= 1.5f;
|
||||
Render2D.FillRectangle(rect, color);
|
||||
}
|
||||
|
||||
@@ -285,7 +328,7 @@ namespace FlaxEditor.GUI
|
||||
/// <summary>
|
||||
/// The keyframes size.
|
||||
/// </summary>
|
||||
protected static readonly Float2 KeyframesSize = new Float2(7.0f);
|
||||
protected static readonly Float2 KeyframesSize = new Float2(8.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The colors for the keyframe points.
|
||||
@@ -326,6 +369,63 @@ namespace FlaxEditor.GUI
|
||||
private Color _labelsColor;
|
||||
private Font _labelsFont;
|
||||
|
||||
/// <summary>
|
||||
/// Preset values for <see cref="CurvePreset"/> to be applied to a <see cref="CurveEditor{T}"/>.
|
||||
/// </summary>
|
||||
protected Dictionary<CurvePreset, CurveEditorPreset> Presets = new Dictionary<CurvePreset, CurveEditorPreset>
|
||||
{
|
||||
{ CurvePreset.Constant, new CurveEditorPreset
|
||||
{
|
||||
LinearTangents = true,
|
||||
Points = new List<CurvePresetPoint>
|
||||
{
|
||||
new CurvePresetPoint { Time = 0f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f },
|
||||
new CurvePresetPoint { Time = 1f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f },
|
||||
}
|
||||
}
|
||||
},
|
||||
{ CurvePreset.EaseIn, new CurveEditorPreset
|
||||
{
|
||||
LinearTangents = false,
|
||||
Points = new List<CurvePresetPoint>
|
||||
{
|
||||
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
|
||||
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = -1.4f, TangentOut = 0f },
|
||||
}
|
||||
}
|
||||
},
|
||||
{ CurvePreset.EaseOut, new CurveEditorPreset
|
||||
{
|
||||
LinearTangents = false,
|
||||
Points = new List<CurvePresetPoint>
|
||||
{
|
||||
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
|
||||
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 1.4f },
|
||||
}
|
||||
}
|
||||
},
|
||||
{ CurvePreset.Linear, new CurveEditorPreset
|
||||
{
|
||||
LinearTangents = true,
|
||||
Points = new List<CurvePresetPoint>
|
||||
{
|
||||
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
|
||||
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
|
||||
}
|
||||
}
|
||||
},
|
||||
{ CurvePreset.Smoothstep, new CurveEditorPreset
|
||||
{
|
||||
LinearTangents = false,
|
||||
Points = new List<CurvePresetPoint>
|
||||
{
|
||||
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
|
||||
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The keyframe UI points.
|
||||
/// </summary>
|
||||
@@ -568,6 +668,28 @@ namespace FlaxEditor.GUI
|
||||
/// <param name="indicesToRemove">The list of indices of the keyframes to remove.</param>
|
||||
protected abstract void RemoveKeyframesInternal(HashSet<int> indicesToRemove);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to convert a float to the type of the type wildcard of the curve editor.
|
||||
/// </summary>
|
||||
/// <param name="value">The float.</param>
|
||||
/// <returns>The converted value.</returns>
|
||||
public static object ConvertCurvePresetValueToCurveEditorType(float value)
|
||||
{
|
||||
if (typeof(T) == typeof(Float2))
|
||||
return new Float2(value);
|
||||
if (typeof(T) == typeof(Float3))
|
||||
return new Float3(value);
|
||||
if (typeof(T) == typeof(Float4))
|
||||
return new Float4(value);
|
||||
if (typeof(T) == typeof(Vector2))
|
||||
return new Vector2(value);
|
||||
if (typeof(T) == typeof(Vector3))
|
||||
return new Vector3(value);
|
||||
if (typeof(T) == typeof(Vector4))
|
||||
return new Vector4(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when showing a context menu. Can be used to add custom buttons with actions.
|
||||
/// </summary>
|
||||
@@ -752,6 +874,17 @@ namespace FlaxEditor.GUI
|
||||
ShowCurve(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a <see cref="CurvePreset"/> to the curve editor.
|
||||
/// </summary>
|
||||
/// <param name="preset">The preset.</param>
|
||||
public virtual void ApplyPreset(CurvePreset preset)
|
||||
{
|
||||
// Remove existing keyframes
|
||||
SelectAll();
|
||||
RemoveKeyframes();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Evaluate(out object result, float time, bool loop = false)
|
||||
{
|
||||
@@ -1028,6 +1161,31 @@ namespace FlaxEditor.GUI
|
||||
return true;
|
||||
}
|
||||
|
||||
bool left = key == KeyboardKeys.ArrowLeft;
|
||||
bool right = key == KeyboardKeys.ArrowRight;
|
||||
bool up = key == KeyboardKeys.ArrowUp;
|
||||
bool down = key == KeyboardKeys.ArrowDown;
|
||||
|
||||
if (left || right || up || down)
|
||||
{
|
||||
bool shift = Root.GetKey(KeyboardKeys.Shift);
|
||||
bool alt = Root.GetKey(KeyboardKeys.Alt);
|
||||
float deltaValue = 10f;
|
||||
if (shift || alt)
|
||||
deltaValue = shift ? 2.5f : 5f;
|
||||
|
||||
Float2 moveDelta = Float2.Zero;
|
||||
if (left || right)
|
||||
moveDelta.X = left ? -deltaValue : deltaValue;
|
||||
if (up || down)
|
||||
moveDelta.Y = up ? -deltaValue : deltaValue;
|
||||
|
||||
_contents.OnMoveStart(Float2.Zero);
|
||||
_contents.OnMove(moveDelta);
|
||||
_contents.OnMoveEnd(Float2.Zero);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1526,6 +1684,22 @@ namespace FlaxEditor.GUI
|
||||
_tangents[i].Visible = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void ApplyPreset(CurvePreset preset)
|
||||
{
|
||||
base.ApplyPreset(preset);
|
||||
|
||||
CurveEditorPreset data = Presets[preset];
|
||||
foreach (var point in data.Points)
|
||||
{
|
||||
float time = point.Time;
|
||||
object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value);
|
||||
AddKeyframe(time, value);
|
||||
}
|
||||
|
||||
ShowWholeCurve();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DrawCurve(ref Rectangle viewRect)
|
||||
{
|
||||
@@ -2312,6 +2486,30 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void ApplyPreset(CurvePreset preset)
|
||||
{
|
||||
base.ApplyPreset(preset);
|
||||
|
||||
CurveEditorPreset data = Presets[preset];
|
||||
|
||||
foreach (var point in data.Points)
|
||||
{
|
||||
float time = point.Time;
|
||||
object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value);
|
||||
object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)point.TangentIn);
|
||||
object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)point.TangentOut);
|
||||
|
||||
AddKeyframe(time, value, tangentIn, tangentOut);
|
||||
}
|
||||
|
||||
SelectAll();
|
||||
if (data.LinearTangents)
|
||||
SetTangentsLinear();
|
||||
|
||||
ShowWholeCurve();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetScaleInternal(ref Float2 scale)
|
||||
{
|
||||
|
||||
@@ -469,7 +469,7 @@ namespace FlaxEditor.GUI.Docking
|
||||
var childPanels = _childPanels.ToArray();
|
||||
if (childPanels.Length != 0)
|
||||
{
|
||||
// Move tabs from child panels into this one
|
||||
// Fallback: move tabs from child panels into this one.
|
||||
DockWindow selectedTab = null;
|
||||
foreach (var childPanel in childPanels)
|
||||
{
|
||||
@@ -490,7 +490,8 @@ namespace FlaxEditor.GUI.Docking
|
||||
{
|
||||
// Unlink splitter
|
||||
var splitterParent = splitter.Parent;
|
||||
Assert.IsNotNull(splitterParent);
|
||||
if (splitterParent == null)
|
||||
return;
|
||||
splitter.Parent = null;
|
||||
|
||||
// Move controls from second split panel to the split panel parent
|
||||
@@ -507,17 +508,63 @@ namespace FlaxEditor.GUI.Docking
|
||||
splitter.Dispose();
|
||||
}
|
||||
}
|
||||
else if (IsMaster && _childPanels.Count != 0)
|
||||
{
|
||||
if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
|
||||
return;
|
||||
}
|
||||
else if (!IsMaster)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
else if (_childPanels.Count != 0)
|
||||
{
|
||||
if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
|
||||
return;
|
||||
}
|
||||
else if (!IsMaster)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool CollapseEmptyTabsProxy()
|
||||
{
|
||||
if (TabsCount == 0 && ChildPanelsCount > 0)
|
||||
{
|
||||
return TryCollapseSplitter(_tabsProxy?.Parent as Panel);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryCollapseSplitter(Panel removedPanelParent)
|
||||
{
|
||||
if (removedPanelParent == null)
|
||||
return false;
|
||||
if (!(removedPanelParent.Parent is SplitPanel tabsSplitter))
|
||||
return false;
|
||||
|
||||
var splitterParent = tabsSplitter.Parent;
|
||||
if (splitterParent == null)
|
||||
return false;
|
||||
tabsSplitter.Parent = null;
|
||||
|
||||
var scrPanel = removedPanelParent == tabsSplitter.Panel2 ? tabsSplitter.Panel1 : tabsSplitter.Panel2;
|
||||
var srcPanelChildrenCount = scrPanel.ChildrenCount;
|
||||
for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--)
|
||||
{
|
||||
scrPanel.GetChild(i).Parent = splitterParent;
|
||||
}
|
||||
Assert.IsTrue(scrPanel.ChildrenCount == 0);
|
||||
Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount);
|
||||
|
||||
tabsSplitter.Dispose();
|
||||
if (_tabsProxy != null && _tabsProxy.Parent == removedPanelParent)
|
||||
_tabsProxy = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal virtual void DockWindowInternal(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null)
|
||||
{
|
||||
DockWindow(state, window, autoSelect, splitterValue);
|
||||
|
||||
@@ -99,6 +99,11 @@ namespace FlaxEditor.GUI.Input
|
||||
/// </summary>
|
||||
public event Action SlidingEnd;
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, pressing the arrow up or down key increments/ decrements the value.
|
||||
/// </summary>
|
||||
public bool ArrowKeysIncrement = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the slider speed. Use value 0 to disable and hide slider UI.
|
||||
/// </summary>
|
||||
@@ -239,6 +244,27 @@ namespace FlaxEditor.GUI.Input
|
||||
ResetViewOffset();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnKeyDown(KeyboardKeys key)
|
||||
{
|
||||
if (ArrowKeysIncrement && (key == KeyboardKeys.ArrowUp || key == KeyboardKeys.ArrowDown))
|
||||
{
|
||||
bool altDown = Root.GetKey(KeyboardKeys.Alt);
|
||||
bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
|
||||
bool controlDown = Root.GetKey(KeyboardKeys.Control);
|
||||
float deltaValue = altDown ? 0.1f : (shiftDown ? 10f : (controlDown ? 100f : 1f));
|
||||
float slideDelta = key == KeyboardKeys.ArrowUp ? deltaValue : -deltaValue;
|
||||
|
||||
_startSlideValue = Value;
|
||||
ApplySliding(slideDelta);
|
||||
EndSliding();
|
||||
Focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
|
||||
@@ -1140,8 +1140,11 @@ namespace FlaxEditor.GUI.Tree
|
||||
ParentTree.DraggedOverNode = this;
|
||||
|
||||
// Expand node if mouse goes over arrow
|
||||
if (ArrowRect.Contains(location) && HasAnyVisibleChild)
|
||||
if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed)
|
||||
{
|
||||
Expand(true);
|
||||
ParentTree?.FlushPendingPerformLayout();
|
||||
}
|
||||
|
||||
result = OnDragEnterHeader(data);
|
||||
}
|
||||
@@ -1172,8 +1175,11 @@ namespace FlaxEditor.GUI.Tree
|
||||
ParentTree.DraggedOverNode = this;
|
||||
|
||||
// Expand node if mouse goes over arrow
|
||||
if (ArrowRect.Contains(location) && HasAnyVisibleChild)
|
||||
if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed)
|
||||
{
|
||||
Expand(true);
|
||||
ParentTree?.FlushPendingPerformLayout();
|
||||
}
|
||||
|
||||
if (!_isDragOverHeader)
|
||||
result = OnDragEnterHeader(data);
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace FlaxEditor.Gizmo
|
||||
public abstract class GizmoBase
|
||||
{
|
||||
private IGizmoOwner _owner;
|
||||
private bool _visible = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gizmo owner.
|
||||
@@ -34,6 +35,11 @@ namespace FlaxEditor.Gizmo
|
||||
/// </summary>
|
||||
public virtual BoundingSphere FocusBounds => BoundingSphere.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this gizmo is visible.
|
||||
/// </summary>
|
||||
public bool Visible { get { return _visible; } set { _visible = value; } }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GizmoBase"/> class.
|
||||
/// </summary>
|
||||
|
||||
@@ -36,11 +36,12 @@ public sealed class ViewportRubberBandSelector
|
||||
/// Triggers the start of a rubber band selection.
|
||||
/// </summary>
|
||||
/// <returns>True if selection started, otherwise false.</returns>
|
||||
public bool TryStartingRubberBandSelection()
|
||||
public bool TryStartingRubberBandSelection(Float2 mousePosition)
|
||||
{
|
||||
if (!_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
|
||||
{
|
||||
_tryStartRubberBand = true;
|
||||
_cachedStartingMousePosition = mousePosition;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -82,12 +83,15 @@ public sealed class ViewportRubberBandSelector
|
||||
return;
|
||||
}
|
||||
|
||||
if (_tryStartRubberBand && (Mathf.Abs(_owner.MouseDelta.X) > 0.1f || Mathf.Abs(_owner.MouseDelta.Y) > 0.1f) && canStart)
|
||||
if (_tryStartRubberBand && canStart)
|
||||
{
|
||||
_isRubberBandSpanning = true;
|
||||
_cachedStartingMousePosition = mousePosition;
|
||||
_rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero);
|
||||
_tryStartRubberBand = false;
|
||||
var delta = mousePosition - _cachedStartingMousePosition;
|
||||
if (Mathf.Abs(delta.X) > 0.1f || Mathf.Abs(delta.Y) > 0.1f)
|
||||
{
|
||||
_isRubberBandSpanning = true;
|
||||
_rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero);
|
||||
_tryStartRubberBand = false;
|
||||
}
|
||||
}
|
||||
else if (_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
|
||||
{
|
||||
|
||||
@@ -229,7 +229,7 @@ namespace FlaxEditor.Modules
|
||||
if (!isPlayMode && options.General.AutoRebuildNavMesh && actor.Scene && node.AffectsNavigationWithChildren)
|
||||
{
|
||||
var bounds = actor.BoxWithChildren;
|
||||
Navigation.BuildNavMesh(actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ namespace FlaxEditor.Modules
|
||||
private ContextMenuButton _menuToolsProfilerWindow;
|
||||
private ContextMenuButton _menuToolsSetTheCurrentSceneViewAsDefault;
|
||||
private ContextMenuButton _menuToolsTakeScreenshot;
|
||||
private ContextMenuButton _menuToolsOpenLocalFolder;
|
||||
private ContextMenuChildMenu _menuWindowApplyWindowLayout;
|
||||
|
||||
private ToolStripButton _toolStripSaveAll;
|
||||
@@ -725,6 +726,16 @@ namespace FlaxEditor.Modules
|
||||
_menuToolsTakeScreenshot = cm.AddButton("Take screenshot", inputOptions.TakeScreenshot, Editor.Windows.TakeScreenshot);
|
||||
cm.AddSeparator();
|
||||
cm.AddButton("Plugins", () => Editor.Windows.PluginsWin.Show());
|
||||
cm.AddSeparator();
|
||||
var childMenu = cm.AddChildMenu("Open Product Local folder");
|
||||
childMenu.ContextMenu.AddButton("Editor", () => FileSystem.ShowFileExplorer(Globals.ProductLocalFolder));
|
||||
_menuToolsOpenLocalFolder = childMenu.ContextMenu.AddButton("Game", () =>
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
GameSettings settings = GameSettings.Load<GameSettings>();
|
||||
string path = Path.Combine(localAppData, settings.CompanyName, settings.ProductName);
|
||||
FileSystem.ShowFileExplorer(path);
|
||||
});
|
||||
|
||||
// Window
|
||||
MenuWindow = MainMenu.AddButton("Window");
|
||||
@@ -1062,6 +1073,10 @@ namespace FlaxEditor.Modules
|
||||
_menuToolsBuildNavMesh.Enabled = canEdit;
|
||||
_menuToolsCancelBuilding.Enabled = GameCooker.IsRunning;
|
||||
_menuToolsSetTheCurrentSceneViewAsDefault.Enabled = Level.ScenesCount > 0;
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
GameSettings settings = GameSettings.Load<GameSettings>();
|
||||
string path = Path.Combine(localAppData, settings.CompanyName, settings.ProductName);
|
||||
_menuToolsOpenLocalFolder.Enabled = Directory.Exists(path);
|
||||
|
||||
c.PerformLayout();
|
||||
}
|
||||
|
||||
@@ -491,10 +491,15 @@ namespace FlaxEditor.Modules
|
||||
Editor.LogWarning("Empty panel inside layout.");
|
||||
p.RemoveIt();
|
||||
}
|
||||
else
|
||||
{
|
||||
p.CollapseEmptyTabsProxy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panel.SelectTab(selectedTab);
|
||||
panel.CollapseEmptyTabsProxy();
|
||||
}
|
||||
|
||||
private static void SaveBounds(XmlWriter writer, Window win)
|
||||
|
||||
@@ -387,6 +387,14 @@ namespace FlaxEditor.Options
|
||||
[EditorDisplay("Viewport"), EditorOrder(1760)]
|
||||
public InputBinding ToggleOrthographic = new InputBinding(KeyboardKeys.NumpadDecimal);
|
||||
|
||||
[DefaultValue(typeof(InputBinding), "G")]
|
||||
[EditorDisplay("Viewport"), EditorOrder(1770)]
|
||||
public InputBinding ToggleGameView = new InputBinding(KeyboardKeys.G);
|
||||
|
||||
[DefaultValue(typeof(InputBinding), "P")]
|
||||
[EditorDisplay("Viewport"), EditorOrder(1770)]
|
||||
public InputBinding ToggleNavMeshVisibility = new InputBinding(KeyboardKeys.P);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Debug Views
|
||||
@@ -571,6 +579,10 @@ namespace FlaxEditor.Options
|
||||
[EditorDisplay("View Flags"), EditorOrder(3260)]
|
||||
public InputBinding DebugDraw = new InputBinding(KeyboardKeys.Alpha4, KeyboardKeys.Control, KeyboardKeys.Shift);
|
||||
|
||||
[DefaultValue(typeof(InputBinding), "None")]
|
||||
[EditorDisplay("View Flags"), EditorOrder(3270)]
|
||||
public InputBinding Particles = new InputBinding(KeyboardKeys.None);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
if (value is BoxCollider collider)
|
||||
collider.AutoResize(!_keepLocalOrientation);
|
||||
}
|
||||
Presenter.OnModified();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -555,7 +555,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
var options = Editor.Instance.Options.Options.General;
|
||||
if (options.AutoRebuildNavMesh)
|
||||
{
|
||||
Navigation.BuildNavMesh(collider.Scene, collider.Box, options.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(collider.Box, options.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model used by this actor.
|
||||
/// </summary>
|
||||
public Model Model => ((StaticModel)Actor).Model;
|
||||
|
||||
/// <inheritdoc />
|
||||
public StaticModelNode(Actor actor)
|
||||
: base(actor)
|
||||
@@ -120,12 +125,12 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
{
|
||||
base.OnContextMenu(contextMenu, window);
|
||||
|
||||
// Check if every selected node is a primitive
|
||||
// Check if every selected node is a primitive or has collision asset
|
||||
var selection = GetSelection(window);
|
||||
bool autoOptionEnabled = true;
|
||||
foreach (var node in selection)
|
||||
{
|
||||
if (node is StaticModelNode staticModelNode && !staticModelNode.IsPrimitive)
|
||||
if (node is StaticModelNode staticModelNode && (!staticModelNode.IsPrimitive && GetCollisionData(staticModelNode.Model) == null))
|
||||
{
|
||||
autoOptionEnabled = false;
|
||||
break;
|
||||
@@ -201,6 +206,54 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
return Array.Empty<SceneGraphNode>();
|
||||
}
|
||||
|
||||
private static bool TryCollisionData(Model model, BinaryAssetItem assetItem, out CollisionData collisionData)
|
||||
{
|
||||
collisionData = FlaxEngine.Content.Load<CollisionData>(assetItem.ID);
|
||||
if (collisionData)
|
||||
{
|
||||
var options = collisionData.Options;
|
||||
if (options.Model == model.ID || options.Model == Guid.Empty)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private CollisionData GetCollisionData(Model model)
|
||||
{
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
// Check if there already is collision data for that model to reuse
|
||||
var modelItem = (AssetItem)Editor.Instance.ContentDatabase.Find(model.ID);
|
||||
if (modelItem?.ParentFolder != null)
|
||||
{
|
||||
foreach (var child in modelItem.ParentFolder.Children)
|
||||
{
|
||||
// Check if there is collision that was made with this model
|
||||
if (child is BinaryAssetItem b && b.IsOfType<CollisionData>())
|
||||
{
|
||||
if (TryCollisionData(model, b, out var collisionData))
|
||||
return collisionData;
|
||||
}
|
||||
|
||||
// Check if there is an auto-imported collision
|
||||
if (child is ContentFolder childFolder && childFolder.ShortName == modelItem.ShortName)
|
||||
{
|
||||
foreach (var childFolderChild in childFolder.Children)
|
||||
{
|
||||
if (childFolderChild is BinaryAssetItem c && c.IsOfType<CollisionData>())
|
||||
{
|
||||
if (TryCollisionData(model, c, out var collisionData))
|
||||
return collisionData;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CreateAuto(StaticModel actor, Spawner spawner, bool singleNode)
|
||||
{
|
||||
// Special case for in-built Editor models that can use analytical collision
|
||||
@@ -243,6 +296,15 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
collider.LocalPosition = new Vector3(0, 50.0f, 0);
|
||||
collider.LocalOrientation = Quaternion.Euler(0, 0, 90.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
var collider = new MeshCollider
|
||||
{
|
||||
Transform = actor.Transform,
|
||||
CollisionData = GetCollisionData(model),
|
||||
};
|
||||
spawner(collider);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateBox(StaticModel actor, Spawner spawner, bool singleNode)
|
||||
|
||||
@@ -304,25 +304,14 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CustomCodeNode : SurfaceNode
|
||||
internal sealed class CustomCodeNode : ResizableSurfaceNode
|
||||
{
|
||||
private Rectangle _resizeButtonRect;
|
||||
private Float2 _startResizingSize;
|
||||
private Float2 _startResizingCornerOffset;
|
||||
private bool _isResizing;
|
||||
private CustomCodeTextBox _textBox;
|
||||
|
||||
private int SizeValueIndex => Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array
|
||||
|
||||
private Float2 SizeValue
|
||||
{
|
||||
get => (Float2)Values[SizeValueIndex];
|
||||
set => SetValue(SizeValueIndex, value, false);
|
||||
}
|
||||
|
||||
public CustomCodeNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
_sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array
|
||||
Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size;
|
||||
if (nodeArch.TypeID == 8)
|
||||
{
|
||||
@@ -345,126 +334,19 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
_textBox.EditEnd += () => SetValue(0, _textBox.Text);
|
||||
}
|
||||
|
||||
public override bool CanSelect(ref Float2 location)
|
||||
{
|
||||
return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
|
||||
}
|
||||
|
||||
public override void OnSurfaceLoaded(SurfaceNodeActions action)
|
||||
{
|
||||
base.OnSurfaceLoaded(action);
|
||||
|
||||
_textBox.Text = (string)Values[0];
|
||||
|
||||
var size = SizeValue;
|
||||
if (Surface != null && Surface.GridSnappingEnabled)
|
||||
size = Surface.SnapToGrid(size, true);
|
||||
Resize(size.X, size.Y);
|
||||
}
|
||||
|
||||
public override void OnValuesChanged()
|
||||
{
|
||||
base.OnValuesChanged();
|
||||
|
||||
var size = SizeValue;
|
||||
Resize(size.X, size.Y);
|
||||
_textBox.Text = (string)Values[0];
|
||||
}
|
||||
|
||||
protected override void UpdateRectangles()
|
||||
{
|
||||
base.UpdateRectangles();
|
||||
|
||||
const float buttonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin;
|
||||
const float buttonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize;
|
||||
_resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize);
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
base.Draw();
|
||||
|
||||
var style = Style.Current;
|
||||
if (_isResizing)
|
||||
{
|
||||
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
|
||||
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
|
||||
}
|
||||
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
}
|
||||
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
if (_isResizing)
|
||||
EndResizing();
|
||||
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
public override void OnEndMouseCapture()
|
||||
{
|
||||
if (_isResizing)
|
||||
EndResizing();
|
||||
|
||||
base.OnEndMouseCapture();
|
||||
}
|
||||
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
if (base.OnMouseDown(location, button))
|
||||
return true;
|
||||
|
||||
if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit)
|
||||
{
|
||||
// Start sliding
|
||||
_isResizing = true;
|
||||
_startResizingSize = Size;
|
||||
_startResizingCornerOffset = Size - location;
|
||||
StartMouseCapture();
|
||||
Cursor = CursorType.SizeNWSE;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void OnMouseMove(Float2 location)
|
||||
{
|
||||
if (_isResizing)
|
||||
{
|
||||
var emptySize = CalculateNodeSize(0, 0);
|
||||
var size = Float2.Max(location - emptySize + _startResizingCornerOffset, new Float2(240, 160));
|
||||
Resize(size.X, size.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnMouseMove(location);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnMouseUp(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && _isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseUp(location, button);
|
||||
}
|
||||
|
||||
private void EndResizing()
|
||||
{
|
||||
Cursor = CursorType.Default;
|
||||
EndMouseCapture();
|
||||
_isResizing = false;
|
||||
if (_startResizingSize != Size)
|
||||
{
|
||||
var emptySize = CalculateNodeSize(0, 0);
|
||||
SizeValue = Size - emptySize;
|
||||
Surface.MarkAsEdited(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum MaterialTemplateInputsMapping
|
||||
|
||||
@@ -1100,6 +1100,27 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116)
|
||||
}
|
||||
},
|
||||
new NodeArchetype
|
||||
{
|
||||
TypeID = 5,
|
||||
Create = (id, context, arch, groupArch) => new SurfaceNodeParamsSet(id, context, arch, groupArch),
|
||||
Title = "Set Parameter",
|
||||
Description = "Parameter value setter invoked when the animation pose is evaluated (output pose comes from input)",
|
||||
Flags = NodeFlags.AnimGraph,
|
||||
Size = new Float2(140, 40),
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
Guid.Empty,
|
||||
null
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 0),
|
||||
NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 2),
|
||||
NodeElementArchetype.Factory.Input(1, string.Empty, true, ScriptType.Null, 1, 1),
|
||||
NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116)
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,14 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
TextureGroup = 4,
|
||||
}
|
||||
|
||||
internal class SampleTextureNode : SurfaceNode
|
||||
internal class TextureSamplerNode : SurfaceNode
|
||||
{
|
||||
private ComboBox _textureGroupPicker;
|
||||
protected int _samplerTypeValueIndex = -1;
|
||||
protected int _textureGroupValueIndex = -1;
|
||||
protected int _level = 5;
|
||||
|
||||
public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
protected TextureSamplerNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
}
|
||||
@@ -48,13 +51,13 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
if ((int)Values[0] == (int)CommonSamplerType.TextureGroup)
|
||||
if ((int)Values[_samplerTypeValueIndex] == (int)CommonSamplerType.TextureGroup)
|
||||
{
|
||||
if (_textureGroupPicker == null)
|
||||
{
|
||||
_textureGroupPicker = new ComboBox
|
||||
{
|
||||
Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight + FlaxEditor.Surface.Constants.LayoutOffsetY * 5),
|
||||
Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * _level),
|
||||
Width = 100,
|
||||
Parent = this,
|
||||
};
|
||||
@@ -71,7 +74,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
_textureGroupPicker.Visible = true;
|
||||
}
|
||||
_textureGroupPicker.SelectedIndexChanged -= OnSelectedTextureGroupChanged;
|
||||
_textureGroupPicker.SelectedIndex = (int)Values[2];
|
||||
_textureGroupPicker.SelectedIndex = (int)Values[_textureGroupValueIndex];
|
||||
_textureGroupPicker.SelectedIndexChanged += OnSelectedTextureGroupChanged;
|
||||
}
|
||||
else if (_textureGroupPicker != null)
|
||||
@@ -83,7 +86,39 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
private void OnSelectedTextureGroupChanged(ComboBox comboBox)
|
||||
{
|
||||
SetValue(2, _textureGroupPicker.SelectedIndex);
|
||||
SetValue(_textureGroupValueIndex, _textureGroupPicker.SelectedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
internal class SampleTextureNode : TextureSamplerNode
|
||||
{
|
||||
public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
_samplerTypeValueIndex = 0;
|
||||
_textureGroupValueIndex = 2;
|
||||
}
|
||||
}
|
||||
|
||||
internal class TriplanarSampleTextureNode : TextureSamplerNode
|
||||
{
|
||||
public TriplanarSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
_samplerTypeValueIndex = 3;
|
||||
_textureGroupValueIndex = 5;
|
||||
_level = 5;
|
||||
}
|
||||
}
|
||||
|
||||
internal class ProceduralSampleTextureNode : TextureSamplerNode
|
||||
{
|
||||
public ProceduralSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
_samplerTypeValueIndex = 0;
|
||||
_textureGroupValueIndex = 2;
|
||||
_level = 4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,9 +315,9 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
ConnectionsHints = ConnectionsHint.Vector,
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
0,
|
||||
-1.0f,
|
||||
0,
|
||||
(int)CommonSamplerType.LinearClamp, // Sampler
|
||||
-1.0f, // Level
|
||||
0, // Texture Group
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
@@ -402,6 +437,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
new NodeArchetype
|
||||
{
|
||||
TypeID = 16,
|
||||
Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch),
|
||||
Title = "Triplanar Texture",
|
||||
Description = "Projects a texture using world-space coordinates with triplanar mapping.",
|
||||
Flags = NodeFlags.MaterialGraph,
|
||||
@@ -411,8 +447,9 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
Float3.One, // Scale
|
||||
1.0f, // Blend
|
||||
Float2.Zero, // Offset
|
||||
2, // Sampler
|
||||
(int)CommonSamplerType.LinearWrap, // Sampler
|
||||
false, // Local
|
||||
0, // Texture Group
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
@@ -430,17 +467,17 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
new NodeArchetype
|
||||
{
|
||||
TypeID = 17,
|
||||
Create = (id, context, arch, groupArch) => new SampleTextureNode(id, context, arch, groupArch),
|
||||
Create = (id, context, arch, groupArch) => new ProceduralSampleTextureNode(id, context, arch, groupArch),
|
||||
Title = "Procedural Sample Texture",
|
||||
Description = "Samples a texture to create a more natural look with less obvious tiling.",
|
||||
Flags = NodeFlags.MaterialGraph,
|
||||
Size = new Float2(240, 110),
|
||||
Size = new Float2(240, 130),
|
||||
ConnectionsHints = ConnectionsHint.Vector,
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
2,
|
||||
-1.0f,
|
||||
0,
|
||||
(int)CommonSamplerType.LinearWrap, // Sampler
|
||||
-1.0f, // Level
|
||||
0, // Texture Group
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
@@ -448,8 +485,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
NodeElementArchetype.Factory.Input(1, "UVs", true, null, 1),
|
||||
NodeElementArchetype.Factory.Input(2, "Offset", true, typeof(Float2), 3),
|
||||
NodeElementArchetype.Factory.Output(0, "Color", typeof(Float4), 4),
|
||||
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 4, "Sampler"),
|
||||
NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 4, 100, 0, typeof(CommonSamplerType))
|
||||
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 3, "Sampler"),
|
||||
NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 3, 100, 0, typeof(CommonSamplerType))
|
||||
}
|
||||
},
|
||||
new NodeArchetype
|
||||
@@ -469,6 +506,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
TypeID = 23,
|
||||
Title = "Triplanar Normal Map",
|
||||
Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch),
|
||||
Description = "Projects a normal map texture using world-space coordinates with triplanar mapping.",
|
||||
Flags = NodeFlags.MaterialGraph,
|
||||
Size = new Float2(280, 100),
|
||||
@@ -477,8 +515,9 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
Float3.One, // Scale
|
||||
1.0f, // Blend
|
||||
Float2.Zero, // Offset
|
||||
2, // Sampler
|
||||
(int)CommonSamplerType.LinearWrap, // Sampler
|
||||
false, // Local
|
||||
0, // Texture Group
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
|
||||
@@ -453,7 +453,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
}
|
||||
}
|
||||
|
||||
private class CurveNode<T> : SurfaceNode where T : struct
|
||||
private class CurveNode<T> : ResizableSurfaceNode where T : struct
|
||||
{
|
||||
private BezierCurveEditor<T> _curve;
|
||||
private bool _isSavingCurve;
|
||||
@@ -492,6 +492,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
0.0f, zero, zero, zero,
|
||||
0.0f, zero, zero, zero,
|
||||
0.0f, zero, zero, zero,
|
||||
|
||||
new Float2(400, 180),
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
@@ -505,30 +507,52 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
public CurveNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
_sizeValueIndex = 29; // Index of the Size stored in Values array
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLoaded(SurfaceNodeActions action)
|
||||
{
|
||||
base.OnLoaded(action);
|
||||
|
||||
// Create curve editor
|
||||
var upperLeft = GetBox(0).BottomLeft;
|
||||
var upperRight = GetBox(1).BottomRight;
|
||||
float curveMargin = 20.0f;
|
||||
|
||||
_curve = new BezierCurveEditor<T>
|
||||
{
|
||||
MaxKeyframes = 7,
|
||||
Bounds = new Rectangle(upperLeft + new Float2(curveMargin, 10.0f), upperRight.X - upperLeft.X - curveMargin * 2.0f, 140.0f),
|
||||
Parent = this
|
||||
Parent = this,
|
||||
AnchorMax = Float2.One,
|
||||
};
|
||||
_curve.Edited += OnCurveEdited;
|
||||
_curve.UnlockChildrenRecursive();
|
||||
_curve.PerformLayout();
|
||||
|
||||
// Sync keyframes
|
||||
UpdateCurveKeyframes();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSurfaceLoaded(SurfaceNodeActions action)
|
||||
{
|
||||
base.OnSurfaceLoaded(action);
|
||||
|
||||
// Ensure the whole curve is shown
|
||||
_curve.ShowWholeCurve();
|
||||
}
|
||||
|
||||
public override void OnValuesChanged()
|
||||
{
|
||||
base.OnValuesChanged();
|
||||
|
||||
if (!_isSavingCurve)
|
||||
{
|
||||
UpdateCurveKeyframes();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCurveEdited()
|
||||
{
|
||||
if (_isSavingCurve)
|
||||
@@ -554,17 +578,6 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
_isSavingCurve = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnValuesChanged()
|
||||
{
|
||||
base.OnValuesChanged();
|
||||
|
||||
if (!_isSavingCurve)
|
||||
{
|
||||
UpdateCurveKeyframes();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCurveKeyframes()
|
||||
{
|
||||
var count = (int)Values[0];
|
||||
@@ -1593,7 +1606,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
Guid.Empty,
|
||||
string.Empty
|
||||
string.Empty,
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
|
||||
182
Source/Editor/Surface/ResizableSurfaceNode.cs
Normal file
182
Source/Editor/Surface/ResizableSurfaceNode.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Surface
|
||||
{
|
||||
/// <summary>
|
||||
/// Visject Surface node control that cna be resized.
|
||||
/// </summary>
|
||||
/// <seealso cref="SurfaceNode" />
|
||||
[HideInEditor]
|
||||
public class ResizableSurfaceNode : SurfaceNode
|
||||
{
|
||||
private Float2 _startResizingSize;
|
||||
private Float2 _startResizingCornerOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the node is currently being resized.
|
||||
/// </summary>
|
||||
protected bool _isResizing;
|
||||
|
||||
/// <summary>
|
||||
/// Index of the Float2 value in the node values list to store node size.
|
||||
/// </summary>
|
||||
protected int _sizeValueIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum node size.
|
||||
/// </summary>
|
||||
protected Float2 _sizeMin = new Float2(240, 160);
|
||||
|
||||
/// <summary>
|
||||
/// Node resizing rectangle bounds.
|
||||
/// </summary>
|
||||
protected Rectangle _resizeButtonRect;
|
||||
|
||||
private Float2 SizeValue
|
||||
{
|
||||
get => (Float2)Values[_sizeValueIndex];
|
||||
set => SetValue(_sizeValueIndex, value, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanSelect(ref Float2 location)
|
||||
{
|
||||
return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSurfaceLoaded(SurfaceNodeActions action)
|
||||
{
|
||||
// Reapply the curve node size
|
||||
var size = SizeValue;
|
||||
if (Surface != null && Surface.GridSnappingEnabled)
|
||||
size = Surface.SnapToGrid(size, true);
|
||||
Resize(size.X, size.Y);
|
||||
|
||||
base.OnSurfaceLoaded(action);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnValuesChanged()
|
||||
{
|
||||
base.OnValuesChanged();
|
||||
|
||||
var size = SizeValue;
|
||||
Resize(size.X, size.Y);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
base.Draw();
|
||||
|
||||
if (Surface.CanEdit)
|
||||
{
|
||||
var style = Style.Current;
|
||||
if (_isResizing)
|
||||
{
|
||||
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
|
||||
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
|
||||
}
|
||||
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
if (_isResizing)
|
||||
EndResizing();
|
||||
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnEndMouseCapture()
|
||||
{
|
||||
if (_isResizing)
|
||||
EndResizing();
|
||||
|
||||
base.OnEndMouseCapture();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
if (base.OnMouseDown(location, button))
|
||||
return true;
|
||||
|
||||
if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit)
|
||||
{
|
||||
// Start resizing
|
||||
_isResizing = true;
|
||||
_startResizingSize = Size;
|
||||
_startResizingCornerOffset = Size - location;
|
||||
StartMouseCapture();
|
||||
Cursor = CursorType.SizeNWSE;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseMove(Float2 location)
|
||||
{
|
||||
if (_isResizing)
|
||||
{
|
||||
var emptySize = CalculateNodeSize(0, 0);
|
||||
var size = Float2.Max(location - emptySize + _startResizingCornerOffset, _sizeMin);
|
||||
Resize(size.X, size.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnMouseMove(location);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseUp(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && _isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseUp(location, button);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateRectangles()
|
||||
{
|
||||
base.UpdateRectangles();
|
||||
|
||||
const float buttonMargin = Constants.NodeCloseButtonMargin;
|
||||
const float buttonSize = Constants.NodeCloseButtonSize;
|
||||
_resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize);
|
||||
}
|
||||
|
||||
private void EndResizing()
|
||||
{
|
||||
Cursor = CursorType.Default;
|
||||
EndMouseCapture();
|
||||
_isResizing = false;
|
||||
if (_startResizingSize != Size)
|
||||
{
|
||||
var emptySize = CalculateNodeSize(0, 0);
|
||||
SizeValue = Size - emptySize;
|
||||
Surface.MarkAsEdited(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,18 +14,11 @@ namespace FlaxEditor.Surface
|
||||
/// </summary>
|
||||
/// <seealso cref="SurfaceNode" />
|
||||
[HideInEditor]
|
||||
public class SurfaceComment : SurfaceNode
|
||||
public class SurfaceComment : ResizableSurfaceNode
|
||||
{
|
||||
private Rectangle _colorButtonRect;
|
||||
private Rectangle _resizeButtonRect;
|
||||
private Float2 _startResizingSize;
|
||||
private readonly TextBox _renameTextBox;
|
||||
|
||||
/// <summary>
|
||||
/// True if sizing tool is in use.
|
||||
/// </summary>
|
||||
protected bool _isResizing;
|
||||
|
||||
/// <summary>
|
||||
/// True if rename textbox is active in order to rename comment
|
||||
/// </summary>
|
||||
@@ -52,12 +45,6 @@ namespace FlaxEditor.Surface
|
||||
set => SetValue(1, value, false);
|
||||
}
|
||||
|
||||
private Float2 SizeValue
|
||||
{
|
||||
get => (Float2)Values[2];
|
||||
set => SetValue(2, value, false);
|
||||
}
|
||||
|
||||
private int OrderValue
|
||||
{
|
||||
get => (int)Values[3];
|
||||
@@ -68,6 +55,8 @@ namespace FlaxEditor.Surface
|
||||
public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
_sizeValueIndex = 2; // Index of the Size stored in Values array
|
||||
_sizeMin = new Float2(140.0f, Constants.NodeHeaderSize);
|
||||
_renameTextBox = new TextBox(false, 0, 0, Width)
|
||||
{
|
||||
Height = Constants.NodeHeaderHeight,
|
||||
@@ -86,10 +75,6 @@ namespace FlaxEditor.Surface
|
||||
// Read node data
|
||||
Title = TitleValue;
|
||||
Color = ColorValue;
|
||||
var size = SizeValue;
|
||||
if (Surface != null && Surface.GridSnappingEnabled)
|
||||
size = Surface.SnapToGrid(size, true);
|
||||
Size = size;
|
||||
|
||||
// Order
|
||||
// Backwards compatibility - When opening with an older version send the old comments to the back
|
||||
@@ -126,27 +111,6 @@ namespace FlaxEditor.Surface
|
||||
// Read node data
|
||||
Title = TitleValue;
|
||||
Color = ColorValue;
|
||||
Size = SizeValue;
|
||||
}
|
||||
|
||||
private void EndResizing()
|
||||
{
|
||||
// Clear state
|
||||
_isResizing = false;
|
||||
|
||||
if (_startResizingSize != Size)
|
||||
{
|
||||
SizeValue = Size;
|
||||
Surface.MarkAsEdited(false);
|
||||
}
|
||||
|
||||
EndMouseCapture();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanSelect(ref Float2 location)
|
||||
{
|
||||
return _headerRect.MakeOffsetted(Location).Contains(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -158,6 +122,8 @@ namespace FlaxEditor.Surface
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateRectangles()
|
||||
{
|
||||
base.UpdateRectangles();
|
||||
|
||||
const float headerSize = Constants.NodeHeaderHeight;
|
||||
const float buttonMargin = Constants.NodeCloseButtonMargin;
|
||||
const float buttonSize = Constants.NodeCloseButtonSize;
|
||||
@@ -222,16 +188,13 @@ namespace FlaxEditor.Surface
|
||||
// Color button
|
||||
Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
|
||||
// Check if is resizing
|
||||
// Resize button
|
||||
if (_isResizing)
|
||||
{
|
||||
// Draw overlay
|
||||
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
|
||||
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
|
||||
}
|
||||
|
||||
// Resize button
|
||||
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey);
|
||||
}
|
||||
|
||||
// Selection outline
|
||||
@@ -247,88 +210,28 @@ namespace FlaxEditor.Surface
|
||||
/// <inheritdoc />
|
||||
protected override Float2 CalculateNodeSize(float width, float height)
|
||||
{
|
||||
return Size;
|
||||
// No margins or headers
|
||||
return new Float2(width, height);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
// Check if was resizing
|
||||
if (_isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
}
|
||||
|
||||
// Check if was renaming
|
||||
if (_isRenaming)
|
||||
{
|
||||
Rename(_renameTextBox.Text);
|
||||
StopRenaming();
|
||||
}
|
||||
|
||||
// Base
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnEndMouseCapture()
|
||||
{
|
||||
// Check if was resizing
|
||||
if (_isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnEndMouseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool ContainsPoint(ref Float2 location, bool precise)
|
||||
{
|
||||
return _headerRect.Contains(ref location) || _resizeButtonRect.Contains(ref location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
if (base.OnMouseDown(location, button))
|
||||
return true;
|
||||
|
||||
// Check if can start resizing
|
||||
if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit)
|
||||
{
|
||||
// Start sliding
|
||||
_isResizing = true;
|
||||
_startResizingSize = Size;
|
||||
StartMouseCapture();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseMove(Float2 location)
|
||||
{
|
||||
// Check if is resizing
|
||||
if (_isResizing)
|
||||
{
|
||||
// Update size
|
||||
var size = Float2.Max(location, new Float2(140.0f, _headerRect.Bottom));
|
||||
if (Surface.GridSnappingEnabled)
|
||||
size = Surface.SnapToGrid(size, true);
|
||||
Size = size;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Base
|
||||
base.OnMouseMove(location);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
|
||||
{
|
||||
@@ -394,12 +297,6 @@ namespace FlaxEditor.Surface
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseUp(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && _isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (base.OnMouseUp(location, button))
|
||||
return true;
|
||||
|
||||
|
||||
@@ -724,7 +724,12 @@ namespace FlaxEditor.Surface
|
||||
|
||||
if (HasNodesSelection)
|
||||
{
|
||||
var keyMoveRange = 50;
|
||||
var keyMoveDelta = 50;
|
||||
bool altDown = RootWindow.GetKey(KeyboardKeys.Alt);
|
||||
bool shiftDown = RootWindow.GetKey(KeyboardKeys.Shift);
|
||||
if (altDown || shiftDown)
|
||||
keyMoveDelta = shiftDown ? 10 : 25;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case KeyboardKeys.Backspace:
|
||||
@@ -752,17 +757,18 @@ namespace FlaxEditor.Surface
|
||||
Box selectedBox = GetSelectedBox(SelectedNodes);
|
||||
if (selectedBox != null)
|
||||
{
|
||||
Box toSelect = (key == KeyboardKeys.ArrowUp) ? selectedBox?.ParentNode.GetPreviousBox(selectedBox) : selectedBox?.ParentNode.GetNextBox(selectedBox);
|
||||
if (toSelect != null && toSelect.IsOutput == selectedBox.IsOutput)
|
||||
{
|
||||
Select(toSelect.ParentNode);
|
||||
toSelect.ParentNode.SelectBox(toSelect);
|
||||
}
|
||||
int delta = key == KeyboardKeys.ArrowDown ? 1 : -1;
|
||||
List<Box> boxes = selectedBox.ParentNode.GetBoxes().FindAll(b => b.IsOutput == selectedBox.IsOutput);
|
||||
int selectedIndex = boxes.IndexOf(selectedBox);
|
||||
Box toSelect = boxes[Mathf.Wrap(selectedIndex + delta, 0, boxes.Count - 1)];
|
||||
|
||||
Select(toSelect.ParentNode);
|
||||
toSelect.ParentNode.SelectBox(toSelect);
|
||||
}
|
||||
else if (!IsMovingSelection && CanEdit)
|
||||
{
|
||||
// Move selected nodes
|
||||
var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveRange : keyMoveRange);
|
||||
var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveDelta : keyMoveDelta);
|
||||
MoveSelectedNodes(delta);
|
||||
}
|
||||
return true;
|
||||
@@ -775,12 +781,8 @@ namespace FlaxEditor.Surface
|
||||
if (selectedBox != null)
|
||||
{
|
||||
Box toSelect = null;
|
||||
if ((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput))
|
||||
if (((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) && selectedBox.HasAnyConnection)
|
||||
{
|
||||
if (_selectedConnectionIndex < 0 || _selectedConnectionIndex >= selectedBox.Connections.Count)
|
||||
{
|
||||
_selectedConnectionIndex = 0;
|
||||
}
|
||||
toSelect = selectedBox.Connections[_selectedConnectionIndex];
|
||||
}
|
||||
else
|
||||
@@ -808,7 +810,7 @@ namespace FlaxEditor.Surface
|
||||
else if (!IsMovingSelection && CanEdit)
|
||||
{
|
||||
// Move selected nodes
|
||||
var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveRange : keyMoveRange, 0);
|
||||
var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveDelta : keyMoveDelta, 0);
|
||||
MoveSelectedNodes(delta);
|
||||
}
|
||||
return true;
|
||||
@@ -824,13 +826,9 @@ namespace FlaxEditor.Surface
|
||||
return true;
|
||||
|
||||
if (Root.GetKey(KeyboardKeys.Shift))
|
||||
{
|
||||
_selectedConnectionIndex = ((_selectedConnectionIndex - 1) % connectionCount + connectionCount) % connectionCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedConnectionIndex = (_selectedConnectionIndex + 1) % connectionCount;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
|
||||
{
|
||||
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ namespace FlaxEditor.Tools.Terrain
|
||||
{
|
||||
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
|
||||
{
|
||||
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ namespace FlaxEditor.Tools.Terrain.Undo
|
||||
if (_navmeshBoundsModifications != null)
|
||||
{
|
||||
foreach (var bounds in _navmeshBoundsModifications)
|
||||
Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs);
|
||||
}
|
||||
|
||||
Editor.Instance.Scene.MarkSceneEdited(scene);
|
||||
@@ -217,11 +217,10 @@ namespace FlaxEditor.Tools.Terrain.Undo
|
||||
}
|
||||
|
||||
// Update navmesh
|
||||
var scene = Terrain.Scene;
|
||||
if (_navmeshBoundsModifications != null)
|
||||
{
|
||||
foreach (var bounds in _navmeshBoundsModifications)
|
||||
Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs);
|
||||
}
|
||||
|
||||
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);
|
||||
|
||||
@@ -303,7 +303,7 @@ namespace FlaxEditor.Actions
|
||||
if (_nodeParents[i] is ActorNode node && node.Actor && node.Actor.Scene && node.AffectsNavigationWithChildren)
|
||||
{
|
||||
var bounds = node.Actor.BoxWithChildren;
|
||||
Navigation.BuildNavMesh(node.Actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,8 @@ namespace FlaxEditor.Actions
|
||||
var obj = Object.Find<SceneObject>(ref item.ID);
|
||||
if (obj != null)
|
||||
{
|
||||
scenes.Add(obj.Parent.Scene);
|
||||
if (obj.Parent != null)
|
||||
scenes.Add(obj.Parent.Scene);
|
||||
if (obj is Actor actor)
|
||||
actor.SetParent(newParent, _worldPositionsStays, true);
|
||||
else
|
||||
|
||||
@@ -121,12 +121,12 @@ namespace FlaxEditor
|
||||
// Handle simple case where objects were moved just a little and use one navmesh build request to improve performance
|
||||
if (data.BeforeBounds.Intersects(ref data.AfterBounds))
|
||||
{
|
||||
Navigation.BuildNavMesh(data.Scene, BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
Navigation.BuildNavMesh(data.Scene, data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(data.Scene, data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
Navigation.BuildNavMesh(data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,6 +444,9 @@ namespace FlaxEditor.Utilities
|
||||
/// <returns>The result value.</returns>
|
||||
public static double Parse(string text)
|
||||
{
|
||||
// Hack to allow parsing numbers while using "_" as a separator (like this: 1_000)
|
||||
text = text.Replace("_", string.Empty);
|
||||
|
||||
var tokens = Tokenize(text);
|
||||
var rpn = OrderTokens(tokens);
|
||||
return EvaluateRPN(rpn);
|
||||
|
||||
@@ -584,7 +584,7 @@ namespace FlaxEditor.Viewport
|
||||
_cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), _editor.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth)
|
||||
{
|
||||
Tag = this,
|
||||
TooltipText = "Camera Settings",
|
||||
TooltipText = "Camera Settings.",
|
||||
Parent = _cameraWidget
|
||||
};
|
||||
_cameraWidget.Parent = this;
|
||||
@@ -593,7 +593,7 @@ namespace FlaxEditor.Viewport
|
||||
_orthographicModeButton = new ViewportWidgetButton(string.Empty, _editor.Icons.CamSpeed32, null, true)
|
||||
{
|
||||
Checked = !_isOrtho,
|
||||
TooltipText = "Toggle Orthographic/Perspective Mode",
|
||||
TooltipText = "Toggle Orthographic/Perspective Mode.",
|
||||
Parent = _cameraWidget
|
||||
};
|
||||
_orthographicModeButton.Toggled += OnOrthographicModeToggled;
|
||||
@@ -832,7 +832,7 @@ namespace FlaxEditor.Viewport
|
||||
ViewWidgetButtonMenu = new ContextMenu();
|
||||
var viewModeButton = new ViewportWidgetButton("View", SpriteHandle.Invalid, ViewWidgetButtonMenu)
|
||||
{
|
||||
TooltipText = "View properties",
|
||||
TooltipText = "View properties.",
|
||||
Parent = viewMode
|
||||
};
|
||||
viewMode.Parent = this;
|
||||
@@ -863,8 +863,10 @@ namespace FlaxEditor.Viewport
|
||||
{
|
||||
}
|
||||
});
|
||||
viewLayers.AddButton("Reset layers", () => Task.ViewLayersMask = LayersMask.Default).Icon = _editor.Icons.Rotate32;
|
||||
viewLayers.AddButton("Disable layers", () => Task.ViewLayersMask = new LayersMask(0));
|
||||
viewLayers.AddButton("Reset layers", () => Task.ViewLayersMask = LayersMask.Default).Icon = Editor.Instance.Icons.Rotate32;
|
||||
viewLayers.AddSeparator();
|
||||
viewLayers.AddButton("Enable all", () => Task.ViewLayersMask = new LayersMask(-1)).Icon = Editor.Instance.Icons.CheckBoxTick12;
|
||||
viewLayers.AddButton("Disable all", () => Task.ViewLayersMask = new LayersMask(0)).Icon = Editor.Instance.Icons.Cross12;
|
||||
viewLayers.AddSeparator();
|
||||
var layers = LayersAndTagsSettings.GetCurrentLayers();
|
||||
if (layers != null && layers.Length > 0)
|
||||
@@ -904,8 +906,10 @@ namespace FlaxEditor.Viewport
|
||||
{
|
||||
}
|
||||
});
|
||||
viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = _editor.Icons.Rotate32;
|
||||
viewFlags.AddButton("Disable flags", () => Task.ViewFlags = ViewFlags.None);
|
||||
viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = Editor.Instance.Icons.Rotate32;
|
||||
viewFlags.AddSeparator();
|
||||
viewFlags.AddButton("Enable all", () => Task.ViewFlags = ViewFlags.All).Icon = Editor.Instance.Icons.CheckBoxTick12;
|
||||
viewFlags.AddButton("Disable all", () => Task.ViewFlags = ViewFlags.None).Icon = Editor.Instance.Icons.Cross12;
|
||||
viewFlags.AddSeparator();
|
||||
for (int i = 0; i < ViewFlagsValues.Length; i++)
|
||||
{
|
||||
@@ -1063,6 +1067,7 @@ namespace FlaxEditor.Viewport
|
||||
InputActions.Add(options => options.Fog, () => Task.ViewFlags ^= ViewFlags.Fog);
|
||||
InputActions.Add(options => options.SpecularLight, () => Task.ViewFlags ^= ViewFlags.SpecularLight);
|
||||
InputActions.Add(options => options.Decals, () => Task.ViewFlags ^= ViewFlags.Decals);
|
||||
InputActions.Add(options => options.Particles, () => Task.ViewFlags ^= ViewFlags.Particles);
|
||||
InputActions.Add(options => options.CustomPostProcess, () => Task.ViewFlags ^= ViewFlags.CustomPostProcess);
|
||||
InputActions.Add(options => options.Bloom, () => Task.ViewFlags ^= ViewFlags.Bloom);
|
||||
InputActions.Add(options => options.ToneMapping, () => Task.ViewFlags ^= ViewFlags.ToneMapping);
|
||||
@@ -2115,6 +2120,7 @@ namespace FlaxEditor.Viewport
|
||||
new ViewFlagOptions(ViewFlags.Fog, "Fog", Editor.Instance.Options.Options.Input.Fog),
|
||||
new ViewFlagOptions(ViewFlags.SpecularLight, "Specular Light", Editor.Instance.Options.Options.Input.SpecularLight),
|
||||
new ViewFlagOptions(ViewFlags.Decals, "Decals", Editor.Instance.Options.Options.Input.Decals),
|
||||
new ViewFlagOptions(ViewFlags.Particles, "Particles", Editor.Instance.Options.Options.Input.Particles),
|
||||
new ViewFlagOptions(ViewFlags.CustomPostProcess, "Custom Post Process", Editor.Instance.Options.Options.Input.CustomPostProcess),
|
||||
new ViewFlagOptions(ViewFlags.Bloom, "Bloom", Editor.Instance.Options.Options.Input.Bloom),
|
||||
new ViewFlagOptions(ViewFlags.ToneMapping, "Tone Mapping", Editor.Instance.Options.Options.Input.ToneMapping),
|
||||
@@ -2134,12 +2140,13 @@ namespace FlaxEditor.Viewport
|
||||
if (cm.Visible == false)
|
||||
return;
|
||||
var ccm = (ContextMenu)cm;
|
||||
var flags = Task.View.Flags;
|
||||
foreach (var e in ccm.Items)
|
||||
{
|
||||
if (e is ContextMenuButton b && b.Tag != null)
|
||||
{
|
||||
var v = (ViewFlags)b.Tag;
|
||||
b.Icon = (Task.View.Flags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
||||
b.Icon = (flags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace FlaxEditor.Viewport
|
||||
private readonly Editor _editor;
|
||||
private readonly ContextMenuButton _showGridButton;
|
||||
private readonly ContextMenuButton _showNavigationButton;
|
||||
private readonly ContextMenuButton _toggleGameViewButton;
|
||||
private SelectionOutline _customSelectionOutline;
|
||||
|
||||
/// <summary>
|
||||
@@ -108,6 +109,13 @@ namespace FlaxEditor.Viewport
|
||||
private EditorSpritesRenderer _editorSpritesRenderer;
|
||||
private ViewportRubberBandSelector _rubberBandSelector;
|
||||
|
||||
private bool _gameViewActive;
|
||||
private ViewFlags _preGameViewFlags;
|
||||
private ViewMode _preGameViewViewMode;
|
||||
private bool _gameViewWasGridShown;
|
||||
private bool _gameViewWasFpsCounterShown;
|
||||
private bool _gameViewWasNagivationShown;
|
||||
|
||||
/// <summary>
|
||||
/// Drag and drop handlers
|
||||
/// </summary>
|
||||
@@ -185,6 +193,7 @@ namespace FlaxEditor.Viewport
|
||||
: base(Object.New<SceneRenderTask>(), editor.Undo, editor.Scene.Root)
|
||||
{
|
||||
_editor = editor;
|
||||
var inputOptions = _editor.Options.Options.Input;
|
||||
DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem);
|
||||
|
||||
// Prepare rendering task
|
||||
@@ -232,9 +241,14 @@ namespace FlaxEditor.Viewport
|
||||
_showGridButton.CloseMenuOnClick = false;
|
||||
|
||||
// Show navigation widget
|
||||
_showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", () => ShowNavigation = !ShowNavigation);
|
||||
_showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", inputOptions.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation);
|
||||
_showNavigationButton.CloseMenuOnClick = false;
|
||||
|
||||
// Game View
|
||||
ViewWidgetButtonMenu.AddSeparator();
|
||||
_toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView);
|
||||
_toggleGameViewButton.CloseMenuOnClick = false;
|
||||
|
||||
// Create camera widget
|
||||
ViewWidgetButtonMenu.AddSeparator();
|
||||
ViewWidgetButtonMenu.AddButton("Create camera here", CreateCameraAtView);
|
||||
@@ -259,6 +273,10 @@ namespace FlaxEditor.Viewport
|
||||
InputActions.Add(options => options.FocusSelection, FocusSelection);
|
||||
InputActions.Add(options => options.RotateSelection, RotateSelection);
|
||||
InputActions.Add(options => options.Delete, _editor.SceneEditing.Delete);
|
||||
InputActions.Add(options => options.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation);
|
||||
|
||||
// Game View
|
||||
InputActions.Add(options => options.ToggleGameView, ToggleGameView);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -373,9 +391,12 @@ namespace FlaxEditor.Viewport
|
||||
public void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth)
|
||||
{
|
||||
// Draw gizmos
|
||||
for (int i = 0; i < Gizmos.Count; i++)
|
||||
foreach (var gizmo in Gizmos)
|
||||
{
|
||||
Gizmos[i].Draw(ref renderContext);
|
||||
if (gizmo.Visible)
|
||||
{
|
||||
gizmo.Draw(ref renderContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw selected objects debug shapes and visuals
|
||||
@@ -481,6 +502,36 @@ namespace FlaxEditor.Viewport
|
||||
TransformGizmo.EndTransforming();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles game view view mode on or off.
|
||||
/// </summary>
|
||||
public void ToggleGameView()
|
||||
{
|
||||
if (!_gameViewActive)
|
||||
{
|
||||
// Cache flags & values
|
||||
_preGameViewFlags = Task.ViewFlags;
|
||||
_preGameViewViewMode = Task.ViewMode;
|
||||
_gameViewWasGridShown = Grid.Enabled;
|
||||
_gameViewWasFpsCounterShown = ShowFpsCounter;
|
||||
_gameViewWasNagivationShown = ShowNavigation;
|
||||
}
|
||||
|
||||
// Set flags & values
|
||||
Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame;
|
||||
Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default;
|
||||
ShowFpsCounter = _gameViewActive ? _gameViewWasFpsCounterShown : false;
|
||||
ShowNavigation = _gameViewActive ? _gameViewWasNagivationShown : false;
|
||||
Grid.Enabled = _gameViewActive ? _gameViewWasGridShown : false;
|
||||
|
||||
_gameViewActive = !_gameViewActive;
|
||||
|
||||
TransformGizmo.Visible = !_gameViewActive;
|
||||
SelectionOutline.ShowSelectionOutline = !_gameViewActive;
|
||||
|
||||
_toggleGameViewButton.Icon = _gameViewActive ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
@@ -620,7 +671,7 @@ namespace FlaxEditor.Viewport
|
||||
{
|
||||
base.OnLeftMouseButtonDown();
|
||||
|
||||
_rubberBandSelector.TryStartingRubberBandSelection();
|
||||
_rubberBandSelector.TryStartingRubberBandSelection(_viewMousePos);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -720,9 +720,12 @@ namespace FlaxEditor.Viewport
|
||||
public override void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth)
|
||||
{
|
||||
// Draw gizmos
|
||||
for (int i = 0; i < Gizmos.Count; i++)
|
||||
foreach (var gizmo in Gizmos)
|
||||
{
|
||||
Gizmos[i].Draw(ref renderContext);
|
||||
if (gizmo.Visible)
|
||||
{
|
||||
gizmo.Draw(ref renderContext);
|
||||
}
|
||||
}
|
||||
|
||||
base.DrawEditorPrimitives(context, ref renderContext, target, targetDepth);
|
||||
|
||||
@@ -99,7 +99,14 @@ namespace FlaxEditor.Windows.Assets
|
||||
Window = window;
|
||||
var surfaceParam = window.Surface.GetParameter(BaseModelId);
|
||||
if (surfaceParam != null)
|
||||
BaseModel = FlaxEngine.Content.LoadAsync<SkinnedModel>((Guid)surfaceParam.Value);
|
||||
{
|
||||
if (surfaceParam.Value is Guid asGuid)
|
||||
BaseModel = FlaxEngine.Content.LoadAsync<SkinnedModel>(asGuid);
|
||||
else if (surfaceParam.Value is SkinnedModel asModel)
|
||||
BaseModel = asModel;
|
||||
else
|
||||
BaseModel = null;
|
||||
}
|
||||
else
|
||||
BaseModel = window.PreviewActor.GetParameterValue(BaseModelId) as SkinnedModel;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
@@ -25,7 +24,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
/// </summary>
|
||||
/// <seealso cref="Animation" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class AnimationWindow : AssetEditorWindowBase<Animation>
|
||||
public sealed class AnimationWindow : ClonedAssetEditorWindowBase<Animation>
|
||||
{
|
||||
private sealed class Preview : AnimationPreview
|
||||
{
|
||||
@@ -255,6 +254,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
private bool _isWaitingForTimelineLoad;
|
||||
private SkinnedModel _initialPreviewModel, _initialBaseModel;
|
||||
private float _initialPanel2Splitter = 0.6f;
|
||||
private bool _timelineIsDirty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the animation timeline editor.
|
||||
@@ -295,7 +295,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
Parent = _panel1.Panel1,
|
||||
Enabled = false
|
||||
};
|
||||
_timeline.Modified += MarkAsEdited;
|
||||
_timeline.Modified += OnTimelineModified;
|
||||
_timeline.SetNoTracksText("Loading...");
|
||||
|
||||
// Asset properties
|
||||
@@ -321,11 +321,31 @@ namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
MarkAsEdited();
|
||||
UpdateToolstrip();
|
||||
_propertiesPresenter.BuildLayout();
|
||||
}
|
||||
|
||||
private void OnTimelineModified()
|
||||
{
|
||||
_timelineIsDirty = true;
|
||||
MarkAsEdited();
|
||||
}
|
||||
|
||||
private bool RefreshTempAsset()
|
||||
{
|
||||
if (_asset == null || _isWaitingForTimelineLoad)
|
||||
return true;
|
||||
if (_timeline.IsModified)
|
||||
{
|
||||
_timeline.Save(_asset);
|
||||
}
|
||||
_propertiesPresenter.BuildLayoutOnUpdate();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetPreviewModelCacheName()
|
||||
{
|
||||
return _asset.ID + ".PreviewModel";
|
||||
return _item.ID + ".PreviewModel";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -361,7 +381,11 @@ namespace FlaxEditor.Windows.Assets
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
_timeline.Save(_asset);
|
||||
if (RefreshTempAsset())
|
||||
return;
|
||||
if (SaveToOriginal())
|
||||
return;
|
||||
|
||||
ClearEditedFlag();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
@@ -414,10 +438,18 @@ namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
// Check if temporary asset need to be updated
|
||||
if (_timelineIsDirty)
|
||||
{
|
||||
_timelineIsDirty = false;
|
||||
RefreshTempAsset();
|
||||
}
|
||||
|
||||
// Check if need to load timeline
|
||||
if (_isWaitingForTimelineLoad && _asset.IsLoaded)
|
||||
{
|
||||
_isWaitingForTimelineLoad = false;
|
||||
_timeline._id = _asset.ID;
|
||||
_timeline._id = _item.ID;
|
||||
_timeline.Load(_asset);
|
||||
_undo.Clear();
|
||||
_timeline.Enabled = true;
|
||||
|
||||
@@ -70,6 +70,13 @@ namespace FlaxEditor.Windows.Assets
|
||||
return;
|
||||
var nodes = proxy.Asset.Nodes;
|
||||
var bones = proxy.Asset.Bones;
|
||||
var blendShapes = proxy.Asset.BlendShapes;
|
||||
|
||||
// Info
|
||||
{
|
||||
var group = layout.Group("Info");
|
||||
group.Label($"Nodes: {nodes.Length}\nBones: {bones.Length}\nBlend Shapes: {blendShapes.Length}").AddCopyContextMenu().Label.Height *= 2.5f;
|
||||
}
|
||||
|
||||
// Skeleton Bones
|
||||
{
|
||||
@@ -109,7 +116,6 @@ namespace FlaxEditor.Windows.Assets
|
||||
}
|
||||
|
||||
// Blend Shapes
|
||||
var blendShapes = proxy.Asset.BlendShapes;
|
||||
if (blendShapes.Length != 0)
|
||||
{
|
||||
var group = layout.Group("Blend Shapes");
|
||||
|
||||
@@ -429,6 +429,7 @@ namespace FlaxEditor.Windows
|
||||
writer.WriteAttributeString("FarPlane", Viewport.FarPlane.ToString());
|
||||
writer.WriteAttributeString("FieldOfView", Viewport.FieldOfView.ToString());
|
||||
writer.WriteAttributeString("MovementSpeed", Viewport.MovementSpeed.ToString());
|
||||
writer.WriteAttributeString("ViewportIconsScale", ViewportIconsRenderer.Scale.ToString());
|
||||
writer.WriteAttributeString("OrthographicScale", Viewport.OrthographicScale.ToString());
|
||||
writer.WriteAttributeString("UseOrthographicProjection", Viewport.UseOrthographicProjection.ToString());
|
||||
writer.WriteAttributeString("ViewFlags", ((ulong)Viewport.Task.View.Flags).ToString());
|
||||
@@ -439,31 +440,24 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
if (bool.TryParse(node.GetAttribute("GridEnabled"), out bool value1))
|
||||
Viewport.Grid.Enabled = value1;
|
||||
|
||||
if (bool.TryParse(node.GetAttribute("ShowFpsCounter"), out value1))
|
||||
Viewport.ShowFpsCounter = value1;
|
||||
|
||||
if (bool.TryParse(node.GetAttribute("ShowNavigation"), out value1))
|
||||
Viewport.ShowNavigation = value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("NearPlane"), out float value2))
|
||||
Viewport.NearPlane = value2;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("FarPlane"), out value2))
|
||||
Viewport.FarPlane = value2;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("FieldOfView"), out value2))
|
||||
Viewport.FieldOfView = value2;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("MovementSpeed"), out value2))
|
||||
Viewport.MovementSpeed = value2;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("ViewportIconsScale"), out value2))
|
||||
ViewportIconsRenderer.Scale = value2;
|
||||
if (float.TryParse(node.GetAttribute("OrthographicScale"), out value2))
|
||||
Viewport.OrthographicScale = value2;
|
||||
|
||||
if (bool.TryParse(node.GetAttribute("UseOrthographicProjection"), out value1))
|
||||
Viewport.UseOrthographicProjection = value1;
|
||||
|
||||
if (ulong.TryParse(node.GetAttribute("ViewFlags"), out ulong value3))
|
||||
Viewport.Task.ViewFlags = (ViewFlags)value3;
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
Parent = this
|
||||
};
|
||||
_saveButton = (ToolStripButton)toolstrip.AddButton(editor.Icons.Save64, SaveData).LinkTooltip("Save");
|
||||
_saveButton = (ToolStripButton)toolstrip.AddButton(editor.Icons.Save64, SaveData).LinkTooltip("Save.");
|
||||
_saveButton.Enabled = false;
|
||||
|
||||
_tabs = new Tabs
|
||||
@@ -104,6 +104,8 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
_saveButton.Enabled = true;
|
||||
_isDataDirty = true;
|
||||
if (!Title.EndsWith('*'))
|
||||
Title += "*";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +115,8 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
_saveButton.Enabled = false;
|
||||
_isDataDirty = false;
|
||||
if (Title.EndsWith('*'))
|
||||
Title = Title.Remove(Title.Length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -336,11 +336,13 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
|
||||
SkeletonData* animResultSkeleton = &skeleton;
|
||||
|
||||
// Retarget animation when using output pose from other skeleton
|
||||
AnimGraphImpulse retargetNodes;
|
||||
if (_graph.BaseModel != data.NodesSkeleton)
|
||||
{
|
||||
ANIM_GRAPH_PROFILE_EVENT("Retarget");
|
||||
auto& targetSkeleton = data.NodesSkeleton->Skeleton;
|
||||
if (context.PoseCacheSize == context.PoseCache.Count())
|
||||
context.PoseCache.AddOne();
|
||||
auto& retargetNodes = context.PoseCache[context.PoseCacheSize++];
|
||||
retargetNodes = *animResult;
|
||||
retargetNodes.Nodes.Resize(targetSkeleton.Nodes.Count());
|
||||
Transform* targetNodes = retargetNodes.Nodes.Get();
|
||||
|
||||
@@ -109,86 +109,84 @@ namespace
|
||||
nodes->RootMotion.Orientation.Normalize();
|
||||
}
|
||||
}
|
||||
|
||||
Matrix ComputeWorldMatrixRecursive(const SkeletonData& skeleton, int32 index, Matrix localMatrix)
|
||||
{
|
||||
const auto& node = skeleton.Nodes[index];
|
||||
index = node.ParentIndex;
|
||||
while (index != -1)
|
||||
{
|
||||
const auto& parent = skeleton.Nodes[index];
|
||||
localMatrix *= parent.LocalTransform.GetWorld();
|
||||
index = parent.ParentIndex;
|
||||
}
|
||||
return localMatrix;
|
||||
}
|
||||
|
||||
Matrix ComputeInverseParentMatrixRecursive(const SkeletonData& skeleton, int32 index)
|
||||
{
|
||||
Matrix inverseParentMatrix = Matrix::Identity;
|
||||
const auto& node = skeleton.Nodes[index];
|
||||
if (node.ParentIndex != -1)
|
||||
{
|
||||
inverseParentMatrix = ComputeWorldMatrixRecursive(skeleton, index, inverseParentMatrix);
|
||||
inverseParentMatrix = Matrix::Invert(inverseParentMatrix);
|
||||
}
|
||||
return inverseParentMatrix;
|
||||
}
|
||||
}
|
||||
|
||||
void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 targetIndex)
|
||||
// Utility for retargeting animation poses between skeletons.
|
||||
struct Retargeting
|
||||
{
|
||||
// sourceSkeleton - skeleton of Anim Graph (Base Locomotion pack)
|
||||
// targetSkeleton - visual mesh skeleton (City Characters pack)
|
||||
// target - anim graph input/output transformation of that node
|
||||
const auto& targetNode = targetSkeleton.Nodes[targetIndex];
|
||||
const int32 sourceIndex = sourceMapping.NodesMapping[targetIndex];
|
||||
if (sourceIndex == -1)
|
||||
private:
|
||||
const Matrix* _sourcePosePtr, * _targetPosePtr;
|
||||
const SkeletonData* _sourceSkeleton, *_targetSkeleton;
|
||||
const SkinnedModel::SkeletonMapping* _sourceMapping;
|
||||
|
||||
public:
|
||||
void Init(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping)
|
||||
{
|
||||
// Use T-pose
|
||||
node = targetNode.LocalTransform;
|
||||
return;
|
||||
ASSERT_LOW_LAYER(targetSkeleton.Nodes.Count() == sourceMapping.NodesMapping.Length());
|
||||
|
||||
// Cache world-space poses for source and target skeletons to avoid redundant calculations during retargeting
|
||||
_sourcePosePtr = sourceSkeleton.GetNodesPose().Get();
|
||||
_targetPosePtr = targetSkeleton.GetNodesPose().Get();
|
||||
|
||||
_sourceSkeleton = &sourceSkeleton;
|
||||
_targetSkeleton = &targetSkeleton;
|
||||
_sourceMapping = &sourceMapping;
|
||||
}
|
||||
const auto& sourceNode = sourceSkeleton.Nodes[sourceIndex];
|
||||
|
||||
// [Reference: https://wickedengine.net/2022/09/animation-retargeting/comment-page-1/]
|
||||
|
||||
// Calculate T-Pose of source node, target node and target parent node
|
||||
Matrix bindMatrix = ComputeWorldMatrixRecursive(sourceSkeleton, sourceIndex, sourceNode.LocalTransform.GetWorld());
|
||||
Matrix inverseBindMatrix = Matrix::Invert(bindMatrix);
|
||||
Matrix targetMatrix = ComputeWorldMatrixRecursive(targetSkeleton, targetIndex, targetNode.LocalTransform.GetWorld());
|
||||
Matrix inverseParentMatrix = ComputeInverseParentMatrixRecursive(targetSkeleton, targetIndex);
|
||||
|
||||
// Target node animation is world-space difference of the animated source node inside the target's parent node world-space
|
||||
Matrix localMatrix = inverseBindMatrix * ComputeWorldMatrixRecursive(sourceSkeleton, sourceIndex, node.GetWorld());
|
||||
localMatrix = targetMatrix * localMatrix * inverseParentMatrix;
|
||||
|
||||
// Extract local node transformation
|
||||
localMatrix.Decompose(node);
|
||||
}
|
||||
|
||||
void RetargetSkeletonPose(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, const Transform* sourceNodes, Transform* targetNodes)
|
||||
{
|
||||
// TODO: cache source and target skeletons world-space poses for faster retargeting (use some pooled memory)
|
||||
ASSERT_LOW_LAYER(targetSkeleton.Nodes.Count() == mapping.NodesMapping.Length());
|
||||
for (int32 targetIndex = 0; targetIndex < targetSkeleton.Nodes.Count(); targetIndex++)
|
||||
void RetargetNode(const Transform& source, Transform& target, int32 sourceIndex, int32 targetIndex)
|
||||
{
|
||||
auto& targetNode = targetSkeleton.Nodes.Get()[targetIndex];
|
||||
const int32 sourceIndex = mapping.NodesMapping.Get()[targetIndex];
|
||||
Transform node;
|
||||
// sourceSkeleton - skeleton of Anim Graph
|
||||
// targetSkeleton - visual mesh skeleton
|
||||
// target - anim graph input/output transformation of that node
|
||||
const SkeletonNode& targetNode = _targetSkeleton->Nodes.Get()[targetIndex];
|
||||
if (sourceIndex == -1)
|
||||
{
|
||||
// Use T-pose
|
||||
node = targetNode.LocalTransform;
|
||||
target = targetNode.LocalTransform;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Retarget
|
||||
node = sourceNodes[sourceIndex];
|
||||
RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, targetIndex);
|
||||
// [Reference: https://wickedengine.net/2022/09/animation-retargeting/comment-page-1/]
|
||||
|
||||
// Calculate T-Pose of source node, target node and target parent node
|
||||
const Matrix* sourcePosePtr = _sourcePosePtr;
|
||||
const Matrix* targetPosePtr = _targetPosePtr;
|
||||
const Matrix& bindMatrix = sourcePosePtr[sourceIndex];
|
||||
const Matrix& targetMatrix = targetPosePtr[targetIndex];
|
||||
Matrix inverseParentMatrix;
|
||||
if (targetNode.ParentIndex != -1)
|
||||
Matrix::Invert(targetPosePtr[targetNode.ParentIndex], inverseParentMatrix);
|
||||
else
|
||||
inverseParentMatrix = Matrix::Identity;
|
||||
|
||||
// Target node animation is world-space difference of the animated source node inside the target's parent node world-space
|
||||
const SkeletonNode& sourceNode = _sourceSkeleton->Nodes.Get()[sourceIndex];
|
||||
Matrix localMatrix = source.GetWorld();
|
||||
if (sourceNode.ParentIndex != -1)
|
||||
localMatrix = localMatrix * sourcePosePtr[sourceNode.ParentIndex];
|
||||
localMatrix = Matrix::Invert(bindMatrix) * localMatrix;
|
||||
localMatrix = targetMatrix * localMatrix * inverseParentMatrix;
|
||||
|
||||
// Extract local node transformation
|
||||
localMatrix.Decompose(target);
|
||||
}
|
||||
targetNodes[targetIndex] = node;
|
||||
}
|
||||
|
||||
FORCE_INLINE void RetargetPose(const Transform* sourceNodes, Transform* targetNodes)
|
||||
{
|
||||
for (int32 targetIndex = 0; targetIndex < _targetSkeleton->Nodes.Count(); targetIndex++)
|
||||
{
|
||||
const int32 sourceIndex = _sourceMapping->NodesMapping.Get()[targetIndex];
|
||||
RetargetNode(sourceNodes[sourceIndex], targetNodes[targetIndex], sourceIndex, targetIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void RetargetSkeletonPose(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, const Transform* sourceNodes, Transform* targetNodes)
|
||||
{
|
||||
Retargeting retargeting;
|
||||
retargeting.Init(sourceSkeleton, targetSkeleton, mapping);
|
||||
retargeting.RetargetPose(sourceNodes, targetNodes);
|
||||
}
|
||||
|
||||
AnimGraphTraceEvent& AnimGraphContext::AddTraceEvent(const AnimGraphNode* node)
|
||||
@@ -431,9 +429,13 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
|
||||
const bool weighted = weight < 1.0f;
|
||||
const bool retarget = mapping.SourceSkeleton && mapping.SourceSkeleton != mapping.TargetSkeleton;
|
||||
const auto emptyNodes = GetEmptyNodes();
|
||||
Retargeting retargeting;
|
||||
SkinnedModel::SkeletonMapping sourceMapping;
|
||||
if (retarget)
|
||||
{
|
||||
sourceMapping = _graph.BaseModel->GetSkeletonMapping(mapping.SourceSkeleton);
|
||||
retargeting.Init(mapping.SourceSkeleton->Skeleton, mapping.TargetSkeleton->Skeleton, mapping);
|
||||
}
|
||||
for (int32 nodeIndex = 0; nodeIndex < nodes->Nodes.Count(); nodeIndex++)
|
||||
{
|
||||
const int32 nodeToChannel = mapping.NodesMapping[nodeIndex];
|
||||
@@ -447,7 +449,8 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
|
||||
// Optionally retarget animation into the skeleton used by the Anim Graph
|
||||
if (retarget)
|
||||
{
|
||||
RetargetSkeletonNode(mapping.SourceSkeleton->Skeleton, mapping.TargetSkeleton->Skeleton, sourceMapping, srcNode, nodeIndex);
|
||||
const int32 sourceIndex = sourceMapping.NodesMapping[nodeIndex];
|
||||
retargeting.RetargetNode(srcNode, srcNode, sourceIndex, nodeIndex);
|
||||
}
|
||||
|
||||
// Mark node as used
|
||||
@@ -958,6 +961,21 @@ void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& valu
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Set Parameter
|
||||
case 5:
|
||||
{
|
||||
// Set parameter value
|
||||
int32 paramIndex;
|
||||
const auto param = _graph.GetParameter((Guid)node->Values[0], paramIndex);
|
||||
if (param)
|
||||
{
|
||||
context.Data->Parameters[paramIndex].Value = tryGetValue(node->GetBox(1), 1, Value::Null);
|
||||
}
|
||||
|
||||
// Pass over the pose
|
||||
value = tryGetValue(node->GetBox(2), Value::Null);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -132,9 +132,7 @@ int32 AudioClip::GetFirstBufferIndex(float time, float& offset) const
|
||||
if (_buffersStartTimes[i + 1] > time)
|
||||
{
|
||||
offset = time - _buffersStartTimes[i];
|
||||
#if BUILD_DEBUG
|
||||
ASSERT(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f);
|
||||
#endif
|
||||
ASSERT_LOW_LAYER(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class AudioSource;
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API AudioClip : public BinaryAsset, public StreamableResource
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(AudioClip, 2);
|
||||
friend class AudioBackendOAL;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
|
||||
@@ -37,15 +37,17 @@ void AudioListener::OnEnable()
|
||||
{
|
||||
_prevPos = GetPosition();
|
||||
_velocity = Vector3::Zero;
|
||||
|
||||
ASSERT(!Audio::Listeners.Contains(this));
|
||||
if (Audio::Listeners.Count() >= AUDIO_MAX_LISTENERS)
|
||||
{
|
||||
LOG(Error, "Unsupported amount of the audio listeners!");
|
||||
if IF_CONSTEXPR (AUDIO_MAX_LISTENERS == 1)
|
||||
LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time.");
|
||||
else
|
||||
LOG(Warning, "Too many Audio Listener active.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(!Audio::Listeners.Contains(this));
|
||||
if (Audio::Listeners.Count() > 0)
|
||||
LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time.");
|
||||
Audio::Listeners.Add(this);
|
||||
AudioBackend::Listener::Reset();
|
||||
AudioBackend::Listener::TransformChanged(GetPosition(), GetOrientation());
|
||||
|
||||
@@ -207,16 +207,16 @@ public:
|
||||
API_PROPERTY() void SetAttenuation(float value);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the doppler effect factor. Scale for source velocity. Default is 1.
|
||||
/// Gets the doppler effect factor. Scale for source velocity. Default is 1. Used by spatial sources only. Cannot scale the effect up (only dim it).
|
||||
/// </summary>
|
||||
API_PROPERTY(Attributes="EditorOrder(75), DefaultValue(1.0f), Limit(0, float.MaxValue, 0.1f), EditorDisplay(\"Audio Source\")")
|
||||
API_PROPERTY(Attributes="EditorOrder(75), DefaultValue(1.0f), Limit(0, 1.0f, 0.1f), EditorDisplay(\"Audio Source\")")
|
||||
FORCE_INLINE float GetDopplerFactor() const
|
||||
{
|
||||
return _dopplerFactor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the doppler effect factor. Scale for source velocity. Default is 1.
|
||||
/// Sets the doppler effect factor. Scale for source velocity. Default is 1. Used by spatial sources only. Cannot scale the effect up (only dim it).
|
||||
/// </summary>
|
||||
API_PROPERTY() void SetDopplerFactor(float value);
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include "Engine/Audio/AudioListener.h"
|
||||
#include "Engine/Audio/AudioSource.h"
|
||||
#include "Engine/Audio/AudioSettings.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/Level/Level.h"
|
||||
#include "Engine/Video/VideoPlayer.h"
|
||||
|
||||
// Include OpenAL library
|
||||
// Source: https://github.com/kcat/openal-soft
|
||||
@@ -73,6 +76,7 @@ namespace ALC
|
||||
ALCdevice* Device = nullptr;
|
||||
ALCcontext* Context = nullptr;
|
||||
AudioBackend::FeatureFlags Features = AudioBackend::FeatureFlags::None;
|
||||
bool Inited = false;
|
||||
CriticalSection Locker;
|
||||
Dictionary<uint32, SourceData> SourcesData;
|
||||
|
||||
@@ -164,12 +168,9 @@ namespace ALC
|
||||
float Time;
|
||||
};
|
||||
|
||||
void RebuildContext(const Array<AudioSourceState>& states)
|
||||
void RebuildContext()
|
||||
{
|
||||
LOG(Info, "Rebuilding audio contexts");
|
||||
|
||||
ClearContext();
|
||||
|
||||
if (Device == nullptr)
|
||||
return;
|
||||
|
||||
@@ -182,10 +183,16 @@ namespace ALC
|
||||
|
||||
Context = alcCreateContext(Device, attrList);
|
||||
alcMakeContextCurrent(Context);
|
||||
|
||||
}
|
||||
|
||||
void RebuildListeners()
|
||||
{
|
||||
for (AudioListener* listener : Audio::Listeners)
|
||||
Listener::Rebuild(listener);
|
||||
|
||||
}
|
||||
|
||||
void RebuildSources(const Array<AudioSourceState>& states)
|
||||
{
|
||||
for (int32 i = 0; i < states.Count(); i++)
|
||||
{
|
||||
AudioSource* source = Audio::Sources[i];
|
||||
@@ -205,6 +212,13 @@ namespace ALC
|
||||
}
|
||||
}
|
||||
|
||||
void RebuildContext(const Array<AudioSourceState>& states)
|
||||
{
|
||||
RebuildContext();
|
||||
RebuildListeners();
|
||||
RebuildSources(states);
|
||||
}
|
||||
|
||||
void RebuildContext(bool isChangingDevice)
|
||||
{
|
||||
Array<AudioSourceState> states;
|
||||
@@ -400,7 +414,7 @@ void AudioBackendOAL::Source_IsLoopingChanged(uint32 sourceID, bool loop)
|
||||
void AudioBackendOAL::Source_SpatialSetupChanged(uint32 sourceID, bool spatial, float attenuation, float minDistance, float doppler)
|
||||
{
|
||||
ALC::Locker.Lock();
|
||||
const bool pan = ALC::SourcesData[sourceID].Spatial;
|
||||
const float pan = ALC::SourcesData[sourceID].Pan;
|
||||
ALC::Locker.Unlock();
|
||||
if (spatial)
|
||||
{
|
||||
@@ -629,6 +643,7 @@ AudioBackend::FeatureFlags AudioBackendOAL::Base_Features()
|
||||
|
||||
void AudioBackendOAL::Base_OnActiveDeviceChanged()
|
||||
{
|
||||
PROFILE_CPU();
|
||||
PROFILE_MEM(Audio);
|
||||
|
||||
// Cleanup
|
||||
@@ -659,9 +674,53 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged()
|
||||
LOG(Fatal, "Failed to open OpenAL device ({0}).", String(name));
|
||||
return;
|
||||
}
|
||||
if (ALC::Inited)
|
||||
LOG(Info, "Changed audio device to: {}", String(Audio::GetActiveDevice()->Name));
|
||||
|
||||
// Setup
|
||||
ALC::RebuildContext(states);
|
||||
// Rebuild context
|
||||
ALC::RebuildContext();
|
||||
if (ALC::Inited)
|
||||
{
|
||||
// Reload all audio clips to recreate their buffers
|
||||
for (AudioClip* audioClip : Content::GetAssets<AudioClip>())
|
||||
{
|
||||
audioClip->WaitForLoaded();
|
||||
ScopeLock lock(audioClip->Locker);
|
||||
|
||||
// Clear old buffer IDs
|
||||
for (uint32& bufferID : audioClip->Buffers)
|
||||
bufferID = 0;
|
||||
|
||||
if (audioClip->IsStreamable())
|
||||
{
|
||||
// Let the streaming recreate missing buffers
|
||||
audioClip->RequestStreamingUpdate();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reload audio clip
|
||||
auto assetLock = audioClip->Storage->Lock();
|
||||
audioClip->LoadChunk(0);
|
||||
audioClip->Buffers[0] = AudioBackend::Buffer::Create();
|
||||
audioClip->WriteBuffer(0);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Reload all videos to recreate their buffers
|
||||
for (VideoPlayer* videoPlayer : Level::GetActors<VideoPlayer>(true))
|
||||
{
|
||||
VideoBackendPlayer& player = videoPlayer->_player;
|
||||
|
||||
// Clear audio state
|
||||
for (uint32& bufferID : player.AudioBuffers)
|
||||
bufferID = 0;
|
||||
player.NextAudioBuffer = 0;
|
||||
player.AudioSource = 0;
|
||||
}
|
||||
}
|
||||
ALC::RebuildListeners();
|
||||
ALC::RebuildSources(states);
|
||||
}
|
||||
|
||||
void AudioBackendOAL::Base_SetDopplerFactor(float value)
|
||||
@@ -770,7 +829,6 @@ bool AudioBackendOAL::Base_Init()
|
||||
}
|
||||
|
||||
// Init
|
||||
Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor);
|
||||
alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Default attenuation model
|
||||
int32 clampedIndex = Math::Clamp(activeDeviceIndex, -1, Audio::Devices.Count() - 1);
|
||||
if (clampedIndex == Audio::GetActiveDeviceIndex())
|
||||
@@ -782,6 +840,8 @@ bool AudioBackendOAL::Base_Init()
|
||||
if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize"))
|
||||
ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::SpatialMultiChannel);
|
||||
#endif
|
||||
Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor);
|
||||
ALC::Inited = true;
|
||||
|
||||
// Log service info
|
||||
LOG(Info, "{0} ({1})", String(alGetString(AL_RENDERER)), String(alGetString(AL_VERSION)));
|
||||
|
||||
@@ -672,7 +672,7 @@ bool AudioBackendXAudio2::Base_Init()
|
||||
HRESULT hr = XAudio2Create(&XAudio2::Instance, 0, XAUDIO2_DEFAULT_PROCESSOR);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
LOG(Error, "Failed to initalize XAudio2. Error: 0x{0:x}", hr);
|
||||
LOG(Error, "Failed to initialize XAudio2. Error: 0x{0:x}", hr);
|
||||
return true;
|
||||
}
|
||||
XAudio2::Instance->RegisterForCallbacks(&XAudio2::Callback);
|
||||
@@ -681,7 +681,7 @@ bool AudioBackendXAudio2::Base_Init()
|
||||
hr = XAudio2::Instance->CreateMasteringVoice(&XAudio2::MasteringVoice);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
LOG(Error, "Failed to initalize XAudio2 mastering voice. Error: 0x{0:x}", hr);
|
||||
LOG(Error, "Failed to initialize XAudio2 mastering voice. Error: 0x{0:x}", hr);
|
||||
return true;
|
||||
}
|
||||
XAUDIO2_VOICE_DETAILS details;
|
||||
|
||||
@@ -487,6 +487,8 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const
|
||||
const auto loadingTask = (ContentLoadTask*)Platform::AtomicRead(&_loadingTask);
|
||||
if (loadingTask == nullptr)
|
||||
{
|
||||
if (IsLoaded())
|
||||
return false;
|
||||
LOG(Warning, "WaitForLoaded asset \'{0}\' failed. No loading task attached and asset is not loaded.", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,35 @@ bool Material::IsMaterialInstance() const
|
||||
return false;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
void Material::GetReferences(Array<Guid>& assets, Array<String>& files) const
|
||||
{
|
||||
ShaderAssetTypeBase<MaterialBase>::GetReferences(assets, files);
|
||||
|
||||
// Collect references from material graph (needs to load it)
|
||||
if (!WaitForLoaded() && HasChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE))
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
if (!LoadChunks(GET_CHUNK_FLAG(SHADER_FILE_CHUNK_VISJECT_SURFACE)))
|
||||
{
|
||||
const auto surfaceChunk = GetChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE);
|
||||
if (surfaceChunk)
|
||||
{
|
||||
MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
|
||||
MaterialGraph graph;
|
||||
if (!graph.Load(&stream, false))
|
||||
{
|
||||
graph.GetReferences(assets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
const MaterialInfo& Material::GetInfo() const
|
||||
{
|
||||
if (_materialShader)
|
||||
|
||||
@@ -38,6 +38,9 @@ public:
|
||||
public:
|
||||
// [MaterialBase]
|
||||
bool IsMaterialInstance() const override;
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& assets, Array<String>& files) const override;
|
||||
#endif
|
||||
|
||||
// [IMaterial]
|
||||
const MaterialInfo& GetInfo() const override;
|
||||
|
||||
@@ -61,16 +61,24 @@ Array<String> SkinnedModel::GetBlendShapes()
|
||||
|
||||
SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source, bool autoRetarget)
|
||||
{
|
||||
// Fast-path to use cached mapping
|
||||
SkeletonMapping mapping;
|
||||
mapping.TargetSkeleton = this;
|
||||
SkeletonMappingData mappingData;
|
||||
if (_skeletonMappingCache.TryGet(source, mappingData))
|
||||
{
|
||||
mapping.SourceSkeleton = mappingData.SourceSkeleton;
|
||||
mapping.NodesMapping = mappingData.NodesMapping;
|
||||
return mapping;
|
||||
}
|
||||
mapping.SourceSkeleton = nullptr;
|
||||
|
||||
if (WaitForLoaded() || !source || source->WaitForLoaded())
|
||||
return mapping;
|
||||
PROFILE_CPU();
|
||||
ScopeLock lock(Locker);
|
||||
SkeletonMappingData mappingData;
|
||||
if (!_skeletonMappingCache.TryGet(source, mappingData))
|
||||
{
|
||||
PROFILE_CPU();
|
||||
|
||||
// Initialize the mapping
|
||||
SkeletonRetarget* retarget = nullptr;
|
||||
const Guid sourceId = source->GetID();
|
||||
@@ -370,6 +378,7 @@ bool SkinnedModel::SetupSkeleton(const Array<SkeletonNode>& nodes)
|
||||
model->Skeleton.Bones[i].LocalTransform = node.LocalTransform;
|
||||
model->Skeleton.Bones[i].NodeIndex = i;
|
||||
}
|
||||
model->Skeleton.Dirty();
|
||||
ClearSkeletonMapping();
|
||||
|
||||
// Calculate offset matrix (inverse bind pose transform) for every bone manually
|
||||
@@ -427,6 +436,7 @@ bool SkinnedModel::SetupSkeleton(const Array<SkeletonNode>& nodes, const Array<S
|
||||
// Setup
|
||||
model->Skeleton.Nodes = nodes;
|
||||
model->Skeleton.Bones = bones;
|
||||
model->Skeleton.Dirty();
|
||||
ClearSkeletonMapping();
|
||||
|
||||
// Calculate offset matrix (inverse bind pose transform) for every bone manually
|
||||
@@ -823,13 +833,13 @@ bool SkinnedModel::SaveMesh(WriteStream& stream, const ModelData& modelData, int
|
||||
|
||||
void SkinnedModel::ClearSkeletonMapping()
|
||||
{
|
||||
for (auto& e : _skeletonMappingCache)
|
||||
for (const auto& e : _skeletonMappingCache)
|
||||
{
|
||||
e.Key->OnUnloaded.Unbind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
|
||||
#if USE_EDITOR
|
||||
e.Key->OnReloading.Unbind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
|
||||
#endif
|
||||
Allocator::Free(e.Value.NodesMapping.Get());
|
||||
Allocator::Free((void*)e.Value.NodesMapping.Get());
|
||||
}
|
||||
_skeletonMappingCache.Clear();
|
||||
}
|
||||
@@ -837,8 +847,9 @@ void SkinnedModel::ClearSkeletonMapping()
|
||||
void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj)
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
auto i = _skeletonMappingCache.Find(obj);
|
||||
ASSERT(i != _skeletonMappingCache.End());
|
||||
SkeletonMappingData mappingData;
|
||||
bool found = _skeletonMappingCache.TryGet(obj, mappingData);
|
||||
ASSERT(found);
|
||||
|
||||
// Unlink event
|
||||
obj->OnUnloaded.Unbind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
|
||||
@@ -847,8 +858,8 @@ void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj)
|
||||
#endif
|
||||
|
||||
// Clear cache
|
||||
Allocator::Free(i->Value.NodesMapping.Get());
|
||||
_skeletonMappingCache.Remove(i);
|
||||
Allocator::Free(mappingData.NodesMapping.Get());
|
||||
_skeletonMappingCache.Remove(obj);
|
||||
}
|
||||
|
||||
uint64 SkinnedModel::GetMemoryUsage() const
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModelBase.h"
|
||||
#include "Engine/Core/Collections/Dictionary.h"
|
||||
#include "Engine/Threading/ConcurrentDictionary.h"
|
||||
#include "Engine/Graphics/Models/SkinnedMesh.h"
|
||||
#include "Engine/Graphics/Models/SkeletonData.h"
|
||||
|
||||
@@ -101,9 +101,9 @@ public:
|
||||
struct FLAXENGINE_API SkeletonMapping
|
||||
{
|
||||
// Target skeleton.
|
||||
AssetReference<SkinnedModel> TargetSkeleton;
|
||||
SkinnedModel* TargetSkeleton;
|
||||
// Source skeleton.
|
||||
AssetReference<SkinnedModel> SourceSkeleton;
|
||||
SkinnedModel* SourceSkeleton;
|
||||
// The node-to-node mapping for the fast animation sampling for the skinned model skeleton nodes. Each item is index of the source skeleton node into target skeleton node.
|
||||
Span<int32> NodesMapping;
|
||||
};
|
||||
@@ -115,7 +115,7 @@ private:
|
||||
Span<int32> NodesMapping;
|
||||
};
|
||||
|
||||
Dictionary<Asset*, SkeletonMappingData> _skeletonMappingCache;
|
||||
ConcurrentDictionary<Asset*, SkeletonMappingData> _skeletonMappingCache;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
|
||||
@@ -1700,6 +1700,8 @@ void VisualScript::CacheScriptingType()
|
||||
VisualScriptingBinaryModule::VisualScriptingBinaryModule()
|
||||
: _name("Visual Scripting")
|
||||
{
|
||||
// Visual Scripts can be unloaded and loaded again even in game
|
||||
CanReload = true;
|
||||
}
|
||||
|
||||
ScriptingObject* VisualScriptingBinaryModule::VisualScriptObjectSpawn(const ScriptingObjectSpawnParams& params)
|
||||
|
||||
@@ -684,6 +684,19 @@ Array<Asset*> Content::GetAssets()
|
||||
return assets;
|
||||
}
|
||||
|
||||
Array<Asset*> Content::GetAssets(const MClass* type)
|
||||
{
|
||||
Array<Asset*> assets;
|
||||
AssetsLocker.Lock();
|
||||
for (auto& e : Assets)
|
||||
{
|
||||
if (e.Value->Is(type))
|
||||
assets.Add(e.Value);
|
||||
}
|
||||
AssetsLocker.Unlock();
|
||||
return assets;
|
||||
}
|
||||
|
||||
const Dictionary<Guid, Asset*>& Content::GetAssetsRaw()
|
||||
{
|
||||
AssetsLocker.Lock();
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Scripting/ScriptingType.h"
|
||||
#ifndef _MSC_VER
|
||||
#include "Engine/Core/Collections/Array.h"
|
||||
#endif
|
||||
#include "AssetInfo.h"
|
||||
#include "Asset.h"
|
||||
#include "Config.h"
|
||||
@@ -122,7 +125,26 @@ public:
|
||||
/// Gets the assets (loaded or during load).
|
||||
/// </summary>
|
||||
/// <returns>The collection of assets.</returns>
|
||||
static Array<Asset*, HeapAllocation> GetAssets();
|
||||
API_FUNCTION() static Array<Asset*, HeapAllocation> GetAssets();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assets (loaded or during load).
|
||||
/// </summary>
|
||||
/// <param name="type">Type of the assets to search for. Includes any assets derived from the type.</param>
|
||||
/// <returns>Found actors list.</returns>
|
||||
API_FUNCTION() static Array<Asset*, HeapAllocation> GetAssets(API_PARAM(Attributes="TypeReference(typeof(Actor))") const MClass* type);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assets (loaded or during load).
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the object.</typeparam>
|
||||
/// <returns>Found actors list.</returns>
|
||||
template<typename T>
|
||||
static Array<T*, HeapAllocation> GetAssets()
|
||||
{
|
||||
Array<Asset*, HeapAllocation> assets = GetAssets(T::GetStaticClass());
|
||||
return *(Array<T*, HeapAllocation>*) & assets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw dictionary of assets (loaded or during load).
|
||||
|
||||
@@ -146,6 +146,10 @@ bool BinaryAssetFactoryBase::UpgradeAsset(const AssetInfo& info, FlaxStorage* st
|
||||
context.Input = context.Output;
|
||||
} while (upgrader->ShouldUpgrade(context.Input.SerializedVersion));
|
||||
|
||||
// Prevent other threads from loading the storage when it is upgrading
|
||||
// It works because CriticalSection allows recursion
|
||||
ScopeLock upgradeLock(storage->_loadLocker);
|
||||
|
||||
// Release storage internal data (should also close file handles)
|
||||
{
|
||||
// HACK: file is locked by some tasks: the current one that called asset data upgrade (LoadAssetTask)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user