Merge remote-tracking branch 'origin/master' into 1.11

# Conflicts:
#	Content/Editor/DebugMaterials/DDGIDebugProbes.flax
#	Source/Engine/Scripting/Scripting.cpp
This commit is contained in:
Wojtek Figat
2025-08-25 23:48:08 +02:00
121 changed files with 2016 additions and 1305 deletions

BIN
Content/Editor/Camera/M_Camera.flax (Stored with Git LFS)

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/Editor/DefaultFontMaterial.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/Gizmo/Material.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Editor/Gizmo/MaterialWire.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/Highlight Material.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Editor/Icons/IconsMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Editor/IconsAtlas.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

View File

@@ -13,7 +13,7 @@
META_CB_BEGIN(0, Data)
float4x4 WorldMatrix;
float4x4 InvWorld;
float4x4 SVPositionToWorld;
float4x4 SvPositionToWorld;
@1META_CB_END
// Use depth buffer for per-pixel decal layering
@@ -27,12 +27,63 @@ struct MaterialInput
float3 WorldPosition;
float TwoSidedSign;
float2 TexCoord;
float4 TexCoord_DDX_DDY;
float3x3 TBN;
float4 SvPosition;
float3 PreSkinnedPosition;
float3 PreSkinnedNormal;
};
// Calculates decal texcoords for a given pixel position (sampels depth buffer and projects value to decal space).
float2 SvPositionToDecalUV(float4 svPosition)
{
float2 screenUV = svPosition.xy * ScreenSize.zw;
svPosition.z = SAMPLE_RT(DepthBuffer, screenUV).r;
float4 positionHS = mul(float4(svPosition.xyz, 1), SvPositionToWorld);
float3 positionWS = positionHS.xyz / positionHS.w;
float3 positionOS = mul(float4(positionWS, 1), InvWorld).xyz;
return positionOS.xz + 0.5f;
}
// Manually compute ddx/ddy for decal texture cooordinates to avoid the 2x2 pixels artifacts on the edges of geometry under decal
// [Reference: https://www.humus.name/index.php?page=3D&ID=84]
float4 CalculateTextureDerivatives(float4 svPosition, float2 texCoord)
{
float4 svDiffX = float4(1, 0, 0, 0);
float2 uvDiffX0 = texCoord - SvPositionToDecalUV(svPosition - svDiffX);
float2 uvDiffX1 = SvPositionToDecalUV(svPosition + svDiffX) - texCoord;
float2 dx = dot(uvDiffX0, uvDiffX0) < dot(uvDiffX1, uvDiffX1) ? uvDiffX0 : uvDiffX1;
float4 svDiffY = float4(0, 1, 0, 0);
float2 uvDiffY0 = texCoord - SvPositionToDecalUV(svPosition - svDiffY);
float2 uvDiffY1 = SvPositionToDecalUV(svPosition + svDiffY) - texCoord;
float2 dy = dot(uvDiffY0, uvDiffY0) < dot(uvDiffY1, uvDiffY1) ? uvDiffY0 : uvDiffY1;
return float4(dx, dy);
}
// Computes the mipmap level for a specific texture dimensions to be sampled at decal texture cooordinates.
// [Reference: https://hugi.scene.org/online/coding/hugi%2014%20-%20comipmap.htm]
float CalculateTextureMipmap(MaterialInput input, float2 textureSize)
{
float2 dx = input.TexCoord_DDX_DDY.xy * textureSize;
float2 dy = input.TexCoord_DDX_DDY.zw * textureSize;
float d = max(dot(dx, dx), dot(dy, dy));
return (0.5 * 0.5) * log2(d); // Hardcoded half-mip rate reduction to avoid artifacts when decal is moved over dither texture
}
float CalculateTextureMipmap(MaterialInput input, Texture2D t)
{
float2 textureSize;
t.GetDimensions(textureSize.x, textureSize.y);
return CalculateTextureMipmap(input, textureSize);
}
float CalculateTextureMipmap(MaterialInput input, TextureCube t)
{
float2 textureSize;
t.GetDimensions(textureSize.x, textureSize.y);
return CalculateTextureMipmap(input, textureSize);
}
// Transforms a vector from tangent space to world space
float3 TransformTangentVectorToWorld(MaterialInput input, float3 tangentVector)
{
@@ -116,7 +167,6 @@ Material GetMaterialPS(MaterialInput input)
}
// Input macro specified by the material: DECAL_BLEND_MODE
#define DECAL_BLEND_MODE_TRANSLUCENT 0
#define DECAL_BLEND_MODE_STAIN 1
#define DECAL_BLEND_MODE_NORMAL 2
@@ -153,7 +203,7 @@ void PS_Decal(
float2 screenUV = SvPosition.xy * ScreenSize.zw;
SvPosition.z = SAMPLE_RT(DepthBuffer, screenUV).r;
float4 positionHS = mul(float4(SvPosition.xyz, 1), SVPositionToWorld);
float4 positionHS = mul(float4(SvPosition.xyz, 1), SvPositionToWorld);
float3 positionWS = positionHS.xyz / positionHS.w;
float3 positionOS = mul(float4(positionWS, 1), InvWorld).xyz;
@@ -166,8 +216,9 @@ void PS_Decal(
materialInput.TexCoord = decalUVs;
materialInput.TwoSidedSign = 1;
materialInput.SvPosition = SvPosition;
// Build tangent to world transformation matrix
materialInput.TexCoord_DDX_DDY = CalculateTextureDerivatives(materialInput.SvPosition, materialInput.TexCoord);
// Calculate tangent-space
float3 ddxWp = ddx(positionWS);
float3 ddyWp = ddy(positionWS);
materialInput.TBN[0] = normalize(ddyWp);

View File

@@ -20,6 +20,8 @@ float TimeParam;
float4 ViewInfo;
float4 ScreenSize;
float4 ViewSize;
float3 ViewPadding0;
float UnscaledTimeParam;
@1META_CB_END
// Shader resources

View File

@@ -19,6 +19,8 @@ float4 ViewInfo;
float4 ScreenSize;
float4 TemporalAAJitter;
float4x4 InverseViewProjectionMatrix;
float3 ViewPadding0;
float UnscaledTimeParam;
@1META_CB_END
// Shader resources

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/SpriteMaterial.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/TexturePreviewMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Editor/Wires Debug Material.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

BIN
Content/Engine/DefaultMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Engine/DefaultRadialMenu.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Engine/DefaultTerrainMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Engine/SingleColorMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Engine/SkyboxMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Shaders/SSAO.flax (Stored with Git LFS)

Binary file not shown.

View File

@@ -73,6 +73,24 @@
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=TYPEDEF/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION_005FMEMBER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Abbreviations/=CCD/@EntryIndexedValue">CCD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Abbreviations/=GPU/@EntryIndexedValue">GPU</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Abbreviations/=ID/@EntryIndexedValue">ID</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=175CE9C669E52F4D92FD2C07848740BD/@EntryIndexedValue">&lt;NamingElement Priority="11" Title="Class and struct public fields"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="PUBLIC"&gt;&lt;type Name="class field" /&gt;&lt;type Name="struct field" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=32EB6D69783B3E4481A733193E338089/@EntryIndexedValue">&lt;NamingElement Priority="9" Title="Class and struct methods"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="member function" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=3C4E0D59F298854F9608A9B454E8FF5E/@EntryIndexedValue">&lt;NamingElement Priority="17" Title="Typedefs"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="type alias" /&gt;&lt;type Name="typedef" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=499C9026DADA2B448BCD0B2C54746A59/@EntryIndexedValue">&lt;NamingElement Priority="14" Title="Other constants"&gt;&lt;Descriptor Static="True" Constexpr="Indeterminate" Const="True" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="class field" /&gt;&lt;type Name="local variable" /&gt;&lt;type Name="struct field" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=50D2535711CE1A43A3B06EF841C36CFD/@EntryIndexedValue">&lt;NamingElement Priority="13" Title="Enum members"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="scoped enumerator" /&gt;&lt;type Name="unscoped enumerator" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=6AD3BADA1260CC4D840AB26323C51827/@EntryIndexedValue">&lt;NamingElement Priority="15" Title="Global constants"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="True" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="global variable" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=904CDCA174AACE4AA52660A247CDF9A0/@EntryIndexedValue">&lt;NamingElement Priority="7" Title="Global variables"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="global variable" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=95BCDE767C97B64DB3DAE800DBBBC758/@EntryIndexedValue">&lt;NamingElement Priority="5" Title="Parameters"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="function parameter" /&gt;&lt;type Name="lambda parameter" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=C03AE454FC2CBA43819AC75E4D6C9C8C/@EntryIndexedValue">&lt;NamingElement Priority="1" Title="Classes and structs"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="__interface" /&gt;&lt;type Name="class" /&gt;&lt;type Name="struct" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=D49E31C610641E4CAD0407DB79ACC851/@EntryIndexedValue">&lt;NamingElement Priority="8" Title="Global functions"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="global function" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=D73FBB3529BC5449B6C85BB37B26A8D4/@EntryIndexedValue">&lt;NamingElement Priority="16" Title="Namespaces"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="namespace" /&gt;&lt;type Name="namespace alias" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=DA41807CE47AEB4CBE1724C44D0B786E/@EntryIndexedValue">&lt;NamingElement Priority="6" Title="Local variables"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="local variable" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=DDF30C9A1DA74B4DBBC56D25FDF886AA/@EntryIndexedValue">&lt;NamingElement Priority="10" Title="Class and struct fields"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="class field" /&gt;&lt;type Name="struct field" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=EF70A6BF54ACE446971DDB32344C25A3/@EntryIndexedValue">&lt;NamingElement Priority="12" Title="Union members"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="union member" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=F37818C54C323A4A80B1A478629985AE/@EntryIndexedValue">&lt;NamingElement Priority="2" Title="Enums"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="enum" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ARGB/@EntryIndexedValue">ARGB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LO/@EntryIndexedValue">LO</s:String>
@@ -213,6 +231,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=YZ/@EntryIndexedValue">YZ</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PublicFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/Environment/Hierarchy/PsiConfigurationSettingsKey/CustomLocation/@EntryValue">C:\Users\Wojtek\AppData\Local\JetBrains\Transient\ReSharperPlatformVs15\v08_f9eacea9\SolutionCaches</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECpp_002ECodeStyle_002ENaming_002ECppNamingOptionsMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECpp_002ECodeStyle_002ESettingsUpgrade_002EFunctionReturnStyleSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECpp_002ECodeStyle_002ESettingsUpgrade_002ENamespaceIndentationSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>

View File

@@ -18,8 +18,8 @@ namespace FlaxEditor
private readonly CustomEditorPresenter _presenter;
private CustomEditorWindow _customEditor;
public Win(CustomEditorWindow customEditor)
: base(Editor.Instance, false, ScrollBars.Vertical)
public Win(CustomEditorWindow customEditor, bool hideOnClose, ScrollBars scrollBars)
: base(Editor.Instance, hideOnClose, scrollBars)
{
Title = customEditor.GetType().Name;
_customEditor = customEditor;
@@ -64,9 +64,9 @@ namespace FlaxEditor
/// <summary>
/// Initializes a new instance of the <see cref="CustomEditorWindow"/> class.
/// </summary>
protected CustomEditorWindow()
protected CustomEditorWindow(bool hideOnClose = false, ScrollBars scrollBars = ScrollBars.Vertical)
{
_win = new Win(this);
_win = new Win(this, hideOnClose, scrollBars);
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
}

View File

@@ -106,7 +106,6 @@ namespace FlaxEditor.CustomEditors.Editors
_linkButton = new Button
{
BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32),
Parent = LinkedLabel,
Width = 18,
Height = 18,
@@ -189,6 +188,7 @@ namespace FlaxEditor.CustomEditors.Editors
_linkButton.SetColors(backgroundColor);
_linkButton.BorderColor = _linkButton.BorderColorSelected = _linkButton.BorderColorHighlighted = Color.Transparent;
_linkButton.TooltipText = LinkValues ? "Unlinks scale components from uniform scaling" : "Links scale components for uniform scaling";
_linkButton.BackgroundBrush = new SpriteBrush(LinkValues ? Editor.Instance.Icons.Link32 : Editor.Instance.Icons.BrokenLink32);
}
}

View File

@@ -14,6 +14,9 @@ namespace FlaxEditor.CustomEditors.GUI
public class PropertiesList : PanelWithMargins
{
// TODO: sync splitter for whole presenter
private const float SplitterPadding = 15;
private const float EditorsMinWidthRatio = 0.4f;
/// <summary>
/// The splitter size (in pixels).
@@ -25,6 +28,7 @@ namespace FlaxEditor.CustomEditors.GUI
private Rectangle _splitterRect;
private bool _splitterClicked, _mouseOverSplitter;
private bool _cursorChanged;
private bool _hasCustomSplitterValue;
/// <summary>
/// Gets or sets the splitter value (always in range [0; 1]).
@@ -66,6 +70,26 @@ namespace FlaxEditor.CustomEditors.GUI
UpdateSplitRect();
}
private void AutoSizeSplitter()
{
if (_hasCustomSplitterValue || !Editor.Instance.Options.Options.Interface.AutoSizePropertiesPanelSplitter)
return;
Font font = Style.Current.FontMedium;
float largestWidth = 0f;
for (int i = 0; i < _element.Labels.Count; i++)
{
Label currentLabel = _element.Labels[i];
Float2 dimensions = font.MeasureText(currentLabel.Text);
float width = dimensions.X + currentLabel.Margin.Left + SplitterPadding;
largestWidth = Mathf.Max(largestWidth, width);
}
SplitterValue = Mathf.Clamp(largestWidth / Width, 0, 1 - EditorsMinWidthRatio);
}
private void UpdateSplitRect()
{
_splitterRect = new Rectangle(Mathf.Clamp(_splitterValue * Width - SplitterSize * 0.5f, 0.0f, Width), 0, SplitterSize, Height);
@@ -122,6 +146,7 @@ namespace FlaxEditor.CustomEditors.GUI
SplitterValue = location.X / Width;
Cursor = CursorType.SizeWE;
_cursorChanged = true;
_hasCustomSplitterValue = true;
}
else if (_mouseOverSplitter)
{
@@ -195,6 +220,7 @@ namespace FlaxEditor.CustomEditors.GUI
// Refresh
UpdateSplitRect();
PerformLayout(true);
AutoSizeSplitter();
}
/// <inheritdoc />

View File

@@ -528,7 +528,11 @@ namespace FlaxEditor
var timeSinceLastSave = Time.UnscaledGameTime - _lastAutoSaveTimer;
var timeToNextSave = options.AutoSaveFrequency * 60.0f - timeSinceLastSave;
if (timeToNextSave <= 0.0f || _autoSaveNow)
if (timeToNextSave <= 0.0f && GetWindows().Any(x => x.GUI.Children.Any(c => c is GUI.ContextMenu.ContextMenuBase)))
{
// Skip aut-save if any context menu is opened to wait for user to end interaction
}
else if (timeToNextSave <= 0.0f || _autoSaveNow)
{
Log("Auto save");
_lastAutoSaveTimer = Time.UnscaledGameTime;

View File

@@ -39,6 +39,7 @@ namespace FlaxEditor
public SpriteHandle Globe32;
public SpriteHandle CamSpeed32;
public SpriteHandle Link32;
public SpriteHandle BrokenLink32;
public SpriteHandle Add32;
public SpriteHandle Left32;
public SpriteHandle Right32;
@@ -94,6 +95,7 @@ namespace FlaxEditor
public SpriteHandle Search64;
public SpriteHandle Bone64;
public SpriteHandle Link64;
public SpriteHandle BrokenLink64;
public SpriteHandle Build64;
public SpriteHandle Add64;
public SpriteHandle ShipIt64;

View File

@@ -215,8 +215,8 @@ namespace FlaxEditor.GUI.Docking
switch (state)
{
case DockState.DockFill:
result.Location.Y += DockPanel.DefaultHeaderHeight;
result.Size.Y -= DockPanel.DefaultHeaderHeight;
result.Location.Y += Editor.Instance.Options.Options.Interface.TabHeight;
result.Size.Y -= Editor.Instance.Options.Options.Interface.TabHeight;
break;
case DockState.DockTop:
result.Size.Y *= DockPanel.DefaultSplitterValue;

View File

@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Options;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -13,12 +14,16 @@ namespace FlaxEditor.GUI.Docking
public class DockPanelProxy : ContainerControl
{
private DockPanel _panel;
private InterfaceOptions.TabCloseButtonVisibility closeButtonVisibility;
private double _dragEnterTime = -1;
#if PLATFORM_WINDOWS
private const bool HideTabForSingleTab = true;
#else
private const bool HideTabForSingleTab = false;
#endif
private float _tabHeight = Editor.Instance.Options.Options.Interface.TabHeight;
private bool _useMinimumTabWidth = Editor.Instance.Options.Options.Interface.UseMinimumTabWidth;
private float _minimumTabWidth = Editor.Instance.Options.Options.Interface.MinimumTabWidth;
#if PLATFORM_WINDOWS
private readonly bool _hideTabForSingleTab = Editor.Instance.Options.Options.Interface.HideSingleTabWindowTabBars;
#else
private readonly bool _hideTabForSingleTab = false;
#endif
/// <summary>
/// The is mouse down flag (left button).
@@ -55,8 +60,8 @@ namespace FlaxEditor.GUI.Docking
/// </summary>
public DockWindow StartDragAsyncWindow;
private Rectangle HeaderRectangle => new Rectangle(0, 0, Width, DockPanel.DefaultHeaderHeight);
private bool IsSingleFloatingWindow => HideTabForSingleTab && _panel.TabsCount == 1 && _panel.IsFloating && _panel.ChildPanelsCount == 0;
private Rectangle HeaderRectangle => new Rectangle(0, 0, Width, _tabHeight);
private bool IsSingleFloatingWindow => _hideTabForSingleTab && _panel.TabsCount == 1 && _panel.IsFloating && _panel.ChildPanelsCount == 0;
/// <summary>
/// Initializes a new instance of the <see cref="DockPanelProxy"/> class.
@@ -68,6 +73,14 @@ namespace FlaxEditor.GUI.Docking
_panel = panel;
AnchorPreset = AnchorPresets.StretchAll;
Offsets = Margin.Zero;
Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged;
OnEditorOptionsChanged(Editor.Instance.Options.Options);
}
private void OnEditorOptionsChanged(EditorOptions options)
{
closeButtonVisibility = options.Interface.ShowTabCloseButton;
}
private DockWindow GetTabAtPos(Float2 position, out bool closeButton)
@@ -78,11 +91,11 @@ namespace FlaxEditor.GUI.Docking
var tabsCount = _panel.TabsCount;
if (tabsCount == 1)
{
var crossRect = new Rectangle(Width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
var crossRect = new Rectangle(Width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
if (HeaderRectangle.Contains(position))
{
closeButton = crossRect.Contains(position);
result = _panel.GetTab(0);
closeButton = crossRect.Contains(position) && IsCloseButtonVisible(result, closeButtonVisibility);
}
}
else
@@ -91,15 +104,17 @@ namespace FlaxEditor.GUI.Docking
for (int i = 0; i < tabsCount; i++)
{
var tab = _panel.GetTab(i);
var titleSize = tab.TitleSize;
var iconWidth = tab.Icon.IsValid ? DockPanel.DefaultButtonsSize + DockPanel.DefaultLeftTextMargin : 0;
var width = titleSize.X + DockPanel.DefaultButtonsSize + 2 * DockPanel.DefaultButtonsMargin + DockPanel.DefaultLeftTextMargin + DockPanel.DefaultRightTextMargin + iconWidth;
var tabRect = new Rectangle(x, 0, width, DockPanel.DefaultHeaderHeight);
float width = CalculateTabWidth(tab, closeButtonVisibility);
if (_useMinimumTabWidth && width < _minimumTabWidth)
width = _minimumTabWidth;
var tabRect = new Rectangle(x, 0, width, HeaderRectangle.Height);
var isMouseOver = tabRect.Contains(position);
if (isMouseOver)
{
var crossRect = new Rectangle(x + width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
closeButton = crossRect.Contains(position);
var crossRect = new Rectangle(x + width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
closeButton = crossRect.Contains(position) && IsCloseButtonVisible(tab, closeButtonVisibility);
result = tab;
break;
}
@@ -110,6 +125,24 @@ namespace FlaxEditor.GUI.Docking
return result;
}
private bool IsCloseButtonVisible(DockWindow win, InterfaceOptions.TabCloseButtonVisibility visibilityMode)
{
return visibilityMode != InterfaceOptions.TabCloseButtonVisibility.Never &&
(visibilityMode == InterfaceOptions.TabCloseButtonVisibility.Always ||
(visibilityMode == InterfaceOptions.TabCloseButtonVisibility.SelectedTab && _panel.SelectedTab == win));
}
private float CalculateTabWidth(DockWindow win, InterfaceOptions.TabCloseButtonVisibility visibilityMode)
{
var iconWidth = win.Icon.IsValid ? DockPanel.DefaultButtonsSize + DockPanel.DefaultLeftTextMargin : 0;
var width = win.TitleSize.X + DockPanel.DefaultLeftTextMargin + DockPanel.DefaultRightTextMargin + iconWidth;
if (IsCloseButtonVisible(win, visibilityMode))
width += 2 * DockPanel.DefaultButtonsMargin + DockPanel.DefaultButtonsSize;
return width;
}
private void GetTabRect(DockWindow win, out Rectangle bounds)
{
FlaxEngine.Assertions.Assert.IsTrue(_panel.ContainsTab(win));
@@ -127,10 +160,10 @@ namespace FlaxEditor.GUI.Docking
{
var tab = _panel.GetTab(i);
var titleSize = tab.TitleSize;
float width = titleSize.X + DockPanel.DefaultButtonsSize + 2 * DockPanel.DefaultButtonsMargin + DockPanel.DefaultLeftTextMargin + DockPanel.DefaultRightTextMargin;
float width = CalculateTabWidth(tab, closeButtonVisibility);
if (tab == win)
{
bounds = new Rectangle(x, 0, width, DockPanel.DefaultHeaderHeight);
bounds = new Rectangle(x, 0, width, HeaderRectangle.Height);
return;
}
x += width;
@@ -210,7 +243,7 @@ namespace FlaxEditor.GUI.Docking
{
Render2D.DrawSprite(
tab.Icon,
new Rectangle(DockPanel.DefaultLeftTextMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize),
new Rectangle(DockPanel.DefaultLeftTextMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize),
style.Foreground);
}
@@ -219,17 +252,20 @@ namespace FlaxEditor.GUI.Docking
Render2D.DrawText(
style.FontMedium,
tab.Title,
new Rectangle(DockPanel.DefaultLeftTextMargin + iconWidth, 0, Width - DockPanel.DefaultLeftTextMargin - DockPanel.DefaultButtonsSize - 2 * DockPanel.DefaultButtonsMargin, DockPanel.DefaultHeaderHeight),
new Rectangle(DockPanel.DefaultLeftTextMargin + iconWidth, 0, Width - DockPanel.DefaultLeftTextMargin - DockPanel.DefaultButtonsSize - 2 * DockPanel.DefaultButtonsMargin, HeaderRectangle.Height),
style.Foreground,
TextAlignment.Near,
TextAlignment.Center);
// Draw cross
var crossRect = new Rectangle(Width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
bool isMouseOverCross = isMouseOver && crossRect.Contains(MousePosition);
if (isMouseOverCross)
Render2D.FillRectangle(crossRect, (containsFocus ? style.BackgroundSelected : style.LightBackground) * 1.3f);
Render2D.DrawSprite(style.Cross, crossRect, isMouseOverCross ? style.Foreground : style.ForegroundGrey);
if (IsCloseButtonVisible(tab, closeButtonVisibility))
{
// Draw cross
var crossRect = new Rectangle(Width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
bool isMouseOverCross = isMouseOver && crossRect.Contains(MousePosition);
if (isMouseOverCross)
Render2D.FillRectangle(crossRect, (containsFocus ? style.BackgroundSelected : style.LightBackground) * 1.3f);
Render2D.DrawSprite(style.Cross, crossRect, isMouseOverCross ? style.Foreground : style.ForegroundGrey);
}
}
else
{
@@ -243,10 +279,14 @@ namespace FlaxEditor.GUI.Docking
// Cache data
var tab = _panel.GetTab(i);
var tabColor = Color.Black;
var titleSize = tab.TitleSize;
var iconWidth = tab.Icon.IsValid ? DockPanel.DefaultButtonsSize + DockPanel.DefaultLeftTextMargin : 0;
var width = titleSize.X + DockPanel.DefaultButtonsSize + 2 * DockPanel.DefaultButtonsMargin + DockPanel.DefaultLeftTextMargin + DockPanel.DefaultRightTextMargin + iconWidth;
var tabRect = new Rectangle(x, 0, width, DockPanel.DefaultHeaderHeight);
float width = CalculateTabWidth(tab, closeButtonVisibility);
if (_useMinimumTabWidth && width < _minimumTabWidth)
width = _minimumTabWidth;
var tabRect = new Rectangle(x, 0, width, headerRect.Height);
var isMouseOver = tabRect.Contains(MousePosition);
var isSelected = _panel.SelectedTab == tab;
@@ -273,7 +313,7 @@ namespace FlaxEditor.GUI.Docking
{
Render2D.DrawSprite(
tab.Icon,
new Rectangle(x + DockPanel.DefaultLeftTextMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize),
new Rectangle(x + DockPanel.DefaultLeftTextMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize),
style.Foreground);
}
@@ -282,27 +322,27 @@ namespace FlaxEditor.GUI.Docking
Render2D.DrawText(
style.FontMedium,
tab.Title,
new Rectangle(x + DockPanel.DefaultLeftTextMargin + iconWidth, 0, 10000, DockPanel.DefaultHeaderHeight),
new Rectangle(x + DockPanel.DefaultLeftTextMargin + iconWidth, 0, 10000, HeaderRectangle.Height),
style.Foreground,
TextAlignment.Near,
TextAlignment.Center);
// Draw cross
if (isSelected || isMouseOver)
if (IsCloseButtonVisible(tab, closeButtonVisibility))
{
var crossRect = new Rectangle(x + width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
var crossRect = new Rectangle(x + width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
bool isMouseOverCross = isMouseOver && crossRect.Contains(MousePosition);
if (isMouseOverCross)
Render2D.FillRectangle(crossRect, tabColor * 1.3f);
Render2D.DrawSprite(style.Cross, crossRect, isMouseOverCross ? style.Foreground : style.ForegroundGrey);
}
// Move
// Set the start position for the next tab
x += width;
}
// Draw selected tab strip
Render2D.FillRectangle(new Rectangle(0, DockPanel.DefaultHeaderHeight - 2, Width, 2), containsFocus ? style.BackgroundSelected : style.BackgroundNormal);
Render2D.FillRectangle(new Rectangle(0, HeaderRectangle.Height - 2, Width, 2), containsFocus ? style.BackgroundSelected : style.BackgroundNormal);
}
}
@@ -520,7 +560,7 @@ namespace FlaxEditor.GUI.Docking
if (IsSingleFloatingWindow)
rect = new Rectangle(0, 0, Width, Height);
else
rect = new Rectangle(0, DockPanel.DefaultHeaderHeight, Width, Height - DockPanel.DefaultHeaderHeight);
rect = new Rectangle(0, HeaderRectangle.Height, Width, Height - HeaderRectangle.Height);
}
private DragDropEffect TrySelectTabUnderLocation(ref Float2 location)

View File

@@ -514,20 +514,15 @@ namespace FlaxEditor.GUI
var items = ItemsPanel.Children;
for (int i = 0; i < items.Count; i++)
{
if (items[i] is Item item && item.Visible)
var currentItem = items[i];
if (currentItem is Item item && item.Visible)
result.Add(item);
}
if (_categoryPanels != null)
{
for (int i = 0; i < _categoryPanels.Count; i++)
else if (currentItem is DropPanel category && (!ignoreFoldedCategories || !category.IsClosed) && currentItem.Visible)
{
var category = _categoryPanels[i];
if (!category.Visible || (ignoreFoldedCategories && category is DropPanel panel && panel.IsClosed))
continue;
for (int j = 0; j < category.Children.Count; j++)
{
if (category.Children[j] is Item item2 && item2.Visible)
result.Add(item2);
if (category.Children[j] is Item categoryItem && categoryItem.Visible)
result.Add(categoryItem);
}
}
}
@@ -591,10 +586,6 @@ namespace FlaxEditor.GUI
var items = GetVisibleItems(!controlDown);
var focusedIndex = items.IndexOf(focusedItem);
// If the user hasn't selected anything yet and is holding control, focus first folded item
if (focusedIndex == -1 && controlDown)
focusedIndex = GetVisibleItems(true).Count - 1;
int delta = key == KeyboardKeys.ArrowDown ? -1 : 1;
int nextIndex = Mathf.Wrap(focusedIndex - delta, 0, items.Count - 1);
var nextItem = items[nextIndex];

View File

@@ -11,6 +11,9 @@ namespace FlaxEditor.GUI
/// <seealso cref="FlaxEngine.GUI.Panel" />
public class NavigationBar : Panel
{
private float _toolstripHeight = 0;
private Margin _toolstripMargin;
/// <summary>
/// The default buttons margin.
/// </summary>
@@ -50,9 +53,42 @@ namespace FlaxEditor.GUI
{
if (toolstrip == null)
return;
if (_toolstripHeight <= 0.0f)
{
// Cache initial toolstrip state
_toolstripHeight = toolstrip.Height;
_toolstripMargin = toolstrip.ItemsMargin;
}
// Control toolstrip bottom margin to prevent navigation bar scroll going over the buttons
var toolstripLocked = toolstrip.IsLayoutLocked;
toolstrip.IsLayoutLocked = true;
var toolstripHeight = _toolstripHeight;
var toolstripMargin = _toolstripMargin;
if (HScrollBar.Visible)
{
float scrollMargin = 8;
toolstripHeight += scrollMargin;
toolstripMargin.Bottom += scrollMargin;
}
toolstrip.Height = toolstripHeight;
toolstrip.IsLayoutLocked = toolstripLocked;
toolstrip.ItemsMargin = toolstripMargin;
var lastToolstripButton = toolstrip.LastButton;
var parentSize = Parent.Size;
Bounds = new Rectangle(lastToolstripButton.Right + 8.0f, 0, parentSize.X - X - 8.0f, toolstrip.Height);
}
/// <inheritdoc />
public override void PerformLayout(bool force = false)
{
base.PerformLayout(force);
// Stretch excluding toolstrip margin to fill the space
if (Parent is ToolStrip toolStrip)
Height = toolStrip.Height;
}
}
}

View File

@@ -130,6 +130,10 @@ namespace FlaxEditor.GUI
/// <returns>Created popup.</returns>
public static RenamePopup Show(Control control, Rectangle area, string value, bool isMultiline)
{
// hardcoded flushing layout for tree controls
if (control is Tree.TreeNode treeNode && treeNode.ParentTree != null)
treeNode.ParentTree.FlushPendingPerformLayout();
// Calculate the control size in the window space to handle scaled controls
var upperLeft = control.PointToWindow(area.UpperLeft);
var bottomRight = control.PointToWindow(area.BottomRight);

View File

@@ -13,15 +13,7 @@ namespace FlaxEditor.GUI
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public class ToolStrip : ContainerControl
{
/// <summary>
/// The default margin vertically.
/// </summary>
public const int DefaultMarginV = 1;
/// <summary>
/// The default margin horizontally.
/// </summary>
public const int DefaultMarginH = 2;
private Margin _itemsMargin;
/// <summary>
/// Event fired when button gets clicked with the primary mouse button.
@@ -66,10 +58,26 @@ namespace FlaxEditor.GUI
}
}
/// <summary>
/// Gets or sets the space around items.
/// </summary>
public Margin ItemsMargin
{
get => _itemsMargin;
set
{
if (_itemsMargin != value)
{
_itemsMargin = value;
PerformLayout();
}
}
}
/// <summary>
/// Gets the height for the items.
/// </summary>
public float ItemsHeight => Height - 2 * DefaultMarginV;
public float ItemsHeight => Height - _itemsMargin.Height;
/// <summary>
/// Initializes a new instance of the <see cref="ToolStrip"/> class.
@@ -82,6 +90,7 @@ namespace FlaxEditor.GUI
AnchorPreset = AnchorPresets.HorizontalStretchTop;
BackgroundColor = Style.Current.LightBackground;
Offsets = new Margin(0, 0, y, height * Editor.Instance.Options.Options.Interface.IconsScale);
_itemsMargin = new Margin(2, 2, 1, 1);
}
/// <summary>
@@ -161,7 +170,7 @@ namespace FlaxEditor.GUI
protected override void PerformLayoutBeforeChildren()
{
// Arrange controls
float x = DefaultMarginH;
float x = _itemsMargin.Left;
float h = ItemsHeight;
for (int i = 0; i < _children.Count; i++)
{
@@ -169,8 +178,8 @@ namespace FlaxEditor.GUI
if (c.Visible)
{
var w = c.Width;
c.Bounds = new Rectangle(x, DefaultMarginV, w, h);
x += w + DefaultMarginH;
c.Bounds = new Rectangle(x, _itemsMargin.Top, w, h);
x += w + _itemsMargin.Width;
}
}
}

View File

@@ -41,6 +41,7 @@ namespace FlaxEditor.GUI.Tree
private Margin _margin;
private bool _autoSize = true;
private bool _deferLayoutUpdate = false;
private TreeNode _lastSelectedNode;
/// <summary>
/// The TreeNode that is being dragged over. This could have a value when not dragging.
@@ -67,7 +68,7 @@ namespace FlaxEditor.GUI.Tree
/// Gets the first selected node or null.
/// </summary>
public TreeNode SelectedNode => Selection.Count > 0 ? Selection[0] : null;
/// <summary>
/// Allow nodes to Draw the root tree line.
/// </summary>
@@ -364,6 +365,19 @@ namespace FlaxEditor.GUI.Tree
BulkSelectUpdateExpanded(false);
}
/// <summary>
/// Flushes any pending layout perming action that has been delayed until next update to optimize performance of the complex tree hierarchy.
/// </summary>
public void FlushPendingPerformLayout()
{
if (_deferLayoutUpdate)
{
base.PerformLayout();
AfterDeferredLayout?.Invoke();
_deferLayoutUpdate = false;
}
}
/// <inheritdoc />
public override void PerformLayout(bool force = false)
{
@@ -378,25 +392,31 @@ namespace FlaxEditor.GUI.Tree
public override void Update(float deltaTime)
{
if (_deferLayoutUpdate)
{
base.PerformLayout();
AfterDeferredLayout?.Invoke();
_deferLayoutUpdate = false;
}
FlushPendingPerformLayout();
var window = Root;
bool shiftDown = window.GetKey(KeyboardKeys.Shift);
bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp);
bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown);
var node = SelectedNode;
// Use last selection for last selected node if sift is down
if (Selection.Count < 2)
_lastSelectedNode = null;
else if (shiftDown)
_lastSelectedNode ??= Selection[^1];
// Skip root to prevent blocking input
if (_lastSelectedNode != null && _lastSelectedNode.IsRoot)
_lastSelectedNode = null;
var node = _lastSelectedNode ?? SelectedNode;
// Check if has focus and if any node is focused and it isn't a root
if (ContainsFocus && node != null && node.AutoFocus)
{
var window = Root;
if (window.GetKeyDown(KeyboardKeys.ArrowUp) || window.GetKeyDown(KeyboardKeys.ArrowDown))
_keyUpdateTime = KeyUpdateTimeout;
if (_keyUpdateTime >= KeyUpdateTimeout && window is WindowRootControl windowRoot && windowRoot.Window.IsFocused)
{
bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp);
bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown);
// Check if arrow flags are different
if (keyDownArrow != keyUpArrow)
{
@@ -406,24 +426,38 @@ namespace FlaxEditor.GUI.Tree
Assert.AreNotEqual(-1, myIndex);
// Up
TreeNode toSelect = null;
List<TreeNode> toSelect = new List<TreeNode>();
if (shiftDown && _supportMultiSelect)
{
toSelect.AddRange(Selection);
}
if (keyUpArrow)
{
if (myIndex == 0)
{
// Select parent
toSelect = parentNode;
if (toSelect.Contains(parentNode))
toSelect.Remove(node);
else if (parentNode != null)
toSelect.Add(parentNode);
_lastSelectedNode = parentNode;
}
else
{
// Select previous parent child
toSelect = nodeParent.GetChild(myIndex - 1) as TreeNode;
var select = nodeParent.GetChild(myIndex - 1) as TreeNode;
// Select last child if is valid and expanded and has any children
if (toSelect != null && toSelect.IsExpanded && toSelect.HasAnyVisibleChild)
if (select != null && select.IsExpanded && select.HasAnyVisibleChild)
{
toSelect = toSelect.GetChild(toSelect.ChildrenCount - 1) as TreeNode;
select = select.GetChild(select.ChildrenCount - 1) as TreeNode;
}
if (select == null || toSelect.Contains(select))
toSelect.Remove(node);
else
toSelect.Add(select);
_lastSelectedNode = select;
}
}
// Down
@@ -432,32 +466,48 @@ namespace FlaxEditor.GUI.Tree
if (node.IsExpanded && node.HasAnyVisibleChild)
{
// Select the first child
toSelect = node.GetChild(0) as TreeNode;
var select = node.GetChild(0) as TreeNode;
if (select == null || toSelect.Contains(select))
toSelect.Remove(node);
else
toSelect.Add(select);
_lastSelectedNode = select;
}
else if (myIndex == nodeParent.ChildrenCount - 1)
{
// Select next node after parent
while (parentNode != null && toSelect == null)
TreeNode select = null;
while (parentNode != null && select == null)
{
int parentIndex = parentNode.IndexInParent;
if (parentIndex != -1 && parentIndex < parentNode.Parent.ChildrenCount - 1)
{
toSelect = parentNode.Parent.GetChild(parentIndex + 1) as TreeNode;
select = parentNode.Parent.GetChild(parentIndex + 1) as TreeNode;
}
parentNode = parentNode.Parent as TreeNode;
}
if (select == null || toSelect.Contains(select))
toSelect.Remove(node);
else
toSelect.Add(select);
_lastSelectedNode = select;
}
else
{
// Select next parent child
toSelect = nodeParent.GetChild(myIndex + 1) as TreeNode;
var select = nodeParent.GetChild(myIndex + 1) as TreeNode;
if (select == null || toSelect.Contains(select))
toSelect.Remove(node);
else
toSelect.Add(select);
_lastSelectedNode = select;
}
}
if (toSelect != null && toSelect.AutoFocus)
if (toSelect.Count > 0)
{
// Select
Select(toSelect);
toSelect.Focus();
_lastSelectedNode?.Focus();
}
// Reset time

View File

@@ -11,6 +11,7 @@
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MException.h"
#include "Engine/Scripting/Internal/MainThreadManagedInvokeAction.h"
#include "Engine/Platform/WindowsManager.h"
#include "Engine/Content/Assets/VisualScript.h"
#include "Engine/Content/Content.h"
#include "Engine/Level/Actor.h"
@@ -627,6 +628,14 @@ void ManagedEditor::WipeOutLeftoverSceneObjects()
ObjectsRemovalService::Flush();
}
Array<Window*> ManagedEditor::GetWindows()
{
WindowsManager::WindowsLocker.Lock();
auto result = WindowsManager::Windows;
WindowsManager::WindowsLocker.Unlock();
return result;
}
void ManagedEditor::OnEditorAssemblyLoaded(MAssembly* assembly)
{
ASSERT(!HasManagedInstance());

View File

@@ -259,6 +259,7 @@ public:
API_FUNCTION(Internal) static Array<VisualScriptLocal> GetVisualScriptLocals();
API_FUNCTION(Internal) static bool EvaluateVisualScriptLocal(VisualScript* script, API_PARAM(Ref) VisualScriptLocal& local);
API_FUNCTION(Internal) static void WipeOutLeftoverSceneObjects();
API_FUNCTION(Internal) static Array<Window*> GetWindows();
private:
void OnEditorAssemblyLoaded(MAssembly* assembly);

View File

@@ -652,43 +652,47 @@ namespace FlaxEditor.Options
#endregion
#region Node editors
#region Node Editors
[DefaultValue(typeof(InputBinding), "Shift+W")]
[EditorDisplay("Node editors"), EditorOrder(4500)]
[EditorDisplay("Node Editors"), EditorOrder(4500)]
public InputBinding NodesAlignTop = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+A")]
[EditorDisplay("Node editors"), EditorOrder(4510)]
[EditorDisplay("Node Editors"), EditorOrder(4510)]
public InputBinding NodesAlignLeft = new InputBinding(KeyboardKeys.A, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+S")]
[EditorDisplay("Node editors"), EditorOrder(4520)]
[EditorDisplay("Node Editors"), EditorOrder(4520)]
public InputBinding NodesAlignBottom = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+D")]
[EditorDisplay("Node editors"), EditorOrder(4530)]
[EditorDisplay("Node Editors"), EditorOrder(4530)]
public InputBinding NodesAlignRight = new InputBinding(KeyboardKeys.D, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Alt+Shift+W")]
[EditorDisplay("Node editors"), EditorOrder(4540)]
[EditorDisplay("Node Editors"), EditorOrder(4540)]
public InputBinding NodesAlignMiddle = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Alt+Shift+S")]
[EditorDisplay("Node editors"), EditorOrder(4550)]
[EditorDisplay("Node Editors"), EditorOrder(4550)]
public InputBinding NodesAlignCenter = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Q")]
[EditorDisplay("Node editors"), EditorOrder(4560)]
[EditorDisplay("Node Editors"), EditorOrder(4560)]
public InputBinding NodesAutoFormat = new InputBinding(KeyboardKeys.Q);
[DefaultValue(typeof(InputBinding), "None")]
[EditorDisplay("Node editors"), EditorOrder(4570)]
public InputBinding NodesDistributeHorizontal = new InputBinding(KeyboardKeys.None);
[DefaultValue(typeof(InputBinding), "Shift+Q")]
[EditorDisplay("Node Editors"), EditorOrder(4560)]
public InputBinding NodesStraightenConnections = new InputBinding(KeyboardKeys.Q, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "None")]
[EditorDisplay("Node editors"), EditorOrder(4580)]
public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.None);
[DefaultValue(typeof(InputBinding), "Alt+W")]
[EditorDisplay("Node Editors"), EditorOrder(4570)]
public InputBinding NodesDistributeHorizontal = new InputBinding(KeyboardKeys.W, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Alt+A")]
[EditorDisplay("Node Editors"), EditorOrder(4580)]
public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.A, KeyboardKeys.Alt);
#endregion
}

View File

@@ -76,6 +76,25 @@ namespace FlaxEditor.Options
DockBottom = DockState.DockBottom
}
/// <summary>
/// Options for the visibility status of the tab close button.
/// </summary>
public enum TabCloseButtonVisibility
{
/// <summary>
/// Never show the close button.
/// </summary>
Never,
/// <summary>
/// Show the close button on tabs that are currently selected.
/// </summary>
SelectedTab,
/// <summary>
/// Show the close button on all tabs that can be closed.
/// </summary>
Always
}
/// <summary>
/// Options for the action taken by the play button.
/// </summary>
@@ -167,15 +186,6 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(10), Tooltip("Editor User Interface scale. Applied to all UI elements, windows and text. Can be used to scale the interface up on a bigger display. Editor restart required.")]
public float InterfaceScale { get; set; } = 1.0f;
#if PLATFORM_WINDOWS
/// <summary>
/// Gets or sets a value indicating whether use native window title bar. Editor restart required.
/// </summary>
[DefaultValue(false)]
[EditorDisplay("Interface"), EditorOrder(70), Tooltip("Determines whether use native window title bar. Editor restart required.")]
public bool UseNativeWindowSystem { get; set; } = false;
#endif
/// <summary>
/// Gets or sets a value indicating whether show selected camera preview in the editor window.
/// </summary>
@@ -183,20 +193,6 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(80), Tooltip("Determines whether show selected camera preview in the edit window.")]
public bool ShowSelectedCameraPreview { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.
/// </summary>
[DefaultValue(false)]
[EditorDisplay("Interface", "Center Mouse On Game Window Focus"), EditorOrder(100), Tooltip("Determines whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.")]
public bool CenterMouseOnGameWinFocus { get; set; } = false;
/// <summary>
/// Gets or sets the method window opening.
/// </summary>
[DefaultValue(DockStateProxy.Float)]
[EditorDisplay("Interface", "New Window Location"), EditorOrder(150), Tooltip("Define the opening method for new windows, open in a new tab by default.")]
public DockStateProxy NewWindowLocation { get; set; } = DockStateProxy.Float;
/// <summary>
/// Gets or sets the editor icons scale. Editor restart required.
/// </summary>
@@ -232,6 +228,13 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(310)]
public bool SeparateValueAndUnit { get; set; }
/// <summary>
/// Gets or sets the option to auto size the Properties panel splitter based on the longest property name. Editor restart recommended.
/// </summary>
[DefaultValue(false)]
[EditorDisplay("Interface"), EditorOrder(311)]
public bool AutoSizePropertiesPanelSplitter { get; set; }
/// <summary>
/// Gets or sets tree line visibility.
/// </summary>
@@ -265,6 +268,66 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(322)]
public bool ScrollToScriptOnAdd { get; set; } = true;
#if PLATFORM_WINDOWS
/// <summary>
/// Gets or sets a value indicating whether use native window title bar. Editor restart required.
/// </summary>
[DefaultValue(false)]
[EditorDisplay("Tabs & Windows"), EditorOrder(70), Tooltip("Determines whether use native window title bar. Editor restart required.")]
public bool UseNativeWindowSystem { get; set; } = false;
#endif
#if PLATFORM_WINDOWS
/// <summary>
/// Gets or sets a value indicating whether a window containing a single tabs hides the tab bar. Editor restart recommended.
/// </summary>
[DefaultValue(true)]
[EditorDisplay("Tabs & Windows", "Hide Single-Tab Window Tab Bars"), EditorOrder(71)]
public bool HideSingleTabWindowTabBars { get; set; } = true;
#endif
/// <summary>
/// Gets or sets a value indicating wether the minum tab width should be used. Editor restart required.
/// </summary>
[DefaultValue(false)]
[EditorDisplay("Tabs & Windows"), EditorOrder(99)]
public bool UseMinimumTabWidth { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating the minimum tab width. If a tab is smaller than this width, its width will be set to this. Editor restart required.
/// </summary>
[DefaultValue(80.0f), Limit(50.0f, 150.0f)]
[EditorDisplay("Tabs & Windows"), EditorOrder(99), VisibleIf(nameof(UseMinimumTabWidth))]
public float MinimumTabWidth { get; set; } = 80.0f;
/// <summary>
/// Gets or sets a value indicating the height of window tabs. Editor restart required.
/// </summary>
[DefaultValue(20.0f), Limit(15.0f, 40.0f)]
[EditorDisplay("Tabs & Windows"), EditorOrder(100)]
public float TabHeight { get; set; } = 20.0f;
/// <summary>
/// Gets or sets a value indicating whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.
/// </summary>
[DefaultValue(false)]
[EditorDisplay("Tabs & Windows", "Center Mouse On Game Window Focus"), EditorOrder(101), Tooltip("Determines whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.")]
public bool CenterMouseOnGameWinFocus { get; set; } = false;
/// <summary>
/// Gets or sets the method window opening.
/// </summary>
[DefaultValue(DockStateProxy.Float)]
[EditorDisplay("Tabs & Windows", "New Window Location"), EditorOrder(150), Tooltip("Define the opening method for new windows, open in a new tab by default.")]
public DockStateProxy NewWindowLocation { get; set; } = DockStateProxy.Float;
/// <summary>
/// Gets or sets a value indicating when the tab close button should be visible.
/// </summary>
[DefaultValue(TabCloseButtonVisibility.SelectedTab)]
[EditorDisplay("Tabs & Windows"), EditorOrder(151)]
public TabCloseButtonVisibility ShowTabCloseButton { get; set; } = TabCloseButtonVisibility.SelectedTab;
/// <summary>
/// Gets or sets the timestamps prefix mode for output log messages.
/// </summary>
@@ -420,6 +483,12 @@ namespace FlaxEditor.Options
[DefaultValue(1), Range(1, 4)]
[EditorDisplay("Cook & Run"), EditorOrder(600)]
public int NumberOfGameClientsToLaunch = 1;
/// <summary>
/// Gets or sets the build configuration to use when using Cook and Run option in the editor.
/// </summary>
[EditorDisplay("Cook & Run"), EditorOrder(601), ExpandGroups, Tooltip("The build configuration to use when using Cook and Run option in the editor.")]
public BuildConfiguration CookAndRunBuildConfiguration { get; set; } = BuildConfiguration.Development;
/// <summary>
/// Gets or sets the curvature of the line connecting to connected visject nodes.

View File

@@ -810,7 +810,7 @@ namespace FlaxEditor.Surface.Archetypes
Create = (id, context, arch, groupArch) => new StateMachineState(id, context, arch, groupArch),
Title = "State",
Description = "The animation states machine state node",
Flags = NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste,
Flags = NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(100, 0),
DefaultValues = new object[]
{

View File

@@ -199,7 +199,7 @@ namespace FlaxEditor.Surface.Archetypes
private Label _labelValue;
private FloatValueBox _timeValue;
private ColorValueBox _colorValue;
private const int MaxStops = 8;
private const int MaxStops = 12;
/// <inheritdoc />
public ColorGradientNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
@@ -1386,10 +1386,11 @@ namespace FlaxEditor.Surface.Archetypes
Title = "Time",
Description = "Game time constant",
Flags = NodeFlags.MaterialGraph,
Size = new Float2(110, 20),
Size = new Float2(110, 40),
Elements = new[]
{
NodeElementArchetype.Factory.Output(0, "", typeof(float), 0),
NodeElementArchetype.Factory.Output(0, "Time", typeof(float), 0),
NodeElementArchetype.Factory.Output(1, "Unscaled Time", typeof(float), 1),
}
},
new NodeArchetype
@@ -1506,7 +1507,11 @@ namespace FlaxEditor.Surface.Archetypes
0.95f,
Color.White,
// Empty stops 2-7
// Empty stops 2-11
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,

View File

@@ -5,39 +5,39 @@ using FlaxEngine;
namespace FlaxEditor.Surface
{
/// <summary>
/// Node Alignment type
/// Node Alignment type.
/// </summary>
[HideInEditor]
public enum NodeAlignmentType
{
/// <summary>
/// Align nodes vertically to top, matching top-most node
/// Align nodes vertically to top, matching top-most node.
/// </summary>
Top,
/// <summary>
/// Align nodes vertically to middle, using average of all nodes
/// Align nodes vertically to middle, using average of all nodes.
/// </summary>
Middle,
/// <summary>
/// Align nodes vertically to bottom, matching bottom-most node
/// Align nodes vertically to bottom, matching bottom-most node.
/// </summary>
Bottom,
/// <summary>
/// Align nodes horizontally to left, matching left-most node
/// Align nodes horizontally to left, matching left-most node.
/// </summary>
Left,
/// <summary>
/// Align nodes horizontally to center, using average of all nodes
/// Align nodes horizontally to center, using average of all nodes.
/// </summary>
Center,
/// <summary>
/// Align nodes horizontally to right, matching right-most node
/// Align nodes horizontally to right, matching right-most node.
/// </summary>
Right,
}
}
}

View File

@@ -28,7 +28,7 @@ namespace FlaxEditor.Surface
/// </summary>
/// <param name="scriptType">The input type to process.</param>
/// <param name="cache">Node groups cache that can be used for reusing groups for different nodes.</param>
/// <param name="version">The cache version number. Can be used to reject any cached data after <see cref="NodesCache"/> rebuilt.</param>
/// <param name="version">The cache version number. Can be used to reject any cached data after.<see cref="NodesCache"/> rebuilt.</param>
public delegate void IterateType(ScriptType scriptType, Dictionary<KeyValuePair<string, ushort>, GroupArchetype> cache, int version);
internal static readonly List<NodesCache> Caches = new List<NodesCache>(8);
@@ -412,6 +412,7 @@ namespace FlaxEditor.Surface
_cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection;
_cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); });
_cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Straighten connections", Editor.Instance.Options.Options.Input.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); });
_cmFormatNodesMenu.ContextMenu.AddSeparator();
_cmAlignNodesTopButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align top", Editor.Instance.Options.Options.Input.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); });

View File

@@ -1,9 +1,9 @@
using FlaxEditor.Surface.Elements;
using FlaxEditor.Surface.Undo;
using FlaxEngine;
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEngine;
using FlaxEditor.Surface.Elements;
using FlaxEditor.Surface.Undo;
namespace FlaxEditor.Surface
{
@@ -14,26 +14,26 @@ namespace FlaxEditor.Surface
private class NodeFormattingData
{
/// <summary>
/// Starting from 0 at the main nodes
/// Starting from 0 at the main nodes.
/// </summary>
public int Layer;
/// <summary>
/// Position in the layer
/// Position in the layer.
/// </summary>
public int Offset;
/// <summary>
/// How far the subtree needs to be moved additionally
/// How far the subtree needs to be moved additionally.
/// </summary>
public int SubtreeOffset;
}
/// <summary>
/// Formats a graph where the nodes can be disjointed.
/// Uses the Sugiyama method
/// Uses the Sugiyama method.
/// </summary>
/// <param name="nodes">List of nodes</param>
/// <param name="nodes">List of nodes.</param>
public void FormatGraph(List<SurfaceNode> nodes)
{
if (nodes.Count <= 1)
@@ -78,9 +78,9 @@ namespace FlaxEditor.Surface
}
/// <summary>
/// Formats a graph where all nodes are connected
/// Formats a graph where all nodes are connected.
/// </summary>
/// <param name="nodes">List of connected nodes</param>
/// <param name="nodes">List of connected nodes.</param>
protected void FormatConnectedGraph(List<SurfaceNode> nodes)
{
if (nodes.Count <= 1)
@@ -160,11 +160,71 @@ namespace FlaxEditor.Surface
}
/// <summary>
/// Assigns a layer to every node
/// Straightens every connection between nodes in <paramref name="nodes"/>.
/// </summary>
/// <param name="nodeData">The exta node data</param>
/// <param name="endNodes">The end nodes</param>
/// <returns>The number of the maximum layer</returns>
/// <param name="nodes">List of nodes.</param>
public void StraightenGraphConnections(List<SurfaceNode> nodes)
{
if (nodes.Count <= 1)
return;
List<MoveNodesAction> undoActions = new List<MoveNodesAction>();
// Only process nodes that have any connection
List<SurfaceNode> connectedNodes = nodes.Where(n => n.GetBoxes().Any(b => b.HasAnyConnection)).ToList();
if (connectedNodes.Count == 0)
return;
for (int i = 0; i < connectedNodes.Count - 1; i++)
{
SurfaceNode nodeA = connectedNodes[i];
List<Box> connectedOutputBoxes = nodeA.GetBoxes().Where(b => b.IsOutput && b.HasAnyConnection).ToList();
for (int j = 0; j < connectedOutputBoxes.Count; j++)
{
Box boxA = connectedOutputBoxes[j];
for (int b = 0; b < boxA.Connections.Count; b++)
{
Box boxB = boxA.Connections[b];
// Ensure the other node is selected
if (!connectedNodes.Contains(boxB.ParentNode))
continue;
// Node with no outgoing connections reached. Advance to next node in list
if (boxA == null || boxB == null)
continue;
SurfaceNode nodeB = boxB.ParentNode;
// Calculate the Y offset needed for nodeB to align boxB's Y to boxA's Y
float boxASurfaceY = boxA.PointToParent(this, Float2.Zero).Y;
float boxBSurfaceY = boxB.PointToParent(this, Float2.Zero).Y;
float deltaY = (boxASurfaceY - boxBSurfaceY) / ViewScale;
Float2 delta = new Float2(0f, deltaY);
nodeB.Location += delta;
if (Undo != null)
undoActions.Add(new MoveNodesAction(Context, new[] { nodeB.ID }, delta));
}
}
}
if (undoActions.Count > 0)
Undo?.AddAction(new MultiUndoAction(undoActions, "Straightned "));
MarkAsEdited(false);
}
/// <summary>
/// Assigns a layer to every node.
/// </summary>
/// <param name="nodeData">The exta node data.</param>
/// <param name="endNodes">The end nodes.</param>
/// <returns>The number of the maximum layer.</returns>
private int SetLayers(Dictionary<SurfaceNode, NodeFormattingData> nodeData, List<SurfaceNode> endNodes)
{
// Longest path layering
@@ -201,12 +261,12 @@ namespace FlaxEditor.Surface
/// <summary>
/// Sets the node offsets
/// Sets the node offsets.
/// </summary>
/// <param name="nodeData">The exta node data</param>
/// <param name="endNodes">The end nodes</param>
/// <param name="maxLayer">The number of the maximum layer</param>
/// <returns>The number of the maximum offset</returns>
/// <param name="nodeData">The exta node data.</param>
/// <param name="endNodes">The end nodes.</param>
/// <param name="maxLayer">The number of the maximum layer.</param>
/// <returns>The number of the maximum offset.</returns>
private int SetOffsets(Dictionary<SurfaceNode, NodeFormattingData> nodeData, List<SurfaceNode> endNodes, int maxLayer)
{
int maxOffset = 0;
@@ -287,10 +347,10 @@ namespace FlaxEditor.Surface
/// Align given nodes on a graph using the given alignment type.
/// Ignores any potential overlap.
/// </summary>
/// <param name="nodes">List of nodes</param>
/// <param name="alignmentType">Alignemnt type</param>
/// <param name="nodes">List of nodes.</param>
/// <param name="alignmentType">Alignemnt type.</param>
public void AlignNodes(List<SurfaceNode> nodes, NodeAlignmentType alignmentType)
{
{
if(nodes.Count <= 1)
return;
@@ -328,8 +388,8 @@ namespace FlaxEditor.Surface
/// <summary>
/// Distribute the given nodes as equally as possible inside the bounding box, if no fit can be done it will use a default pad of 10 pixels between nodes.
/// </summary>
/// <param name="nodes">List of nodes</param>
/// <param name="vertically">If false will be done horizontally, if true will be done vertically</param>
/// <param name="nodes">List of nodes.</param>
/// <param name="vertically">If false will be done horizontally, if true will be done vertically.</param>
public void DistributeNodes(List<SurfaceNode> nodes, bool vertically)
{
if(nodes.Count <= 1)

View File

@@ -416,6 +416,7 @@ namespace FlaxEditor.Surface
new InputActionsContainer.Binding(options => options.Cut, Cut),
new InputActionsContainer.Binding(options => options.Duplicate, Duplicate),
new InputActionsContainer.Binding(options => options.NodesAutoFormat, () => { FormatGraph(SelectedNodes); }),
new InputActionsContainer.Binding(options => options.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); }),
new InputActionsContainer.Binding(options => options.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }),
new InputActionsContainer.Binding(options => options.NodesAlignMiddle, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }),
new InputActionsContainer.Binding(options => options.NodesAlignBottom, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }),
@@ -542,11 +543,12 @@ namespace FlaxEditor.Surface
nodes.Add(context);
context = context.Parent;
}
float margin = 1;
float x = NavigationBar.DefaultButtonsMargin;
float h = toolStrip.ItemsHeight - 2 * ToolStrip.DefaultMarginV;
float h = toolStrip.ItemsHeight - 2 * margin;
for (int i = nodes.Count - 1; i >= 0; i--)
{
var button = new VisjectContextNavigationButton(this, nodes[i].Context, x, ToolStrip.DefaultMarginV, h);
var button = new VisjectContextNavigationButton(this, nodes[i].Context, x, margin, h);
button.PerformLayout();
x += button.Width + NavigationBar.DefaultButtonsMargin;
navigationBar.AddChild(button);

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml;
@@ -471,7 +470,7 @@ namespace FlaxEditor.Windows.Assets
private void OnOverrideMethodClicked()
{
var cm = new ContextMenu();
var cm = new ItemsListContextMenu(235);
var window = (VisualScriptWindow)Values[0];
var scriptMeta = window.Asset.Meta;
var baseType = TypeUtils.GetType(scriptMeta.BaseTypename);
@@ -499,27 +498,39 @@ namespace FlaxEditor.Windows.Assets
if (isAlreadyAdded)
continue;
var cmButton = cm.AddButton($"{name} (in {member.DeclaringType.Name})");
cmButton.TooltipText = Editor.Instance.CodeDocs.GetTooltip(member);
cmButton.Clicked += () =>
var item = new ItemsListContextMenu.Item
{
var surface = ((VisualScriptWindow)Values[0]).Surface;
var surfaceBounds = surface.AllNodesBounds;
surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Float2(200, 150)).MakeExpanded(400.0f));
var node = surface.Context.SpawnNode(16, 3, surfaceBounds.BottomLeft + new Float2(0, 50), new object[]
{
name,
parameters.Length,
Utils.GetEmptyArray<byte>()
});
surface.Select(node);
Name = $"{name} (in {member.DeclaringType.Name})",
TooltipText = Editor.Instance.CodeDocs.GetTooltip(member),
Tag = new object[] { name, parameters.Length, Utils.GetEmptyArray<byte>() },
// Do some basic sorting based on if the method is defined directly in the script base class
SortScore = member.DeclaringType == member.Type.ReflectedType ? 1 : 0,
};
cm.AddItem(item);
}
}
if (!cm.Items.Any())
cm.ItemClicked += (item) =>
{
cm.AddButton("Nothing to override");
var surface = ((VisualScriptWindow)Values[0]).Surface;
var surfaceBounds = surface.AllNodesBounds;
surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Float2(200, 150)).MakeExpanded(400.0f));
var node = surface.Context.SpawnNode(16, 3, surfaceBounds.BottomLeft + new Float2(0, 50), item.Tag as object[]);
surface.Select(node);
};
if (cm.ItemsPanel.ChildrenCount == 0)
{
var item = new ItemsListContextMenu.Item
{
Name = "Nothing to override"
};
item.Enabled = false;
cm.AddItem(item);
}
cm.SortItems();
cm.Show(_overrideButton, new Float2(0, _overrideButton.Height));
}
}

View File

@@ -194,17 +194,18 @@ namespace FlaxEditor.Windows
nodes.Add(node);
node = node.ParentNode;
}
float margin = 1;
float x = NavigationBar.DefaultButtonsMargin;
float h = _toolStrip.ItemsHeight - 2 * ToolStrip.DefaultMarginV;
float h = _toolStrip.ItemsHeight - 2 * margin;
for (int i = nodes.Count - 1; i >= 0; i--)
{
var button = new ContentNavigationButton(nodes[i], x, ToolStrip.DefaultMarginV, h);
var button = new ContentNavigationButton(nodes[i], x, margin, h);
button.PerformLayout();
x += button.Width + NavigationBar.DefaultButtonsMargin;
_navigationBar.AddChild(button);
if (i > 0)
{
var separator = new ContentNavigationSeparator(button, x, ToolStrip.DefaultMarginV, h);
var separator = new ContentNavigationSeparator(button, x, margin, h);
separator.PerformLayout();
x += separator.Width + NavigationBar.DefaultButtonsMargin;
_navigationBar.AddChild(separator);
@@ -215,6 +216,7 @@ namespace FlaxEditor.Windows
// Update
_navigationBar.IsLayoutLocked = wasLayoutLocked;
_navigationBar.PerformLayout();
UpdateNavigationBarBounds();
}
/// <summary>

View File

@@ -1016,6 +1016,21 @@ namespace FlaxEditor.Windows
_navigateUpButton.Enabled = folder != null && _tree.SelectedNode != _root;
}
private void UpdateNavigationBarBounds()
{
if (_navigationBar != null && _toolStrip != null)
{
var bottomPrev = _toolStrip.Bottom;
_navigationBar.UpdateBounds(_toolStrip);
if (bottomPrev != _toolStrip.Bottom)
{
// Navigation bar changed toolstrip height
_split.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0);
PerformLayout();
}
}
}
/// <inheritdoc />
public override void OnInit()
{
@@ -1200,9 +1215,9 @@ namespace FlaxEditor.Windows
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
base.PerformLayoutBeforeChildren();
UpdateNavigationBarBounds();
_navigationBar?.UpdateBounds(_toolStrip);
base.PerformLayoutBeforeChildren();
}
/// <inheritdoc />

View File

@@ -335,12 +335,12 @@ namespace FlaxEditor.Windows
{
Parent = this,
};
toolstrip.AddButton("Clear", Clear).LinkTooltip("Clears all log entries");
toolstrip.AddButton("Clear", Clear).LinkTooltip("Clears all log entries.");
_clearOnPlayButton = (ToolStripButton)toolstrip.AddButton("Clear on Play", () =>
{
editor.Options.Options.Interface.DebugLogClearOnPlay = _clearOnPlayButton.Checked;
editor.Options.Apply(editor.Options.Options);
}).SetAutoCheck(true).LinkTooltip("Clears all log entries on enter playmode");
}).SetAutoCheck(true).LinkTooltip("Clears all log entries on enter playmode.");
_collapseLogsButton = (ToolStripButton)toolstrip.AddButton("Collapse", () =>
{
editor.Options.Options.Interface.DebugLogCollapse = _collapseLogsButton.Checked;
@@ -350,14 +350,14 @@ namespace FlaxEditor.Windows
{
editor.Options.Options.Interface.DebugLogPauseOnError = _pauseOnErrorButton.Checked;
editor.Options.Apply(editor.Options.Options);
}).SetAutoCheck(true).LinkTooltip("Performs auto pause on error");
}).SetAutoCheck(true).LinkTooltip("Performs auto pause on error.");
toolstrip.AddSeparator();
_groupButtons[0] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Error32, () => { OnGroupButtonPressed(0); })
.SetAutoCheck(true).LinkTooltip("Shows/hides error messages");
.SetAutoCheck(true).LinkTooltip("Shows/hides error messages.");
_groupButtons[1] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Warning32, () => { OnGroupButtonPressed(1); })
.SetAutoCheck(true).LinkTooltip("Shows/hides warning messages");
.SetAutoCheck(true).LinkTooltip("Shows/hides warning messages.");
_groupButtons[2] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Info32, () => { OnGroupButtonPressed(2); })
.SetAutoCheck(true).LinkTooltip("Shows/hides info messages");
.SetAutoCheck(true).LinkTooltip("Shows/hides info messages.");
UpdateCount();
// Split panel
@@ -495,6 +495,7 @@ namespace FlaxEditor.Windows
// Pause on Error (we should do it as fast as possible)
if (newEntry.Group == LogGroup.Error && _pauseOnErrorButton.Checked && Editor.StateMachine.CurrentState == Editor.StateMachine.PlayingState)
{
Editor.Log("Pause Play mode on error (toggle this behaviour in the Debug Log panel)");
Editor.Simulation.RequestPausePlay();
}
}

View File

@@ -970,8 +970,9 @@ namespace FlaxEditor.Windows
public void BuildAndRun()
{
Editor.Log("Building and running");
GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out var buildConfiguration);
GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out _);
var numberOfClients = Editor.Options.Options.Interface.NumberOfGameClientsToLaunch;
var buildConfig = Editor.Options.Options.Interface.CookAndRunBuildConfiguration;
for (int i = 0; i < numberOfClients; i++)
{
var buildOptions = BuildOptions.AutoRun;
@@ -984,7 +985,7 @@ namespace FlaxEditor.Windows
{
Output = _buildTabProxy.PerPlatformOptions[platform].Output,
Platform = buildPlatform,
Mode = buildConfiguration,
Mode = buildConfig,
},
Options = buildOptions,
});
@@ -997,8 +998,9 @@ namespace FlaxEditor.Windows
public void RunCooked()
{
Editor.Log("Running cooked build");
GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out var buildConfiguration);
GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out _);
var numberOfClients = Editor.Options.Options.Interface.NumberOfGameClientsToLaunch;
var buildConfig = Editor.Options.Options.Interface.CookAndRunBuildConfiguration;
for (int i = 0; i < numberOfClients; i++)
{
_buildingQueue.Enqueue(new QueueItem
@@ -1007,7 +1009,7 @@ namespace FlaxEditor.Windows
{
Output = _buildTabProxy.PerPlatformOptions[platform].Output,
Platform = buildPlatform,
Mode = buildConfiguration,
Mode = buildConfig,
},
Options = BuildOptions.AutoRun | BuildOptions.NoCook,
});

View File

@@ -63,6 +63,16 @@ namespace FlaxEditor.Windows
},
};
/// <summary>
/// Fired when the game window audio is muted.
/// </summary>
public event Action MuteAudio;
/// <summary>
/// Fired when the game window master audio volume is changed.
/// </summary>
public event Action<float> MasterVolumeChanged;
/// <summary>
/// Gets the viewport.
/// </summary>
@@ -120,6 +130,7 @@ namespace FlaxEditor.Windows
{
Audio.MasterVolume = value ? 0 : AudioVolume;
_audioMuted = value;
MuteAudio?.Invoke();
}
}
@@ -134,6 +145,7 @@ namespace FlaxEditor.Windows
if (!AudioMuted)
Audio.MasterVolume = value;
_audioVolume = value;
MasterVolumeChanged?.Invoke(value);
}
}

View File

@@ -9,6 +9,7 @@
#include "Engine/Render2D/Font.h"
#include "Engine/Render2D/TextLayoutOptions.h"
#include "Engine/Render2D/Render2D.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Content/Content.h"
#include "FlaxEngine.Gen.h"
@@ -188,8 +189,7 @@ void SplashScreen::Show()
// Setup
_dpiScale = dpiScale;
_width = settings.Size.X;
_height = settings.Size.Y;
_size = settings.Size;
_startTime = DateTime::NowUTC();
auto str = Globals::ProjectFolder;
#if PLATFORM_WIN32
@@ -214,6 +214,12 @@ void SplashScreen::Show()
font->OnLoaded.Bind<SplashScreen, &SplashScreen::OnFontLoaded>(this);
}
// Load custom image
_splashTexture.Loaded.Bind<SplashScreen, &SplashScreen::OnSplashLoaded>(this);
String splashImagePath = Globals::ProjectContentFolder / TEXT("SplashImage.flax");
if (FileSystem::FileExists(splashImagePath))
_splashTexture = Content::LoadAsync<Texture>(splashImagePath);
_window->Show();
}
@@ -227,6 +233,10 @@ void SplashScreen::Close()
// Close window
_window->Close(ClosingReason::CloseEvent);
_window = nullptr;
_titleFont = nullptr;
_subtitleFont = nullptr;
_splashTexture = nullptr;
}
void SplashScreen::OnShown()
@@ -239,16 +249,29 @@ void SplashScreen::OnShown()
void SplashScreen::OnDraw()
{
const float s = _dpiScale;
const float width = _width;
const float height = _height;
const float width = _size.X;
const float height = _size.Y;
// Peek time
const float time = static_cast<float>((DateTime::NowUTC() - _startTime).GetTotalSeconds());
// Background
const float lightBarHeight = 112 * s;
Render2D::FillRectangle(Rectangle(0, 0, width, 150 * s), Color::FromRGB(0x1C1C1C));
Render2D::FillRectangle(Rectangle(0, lightBarHeight, width, height), Color::FromRGB(0x0C0C0C));
float lightBarHeight = 112 * s;
if (_splashTexture != nullptr)
{
if (_splashTexture->IsLoaded())
{
lightBarHeight = height - lightBarHeight + 20 * s;
Render2D::DrawTexture(_splashTexture, Rectangle(0, 0, width, height));
Color rectColor = Color::FromRGB(0x0C0C0C);
Render2D::FillRectangle(Rectangle(0, lightBarHeight, width, height - lightBarHeight),rectColor.AlphaMultiplied(0.85f), rectColor.AlphaMultiplied(0.85f), rectColor, rectColor);
}
}
else
{
Render2D::FillRectangle(Rectangle(0, 0, width, 150 * s), Color::FromRGB(0x1C1C1C));
Render2D::FillRectangle(Rectangle(0, lightBarHeight, width, height), Color::FromRGB(0x0C0C0C));
}
// Animated border
const float anim = Math::Sin(time * 4.0f) * 0.5f + 0.5f;
@@ -276,15 +299,27 @@ void SplashScreen::OnDraw()
for (int32 i = 0; i < 4 - static_cast<int32>(time * 2.0f) % 4; i++)
subtitle += TEXT(' ');
}
layout.Bounds = Rectangle(width - 224 * s, lightBarHeight - 39 * s, 220 * s, 35 * s);
if (_splashTexture != nullptr)
{
layout.Bounds = Rectangle(width - 224 * s, lightBarHeight + 4 * s, 220 * s, 35 * s);
layout.VerticalAlignment = TextAlignment::Near;
}
else
{
layout.Bounds = Rectangle(width - 224 * s, lightBarHeight - 39 * s, 220 * s, 35 * s);
layout.VerticalAlignment = TextAlignment::Far;
}
layout.Scale = 1.0f;
layout.HorizontalAlignment = TextAlignment::Far;
layout.VerticalAlignment = TextAlignment::Far;
Render2D::DrawText(_subtitleFont, subtitle, Color::FromRGB(0x8C8C8C), layout);
// Additional info
const float infoMargin = 6 * s;
layout.Bounds = Rectangle(infoMargin, lightBarHeight + infoMargin, width - (2 * infoMargin), height - lightBarHeight - (2 * infoMargin));
if (_splashTexture != nullptr)
layout.Bounds = Rectangle(infoMargin + 4 * s, lightBarHeight + infoMargin, width - (2 * infoMargin), height - lightBarHeight - (2 * infoMargin));
else
layout.Bounds = Rectangle(infoMargin, lightBarHeight + infoMargin, width - (2 * infoMargin), height - lightBarHeight - (2 * infoMargin));
layout.HorizontalAlignment = TextAlignment::Near;
layout.VerticalAlignment = TextAlignment::Center;
Render2D::DrawText(_subtitleFont, _infoText, Color::FromRGB(0xFFFFFF) * 0.9f, layout);
@@ -307,3 +342,14 @@ void SplashScreen::OnFontLoaded(Asset* asset)
_titleFont = font->CreateFont(35 * s);
_subtitleFont = font->CreateFont(9 * s);
}
void SplashScreen::OnSplashLoaded()
{
// Resize window to be larger if texture is being used
auto desktopSize = Platform::GetDesktopSize();
auto xSize = (desktopSize.X / (600.0f * 3.0f)) * 600.0f;
auto ySize = (desktopSize.Y / (200.0f * 3.0f)) * 200.0f;
_window->SetClientSize(Float2(xSize, ySize));
_size = _window->GetSize();
_window->SetPosition((desktopSize - _size) * 0.5f);
}

View File

@@ -2,6 +2,8 @@
#pragma once
#include "Engine/Content/Assets/Texture.h"
#include "Engine/Content/AssetReference.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Platform/Window.h"
@@ -18,10 +20,12 @@ private:
Window* _window = nullptr;
Font* _titleFont = nullptr;
Font* _subtitleFont = nullptr;
AssetReference<Texture> _splashTexture;
String _title;
DateTime _startTime;
String _infoText;
float _dpiScale, _width, _height;
float _dpiScale;
Float2 _size;
StringView _quote;
public:
@@ -78,4 +82,5 @@ private:
void OnDraw();
bool HasLoadedFonts() const;
void OnFontLoaded(Asset* asset);
void OnSplashLoaded();
};

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
using FlaxEditor.GUI.Tabs;
using FlaxEditor.GUI.Tree;
@@ -98,10 +99,22 @@ namespace FlaxEditor.Windows
}
}
[Flags]
private enum SearchFilter
{
UI = 1,
Actors = 2,
Primitives = 4,
[HideInEditor]
Default = UI | Actors | Primitives,
}
private TextBox _searchBox;
private ContainerControl _groupSearch;
private Tabs _actorGroups;
private ContainerControl groupPrimitives;
private Button _viewDropdown;
private int _searchFilterMask = (int)SearchFilter.Default;
/// <summary>
/// The editor instance.
@@ -127,16 +140,25 @@ namespace FlaxEditor.Windows
UseScroll = true,
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
TabsSize = new Float2(120, 32),
TabsSize = new Float2(90, 32),
Parent = this,
};
_groupSearch = CreateGroupWithList(_actorGroups, "Search", 26);
_viewDropdown = new Button(2, 2, 45.0f, TextBoxBase.DefaultHeight)
{
TooltipText = "Change search filter options.",
Text = "Filters",
Parent = _groupSearch.Parent.Parent,
};
_viewDropdown.Clicked += OnViewButtonClicked;
_searchBox = new SearchBox
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Parent = _groupSearch.Parent.Parent,
Bounds = new Rectangle(4, 4, _actorGroups.Width - 8, 18),
Bounds = new Rectangle(_viewDropdown.Right + 2, 2, _actorGroups.Width - 4, TextBoxBase.DefaultHeight),
};
_searchBox.TextChanged += OnSearchBoxTextChanged;
@@ -145,10 +167,38 @@ namespace FlaxEditor.Windows
_actorGroups.SelectedTabIndex = 1;
}
private void OnViewButtonClicked()
{
var menu = new ContextMenu();
AddSearchFilterButton(menu, SearchFilter.UI, "UI");
AddSearchFilterButton(menu, SearchFilter.Actors, "Actors");
AddSearchFilterButton(menu, SearchFilter.Primitives, "Primitives");
menu.Show(_viewDropdown.Parent, _viewDropdown.BottomLeft);
}
private void AddSearchFilterButton(ContextMenu menu, SearchFilter value, string name)
{
var button = menu.AddButton(name);
button.AutoCheck = true;
button.Checked = (_searchFilterMask & (int)value) != 0;
button.Clicked += () =>
{
_searchFilterMask ^= (int)value;
OnSearchBoxTextChanged();
};
}
/// <inheritdoc/>
protected override void PerformLayoutBeforeChildren()
{
base.PerformLayoutBeforeChildren();
_searchBox.Width = _groupSearch.Width - _viewDropdown.Right - 4;
}
private void OnScriptsReload()
{
// Prevent any references to actor types from the game assemblies that will be reloaded
_searchBox.Clear();
_groupSearch.DisposeChildren();
_groupSearch.PerformLayout();
@@ -172,6 +222,7 @@ namespace FlaxEditor.Windows
private void OnScriptsReloadEnd()
{
RefreshActorTabs();
OnSearchBoxTextChanged();
}
private void RefreshActorTabs()
@@ -192,14 +243,21 @@ namespace FlaxEditor.Windows
group.Dispose();
}
// Setup primitives tabs
// Add primitives to primtives and search tab
groupPrimitives = CreateGroupWithList(_actorGroups, "Primitives");
groupPrimitives.AddChild(CreateEditorAssetItem("Cube", "Primitives/Cube.flax"));
_groupSearch.AddChild(CreateEditorAssetItem("Cube", "Primitives/Cube.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Sphere", "Primitives/Sphere.flax"));
_groupSearch.AddChild(CreateEditorAssetItem("Sphere", "Primitives/Sphere.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Plane", "Primitives/Plane.flax"));
_groupSearch.AddChild(CreateEditorAssetItem("Plane", "Primitives/Plane.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Cylinder", "Primitives/Cylinder.flax"));
_groupSearch.AddChild(CreateEditorAssetItem("Cylinder", "Primitives/Cylinder.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Cone", "Primitives/Cone.flax"));
_groupSearch.AddChild(CreateEditorAssetItem("Cone", "Primitives/Cone.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Capsule", "Primitives/Capsule.flax"));
_groupSearch.AddChild(CreateEditorAssetItem("Capsule", "Primitives/Capsule.flax"));
// Created first to order specific tabs
CreateGroupWithList(_actorGroups, "Lights");
@@ -312,57 +370,115 @@ namespace FlaxEditor.Windows
_groupSearch.LockChildrenRecursive();
_groupSearch.DisposeChildren();
foreach (var actorType in Editor.CodeEditing.Actors.Get())
if (((int)SearchFilter.Actors & _searchFilterMask) != 0)
{
ActorToolboxAttribute attribute = null;
foreach (var e in actorType.GetAttributes(false))
foreach (var actorType in Editor.CodeEditing.Actors.Get())
{
if (e is ActorToolboxAttribute actorToolboxAttribute)
ActorToolboxAttribute attribute = null;
foreach (var e in actorType.GetAttributes(false))
{
attribute = actorToolboxAttribute;
break;
if (e is ActorToolboxAttribute actorToolboxAttribute)
{
attribute = actorToolboxAttribute;
break;
}
}
}
var text = (attribute == null) ? actorType.Name : string.IsNullOrEmpty(attribute.Name) ? actorType.Name : attribute.Name;
var text = (attribute == null) ? actorType.Name : string.IsNullOrEmpty(attribute.Name) ? actorType.Name : attribute.Name;
// Display all actors on no search
if (string.IsNullOrEmpty(filterText))
_groupSearch.AddChild(CreateActorItem(Utilities.Utils.GetPropertyNameUI(text), actorType));
if (!QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
continue;
var item = CreateActorItem(Utilities.Utils.GetPropertyNameUI(text), actorType);
SearchFilterHighlights(item, text, ranges);
}
// Hack primitive models into the search results
foreach (var child in groupPrimitives.Children)
{
if (child is Item primitiveAssetItem)
{
var text = primitiveAssetItem.Text;
// Display all actors on no search
if (string.IsNullOrEmpty(filterText))
_groupSearch.AddChild(CreateActorItem(Utilities.Utils.GetPropertyNameUI(text), actorType));
if (!QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
continue;
// Rebuild the path based on item name (it would be better to convert the drag data back to a string somehow)
string path = $"Primitives/{text}.flax";
var item = CreateEditorAssetItem(text, path);
var item = CreateActorItem(Utilities.Utils.GetPropertyNameUI(text), actorType);
SearchFilterHighlights(item, text, ranges);
}
}
if (string.IsNullOrEmpty(filterText))
_groupSearch.SortChildren();
if (((int)SearchFilter.Primitives & _searchFilterMask) != 0)
{
// Hack primitive models into the search results
foreach (var child in groupPrimitives.Children)
{
if (child is Item primitiveAssetItem)
{
var text = primitiveAssetItem.Text;
// Rebuild the path based on item name (it would be better to convert the drag data back to a string somehow)
string path = $"Primitives/{text}.flax";
// Display all primitives on no search
if (string.IsNullOrEmpty(filterText))
_groupSearch.AddChild(CreateEditorAssetItem(text, path));
if (!QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
continue;
var item = CreateEditorAssetItem(text, path);
SearchFilterHighlights(item, text, ranges);
}
}
}
if (((int)SearchFilter.UI & _searchFilterMask) != 0)
{
foreach (var controlType in Editor.Instance.CodeEditing.Controls.Get())
{
if (controlType.IsAbstract)
continue;
ActorToolboxAttribute attribute = null;
foreach (var e in controlType.GetAttributes(false))
{
if (e is ActorToolboxAttribute actorToolboxAttribute)
{
attribute = actorToolboxAttribute;
break;
}
}
var text = (attribute == null) ? controlType.Name : string.IsNullOrEmpty(attribute.Name) ? controlType.Name : attribute.Name;
// Display all controls on no search
if (string.IsNullOrEmpty(filterText))
_groupSearch.AddChild(CreateControlItem(Utilities.Utils.GetPropertyNameUI(controlType.Name), controlType));
if (!QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
continue;
var item = CreateControlItem(Utilities.Utils.GetPropertyNameUI(controlType.Name), controlType);
SearchFilterHighlights(item, text, ranges);
}
}
// Sort the search results alphabetically
_groupSearch.SortChildren();
_groupSearch.UnlockChildrenRecursive();
PerformLayout();
PerformLayout();
}
/// <inheritdoc/>
public override void Draw()
{
base.Draw();
// Show a text to hint the user that either no filter is active or the search does not return any results
bool noSearchResults = _groupSearch.Children.Count == 0 && !string.IsNullOrEmpty(_searchBox.Text);
bool showHint = _searchFilterMask == 0 || noSearchResults;
if (showHint)
{
string hint = noSearchResults ? "No results" : "No search filter active, please enable at least one filter";
var textRect = _groupSearch.Parent.Parent.Bounds;
var style = Style.Current;
Render2D.DrawText(style.FontMedium, hint, textRect, style.ForegroundGrey, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapWords);
}
}
private void SearchFilterHighlights(Item item, string text, QueryFilterHelper.Range[] ranges)
{
_groupSearch.AddChild(item);

View File

@@ -229,18 +229,12 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
auto& context = *Context.Get();
float eventTimeMin = animPrevPos;
float eventTimeMax = animPos;
if (loop && context.DeltaTime * speed < 0)
if (eventTimeMin > eventTimeMax)
{
// Check if animation looped (for anim events shooting during backwards playback)
//const float posNotLooped = startTimePos + oldTimePos;
//if (posNotLooped < 0.0f || posNotLooped > length)
//const int32 animPosCycle = Math::CeilToInt(animPos / anim->GetDuration());
//const int32 animPrevPosCycle = Math::CeilToInt(animPrevPos / anim->GetDuration());
//if (animPosCycle != animPrevPosCycle)
{
Swap(eventTimeMin, eventTimeMax);
}
Swap(eventTimeMin, eventTimeMax);
}
const float eventTime = (float)(animPos / anim->Data.FramesPerSecond);
const float eventDeltaTime = (float)((animPos - animPrevPos) / anim->Data.FramesPerSecond);
for (const auto& track : anim->Events)
@@ -251,7 +245,13 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
continue;
const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f;
#define ADD_OUTGOING_EVENT(type) context.Data->OutgoingEvents.Add({ k.Value.Instance, (AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime, AnimGraphInstanceData::OutgoingEvent::type })
if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration)
if ((k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration
&& (Math::FloorToInt(animPos) != 0 && Math::CeilToInt(animPrevPos) != Math::CeilToInt(anim->GetDuration()) && Math::FloorToInt(animPrevPos) != 0 && Math::CeilToInt(animPos) != Math::CeilToInt(anim->GetDuration())))
// Handle the edge case of an event on 0 or on max animation duration during looping
|| (loop && duration == 0.0f && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == anim->GetDuration())
|| (loop && Math::FloorToInt(animPos) == 0 && Math::CeilToInt(animPrevPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == 0.0f)
|| (loop && Math::FloorToInt(animPrevPos) == 0 && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == 0.0f)
)
{
int32 stateIndex = -1;
if (duration > 1)

View File

@@ -532,7 +532,7 @@ bool Animation::SaveHeader(const ModelData& modelData, WriteStream& stream, int3
// Nested animations
stream.WriteInt32(0); // Empty list
return false;
return stream.HasError();
}
void Animation::GetReferences(Array<Guid>& assets, Array<String>& files) const

View File

@@ -398,7 +398,7 @@ bool Model::LoadHeader(ReadStream& stream, byte& headerVersion)
}
}
return false;
return stream.HasError();
}
#if USE_EDITOR
@@ -457,7 +457,7 @@ bool Model::SaveHeader(WriteStream& stream, const ModelData& modelData)
}
}
return false;
return stream.HasError();
}
bool Model::Save(bool withMeshDataFromGpu, Function<FlaxChunk*(int32)>& getChunk) const

View File

@@ -332,7 +332,7 @@ bool ModelBase::LoadHeader(ReadStream& stream, byte& headerVersion)
stream.Read(slot.Name, 11);
}
return false;
return stream.HasError();
}
bool ModelBase::LoadMesh(MemoryReadStream& stream, byte meshVersion, MeshBase* mesh, MeshData* dataIfReadOnly)

View File

@@ -661,7 +661,7 @@ bool SkinnedModel::LoadHeader(ReadStream& stream, byte& headerVersion)
}
}
return false;
return stream.HasError();
}
#if USE_EDITOR
@@ -691,7 +691,7 @@ bool SkinnedModel::SaveHeader(WriteStream& stream) const
const int32 blendShapes = mesh.BlendShapes.Count();
stream.Write((uint16)blendShapes);
for (const auto& blendShape : mesh.BlendShapes)
blendShape.Save(stream);
blendShape.SaveHeader(stream);
}
}

View File

@@ -55,6 +55,27 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
}
}
}
else
{
// Try model prefab
String pathPrefab = String(StringUtils::GetPathWithoutExtension(path)) + DEFAULT_PREFAB_EXTENSION_DOT;
if (FileSystem::FileExists(pathPrefab))
{
auto prefab = Content::Load<Prefab>(pathPrefab);
if (prefab)
{
for (const auto& e : prefab->ObjectsDataCache)
{
auto importOptionsMember = e.Value->FindMember("ImportOptions");
if (importOptionsMember != e.Value->MemberEnd() && importOptionsMember->value.IsObject())
{
options.Deserialize(*(ISerializable::DeserializeStream*)&importOptionsMember->value, nullptr);
return true;
}
}
}
}
}
return false;
}

View File

@@ -32,13 +32,53 @@ namespace FlaxEngine
/// <summary>
/// Determines whether the specified layer is set in the mask.
/// </summary>
/// <param name="layerName">Name of the layer (from layers settings).</param>
/// <param name="layerName">Name of the layer (from Layers settings).</param>
/// <returns><c>true</c> if the specified layer is set; otherwise, <c>false</c>.</returns>
public bool HasLayer(string layerName)
{
return HasLayer(Level.GetLayerIndex(layerName));
}
/// <summary>
/// Gets a layer mask based on a specific layer names.
/// </summary>
/// <param name="layerNames">The names of the layers (from Layers settings).</param>
/// <returns>A layer mask with the mask set to the layers found. Returns a mask with 0 if not found.</returns>
public static LayersMask GetMask(params string[] layerNames)
{
LayersMask mask = new LayersMask();
foreach (var layerName in layerNames)
{
// Ignore blank entries
if (string.IsNullOrEmpty(layerName))
continue;
int index = Level.GetLayerIndex(layerName);
if (index != -1)
mask.Mask |= (uint)(1 << index);
}
return mask;
}
/// <summary>
/// Gets the layer index based on the layer name.
/// </summary>
/// <param name="layerName">The name of the layer.</param>
/// <returns>The index if found, otherwise, returns -1.</returns>
public static int GetLayerIndex(string layerName)
{
return Level.GetLayerIndex(layerName);
}
/// <summary>
/// Gets the layer name based on the layer index.
/// </summary>
/// <param name="layerIndex">The index of the layer.</param>
/// <returns>The name of the layer if found, otherwise, a blank string.</returns>
public static string GetLayerName(int layerIndex)
{
return Level.GetLayerName(layerIndex);
}
/// <summary>
/// Adds two masks.
/// </summary>

View File

@@ -27,13 +27,30 @@ public:
{
}
/// <summary>
/// Determines whether the specified layer index is set in the mask.
/// </summary>
/// <param name="layerIndex">Index of the layer (zero-based).</param>
/// <returns><c>true</c> if the specified layer is set; otherwise, <c>false</c>.</returns>
FORCE_INLINE bool HasLayer(int32 layerIndex) const
{
return (Mask & (1 << layerIndex)) != 0;
}
/// <summary>
/// Determines whether the specified layer name is set in the mask.
/// </summary>
/// <param name="layerName">Name of the layer (from Layers settings).</param>
/// <returns><c>true</c> if the specified layer is set; otherwise, <c>false</c>.</returns>
bool HasLayer(const StringView& layerName) const;
/// <summary>
/// Gets a layers mask from a specific layer name.
/// </summary>
/// <param name="layerNames">The names of the layers (from Layers settings).</param>
/// <returns>A layers mask with the Mask set to the same Mask as the layer name passed in. Returns a LayersMask with a mask of 0 if no layer found.</returns>
static LayersMask GetMask(Span<StringView> layerNames);
operator uint32() const
{
return Mask;

View File

@@ -4,7 +4,7 @@
#include "StringView.h"
#include "Engine/Core/Collections/Array.h"
String String::Empty;
const String String::Empty;
String::String(const StringAnsi& str)
{

View File

@@ -548,7 +548,7 @@ public:
/// <summary>
/// Instance of the empty string.
/// </summary>
static String Empty;
static const String Empty;
public:
/// <summary>

View File

@@ -9,7 +9,7 @@ StringView StringBuilder::ToStringView() const
return StringView(_data.Get(), _data.Count());
}
StringView StringView::Empty;
const StringView StringView::Empty;
StringView::StringView(const String& str)
: StringViewBase<Char>(str.Get(), str.Length())

View File

@@ -227,7 +227,7 @@ public:
/// <summary>
/// Instance of the empty string.
/// </summary>
static StringView Empty;
static const StringView Empty;
public:
/// <summary>

View File

@@ -16,7 +16,7 @@
PACK_STRUCT(struct DecalMaterialShaderData {
Matrix WorldMatrix;
Matrix InvWorld;
Matrix SVPositionToWorld;
Matrix SvPositionToWorld;
});
DrawPass DecalMaterialShader::GetDrawModes() const
@@ -47,7 +47,9 @@ void DecalMaterialShader::Bind(BindParameters& params)
MaterialParams::Bind(params.ParamsLink, bindMeta);
// Decals use depth buffer to draw on top of the objects
context->BindSR(0, GET_TEXTURE_VIEW_SAFE(params.RenderContext.Buffers->DepthBuffer));
GPUTexture* depthBuffer = params.RenderContext.Buffers->DepthBuffer;
GPUTextureView* depthBufferView = EnumHasAnyFlags(depthBuffer->Flags(), GPUTextureFlags::ReadOnlyDepthView) ? depthBuffer->ViewReadOnlyDepth() : depthBuffer->View();
context->BindSR(0, depthBufferView);
// Setup material constants
{
@@ -65,7 +67,7 @@ void DecalMaterialShader::Bind(BindParameters& params)
0, 0, 1, 0,
-1.0f, 1.0f, 0, 1);
const Matrix svPositionToWorld = offsetMatrix * view.IVP;
Matrix::Transpose(svPositionToWorld, materialData->SVPositionToWorld);
Matrix::Transpose(svPositionToWorld, materialData->SvPositionToWorld);
}
// Bind constants
@@ -90,16 +92,20 @@ void DecalMaterialShader::Unload()
bool DecalMaterialShader::Load()
{
GPUPipelineState::Description psDesc0 = GPUPipelineState::Description::DefaultNoDepth;
psDesc0.VS = _shader->GetVS("VS_Decal");
psDesc0.VS = _shader->GetVS("VS_Decal"); // TODO: move VS_Decal to be shared (eg. in GBuffer.shader)
if (psDesc0.VS == nullptr)
return true;
psDesc0.PS = _shader->GetPS("PS_Decal");
psDesc0.CullMode = CullMode::Normal;
if (GPUDevice::Instance->Limits.HasReadOnlyDepth)
{
psDesc0.DepthEnable = true;
psDesc0.DepthWriteEnable = false;
}
switch (_info.DecalBlendingMode)
{
case MaterialDecalBlendingMode::Translucent:
{
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::SrcAlpha;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -107,9 +113,7 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
}
case MaterialDecalBlendingMode::Stain:
{
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::DestColor;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -117,9 +121,7 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
}
case MaterialDecalBlendingMode::Normal:
{
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::SrcAlpha;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -127,13 +129,10 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
}
case MaterialDecalBlendingMode::Emissive:
{
psDesc0.BlendMode = BlendingMode::Additive;
break;
}
}
_cache.Outside = GPUDevice::Instance->CreatePipelineState();
if (_cache.Outside->Init(psDesc0))
@@ -143,6 +142,7 @@ bool DecalMaterialShader::Load()
}
psDesc0.CullMode = CullMode::Inverted;
psDesc0.DepthEnable = false;
_cache.Inside = GPUDevice::Instance->CreatePipelineState();
if (_cache.Inside->Init(psDesc0))
{

View File

@@ -23,6 +23,8 @@ PACK_STRUCT(struct GUIMaterialShaderData {
Float4 ViewInfo;
Float4 ScreenSize;
Float4 ViewSize;
Float3 ViewPadding0;
float UnscaledTimeParam;
});
void GUIMaterialShader::Bind(BindParameters& params)
@@ -55,7 +57,8 @@ void GUIMaterialShader::Bind(BindParameters& params)
materialData->ViewPos = Float3::Zero;
materialData->ViewFar = 0.0f;
materialData->ViewDir = Float3::Forward;
materialData->TimeParam = params.TimeParam;
materialData->TimeParam = params.Time;
materialData->UnscaledTimeParam = params.UnscaledTime;
materialData->ViewInfo = Float4::Zero;
auto& viewport = Render2D::GetViewport();
materialData->ScreenSize = Float4(viewport.Width, viewport.Height, 1.0f / viewport.Width, 1.0f / viewport.Height);

View File

@@ -148,7 +148,7 @@ public:
const ::DrawCall* DrawCall = nullptr;
MaterialParamsLink* ParamsLink = nullptr;
void* CustomData = nullptr;
float TimeParam;
float Time, UnscaledTime;
bool Instanced = false;
/// <summary>

View File

@@ -37,12 +37,15 @@ GPU_CB_STRUCT(MaterialShaderDataPerView {
Float4 TemporalAAJitter;
Float3 LargeWorldsChunkIndex;
float LargeWorldsChunkSize;
Float3 ViewPadding0;
float UnscaledTimeParam;
});
IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderContext& renderContext)
: GPUContext(context)
, RenderContext(renderContext)
, TimeParam(Time::Draw.UnscaledTime.GetTotalSeconds())
, Time(Time::Draw.Time.GetTotalSeconds())
, UnscaledTime(Time::Draw.UnscaledTime.GetTotalSeconds())
{
}
@@ -50,7 +53,8 @@ IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderC
: GPUContext(context)
, RenderContext(renderContext)
, DrawCall(&drawCall)
, TimeParam(Time::Draw.UnscaledTime.GetTotalSeconds())
, Time(Time::Draw.Time.GetTotalSeconds())
, UnscaledTime(Time::Draw.UnscaledTime.GetTotalSeconds())
, Instanced(instanced)
{
}
@@ -78,7 +82,8 @@ void IMaterial::BindParameters::BindViewData()
cb.ViewPos = view.Position;
cb.ViewFar = view.Far;
cb.ViewDir = view.Direction;
cb.TimeParam = TimeParam;
cb.TimeParam = Time;
cb.UnscaledTimeParam = UnscaledTime;
cb.ViewInfo = view.ViewInfo;
cb.ScreenSize = view.ScreenSize;
cb.TemporalAAJitter = view.TemporalAAJitter;

View File

@@ -10,7 +10,7 @@
/// <summary>
/// Current materials shader version.
/// </summary>
#define MATERIAL_GRAPH_VERSION 173
#define MATERIAL_GRAPH_VERSION 174
class Material;
class GPUShader;

View File

@@ -21,6 +21,8 @@ PACK_STRUCT(struct PostFxMaterialShaderData {
Float4 ScreenSize;
Float4 TemporalAAJitter;
Matrix InverseViewProjectionMatrix;
Float3 ViewPadding0;
float UnscaledTimeParam;
});
void PostFxMaterialShader::Bind(BindParameters& params)
@@ -51,7 +53,8 @@ void PostFxMaterialShader::Bind(BindParameters& params)
materialData->ViewPos = view.Position;
materialData->ViewFar = view.Far;
materialData->ViewDir = view.Direction;
materialData->TimeParam = params.TimeParam;
materialData->TimeParam = params.Time;
materialData->UnscaledTimeParam = params.UnscaledTime;
materialData->ViewInfo = view.ViewInfo;
materialData->ScreenSize = view.ScreenSize;
materialData->TemporalAAJitter = view.TemporalAAJitter;

View File

@@ -175,6 +175,12 @@ public:
return (const T*)FindCustomBuffer(name, withLinked);
}
template<class T>
const T* FindLinkedBuffer(const StringView& name) const
{
return LinkedCustomBuffers ? (const T*)LinkedCustomBuffers->FindCustomBuffer(name, true) : nullptr;
}
template<class T>
T* GetCustomBuffer(const StringView& name, bool withLinked = true)
{

View File

@@ -184,6 +184,14 @@ void AnimatedModel::GetNodeTransformation(const StringView& nodeName, Matrix& no
GetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
}
void AnimatedModel::GetNodeTransformation(Array<NodeTransformation>& nodeTransformations, bool worldSpace) const
{
for (NodeTransformation& item : nodeTransformations)
{
GetNodeTransformation(item.NodeIndex, item.NodeMatrix, worldSpace);
}
}
void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTransformation, bool worldSpace)
{
if (GraphInstance.NodesPose.IsEmpty())
@@ -201,6 +209,33 @@ void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTra
OnAnimationUpdated();
}
void AnimatedModel::SetNodeTransformation(const Array<NodeTransformation>& nodeTransformations, bool worldSpace)
{
if (GraphInstance.NodesPose.IsEmpty())
const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
// Calculate it once, outside loop
Matrix invWorld;
if (worldSpace)
{
Matrix world;
GetLocalToWorldMatrix(world);
Matrix::Invert(world, invWorld);
}
for (int i = 0; i < nodeTransformations.Count(); i++)
{
int nodeIndex = nodeTransformations[i].NodeIndex;
CHECK(nodeIndex >= 0 && nodeIndex < GraphInstance.NodesPose.Count());
GraphInstance.NodesPose[nodeIndex] = nodeTransformations[i].NodeMatrix;
if (worldSpace)
{
GraphInstance.NodesPose[nodeIndex] = GraphInstance.NodesPose[nodeIndex] * invWorld;
}
}
OnAnimationUpdated();
}
void AnimatedModel::SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace)
{
SetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
@@ -830,7 +865,10 @@ void AnimatedModel::OnAnimationUpdated_Async()
_skinningData.OnDataChanged(!PerBoneMotionBlur);
}
UpdateBounds();
if (UpdateWhenOffscreen)
{
UpdateBounds();
}
}
void AnimatedModel::OnAnimationUpdated_Sync()

View File

@@ -18,6 +18,24 @@ class FLAXENGINE_API AnimatedModel : public ModelInstanceActor, IAssetReference
DECLARE_SCENE_OBJECT(AnimatedModel);
friend class AnimationsSystem;
/// <summary>
/// Keeps the data of a Node and its relevant Transform Matrix together when passing it between functions.
/// </summary>
API_STRUCT() struct NodeTransformation
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NodeTransformation);
/// <summary>
/// The index of the node in the node hierarchy.
/// </summary>
API_FIELD() uint32 NodeIndex;
/// <summary>
/// The transformation matrix of the node
/// </summary>
API_FIELD() Matrix NodeMatrix;
};
/// <summary>
/// Describes the animation graph updates frequency for the animated model.
/// </summary>
@@ -242,6 +260,14 @@ public:
/// <param name="worldSpace">True if convert matrices into world-space, otherwise returned values will be in local-space of the actor.</param>
API_FUNCTION() void GetNodeTransformation(const StringView& nodeName, API_PARAM(Out) Matrix& nodeTransformation, bool worldSpace = false) const;
/// <summary>
/// Gets the node final transformation for a series of nodes.
/// </summary>
/// <param name="nodeTransformations">The series of nodes that will be returned</param>
/// <param name="worldSpace">True if convert matrices into world-space, otherwise returned values will be in local-space of the actor.</param>
/// <returns></returns>
API_FUNCTION() void GetNodeTransformation(API_PARAM(Ref) Array<NodeTransformation>& nodeTransformations, bool worldSpace = false) const;
/// <summary>
/// Sets the node final transformation. If multiple nodes are to be set within a frame, do not use set worldSpace to true, and do the conversion yourself to avoid recalculation of inv matrices.
/// </summary>
@@ -258,6 +284,14 @@ public:
/// <param name="worldSpace">True if convert matrices from world-space, otherwise values will be in local-space of the actor.</param>
API_FUNCTION() void SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace = false);
/// <summary>
/// Sets a group of nodes final transformation.
/// </summary>
/// <param name="nodeTransformations">Array of the final node transformation matrix.</param>
/// <param name="worldSpace">True if convert matrices from world-space, otherwise values will be in local-space of the actor.</param>
/// <returns></returns>
API_FUNCTION() void SetNodeTransformation(const Array<NodeTransformation>& nodeTransformations, bool worldSpace = false);
/// <summary>
/// Finds the closest node to a given location.
/// </summary>

View File

@@ -65,6 +65,21 @@ bool LayersMask::HasLayer(const StringView& layerName) const
return HasLayer(Level::GetLayerIndex(layerName));
}
LayersMask LayersMask::GetMask(Span<StringView> layerNames)
{
LayersMask mask(0);
for (StringView& layerName : layerNames)
{
// Ignore blank entries
if (layerName.Length() == 0)
continue;
int32 index = Level::GetLayerIndex(layerName);
if (index != -1)
mask.Mask |= (uint32)(1 << index);
}
return mask;
}
enum class SceneEventType
{
OnSceneSaving = 0,
@@ -824,6 +839,18 @@ int32 Level::GetLayerIndex(const StringView& layer)
return result;
}
StringView Level::GetLayerName(const int32 layerIndex)
{
for (int32 i = 0; i < 32; i++)
{
if (i == layerIndex)
{
return Layers[i];
}
}
return TEXT("");
}
void Level::callActorEvent(ActorEventType eventType, Actor* a, Actor* b)
{
PROFILE_CPU();

View File

@@ -539,6 +539,13 @@ public:
/// </summary>
API_FUNCTION() static int32 GetLayerIndex(const StringView& layer);
/// <summary>
/// Gets the name of the layer based on the index.
/// </summary>
/// <param name="layerIndex">The index to find the layer string. 0 - 32.</param>
/// <returns>The layer string. Returns a blank string if index not found.</returns>
API_FUNCTION() static StringView GetLayerName(const int32 layerIndex);
private:
// Actor API
enum class ActorEventType

View File

@@ -549,6 +549,19 @@ void ParticleEffect::OnAssetChanged(Asset* asset, void* caller)
}
void ParticleEffect::OnAssetLoaded(Asset* asset, void* caller)
{
ApplyModifiedParameters();
#if USE_EDITOR
// When one of the emitters gets edited, cached parameters need to be applied
auto& emitters = ParticleSystem.Get()->Emitters;
for (auto& emitter : emitters)
{
emitter.Loaded.BindUnique<ParticleEffect, &ParticleEffect::OnParticleEmitterLoaded>(this);
}
#endif
}
void ParticleEffect::OnParticleEmitterLoaded()
{
ApplyModifiedParameters();
}
@@ -844,6 +857,10 @@ void ParticleEffect::OnActiveInTreeChanged()
CacheModifiedParameters();
Instance.ClearState();
}
else
{
ApplyModifiedParameters();
}
}
void ParticleEffect::OnTransformChanged()

View File

@@ -394,6 +394,7 @@ private:
void ApplyModifiedParameters();
void OnParticleSystemModified();
void OnParticleSystemLoaded();
void OnParticleEmitterLoaded();
// [IAssetReference]
void OnAssetChanged(Asset* asset, void* caller) override;

View File

@@ -7,10 +7,6 @@
#include "Engine/Platform/File.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringView.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
#include <sys/types.h>
#include <sys/stat.h>
@@ -18,281 +14,9 @@
#include <stdio.h>
#include <cerrno>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
const DateTime UnixEpoch(1970, 1, 1);
bool AppleFileSystem::CreateDirectory(const StringView& path)
{
const StringAsANSI<> pathAnsi(*path, path.Length());
// Skip if already exists
struct stat fileInfo;
if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
{
return false;
}
// Recursively do it all again for the parent directory, if any
const int32 slashIndex = path.FindLast('/');
if (slashIndex > 1)
{
if (CreateDirectory(path.Substring(0, slashIndex)))
{
return true;
}
}
// Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
}
bool DeletePathTree(const char* path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Recursively remove a nested directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (DeletePathTree(full_path))
return true;
continue;
}
// Remove a file object
if (unlink(full_path) != 0)
return true;
}
// Remove the devastated directory and close the object of it
if (rmdir(path) != 0)
return true;
closedir(dir);
return false;
}
bool AppleFileSystem::DeleteDirectory(const String& path, bool deleteContents)
{
const StringAsANSI<> pathANSI(*path, path.Length());
if (deleteContents)
return DeletePathTree(pathANSI.Get());
return rmdir(pathANSI.Get()) != 0;
}
bool AppleFileSystem::DirectoryExists(const StringView& path)
{
struct stat fileInfo;
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISDIR(fileInfo.st_mode);
}
return false;
}
bool AppleFileSystem::DirectoryGetFiles(Array<String>& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
{
const StringAsANSI<> pathANSI(*path, path.Length());
const StringAsANSI<> searchPatternANSI(searchPattern);
// Check if use only top directory
if (option == DirectorySearchOption::TopDirectoryOnly)
return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
}
bool AppleFileSystem::GetChildDirectories(Array<String>& results, const String& path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
const StringAsANSI<> pathANSI(*path, path.Length());
const char* pathStr = pathANSI.Get();
// Stat for the path
stat(pathStr, &statPath);
// If path does not exist or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(pathStr)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(pathStr);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, pathStr);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
// Add directory
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool AppleFileSystem::FileExists(const StringView& path)
{
struct stat fileInfo;
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISREG(fileInfo.st_mode);
}
return false;
}
bool AppleFileSystem::DeleteFile(const StringView& path)
{
const StringAsANSI<> pathANSI(*path, path.Length());
return unlink(pathANSI.Get()) != 0;
}
uint64 AppleFileSystem::GetFileSize(const StringView& path)
{
struct stat fileInfo;
fileInfo.st_size = 0;
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
// Check for directories
if (S_ISDIR(fileInfo.st_mode))
{
fileInfo.st_size = 0;
}
}
return fileInfo.st_size;
}
bool AppleFileSystem::IsReadOnly(const StringView& path)
{
const StringAsANSI<> pathANSI(*path, path.Length());
if (access(pathANSI.Get(), W_OK) == -1)
{
return errno == EACCES;
}
return false;
}
bool AppleFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
{
const StringAsANSI<> pathANSI(*path, path.Length());
struct stat fileInfo;
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
if (isReadOnly)
{
fileInfo.st_mode &= ~S_IWUSR;
}
else
{
fileInfo.st_mode |= S_IWUSR;
}
return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
}
return false;
}
bool AppleFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
{
if (!overwrite && FileExists(dst))
{
// Already exists
return true;
}
if (overwrite)
{
unlink(StringAsANSI<>(*dst, dst.Length()).Get());
}
if (rename(StringAsANSI<>(*src, src.Length()).Get(), StringAsANSI<>(*dst, dst.Length()).Get()) != 0)
{
if (errno == EXDEV)
{
if (!CopyFile(dst, src))
{
unlink(StringAsANSI<>(*src, src.Length()).Get());
return false;
}
}
return true;
}
return false;
}
bool AppleFileSystem::CopyFile(const StringView& dst, const StringView& src)
{
const StringAsANSI<> srcANSI(*src, src.Length());
@@ -352,156 +76,6 @@ out_error:
return true;
}
bool AppleFileSystem::getFilesFromDirectoryTop(Array<String>& results, const char* path, const char* searchPattern)
{
size_t pathLength;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
DIR* dir = opendir(path);
if (dir == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, path);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for file
if (S_ISREG(statEntry.st_mode) != 0)
{
// Validate with filter
const int32 fullPathLength = StringUtils::Length(fullPath);
const int32 searchPatternLength = StringUtils::Length(searchPattern);
if (searchPatternLength == 0 || StringUtils::Compare(searchPattern, "*") == 0)
{
// All files
}
else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
{
// Path ending
}
else
{
// TODO: implement all cases in a generic way
continue;
}
// Add file
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool AppleFileSystem::getFilesFromDirectoryAll(Array<String>& results, const char* path, const char* searchPattern)
{
// Find all files in this directory
getFilesFromDirectoryTop(results, path, searchPattern);
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (getFilesFromDirectoryAll(results, full_path, searchPattern))
{
closedir(dir);
return true;
}
}
}
closedir(dir);
return false;
}
DateTime AppleFileSystem::GetFileLastEditTime(const StringView& path)
{
struct stat fileInfo;
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) == -1)
{
return DateTime::MinValue();
}
const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
return UnixEpoch + timeSinceEpoch;
}
void AppleFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& result)
{
String home;

View File

@@ -4,33 +4,17 @@
#if PLATFORM_MAC || PLATFORM_IOS
#include "Engine/Platform/Base/FileSystemBase.h"
#include "Engine/Platform/Unix/UnixFileSystem.h"
/// <summary>
/// Apple platform implementation of filesystem service.
/// </summary>
class FLAXENGINE_API AppleFileSystem : public FileSystemBase
class FLAXENGINE_API AppleFileSystem : public UnixFileSystem
{
public:
// [FileSystemBase]
static bool CreateDirectory(const StringView& path);
static bool DeleteDirectory(const String& path, bool deleteContents = true);
static bool DirectoryExists(const StringView& path);
static bool DirectoryGetFiles(Array<String, HeapAllocation>& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
static bool GetChildDirectories(Array<String, HeapAllocation>& results, const String& path);
static bool FileExists(const StringView& path);
static bool DeleteFile(const StringView& path);
static uint64 GetFileSize(const StringView& path);
static bool IsReadOnly(const StringView& path);
static bool SetReadOnly(const StringView& path, bool isReadOnly);
static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
static bool CopyFile(const StringView& dst, const StringView& src);
static DateTime GetFileLastEditTime(const StringView& path);
static void GetSpecialFolderPath(const SpecialFolder type, String& result);
private:
static bool getFilesFromDirectoryTop(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
static bool getFilesFromDirectoryAll(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
};
#endif

View File

@@ -4,22 +4,16 @@
#include "LinuxFileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Platform/StringUtils.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringBuilder.h"
#include "Engine/Core/Types/StringView.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <unistd.h>
#include <stdio.h>
#include <cerrno>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
@@ -165,280 +159,6 @@ bool LinuxFileSystem::ShowFileExplorer(const StringView& path)
return false;
}
bool LinuxFileSystem::CreateDirectory(const StringView& path)
{
const StringAsUTF8<> pathAnsi(*path, path.Length());
// Skip if already exists
struct stat fileInfo;
if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
{
return false;
}
// Recursively do it all again for the parent directory, if any
const int32 slashIndex = path.FindLast('/');
if (slashIndex > 1)
{
if (CreateDirectory(path.Substring(0, slashIndex)))
{
return true;
}
}
// Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
}
bool DeleteUnixPathTree(const char* path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Recursively remove a nested directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (DeleteUnixPathTree(full_path))
return true;
continue;
}
// Remove a file object
if (unlink(full_path) != 0)
return true;
}
// Remove the devastated directory and close the object of it
if (rmdir(path) != 0)
return true;
closedir(dir);
return false;
}
bool LinuxFileSystem::DeleteDirectory(const String& path, bool deleteContents)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
if (deleteContents)
{
return DeleteUnixPathTree(pathANSI.Get());
}
else
{
return rmdir(pathANSI.Get()) != 0;
}
}
bool LinuxFileSystem::DirectoryExists(const StringView& path)
{
struct stat fileInfo;
const StringAsUTF8<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISDIR(fileInfo.st_mode);
}
return false;
}
bool LinuxFileSystem::DirectoryGetFiles(Array<String>& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
const StringAsUTF8<> searchPatternANSI(searchPattern);
// Check if use only top directory
if (option == DirectorySearchOption::TopDirectoryOnly)
return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
}
bool LinuxFileSystem::GetChildDirectories(Array<String>& results, const String& path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
const StringAsUTF8<> pathANSI(*path, path.Length());
const char* pathStr = pathANSI.Get();
// Stat for the path
stat(pathStr, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(pathStr)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(pathStr);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, pathStr);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
// Add directory
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool LinuxFileSystem::FileExists(const StringView& path)
{
struct stat fileInfo;
const StringAsUTF8<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISREG(fileInfo.st_mode);
}
return false;
}
bool LinuxFileSystem::DeleteFile(const StringView& path)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
return unlink(pathANSI.Get()) != 0;
}
uint64 LinuxFileSystem::GetFileSize(const StringView& path)
{
struct stat fileInfo;
fileInfo.st_size = 0;
const StringAsUTF8<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
// Check for directories
if (S_ISDIR(fileInfo.st_mode))
{
fileInfo.st_size = 0;
}
}
return fileInfo.st_size;
}
bool LinuxFileSystem::IsReadOnly(const StringView& path)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
if (access(pathANSI.Get(), W_OK) == -1)
{
return errno == EACCES;
}
return false;
}
bool LinuxFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
struct stat fileInfo;
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
if (isReadOnly)
{
fileInfo.st_mode &= ~S_IWUSR;
}
else
{
fileInfo.st_mode |= S_IWUSR;
}
return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
}
return false;
}
bool LinuxFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
{
if (!overwrite && FileExists(dst))
{
// Already exists
return true;
}
if (overwrite)
{
unlink(StringAsUTF8<>(*dst, dst.Length()).Get());
}
if (rename(StringAsUTF8<>(*src, src.Length()).Get(), StringAsUTF8<>(*dst, dst.Length()).Get()) != 0)
{
if (errno == EXDEV)
{
if (!CopyFile(dst, src))
{
unlink(StringAsUTF8<>(*src, src.Length()).Get());
return false;
}
}
return true;
}
return false;
}
bool LinuxFileSystem::CopyFile(const StringView& dst, const StringView& src)
{
const StringAsUTF8<> srcANSI(*src, src.Length());
@@ -612,156 +332,6 @@ bool LinuxFileSystem::UrnEncodePath(const char *path, char *result, const int ma
return true;
}
bool LinuxFileSystem::getFilesFromDirectoryTop(Array<String>& results, const char* path, const char* searchPattern)
{
size_t pathLength;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
DIR* dir = opendir(path);
if (dir == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, path);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for file
if (S_ISREG(statEntry.st_mode) != 0)
{
// Validate with filter
const int32 fullPathLength = StringUtils::Length(fullPath);
const int32 searchPatternLength = StringUtils::Length(searchPattern);
if (searchPatternLength == 0 || StringUtils::Compare(searchPattern, "*") == 0)
{
// All files
}
else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
{
// Path ending
}
else
{
// TODO: implement all cases in a generic way
continue;
}
// Add file
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool LinuxFileSystem::getFilesFromDirectoryAll(Array<String>& results, const char* path, const char* searchPattern)
{
// Find all files in this directory
getFilesFromDirectoryTop(results, path, searchPattern);
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (getFilesFromDirectoryAll(results, full_path, searchPattern))
{
closedir(dir);
return true;
}
}
}
closedir(dir);
return false;
}
DateTime LinuxFileSystem::GetFileLastEditTime(const StringView& path)
{
struct stat fileInfo;
const StringAsUTF8<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) == -1)
{
return DateTime::MinValue();
}
const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
return UnixEpoch + timeSinceEpoch;
}
void LinuxFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& result)
{
const String& home = LinuxPlatform::GetHomeDirectory();

View File

@@ -4,38 +4,24 @@
#if PLATFORM_LINUX
#include "Engine/Platform/Base/FileSystemBase.h"
#include "Engine/Platform/Unix/UnixFileSystem.h"
/// <summary>
/// Linux platform implementation of filesystem service.
/// </summary>
class FLAXENGINE_API LinuxFileSystem : public FileSystemBase
class FLAXENGINE_API LinuxFileSystem : public UnixFileSystem
{
public:
// [FileSystemBase]
static bool ShowOpenFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array<String, HeapAllocation>& filenames);
static bool ShowBrowseFolderDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& title, String& path);
static bool ShowFileExplorer(const StringView& path);
static bool CreateDirectory(const StringView& path);
static bool DeleteDirectory(const String& path, bool deleteContents = true);
static bool DirectoryExists(const StringView& path);
static bool DirectoryGetFiles(Array<String, HeapAllocation>& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
static bool GetChildDirectories(Array<String, HeapAllocation>& results, const String& path);
static bool FileExists(const StringView& path);
static bool DeleteFile(const StringView& path);
static bool MoveFileToRecycleBin(const StringView& path);
static uint64 GetFileSize(const StringView& path);
static bool IsReadOnly(const StringView& path);
static bool SetReadOnly(const StringView& path, bool isReadOnly);
static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
static bool CopyFile(const StringView& dst, const StringView& src);
static DateTime GetFileLastEditTime(const StringView& path);
static bool MoveFileToRecycleBin(const StringView& path);
static void GetSpecialFolderPath(const SpecialFolder type, String& result);
private:
static bool UrnEncodePath(const char *path, char *result, int maxLength);
static bool getFilesFromDirectoryTop(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
static bool getFilesFromDirectoryAll(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
static String getBaseName(const StringView& path);
static String getNameWithoutExtension(const StringView& path);
};

View File

@@ -0,0 +1,470 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#if PLATFORM_UNIX
#include "UnixFileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <cerrno>
#include <dirent.h>
#if PLATFORM_MAC || PLATFORM_IOS
typedef StringAsANSI<> UnixString;
#else
typedef StringAsUTF8<> UnixString;
#endif
const DateTime UnixEpoch(1970, 1, 1);
bool UnixFileSystem::CreateDirectory(const StringView& path)
{
const UnixString pathAnsi(*path, path.Length());
// Skip if already exists
struct stat fileInfo;
if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
{
return false;
}
// Recursively do it all again for the parent directory, if any
const int32 slashIndex = path.FindLast('/');
if (slashIndex > 1)
{
if (CreateDirectory(path.Substring(0, slashIndex)))
{
return true;
}
}
// Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
}
bool DeleteUnixPathTree(const char* path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Recursively remove a nested directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (DeleteUnixPathTree(full_path))
return true;
continue;
}
// Remove a file object
if (unlink(full_path) != 0)
return true;
}
// Remove the devastated directory and close the object of it
if (rmdir(path) != 0)
return true;
closedir(dir);
return false;
}
bool UnixFileSystem::DeleteDirectory(const String& path, bool deleteContents)
{
const UnixString pathANSI(*path, path.Length());
if (deleteContents)
{
return DeleteUnixPathTree(pathANSI.Get());
}
else
{
return rmdir(pathANSI.Get()) != 0;
}
}
bool UnixFileSystem::DirectoryExists(const StringView& path)
{
struct stat fileInfo;
const UnixString pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISDIR(fileInfo.st_mode);
}
return false;
}
bool UnixFileSystem::DirectoryGetFiles(Array<String>& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
{
const UnixString pathANSI(*path, path.Length());
const UnixString searchPatternANSI(searchPattern);
// Check if use only top directory
if (option == DirectorySearchOption::TopDirectoryOnly)
return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
}
bool UnixFileSystem::GetChildDirectories(Array<String>& results, const String& path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
const UnixString pathANSI(*path, path.Length());
const char* pathStr = pathANSI.Get();
// Stat for the path
stat(pathStr, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(pathStr)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(pathStr);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, pathStr);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
// Add directory
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool UnixFileSystem::FileExists(const StringView& path)
{
struct stat fileInfo;
const UnixString pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISREG(fileInfo.st_mode);
}
return false;
}
bool UnixFileSystem::DeleteFile(const StringView& path)
{
const UnixString pathANSI(*path, path.Length());
return unlink(pathANSI.Get()) != 0;
}
uint64 UnixFileSystem::GetFileSize(const StringView& path)
{
struct stat fileInfo;
fileInfo.st_size = 0;
const UnixString pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
// Check for directories
if (S_ISDIR(fileInfo.st_mode))
{
fileInfo.st_size = 0;
}
}
return fileInfo.st_size;
}
bool UnixFileSystem::IsReadOnly(const StringView& path)
{
const UnixString pathANSI(*path, path.Length());
if (access(pathANSI.Get(), W_OK) == -1)
{
return errno == EACCES;
}
return false;
}
bool UnixFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
{
const UnixString pathANSI(*path, path.Length());
struct stat fileInfo;
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
if (isReadOnly)
{
fileInfo.st_mode &= ~S_IWUSR;
}
else
{
fileInfo.st_mode |= S_IWUSR;
}
return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
}
return false;
}
bool UnixFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
{
if (!overwrite && FileExists(dst))
{
// Already exists
return true;
}
if (overwrite)
{
unlink(UnixString(*dst, dst.Length()).Get());
}
if (rename(UnixString(*src, src.Length()).Get(), UnixString(*dst, dst.Length()).Get()) != 0)
{
if (errno == EXDEV)
{
if (!CopyFile(dst, src))
{
unlink(UnixString(*src, src.Length()).Get());
return false;
}
}
return true;
}
return false;
}
bool UnixFileSystem::getFilesFromDirectoryTop(Array<String>& results, const char* path, const char* searchPattern)
{
size_t pathLength;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
DIR* dir = opendir(path);
if (dir == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
const int32 pathLength = strlen(entry->d_name);
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, path);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for file
if (S_ISREG(statEntry.st_mode) != 0)
{
// Validate with filter
const int32 fullPathLength = StringUtils::Length(fullPath);
const int32 searchPatternLength = StringUtils::Length(searchPattern);
if (searchPatternLength == 0 ||
StringUtils::Compare(searchPattern, "*") == 0 ||
StringUtils::Compare(searchPattern, "*.*") == 0)
{
// All files
}
else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
{
// Path ending
}
else if (searchPattern[0] == '*' && searchPatternLength > 2 && searchPattern[searchPatternLength-1] == '*')
{
// Contains pattern
bool match = false;
for (int32 i = 0; i < pathLength - searchPatternLength - 1; i++)
{
int32 len = Math::Min(searchPatternLength - 2, pathLength - i);
if (StringUtils::Compare(&entry->d_name[i], &searchPattern[1], len) == 0)
{
match = true;
break;
}
}
if (!match)
continue;
}
else
{
// TODO: implement all cases in a generic way
LOG(Warning, "DirectoryGetFiles: Wildcard filter is not implemented");
continue;
}
// Add file
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool UnixFileSystem::getFilesFromDirectoryAll(Array<String>& results, const char* path, const char* searchPattern)
{
// Find all files in this directory
getFilesFromDirectoryTop(results, path, searchPattern);
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (getFilesFromDirectoryAll(results, full_path, searchPattern))
{
closedir(dir);
return true;
}
}
}
closedir(dir);
return false;
}
DateTime UnixFileSystem::GetFileLastEditTime(const StringView& path)
{
struct stat fileInfo;
const UnixString pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) == -1)
{
return DateTime::MinValue();
}
const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
return UnixEpoch + timeSinceEpoch;
}
#endif

View File

@@ -0,0 +1,35 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#if PLATFORM_UNIX
#include "Engine/Platform/Base/FileSystemBase.h"
/// <summary>
/// Unix platform implementation of filesystem service.
/// </summary>
class FLAXENGINE_API UnixFileSystem : public FileSystemBase
{
public:
// [FileSystemBase]
static bool CreateDirectory(const StringView& path);
static bool DeleteDirectory(const String& path, bool deleteContents = true);
static bool DirectoryExists(const StringView& path);
static bool DirectoryGetFiles(Array<String, HeapAllocation>& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
static bool GetChildDirectories(Array<String, HeapAllocation>& results, const String& path);
static bool FileExists(const StringView& path);
static bool DeleteFile(const StringView& path);
static bool MoveFileToRecycleBin(const StringView& path);
static uint64 GetFileSize(const StringView& path);
static bool IsReadOnly(const StringView& path);
static bool SetReadOnly(const StringView& path, bool isReadOnly);
static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
static DateTime GetFileLastEditTime(const StringView& path);
private:
static bool getFilesFromDirectoryTop(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
static bool getFilesFromDirectoryAll(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
};
#endif

View File

@@ -434,6 +434,7 @@ void GBufferPass::DrawDecals(RenderContext& renderContext, GPUTextureView* light
PROFILE_GPU_CPU("Decals");
auto context = GPUDevice::Instance->GetMainContext();
auto buffers = renderContext.Buffers;
GPUTextureView* depthBuffer = EnumHasAnyFlags(buffers->DepthBuffer->Flags(), GPUTextureFlags::ReadOnlyDepthView) ? buffers->DepthBuffer->ViewReadOnlyDepth() : nullptr;
// Sort decals from the lowest order to the highest order
Sorting::QuickSort(decals.Get(), decals.Count(), &SortDecal);
@@ -484,22 +485,22 @@ void GBufferPass::DrawDecals(RenderContext& renderContext, GPUTextureView* light
count++;
targetBuffers[2] = buffers->GBuffer1->View();
}
context->SetRenderTarget(nullptr, ToSpan(targetBuffers, count));
context->SetRenderTarget(depthBuffer, ToSpan(targetBuffers, count));
break;
}
case MaterialDecalBlendingMode::Stain:
{
context->SetRenderTarget(buffers->GBuffer0->View());
context->SetRenderTarget(depthBuffer, buffers->GBuffer0->View());
break;
}
case MaterialDecalBlendingMode::Normal:
{
context->SetRenderTarget(buffers->GBuffer1->View());
context->SetRenderTarget(depthBuffer, buffers->GBuffer1->View());
break;
}
case MaterialDecalBlendingMode::Emissive:
{
context->SetRenderTarget(lightBuffer);
context->SetRenderTarget(depthBuffer, lightBuffer);
break;
}
}

View File

@@ -69,6 +69,7 @@ struct ShadowAtlasLightTile
{
ShadowsAtlasRectTile* RectTile;
ShadowsAtlasRectTile* StaticRectTile;
const ShadowsAtlasRectTile* LinkedRectTile;
Matrix WorldToShadow;
float FramesToUpdate; // Amount of frames (with fraction) until the next shadow update can happen
bool SkipUpdate;
@@ -94,6 +95,7 @@ struct ShadowAtlasLightTile
void ClearStatic()
{
StaticRectTile = nullptr;
LinkedRectTile = nullptr;
FramesToUpdate = 0;
SkipUpdate = false;
}
@@ -301,6 +303,7 @@ public:
GPUTexture* StaticShadowMapAtlas = nullptr;
DynamicTypedBuffer ShadowsBuffer;
GPUBufferView* ShadowsBufferView = nullptr;
const ShadowsCustomBuffer* LinkedShadows = nullptr;
RectPackAtlas<ShadowsAtlasRectTile> Atlas;
RectPackAtlas<ShadowsAtlasRectTile> StaticAtlas;
Dictionary<Guid, ShadowAtlasLight> Lights;
@@ -1044,6 +1047,32 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
}
}
void ShadowsPass::ClearShadowMapTile(GPUContext* context, GPUConstantBuffer* quadShaderCB, QuadShaderData& quadShaderData) const
{
// Color.r is used by PS_DepthClear in Quad shader to clear depth
quadShaderData.Color = Float4::One;
context->UpdateCB(quadShaderCB, &quadShaderData);
context->BindCB(0, quadShaderCB);
// Clear tile depth
context->SetState(_psDepthClear);
context->DrawFullscreenTriangle();
}
void ShadowsPass::CopyShadowMapTile(GPUContext* context, GPUConstantBuffer* quadShaderCB, QuadShaderData& quadShaderData, const GPUTexture* srcShadowMap, const ShadowsAtlasRectTile* srcTile) const
{
// Color.xyzw is used by PS_DepthCopy in Quad shader to scale input texture UVs
const float staticAtlasResolutionInv = 1.0f / (float)srcShadowMap->Width();
quadShaderData.Color = Float4(srcTile->Width, srcTile->Height, srcTile->X, srcTile->Y) * staticAtlasResolutionInv;
context->UpdateCB(quadShaderCB, &quadShaderData);
context->BindCB(0, quadShaderCB);
// Copy tile depth
context->BindSR(0, srcShadowMap->View());
context->SetState(_psDepthCopy);
context->DrawFullscreenTriangle();
}
void ShadowsPass::Dispose()
{
// Base
@@ -1068,26 +1097,26 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch&
// Early out and skip shadows setup if no lights is actively casting shadows
// RenderBuffers will automatically free any old ShadowsCustomBuffer after a few frames if we don't update LastFrameUsed
Array<RenderLightData*, RendererAllocation> shadowedLights;
for (auto& light : renderContext.List->DirectionalLights)
if (_shadowMapFormat != PixelFormat::Unknown && EnumHasAllFlags(renderContext.View.Flags, ViewFlags::Shadows) && !checkIfSkipPass())
{
if (light.CanRenderShadow(renderContext.View))
shadowedLights.Add(&light);
}
for (auto& light : renderContext.List->SpotLights)
{
if (light.CanRenderShadow(renderContext.View))
shadowedLights.Add(&light);
}
for (auto& light : renderContext.List->PointLights)
{
if (light.CanRenderShadow(renderContext.View))
shadowedLights.Add(&light);
for (auto& light : renderContext.List->DirectionalLights)
{
if (light.CanRenderShadow(renderContext.View))
shadowedLights.Add(&light);
}
for (auto& light : renderContext.List->SpotLights)
{
if (light.CanRenderShadow(renderContext.View))
shadowedLights.Add(&light);
}
for (auto& light : renderContext.List->PointLights)
{
if (light.CanRenderShadow(renderContext.View))
shadowedLights.Add(&light);
}
}
const auto currentFrame = Engine::FrameCount;
if (_shadowMapFormat == PixelFormat::Unknown ||
EnumHasNoneFlags(renderContext.View.Flags, ViewFlags::Shadows) ||
checkIfSkipPass() ||
shadowedLights.IsEmpty())
if (shadowedLights.IsEmpty())
{
// Invalidate any existing custom buffer that could have been used by the same task (eg. when rendering 6 sides of env probe)
if (auto* old = (ShadowsCustomBuffer*)renderContext.Buffers->FindCustomBuffer<ShadowsCustomBuffer>(TEXT("Shadows"), false))
@@ -1100,11 +1129,14 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch&
// Initialize shadow atlas
auto& shadows = *renderContext.Buffers->GetCustomBuffer<ShadowsCustomBuffer>(TEXT("Shadows"), false);
shadows.LinkedShadows = renderContext.Buffers->FindLinkedBuffer<ShadowsCustomBuffer>(TEXT("Shadows"));
if (shadows.LinkedShadows && (shadows.LinkedShadows->LastFrameUsed != currentFrame || shadows.LinkedShadows->ViewOrigin != renderContext.View.Origin))
shadows.LinkedShadows = nullptr; // Don't use incompatible linked shadows buffer
if (shadows.LastFrameUsed == currentFrame)
shadows.Reset();
shadows.LastFrameUsed = currentFrame;
shadows.MaxShadowsQuality = Math::Clamp(Math::Min<int32>((int32)Graphics::ShadowsQuality, (int32)renderContext.View.MaxShadowsQuality), 0, (int32)Quality::MAX - 1);
shadows.EnableStaticShadows = !renderContext.View.IsOfflinePass && !renderContext.View.IsSingleFrame;
shadows.EnableStaticShadows = !renderContext.View.IsOfflinePass && !renderContext.View.IsSingleFrame && !shadows.LinkedShadows;
int32 atlasResolution;
switch (Graphics::ShadowMapsQuality)
{
@@ -1323,6 +1355,29 @@ RETRY_ATLAS_SETUP:
SetupLight(shadows, renderContext, renderContextBatch, *(RenderSpotLightData*)light, atlasLight);
else //if (light->IsDirectionalLight)
SetupLight(shadows, renderContext, renderContextBatch, *(RenderDirectionalLightData*)light, atlasLight);
// Check if that light exists in linked shadows buffer to reuse shadow maps
const ShadowAtlasLight* linkedAtlasLight;
if (shadows.LinkedShadows && ((linkedAtlasLight = shadows.LinkedShadows->Lights.TryGet(light->ID))) && linkedAtlasLight->TilesCount == atlasLight.TilesCount)
{
for (int32 tileIndex = 0; tileIndex < atlasLight.TilesCount; tileIndex++)
{
auto& tile = atlasLight.Tiles[tileIndex];
tile.LinkedRectTile = nullptr;
auto& linkedTile = linkedAtlasLight->Tiles[tileIndex];
// Check if both lights use the same projections
if (tile.WorldToShadow == linkedTile.WorldToShadow && linkedTile.RectTile)
{
tile.LinkedRectTile = linkedTile.RectTile;
}
}
}
else
{
for (auto& tile : atlasLight.Tiles)
tile.LinkedRectTile = nullptr;
}
}
}
if (shadows.StaticAtlas.IsInitialized())
@@ -1493,29 +1548,21 @@ void ShadowsPass::RenderShadowMaps(RenderContextBatch& renderContextBatch)
// Set viewport for tile
context->SetViewportAndScissors(tile.CachedViewport);
if (tile.StaticRectTile && atlasLight.StaticState == ShadowAtlasLight::CopyStaticShadow)
if (tile.LinkedRectTile)
{
// Color.xyzw is used by PS_DepthCopy in Quad shader to scale input texture UVs
const float staticAtlasResolutionInv = 1.0f / shadows.StaticShadowMapAtlas->Width();
quadShaderData.Color = Float4(tile.StaticRectTile->Width, tile.StaticRectTile->Height, tile.StaticRectTile->X, tile.StaticRectTile->Y) * staticAtlasResolutionInv;
context->UpdateCB(quadShaderCB, &quadShaderData);
context->BindCB(0, quadShaderCB);
// Copy tile depth
context->BindSR(0, shadows.StaticShadowMapAtlas->View());
context->SetState(_psDepthCopy);
context->DrawFullscreenTriangle();
// Copy linked shadow
ASSERT(shadows.LinkedShadows);
CopyShadowMapTile(context, quadShaderCB, quadShaderData, shadows.LinkedShadows->ShadowMapAtlas, tile.LinkedRectTile);
}
else if (tile.StaticRectTile && atlasLight.StaticState == ShadowAtlasLight::CopyStaticShadow)
{
// Copy static shadow
CopyShadowMapTile(context, quadShaderCB, quadShaderData, shadows.StaticShadowMapAtlas, tile.StaticRectTile);
}
else if (!shadows.ClearShadowMapAtlas)
{
// Color.r is used by PS_DepthClear in Quad shader to clear depth
quadShaderData.Color = Float4::One;
context->UpdateCB(quadShaderCB, &quadShaderData);
context->BindCB(0, quadShaderCB);
// Clear tile depth
context->SetState(_psDepthClear);
context->DrawFullscreenTriangle();
// Clear shadow
ClearShadowMapTile(context, quadShaderCB, quadShaderData);
}
// Draw objects depth

View File

@@ -60,6 +60,8 @@ private:
static void SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderDirectionalLightData& light, ShadowAtlasLight& atlasLight);
static void SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderPointLightData& light, ShadowAtlasLight& atlasLight);
static void SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderSpotLightData& light, ShadowAtlasLight& atlasLight);
void ClearShadowMapTile(GPUContext* context, GPUConstantBuffer* quadShaderCB, struct QuadShaderData& quadShaderData) const;
void CopyShadowMapTile(GPUContext* context, GPUConstantBuffer* quadShaderCB, struct QuadShaderData& quadShaderData, const GPUTexture* srcShadowMap, const struct ShadowsAtlasRectTile* srcTile) const;
#if COMPILE_WITH_DEV_ENV
void OnShaderReloading(Asset* obj)

Some files were not shown because too many files have changed in this diff Show More