diff --git a/Content/Editor/Camera/M_Camera.flax b/Content/Editor/Camera/M_Camera.flax index 7ac9bab2d..5012b16e9 100644 --- a/Content/Editor/Camera/M_Camera.flax +++ b/Content/Editor/Camera/M_Camera.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dc3a7d0a8287c7c2c26b7685b726583677f96e1fe86ccb5f8518cb189b51a92 -size 29407 +oid sha256:6a2936be1789e6a7c663f84ddfea8c897fe8273cd2d29910ac37720907d7b930 +size 29533 diff --git a/Content/Editor/CubeTexturePreviewMaterial.flax b/Content/Editor/CubeTexturePreviewMaterial.flax index b0b54b394..973a01177 100644 --- a/Content/Editor/CubeTexturePreviewMaterial.flax +++ b/Content/Editor/CubeTexturePreviewMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:857edec8daa8bbd5bf16c839f65042df7a921154dcf48c1ac7a06aab4e40eab6 -size 30999 +oid sha256:6ecf44ea82025d0f491c68b0470e1704ca5f385bd54ad196d6912aeb2f3aee0f +size 31125 diff --git a/Content/Editor/DebugMaterials/SingleColor/Decal.flax b/Content/Editor/DebugMaterials/SingleColor/Decal.flax index 02126626b..6024d3961 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Decal.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Decal.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fddc7cd2d8732f8eef008d0a2bdc47948b87a14849d9e6fabcfe60ddb218aa42 -size 7617 +oid sha256:d8301da35f0b45f58f5c59221bd22bf0a8500555d31ab84ec1c8cd5eca9fc101 +size 9973 diff --git a/Content/Editor/DebugMaterials/SingleColor/Particle.flax b/Content/Editor/DebugMaterials/SingleColor/Particle.flax index bbab3ee4b..9c2c0755a 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Particle.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Particle.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e06851af881852106892b2bd03df0127c84db3a601abd9d7aaac7baf40343369 -size 32040 +oid sha256:4e432328bb19eaa58caf35f60cd6495a46cca694314828010f3be401d7de9434 +size 32108 diff --git a/Content/Editor/DebugMaterials/SingleColor/Surface.flax b/Content/Editor/DebugMaterials/SingleColor/Surface.flax index dd9785be9..0fb0d1c6f 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Surface.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Surface.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34080fbc40ed62bc5501969640403013291b3104dbb4374d9268994f3902e5a8 -size 29180 +oid sha256:1688861997cc2e8a433cdea81ee62662f45b261bc863cdb9431beed612f0aad7 +size 29306 diff --git a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax index 2ec83813f..e5bda89f6 100644 --- a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax +++ b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71acbd5242dd2d2f02554af0e5a95da9a5bd90d77aef80dc6fcdaefb88251d23 -size 31365 +oid sha256:42363fad30e29b0e0c4cf7ad9e02dc91040eb6821c3c28bd49996771e65893c4 +size 31559 diff --git a/Content/Editor/DebugMaterials/SingleColor/Terrain.flax b/Content/Editor/DebugMaterials/SingleColor/Terrain.flax index ac31c7161..09a253a8d 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Terrain.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Terrain.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcc185a2ec2e02ee9f52c806ba3756827c6d12f586c91a12b44295a95dd093dc -size 21331 +oid sha256:eb957ea2ee358b0e611d6612703261c9837099b6d03d48484cdab1151a461d8f +size 21004 diff --git a/Content/Editor/DefaultFontMaterial.flax b/Content/Editor/DefaultFontMaterial.flax index 23676ca21..b4f659e34 100644 --- a/Content/Editor/DefaultFontMaterial.flax +++ b/Content/Editor/DefaultFontMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1500a05e700f3b12d7e3984a978773c61f4c9ecc34ec96720fae724a87bab8e -size 29501 +oid sha256:7a46410b49a7e38c4c2569e4d8d8c8f4744cf26f79c632a53899ce753ab7c88a +size 29627 diff --git a/Content/Editor/Gizmo/FoliageBrushMaterial.flax b/Content/Editor/Gizmo/FoliageBrushMaterial.flax index d88a8eea3..1d8e89a65 100644 --- a/Content/Editor/Gizmo/FoliageBrushMaterial.flax +++ b/Content/Editor/Gizmo/FoliageBrushMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10aff82173474030b7ce41b491c803cc0de265ea0317a3aca87102099d65ca79 -size 37392 +oid sha256:f377cc4e05d89e4edbec6382a052abe571f3261bc273cd4475541b7b7051cffb +size 37586 diff --git a/Content/Editor/Gizmo/Material.flax b/Content/Editor/Gizmo/Material.flax index d7815f10f..cce270410 100644 --- a/Content/Editor/Gizmo/Material.flax +++ b/Content/Editor/Gizmo/Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e68032a48b3196f73c614a6302223765485e8fa3e1e278a2395c9d9ae1c5064 -size 32519 +oid sha256:271c80d9d9971d96d6ea430dfaf8f8f57d9b5f9fe1770b387f426d3c8721c3d8 +size 32713 diff --git a/Content/Editor/Gizmo/MaterialWire.flax b/Content/Editor/Gizmo/MaterialWire.flax index 26f3956c5..4b30df5f5 100644 --- a/Content/Editor/Gizmo/MaterialWire.flax +++ b/Content/Editor/Gizmo/MaterialWire.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbc08de78faa00aab330168dce166867d85dcae142081219d6a7eeb5f285ffd7 -size 31216 +oid sha256:ff3e7b3e77afa7191f1db9cf12f21908b80bb8f71e832c37e55547dc9dcab31c +size 31410 diff --git a/Content/Editor/Gizmo/SelectionOutlineMaterial.flax b/Content/Editor/Gizmo/SelectionOutlineMaterial.flax index 70a58006b..962179da0 100644 --- a/Content/Editor/Gizmo/SelectionOutlineMaterial.flax +++ b/Content/Editor/Gizmo/SelectionOutlineMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de0b19b33a2aac03a0fb7beda935c8a22cbb506f9ca9383bcce3eaac858a54e3 -size 16166 +oid sha256:fc2facc8fa980e5baa399fa7510a87d33d21bbd4c97eaab24856f6db49b13172 +size 16212 diff --git a/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax b/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax index 01ba9f46a..305c6a4c2 100644 --- a/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax +++ b/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a32d40e1f13606fea29e45cb0df1a45dbcc51eb329963d8db18ff64fa66bd4e -size 30293 +oid sha256:4458a9483e81fb0526cc395f93eeae238f4f91fa5d4889e3196b6530a8f17ec2 +size 30419 diff --git a/Content/Editor/Highlight Material.flax b/Content/Editor/Highlight Material.flax index 7c4be9cc2..e944ae62d 100644 --- a/Content/Editor/Highlight Material.flax +++ b/Content/Editor/Highlight Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1ec249ecd34cc66f9236431705adfaccff3e9ac5825ea72eb8b18449a96ccb7 -size 29910 +oid sha256:391606b1f7563d9a8e414baf6c19f3d99694a28f3f574b7aca64a325294d8e39 +size 30104 diff --git a/Content/Editor/Icons/IconsMaterial.flax b/Content/Editor/Icons/IconsMaterial.flax index 27e1e992f..320547f9a 100644 --- a/Content/Editor/Icons/IconsMaterial.flax +++ b/Content/Editor/Icons/IconsMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:652de424a7e366c1dcafd42cfe552f2d4f1327cd9ef033fb554d961e0a29ae2b -size 29827 +oid sha256:7951a44b138e60aa9dee4fdaf000eba8a7faef7b31c2e387f78b4a393d0cd0bc +size 30021 diff --git a/Content/Editor/IconsAtlas.flax b/Content/Editor/IconsAtlas.flax index bbac95de6..1c65c9f31 100644 --- a/Content/Editor/IconsAtlas.flax +++ b/Content/Editor/IconsAtlas.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f43bf2050d47a1e4d6db35e5da66c2f999236939b9d962cd0fba56dcb171852 -size 5611913 +oid sha256:8f2236762a9e54d6de50415e39b9a6b055bb88ba1ff0621787b96f87c437e520 +size 5612435 diff --git a/Content/Editor/IesProfilePreviewMaterial.flax b/Content/Editor/IesProfilePreviewMaterial.flax index c23d9981d..c7a025108 100644 --- a/Content/Editor/IesProfilePreviewMaterial.flax +++ b/Content/Editor/IesProfilePreviewMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6388c4a40a0de927abdd532d5bfaff5b7c651f8daa434c6f04a0579e33a1015b -size 20399 +oid sha256:41210c6c490513503f01e8a628d80dd98e58fc0482f9966f9342700118ccd04c +size 20445 diff --git a/Content/Editor/MaterialTemplates/Decal.shader b/Content/Editor/MaterialTemplates/Decal.shader index 06b44d498..b933fcbb3 100644 --- a/Content/Editor/MaterialTemplates/Decal.shader +++ b/Content/Editor/MaterialTemplates/Decal.shader @@ -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); diff --git a/Content/Editor/MaterialTemplates/GUI.shader b/Content/Editor/MaterialTemplates/GUI.shader index fad7afa32..69b6d3539 100644 --- a/Content/Editor/MaterialTemplates/GUI.shader +++ b/Content/Editor/MaterialTemplates/GUI.shader @@ -20,6 +20,8 @@ float TimeParam; float4 ViewInfo; float4 ScreenSize; float4 ViewSize; +float3 ViewPadding0; +float UnscaledTimeParam; @1META_CB_END // Shader resources diff --git a/Content/Editor/MaterialTemplates/PostProcess.shader b/Content/Editor/MaterialTemplates/PostProcess.shader index 927d63c8c..753ccb253 100644 --- a/Content/Editor/MaterialTemplates/PostProcess.shader +++ b/Content/Editor/MaterialTemplates/PostProcess.shader @@ -19,6 +19,8 @@ float4 ViewInfo; float4 ScreenSize; float4 TemporalAAJitter; float4x4 InverseViewProjectionMatrix; +float3 ViewPadding0; +float UnscaledTimeParam; @1META_CB_END // Shader resources diff --git a/Content/Editor/Particles/Particle Material Color.flax b/Content/Editor/Particles/Particle Material Color.flax index 07d6d0587..bde834456 100644 --- a/Content/Editor/Particles/Particle Material Color.flax +++ b/Content/Editor/Particles/Particle Material Color.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c71a6394a3a0668f6d2b18281aaa36d83b1b150fd10a869edace21e1ffa5b07 -size 30361 +oid sha256:b600cd725f5550de72d5a2544571ca2c1ea8de1a3d45038bac273d2b6f3b04c2 +size 30429 diff --git a/Content/Editor/Particles/Smoke Material.flax b/Content/Editor/Particles/Smoke Material.flax index d5b8cb872..7a8fdeb1a 100644 --- a/Content/Editor/Particles/Smoke Material.flax +++ b/Content/Editor/Particles/Smoke Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a16a3fa5bed3bc8030c40fbe0e946f2bdec28745542bf08db1d7b4a43180f785 -size 38900 +oid sha256:96d3865771cfa47ad59e0493c8f1b6e9fd9950593b2b929c91ea2692eca71efd +size 38495 diff --git a/Content/Editor/SpriteMaterial.flax b/Content/Editor/SpriteMaterial.flax index 8d001b9c7..876a38a56 100644 --- a/Content/Editor/SpriteMaterial.flax +++ b/Content/Editor/SpriteMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ed88e5e8b513f7460e94942ad722d8b193c013434586f4382673f7dc3074e98 -size 30376 +oid sha256:5d02a6d11ea83ea6c519a1644950750e58433e6a2aea9a2daa646db1d2b0c293 +size 30502 diff --git a/Content/Editor/Terrain/Circle Brush Material.flax b/Content/Editor/Terrain/Circle Brush Material.flax index f2f21d189..af191fdaa 100644 --- a/Content/Editor/Terrain/Circle Brush Material.flax +++ b/Content/Editor/Terrain/Circle Brush Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ab7e9c88c9896f37ac5a43efe176e13f98eafacc3117b49cd7173b31376b21d -size 28003 +oid sha256:8e9ef5186642a38af8ebb5856a891215686576b23841aabe16b7dde7f2bcb57f +size 27676 diff --git a/Content/Editor/Terrain/Highlight Terrain Material.flax b/Content/Editor/Terrain/Highlight Terrain Material.flax index d1e4226f5..14bd86c35 100644 --- a/Content/Editor/Terrain/Highlight Terrain Material.flax +++ b/Content/Editor/Terrain/Highlight Terrain Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40e38d672c13c06a84366689b8ce2b346179b4aa0de3cf1fc6035b33ad036e4a -size 21506 +oid sha256:5f1a9524d9bc7ee41df761b9fb34613fc04357377a81836fb28b3ee5a6f2dcf4 +size 21179 diff --git a/Content/Editor/TexturePreviewMaterial.flax b/Content/Editor/TexturePreviewMaterial.flax index 0bcb9b77f..fac30b4f1 100644 --- a/Content/Editor/TexturePreviewMaterial.flax +++ b/Content/Editor/TexturePreviewMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93b0220308d2a3051d7e449505ca18dab7a82d46bf6a86e4ecfb9b741909f91b -size 10698 +oid sha256:c178081b59417439b2d523486f3c7e4f7f391ff57e5ab5a565aadf3fd31f5488 +size 10744 diff --git a/Content/Editor/Wires Debug Material.flax b/Content/Editor/Wires Debug Material.flax index bdf2bde08..8fff1a174 100644 --- a/Content/Editor/Wires Debug Material.flax +++ b/Content/Editor/Wires Debug Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7e22035ac54615fee78cad71d5e5268ff139811d4390fe9a5025ab4d234ff4e -size 29910 +oid sha256:c31daed51b38e6aa9eeceaad85498d6ae7079f7b125c6f71f036799278c34e22 +size 30104 diff --git a/Content/Engine/DefaultDeformableMaterial.flax b/Content/Engine/DefaultDeformableMaterial.flax index b9e972b63..8af3db999 100644 --- a/Content/Engine/DefaultDeformableMaterial.flax +++ b/Content/Engine/DefaultDeformableMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:feac2f9ced2d8edf6a43f1c56cdfdc27bbd018a4c23a527a403c30bdb9217e63 -size 18922 +oid sha256:df767eeb060d058d502271f1cd89581c57ad339cd690cc9c588f9a34cc9344b1 +size 18985 diff --git a/Content/Engine/DefaultMaterial.flax b/Content/Engine/DefaultMaterial.flax index a7949d435..a253452df 100644 --- a/Content/Engine/DefaultMaterial.flax +++ b/Content/Engine/DefaultMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1db3d343643eec7c6f7c3ac33a205618480ce41b4b196ebfc6b63e00085a555f -size 31205 +oid sha256:b00388390410aabb11f1d9b032361902d2f284daa765d536c8f2a821f659effe +size 31331 diff --git a/Content/Engine/DefaultRadialMenu.flax b/Content/Engine/DefaultRadialMenu.flax index db233a854..2b3d7f0de 100644 --- a/Content/Engine/DefaultRadialMenu.flax +++ b/Content/Engine/DefaultRadialMenu.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9e9e01ae0222d9dc0db7c51741c3e2c2595bb550fa9dd1665d11ce4fac0afc9 -size 20468 +oid sha256:2b0272c8f2df095e6609f49845a3d329daaf634e0776ca764e4c51596cac60ff +size 20514 diff --git a/Content/Engine/DefaultTerrainMaterial.flax b/Content/Engine/DefaultTerrainMaterial.flax index 83e892360..8d195ab98 100644 --- a/Content/Engine/DefaultTerrainMaterial.flax +++ b/Content/Engine/DefaultTerrainMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c890a5b43c0158d1885c3905ab02c8b0ee36a558c4770d79892837960ee07e5 -size 23628 +oid sha256:a86caeef4de5a84783ba34208701c0f272f3b4b3ff82c64c2553d6aec631e07b +size 23301 diff --git a/Content/Engine/SingleColorMaterial.flax b/Content/Engine/SingleColorMaterial.flax index 515c48cea..b6906b930 100644 --- a/Content/Engine/SingleColorMaterial.flax +++ b/Content/Engine/SingleColorMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27acfeeafdb50abe76a2feee0616ea90ee5d4fd72c678244d80f15ed4a97894a -size 29381 +oid sha256:74d77dbfdf72c9281b0760a266adac7f1eb849f9656ea8da5cd8951f2fab5343 +size 29507 diff --git a/Content/Engine/SkyboxMaterial.flax b/Content/Engine/SkyboxMaterial.flax index a8b1b327f..8faccf8c0 100644 --- a/Content/Engine/SkyboxMaterial.flax +++ b/Content/Engine/SkyboxMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e7d0fe3697a0b65eefb8756e0f9aa48bc2d505eb94710b5972097eb61b6b029 -size 30944 +oid sha256:8149367ccbef36932866e6af53fedf79931f26677db5dfcce71ba33caeff5980 +size 31070 diff --git a/Content/Shaders/SSAO.flax b/Content/Shaders/SSAO.flax index d628818a4..d9374b133 100644 --- a/Content/Shaders/SSAO.flax +++ b/Content/Shaders/SSAO.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0a576e8a8b818d06ff258874c8d73b273d05c2b8856ff6c14cf0accb2f23f52 -size 36832 +oid sha256:3470722e78ef45616e1d86373e32e40e219f911c88dd98313ee567cccfd10cf5 +size 36833 diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings index 3611934dd..f54190003 100644 --- a/Flax.sln.DotSettings +++ b/Flax.sln.DotSettings @@ -73,6 +73,24 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + CCD + GPU + ID + <NamingElement Priority="11" Title="Class and struct public fields"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="PUBLIC"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="9" Title="Class and struct methods"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="member function" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="17" Title="Typedefs"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="type alias" /><type Name="typedef" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="14" Title="Other constants"><Descriptor Static="True" Constexpr="Indeterminate" Const="True" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="local variable" /><type Name="struct field" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="13" Title="Enum members"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="scoped enumerator" /><type Name="unscoped enumerator" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="15" Title="Global constants"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="True" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="global variable" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="7" Title="Global variables"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="global variable" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="5" Title="Parameters"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="function parameter" /><type Name="lambda parameter" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></NamingElement> + <NamingElement Priority="1" Title="Classes and structs"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="__interface" /><type Name="class" /><type Name="struct" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="8" Title="Global functions"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="global function" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="16" Title="Namespaces"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="namespace" /><type Name="namespace alias" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="6" Title="Local variables"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="local variable" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></NamingElement> + <NamingElement Priority="10" Title="Class and struct fields"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb" /></NamingElement> + <NamingElement Priority="12" Title="Union members"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="union member" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="2" Title="Enums"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="enum" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> AI ARGB LO @@ -213,6 +231,7 @@ YZ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> C:\Users\Wojtek\AppData\Local\JetBrains\Transient\ReSharperPlatformVs15\v08_f9eacea9\SolutionCaches + True True True True diff --git a/Source/Editor/CustomEditorWindow.cs b/Source/Editor/CustomEditorWindow.cs index f28a844ef..3df453ef8 100644 --- a/Source/Editor/CustomEditorWindow.cs +++ b/Source/Editor/CustomEditorWindow.cs @@ -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 /// /// Initializes a new instance of the class. /// - protected CustomEditorWindow() + protected CustomEditorWindow(bool hideOnClose = false, ScrollBars scrollBars = ScrollBars.Vertical) { - _win = new Win(this); + _win = new Win(this, hideOnClose, scrollBars); ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; } diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index 2daf2f6af..02f5ca7ec 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -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); } } diff --git a/Source/Editor/CustomEditors/GUI/PropertiesList.cs b/Source/Editor/CustomEditors/GUI/PropertiesList.cs index ef90fc706..3847a8f36 100644 --- a/Source/Editor/CustomEditors/GUI/PropertiesList.cs +++ b/Source/Editor/CustomEditors/GUI/PropertiesList.cs @@ -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; /// /// 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; /// /// 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(); } /// diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index 8c3256eb9..853a6eb7d 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -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; diff --git a/Source/Editor/EditorIcons.cs b/Source/Editor/EditorIcons.cs index 6deb65640..8688c502a 100644 --- a/Source/Editor/EditorIcons.cs +++ b/Source/Editor/EditorIcons.cs @@ -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; diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs index cca696d9a..30ffd6287 100644 --- a/Source/Editor/GUI/Docking/DockHintWindow.cs +++ b/Source/Editor/GUI/Docking/DockHintWindow.cs @@ -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; diff --git a/Source/Editor/GUI/Docking/DockPanelProxy.cs b/Source/Editor/GUI/Docking/DockPanelProxy.cs index 5cf64266a..888b78232 100644 --- a/Source/Editor/GUI/Docking/DockPanelProxy.cs +++ b/Source/Editor/GUI/Docking/DockPanelProxy.cs @@ -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 /// /// The is mouse down flag (left button). @@ -55,8 +60,8 @@ namespace FlaxEditor.GUI.Docking /// 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; /// /// Initializes a new instance of the 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) diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs index 6cb5a84fb..8fdf21e4c 100644 --- a/Source/Editor/GUI/ItemsListContextMenu.cs +++ b/Source/Editor/GUI/ItemsListContextMenu.cs @@ -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]; diff --git a/Source/Editor/GUI/NavigationBar.cs b/Source/Editor/GUI/NavigationBar.cs index bedf4c070..eb87eaafc 100644 --- a/Source/Editor/GUI/NavigationBar.cs +++ b/Source/Editor/GUI/NavigationBar.cs @@ -11,6 +11,9 @@ namespace FlaxEditor.GUI /// public class NavigationBar : Panel { + private float _toolstripHeight = 0; + private Margin _toolstripMargin; + /// /// The default buttons margin. /// @@ -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); } + + /// + 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; + } } } diff --git a/Source/Editor/GUI/Popups/RenamePopup.cs b/Source/Editor/GUI/Popups/RenamePopup.cs index 353830a21..d17a4180d 100644 --- a/Source/Editor/GUI/Popups/RenamePopup.cs +++ b/Source/Editor/GUI/Popups/RenamePopup.cs @@ -130,6 +130,10 @@ namespace FlaxEditor.GUI /// Created popup. 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); diff --git a/Source/Editor/GUI/ToolStrip.cs b/Source/Editor/GUI/ToolStrip.cs index 09f61b71d..eeba67cde 100644 --- a/Source/Editor/GUI/ToolStrip.cs +++ b/Source/Editor/GUI/ToolStrip.cs @@ -13,15 +13,7 @@ namespace FlaxEditor.GUI /// public class ToolStrip : ContainerControl { - /// - /// The default margin vertically. - /// - public const int DefaultMarginV = 1; - - /// - /// The default margin horizontally. - /// - public const int DefaultMarginH = 2; + private Margin _itemsMargin; /// /// Event fired when button gets clicked with the primary mouse button. @@ -66,10 +58,26 @@ namespace FlaxEditor.GUI } } + /// + /// Gets or sets the space around items. + /// + public Margin ItemsMargin + { + get => _itemsMargin; + set + { + if (_itemsMargin != value) + { + _itemsMargin = value; + PerformLayout(); + } + } + } + /// /// Gets the height for the items. /// - public float ItemsHeight => Height - 2 * DefaultMarginV; + public float ItemsHeight => Height - _itemsMargin.Height; /// /// Initializes a new instance of the 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); } /// @@ -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; } } } diff --git a/Source/Editor/GUI/Tree/Tree.cs b/Source/Editor/GUI/Tree/Tree.cs index 8df26c211..5530a1738 100644 --- a/Source/Editor/GUI/Tree/Tree.cs +++ b/Source/Editor/GUI/Tree/Tree.cs @@ -41,6 +41,7 @@ namespace FlaxEditor.GUI.Tree private Margin _margin; private bool _autoSize = true; private bool _deferLayoutUpdate = false; + private TreeNode _lastSelectedNode; /// /// 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. /// public TreeNode SelectedNode => Selection.Count > 0 ? Selection[0] : null; - + /// /// Allow nodes to Draw the root tree line. /// @@ -364,6 +365,19 @@ namespace FlaxEditor.GUI.Tree BulkSelectUpdateExpanded(false); } + /// + /// Flushes any pending layout perming action that has been delayed until next update to optimize performance of the complex tree hierarchy. + /// + public void FlushPendingPerformLayout() + { + if (_deferLayoutUpdate) + { + base.PerformLayout(); + AfterDeferredLayout?.Invoke(); + _deferLayoutUpdate = false; + } + } + /// 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 toSelect = new List(); + 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 diff --git a/Source/Editor/Managed/ManagedEditor.cpp b/Source/Editor/Managed/ManagedEditor.cpp index c020fe7ce..b4178fb89 100644 --- a/Source/Editor/Managed/ManagedEditor.cpp +++ b/Source/Editor/Managed/ManagedEditor.cpp @@ -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 ManagedEditor::GetWindows() +{ + WindowsManager::WindowsLocker.Lock(); + auto result = WindowsManager::Windows; + WindowsManager::WindowsLocker.Unlock(); + return result; +} + void ManagedEditor::OnEditorAssemblyLoaded(MAssembly* assembly) { ASSERT(!HasManagedInstance()); diff --git a/Source/Editor/Managed/ManagedEditor.h b/Source/Editor/Managed/ManagedEditor.h index 3d4f49708..15649b97b 100644 --- a/Source/Editor/Managed/ManagedEditor.h +++ b/Source/Editor/Managed/ManagedEditor.h @@ -259,6 +259,7 @@ public: API_FUNCTION(Internal) static Array 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 GetWindows(); private: void OnEditorAssemblyLoaded(MAssembly* assembly); diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index 871156662..af919c1f3 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -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 } diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index 617c1c3de..f26506c74 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -76,6 +76,25 @@ namespace FlaxEditor.Options DockBottom = DockState.DockBottom } + /// + /// Options for the visibility status of the tab close button. + /// + public enum TabCloseButtonVisibility + { + /// + /// Never show the close button. + /// + Never, + /// + /// Show the close button on tabs that are currently selected. + /// + SelectedTab, + /// + /// Show the close button on all tabs that can be closed. + /// + Always + } + /// /// Options for the action taken by the play button. /// @@ -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 - /// - /// Gets or sets a value indicating whether use native window title bar. Editor restart required. - /// - [DefaultValue(false)] - [EditorDisplay("Interface"), EditorOrder(70), Tooltip("Determines whether use native window title bar. Editor restart required.")] - public bool UseNativeWindowSystem { get; set; } = false; -#endif - /// /// Gets or sets a value indicating whether show selected camera preview in the editor window. /// @@ -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; - /// - /// 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. - /// - [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; - - /// - /// Gets or sets the method window opening. - /// - [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; - /// /// Gets or sets the editor icons scale. Editor restart required. /// @@ -232,6 +228,13 @@ namespace FlaxEditor.Options [EditorDisplay("Interface"), EditorOrder(310)] public bool SeparateValueAndUnit { get; set; } + /// + /// Gets or sets the option to auto size the Properties panel splitter based on the longest property name. Editor restart recommended. + /// + [DefaultValue(false)] + [EditorDisplay("Interface"), EditorOrder(311)] + public bool AutoSizePropertiesPanelSplitter { get; set; } + /// /// Gets or sets tree line visibility. /// @@ -265,6 +268,66 @@ namespace FlaxEditor.Options [EditorDisplay("Interface"), EditorOrder(322)] public bool ScrollToScriptOnAdd { get; set; } = true; +#if PLATFORM_WINDOWS + /// + /// Gets or sets a value indicating whether use native window title bar. Editor restart required. + /// + [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 + /// + /// Gets or sets a value indicating whether a window containing a single tabs hides the tab bar. Editor restart recommended. + /// + [DefaultValue(true)] + [EditorDisplay("Tabs & Windows", "Hide Single-Tab Window Tab Bars"), EditorOrder(71)] + public bool HideSingleTabWindowTabBars { get; set; } = true; +#endif + + /// + /// Gets or sets a value indicating wether the minum tab width should be used. Editor restart required. + /// + [DefaultValue(false)] + [EditorDisplay("Tabs & Windows"), EditorOrder(99)] + public bool UseMinimumTabWidth { get; set; } = false; + + /// + /// 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. + /// + [DefaultValue(80.0f), Limit(50.0f, 150.0f)] + [EditorDisplay("Tabs & Windows"), EditorOrder(99), VisibleIf(nameof(UseMinimumTabWidth))] + public float MinimumTabWidth { get; set; } = 80.0f; + + /// + /// Gets or sets a value indicating the height of window tabs. Editor restart required. + /// + [DefaultValue(20.0f), Limit(15.0f, 40.0f)] + [EditorDisplay("Tabs & Windows"), EditorOrder(100)] + public float TabHeight { get; set; } = 20.0f; + + /// + /// 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. + /// + [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; + + /// + /// Gets or sets the method window opening. + /// + [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; + + /// + /// Gets or sets a value indicating when the tab close button should be visible. + /// + [DefaultValue(TabCloseButtonVisibility.SelectedTab)] + [EditorDisplay("Tabs & Windows"), EditorOrder(151)] + public TabCloseButtonVisibility ShowTabCloseButton { get; set; } = TabCloseButtonVisibility.SelectedTab; + /// /// Gets or sets the timestamps prefix mode for output log messages. /// @@ -420,6 +483,12 @@ namespace FlaxEditor.Options [DefaultValue(1), Range(1, 4)] [EditorDisplay("Cook & Run"), EditorOrder(600)] public int NumberOfGameClientsToLaunch = 1; + + /// + /// Gets or sets the build configuration to use when using Cook and Run option in the editor. + /// + [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; /// /// Gets or sets the curvature of the line connecting to connected visject nodes. diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index f71f50596..9f3112905 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -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[] { diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 1debf5b60..091fbe48f 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -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; /// 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, diff --git a/Source/Editor/Surface/NodeAlignmentType.cs b/Source/Editor/Surface/NodeAlignmentType.cs index 141235783..07e1d8dc4 100644 --- a/Source/Editor/Surface/NodeAlignmentType.cs +++ b/Source/Editor/Surface/NodeAlignmentType.cs @@ -5,39 +5,39 @@ using FlaxEngine; namespace FlaxEditor.Surface { /// - /// Node Alignment type + /// Node Alignment type. /// [HideInEditor] public enum NodeAlignmentType { /// - /// Align nodes vertically to top, matching top-most node + /// Align nodes vertically to top, matching top-most node. /// Top, /// - /// Align nodes vertically to middle, using average of all nodes + /// Align nodes vertically to middle, using average of all nodes. /// Middle, /// - /// Align nodes vertically to bottom, matching bottom-most node + /// Align nodes vertically to bottom, matching bottom-most node. /// Bottom, /// - /// Align nodes horizontally to left, matching left-most node + /// Align nodes horizontally to left, matching left-most node. /// Left, /// - /// Align nodes horizontally to center, using average of all nodes + /// Align nodes horizontally to center, using average of all nodes. /// Center, /// - /// Align nodes horizontally to right, matching right-most node + /// Align nodes horizontally to right, matching right-most node. /// Right, } -} \ No newline at end of file +} diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs index b1af9589b..2cd46593b 100644 --- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs +++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs @@ -28,7 +28,7 @@ namespace FlaxEditor.Surface /// /// The input type to process. /// Node groups cache that can be used for reusing groups for different nodes. - /// The cache version number. Can be used to reject any cached data after rebuilt. + /// The cache version number. Can be used to reject any cached data after. rebuilt. public delegate void IterateType(ScriptType scriptType, Dictionary, GroupArchetype> cache, int version); internal static readonly List Caches = new List(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); }); diff --git a/Source/Editor/Surface/VisjectSurface.Formatting.cs b/Source/Editor/Surface/VisjectSurface.Formatting.cs index 2ff48b290..39ac58242 100644 --- a/Source/Editor/Surface/VisjectSurface.Formatting.cs +++ b/Source/Editor/Surface/VisjectSurface.Formatting.cs @@ -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 { /// - /// Starting from 0 at the main nodes + /// Starting from 0 at the main nodes. /// public int Layer; /// - /// Position in the layer + /// Position in the layer. /// public int Offset; /// - /// How far the subtree needs to be moved additionally + /// How far the subtree needs to be moved additionally. /// public int SubtreeOffset; } /// /// Formats a graph where the nodes can be disjointed. - /// Uses the Sugiyama method + /// Uses the Sugiyama method. /// - /// List of nodes + /// List of nodes. public void FormatGraph(List nodes) { if (nodes.Count <= 1) @@ -78,9 +78,9 @@ namespace FlaxEditor.Surface } /// - /// Formats a graph where all nodes are connected + /// Formats a graph where all nodes are connected. /// - /// List of connected nodes + /// List of connected nodes. protected void FormatConnectedGraph(List nodes) { if (nodes.Count <= 1) @@ -160,11 +160,71 @@ namespace FlaxEditor.Surface } /// - /// Assigns a layer to every node + /// Straightens every connection between nodes in . /// - /// The exta node data - /// The end nodes - /// The number of the maximum layer + /// List of nodes. + public void StraightenGraphConnections(List nodes) + { + if (nodes.Count <= 1) + return; + + List undoActions = new List(); + + // Only process nodes that have any connection + List 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 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); + } + + /// + /// Assigns a layer to every node. + /// + /// The exta node data. + /// The end nodes. + /// The number of the maximum layer. private int SetLayers(Dictionary nodeData, List endNodes) { // Longest path layering @@ -201,12 +261,12 @@ namespace FlaxEditor.Surface /// - /// Sets the node offsets + /// Sets the node offsets. /// - /// The exta node data - /// The end nodes - /// The number of the maximum layer - /// The number of the maximum offset + /// The exta node data. + /// The end nodes. + /// The number of the maximum layer. + /// The number of the maximum offset. private int SetOffsets(Dictionary nodeData, List 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. /// - /// List of nodes - /// Alignemnt type + /// List of nodes. + /// Alignemnt type. public void AlignNodes(List nodes, NodeAlignmentType alignmentType) - { + { if(nodes.Count <= 1) return; @@ -328,8 +388,8 @@ namespace FlaxEditor.Surface /// /// 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. /// - /// List of nodes - /// If false will be done horizontally, if true will be done vertically + /// List of nodes. + /// If false will be done horizontally, if true will be done vertically. public void DistributeNodes(List nodes, bool vertically) { if(nodes.Count <= 1) diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 1201318d8..8cbcb4a21 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -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); diff --git a/Source/Editor/Windows/Assets/VisualScriptWindow.cs b/Source/Editor/Windows/Assets/VisualScriptWindow.cs index 756daf66a..35b2d927d 100644 --- a/Source/Editor/Windows/Assets/VisualScriptWindow.cs +++ b/Source/Editor/Windows/Assets/VisualScriptWindow.cs @@ -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() - }); - surface.Select(node); + Name = $"{name} (in {member.DeclaringType.Name})", + TooltipText = Editor.Instance.CodeDocs.GetTooltip(member), + Tag = new object[] { name, parameters.Length, Utils.GetEmptyArray() }, + // 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)); } } diff --git a/Source/Editor/Windows/ContentWindow.Navigation.cs b/Source/Editor/Windows/ContentWindow.Navigation.cs index 1033db472..53dd6447b 100644 --- a/Source/Editor/Windows/ContentWindow.Navigation.cs +++ b/Source/Editor/Windows/ContentWindow.Navigation.cs @@ -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(); } /// diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index cf33d0b63..3d2ec4a66 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -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(); + } + } + } + /// public override void OnInit() { @@ -1200,9 +1215,9 @@ namespace FlaxEditor.Windows /// protected override void PerformLayoutBeforeChildren() { - base.PerformLayoutBeforeChildren(); + UpdateNavigationBarBounds(); - _navigationBar?.UpdateBounds(_toolStrip); + base.PerformLayoutBeforeChildren(); } /// diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index ed133d4e8..5b5abf1e2 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -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(); } } diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs index 232b51d8d..8aab5d759 100644 --- a/Source/Editor/Windows/GameCookerWindow.cs +++ b/Source/Editor/Windows/GameCookerWindow.cs @@ -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, }); diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 5b243cfed..080744c2f 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -63,6 +63,16 @@ namespace FlaxEditor.Windows }, }; + /// + /// Fired when the game window audio is muted. + /// + public event Action MuteAudio; + + /// + /// Fired when the game window master audio volume is changed. + /// + public event Action MasterVolumeChanged; + /// /// Gets the viewport. /// @@ -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); } } diff --git a/Source/Editor/Windows/SplashScreen.cpp b/Source/Editor/Windows/SplashScreen.cpp index 2700c9694..7e2889ca1 100644 --- a/Source/Editor/Windows/SplashScreen.cpp +++ b/Source/Editor/Windows/SplashScreen.cpp @@ -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(this); } + // Load custom image + _splashTexture.Loaded.Bind(this); + String splashImagePath = Globals::ProjectContentFolder / TEXT("SplashImage.flax"); + if (FileSystem::FileExists(splashImagePath)) + _splashTexture = Content::LoadAsync(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((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(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); +} diff --git a/Source/Editor/Windows/SplashScreen.h b/Source/Editor/Windows/SplashScreen.h index e8afcb97f..6f8f17a7f 100644 --- a/Source/Editor/Windows/SplashScreen.h +++ b/Source/Editor/Windows/SplashScreen.h @@ -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 _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(); }; diff --git a/Source/Editor/Windows/ToolboxWindow.cs b/Source/Editor/Windows/ToolboxWindow.cs index 3c9c605a4..7ad66794d 100644 --- a/Source/Editor/Windows/ToolboxWindow.cs +++ b/Source/Editor/Windows/ToolboxWindow.cs @@ -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; /// /// 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(); + }; + } + + /// + 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(); } + /// + 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); diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index c42b500fd..f75a8abd1 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -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) diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp index 8558e601d..93397397a 100644 --- a/Source/Engine/Content/Assets/Animation.cpp +++ b/Source/Engine/Content/Assets/Animation.cpp @@ -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& assets, Array& files) const diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp index 47335ae1f..1830f40de 100644 --- a/Source/Engine/Content/Assets/Model.cpp +++ b/Source/Engine/Content/Assets/Model.cpp @@ -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& getChunk) const diff --git a/Source/Engine/Content/Assets/ModelBase.cpp b/Source/Engine/Content/Assets/ModelBase.cpp index 2521966a2..0946e6160 100644 --- a/Source/Engine/Content/Assets/ModelBase.cpp +++ b/Source/Engine/Content/Assets/ModelBase.cpp @@ -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) diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index 9d10e0150..ed41c4aab 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -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); } } diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp index d2b4256b3..be71236ab 100644 --- a/Source/Engine/ContentImporters/ImportModel.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -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(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; } diff --git a/Source/Engine/Core/Types/LayersMask.cs b/Source/Engine/Core/Types/LayersMask.cs index a6bd4bcab..7d5c57066 100644 --- a/Source/Engine/Core/Types/LayersMask.cs +++ b/Source/Engine/Core/Types/LayersMask.cs @@ -32,13 +32,53 @@ namespace FlaxEngine /// /// Determines whether the specified layer is set in the mask. /// - /// Name of the layer (from layers settings). + /// Name of the layer (from Layers settings). /// true if the specified layer is set; otherwise, false. public bool HasLayer(string layerName) { return HasLayer(Level.GetLayerIndex(layerName)); } + /// + /// Gets a layer mask based on a specific layer names. + /// + /// The names of the layers (from Layers settings). + /// A layer mask with the mask set to the layers found. Returns a mask with 0 if not found. + 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; + } + + /// + /// Gets the layer index based on the layer name. + /// + /// The name of the layer. + /// The index if found, otherwise, returns -1. + public static int GetLayerIndex(string layerName) + { + return Level.GetLayerIndex(layerName); + } + + /// + /// Gets the layer name based on the layer index. + /// + /// The index of the layer. + /// The name of the layer if found, otherwise, a blank string. + public static string GetLayerName(int layerIndex) + { + return Level.GetLayerName(layerIndex); + } + /// /// Adds two masks. /// diff --git a/Source/Engine/Core/Types/LayersMask.h b/Source/Engine/Core/Types/LayersMask.h index 3643af19e..25edbc05b 100644 --- a/Source/Engine/Core/Types/LayersMask.h +++ b/Source/Engine/Core/Types/LayersMask.h @@ -27,13 +27,30 @@ public: { } + /// + /// Determines whether the specified layer index is set in the mask. + /// + /// Index of the layer (zero-based). + /// true if the specified layer is set; otherwise, false. FORCE_INLINE bool HasLayer(int32 layerIndex) const { return (Mask & (1 << layerIndex)) != 0; } + /// + /// Determines whether the specified layer name is set in the mask. + /// + /// Name of the layer (from Layers settings). + /// true if the specified layer is set; otherwise, false. bool HasLayer(const StringView& layerName) const; + /// + /// Gets a layers mask from a specific layer name. + /// + /// The names of the layers (from Layers settings). + /// 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. + static LayersMask GetMask(Span layerNames); + operator uint32() const { return Mask; diff --git a/Source/Engine/Core/Types/String.cpp b/Source/Engine/Core/Types/String.cpp index 112434426..ff0e19eba 100644 --- a/Source/Engine/Core/Types/String.cpp +++ b/Source/Engine/Core/Types/String.cpp @@ -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) { diff --git a/Source/Engine/Core/Types/String.h b/Source/Engine/Core/Types/String.h index 4578ac0b3..0630ff6e3 100644 --- a/Source/Engine/Core/Types/String.h +++ b/Source/Engine/Core/Types/String.h @@ -548,7 +548,7 @@ public: /// /// Instance of the empty string. /// - static String Empty; + static const String Empty; public: /// diff --git a/Source/Engine/Core/Types/StringView.cpp b/Source/Engine/Core/Types/StringView.cpp index 505d9ec05..168a537c1 100644 --- a/Source/Engine/Core/Types/StringView.cpp +++ b/Source/Engine/Core/Types/StringView.cpp @@ -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(str.Get(), str.Length()) diff --git a/Source/Engine/Core/Types/StringView.h b/Source/Engine/Core/Types/StringView.h index cbc221e7b..b1025fc91 100644 --- a/Source/Engine/Core/Types/StringView.h +++ b/Source/Engine/Core/Types/StringView.h @@ -227,7 +227,7 @@ public: /// /// Instance of the empty string. /// - static StringView Empty; + static const StringView Empty; public: /// diff --git a/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp b/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp index e93338604..ffc8fa241 100644 --- a/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp @@ -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)) { diff --git a/Source/Engine/Graphics/Materials/GUIMaterialShader.cpp b/Source/Engine/Graphics/Materials/GUIMaterialShader.cpp index 0811570ea..874694702 100644 --- a/Source/Engine/Graphics/Materials/GUIMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/GUIMaterialShader.cpp @@ -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); diff --git a/Source/Engine/Graphics/Materials/IMaterial.h b/Source/Engine/Graphics/Materials/IMaterial.h index 186be0830..23d06589b 100644 --- a/Source/Engine/Graphics/Materials/IMaterial.h +++ b/Source/Engine/Graphics/Materials/IMaterial.h @@ -148,7 +148,7 @@ public: const ::DrawCall* DrawCall = nullptr; MaterialParamsLink* ParamsLink = nullptr; void* CustomData = nullptr; - float TimeParam; + float Time, UnscaledTime; bool Instanced = false; /// diff --git a/Source/Engine/Graphics/Materials/MaterialShader.cpp b/Source/Engine/Graphics/Materials/MaterialShader.cpp index c24631d56..f1e9fd542 100644 --- a/Source/Engine/Graphics/Materials/MaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/MaterialShader.cpp @@ -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; diff --git a/Source/Engine/Graphics/Materials/MaterialShader.h b/Source/Engine/Graphics/Materials/MaterialShader.h index ccc11c8cf..aedf2e870 100644 --- a/Source/Engine/Graphics/Materials/MaterialShader.h +++ b/Source/Engine/Graphics/Materials/MaterialShader.h @@ -10,7 +10,7 @@ /// /// Current materials shader version. /// -#define MATERIAL_GRAPH_VERSION 173 +#define MATERIAL_GRAPH_VERSION 174 class Material; class GPUShader; diff --git a/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp b/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp index 52ba7bc58..c82dd7447 100644 --- a/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp @@ -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; diff --git a/Source/Engine/Graphics/RenderBuffers.h b/Source/Engine/Graphics/RenderBuffers.h index 159ab13a9..647d28c4c 100644 --- a/Source/Engine/Graphics/RenderBuffers.h +++ b/Source/Engine/Graphics/RenderBuffers.h @@ -175,6 +175,12 @@ public: return (const T*)FindCustomBuffer(name, withLinked); } + template + const T* FindLinkedBuffer(const StringView& name) const + { + return LinkedCustomBuffers ? (const T*)LinkedCustomBuffers->FindCustomBuffer(name, true) : nullptr; + } + template T* GetCustomBuffer(const StringView& name, bool withLinked = true) { diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index ec50cfd56..18342dbbb 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -184,6 +184,14 @@ void AnimatedModel::GetNodeTransformation(const StringView& nodeName, Matrix& no GetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace); } +void AnimatedModel::GetNodeTransformation(Array& 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& nodeTransformations, bool worldSpace) +{ + if (GraphInstance.NodesPose.IsEmpty()) + const_cast(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() diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index deadf8c7c..a011be0d0 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -18,6 +18,24 @@ class FLAXENGINE_API AnimatedModel : public ModelInstanceActor, IAssetReference DECLARE_SCENE_OBJECT(AnimatedModel); friend class AnimationsSystem; + /// + /// Keeps the data of a Node and its relevant Transform Matrix together when passing it between functions. + /// + API_STRUCT() struct NodeTransformation + { + DECLARE_SCRIPTING_TYPE_MINIMAL(NodeTransformation); + + /// + /// The index of the node in the node hierarchy. + /// + API_FIELD() uint32 NodeIndex; + + /// + /// The transformation matrix of the node + /// + API_FIELD() Matrix NodeMatrix; + }; + /// /// Describes the animation graph updates frequency for the animated model. /// @@ -242,6 +260,14 @@ public: /// True if convert matrices into world-space, otherwise returned values will be in local-space of the actor. API_FUNCTION() void GetNodeTransformation(const StringView& nodeName, API_PARAM(Out) Matrix& nodeTransformation, bool worldSpace = false) const; + /// + /// Gets the node final transformation for a series of nodes. + /// + /// The series of nodes that will be returned + /// True if convert matrices into world-space, otherwise returned values will be in local-space of the actor. + /// + API_FUNCTION() void GetNodeTransformation(API_PARAM(Ref) Array& nodeTransformations, bool worldSpace = false) const; + /// /// 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. /// @@ -258,6 +284,14 @@ public: /// True if convert matrices from world-space, otherwise values will be in local-space of the actor. API_FUNCTION() void SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace = false); + /// + /// Sets a group of nodes final transformation. + /// + /// Array of the final node transformation matrix. + /// True if convert matrices from world-space, otherwise values will be in local-space of the actor. + /// + API_FUNCTION() void SetNodeTransformation(const Array& nodeTransformations, bool worldSpace = false); + /// /// Finds the closest node to a given location. /// diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index ddb871846..9cf6794be 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -65,6 +65,21 @@ bool LayersMask::HasLayer(const StringView& layerName) const return HasLayer(Level::GetLayerIndex(layerName)); } +LayersMask LayersMask::GetMask(Span 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(); diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index 484ba35b8..110b9ac61 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -539,6 +539,13 @@ public: /// API_FUNCTION() static int32 GetLayerIndex(const StringView& layer); + /// + /// Gets the name of the layer based on the index. + /// + /// The index to find the layer string. 0 - 32. + /// The layer string. Returns a blank string if index not found. + API_FUNCTION() static StringView GetLayerName(const int32 layerIndex); + private: // Actor API enum class ActorEventType diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 183438ba2..f30f6a495 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -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(this); + } +#endif +} + +void ParticleEffect::OnParticleEmitterLoaded() { ApplyModifiedParameters(); } @@ -844,6 +857,10 @@ void ParticleEffect::OnActiveInTreeChanged() CacheModifiedParameters(); Instance.ClearState(); } + else + { + ApplyModifiedParameters(); + } } void ParticleEffect::OnTransformChanged() diff --git a/Source/Engine/Particles/ParticleEffect.h b/Source/Engine/Particles/ParticleEffect.h index 4ec06e82b..c54fa5639 100644 --- a/Source/Engine/Particles/ParticleEffect.h +++ b/Source/Engine/Particles/ParticleEffect.h @@ -394,6 +394,7 @@ private: void ApplyModifiedParameters(); void OnParticleSystemModified(); void OnParticleSystemLoaded(); + void OnParticleEmitterLoaded(); // [IAssetReference] void OnAssetChanged(Asset* asset, void* caller) override; diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.cpp b/Source/Engine/Platform/Apple/AppleFileSystem.cpp index 6e596bf1b..450507009 100644 --- a/Source/Engine/Platform/Apple/AppleFileSystem.cpp +++ b/Source/Engine/Platform/Apple/AppleFileSystem.cpp @@ -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 #include @@ -18,281 +14,9 @@ #include #include #include -#include #include #include -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& 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& 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& 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& 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; diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.h b/Source/Engine/Platform/Apple/AppleFileSystem.h index 5fa6ad1b7..45d9e4df6 100644 --- a/Source/Engine/Platform/Apple/AppleFileSystem.h +++ b/Source/Engine/Platform/Apple/AppleFileSystem.h @@ -4,33 +4,17 @@ #if PLATFORM_MAC || PLATFORM_IOS -#include "Engine/Platform/Base/FileSystemBase.h" +#include "Engine/Platform/Unix/UnixFileSystem.h" /// /// Apple platform implementation of filesystem service. /// -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& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories); - static bool GetChildDirectories(Array& 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& results, const char* path, const char* searchPattern); - static bool getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern); }; #endif diff --git a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp index 3e2055572..9b31dc28f 100644 --- a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp +++ b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp @@ -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 #include #include #include #include #include -#include #include #include #include @@ -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& 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& 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& 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& 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(); diff --git a/Source/Engine/Platform/Linux/LinuxFileSystem.h b/Source/Engine/Platform/Linux/LinuxFileSystem.h index f77c3dd4f..377bbbd99 100644 --- a/Source/Engine/Platform/Linux/LinuxFileSystem.h +++ b/Source/Engine/Platform/Linux/LinuxFileSystem.h @@ -4,38 +4,24 @@ #if PLATFORM_LINUX -#include "Engine/Platform/Base/FileSystemBase.h" +#include "Engine/Platform/Unix/UnixFileSystem.h" /// /// Linux platform implementation of filesystem service. /// -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& 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& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories); - static bool GetChildDirectories(Array& 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& results, const char* path, const char* searchPattern); - static bool getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern); static String getBaseName(const StringView& path); static String getNameWithoutExtension(const StringView& path); }; diff --git a/Source/Engine/Platform/Unix/UnixFileSystem.cpp b/Source/Engine/Platform/Unix/UnixFileSystem.cpp new file mode 100644 index 000000000..1fb47f0f5 --- /dev/null +++ b/Source/Engine/Platform/Unix/UnixFileSystem.cpp @@ -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 +#include +#include +#include +#include + +#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& 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& 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& 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& 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 diff --git a/Source/Engine/Platform/Unix/UnixFileSystem.h b/Source/Engine/Platform/Unix/UnixFileSystem.h new file mode 100644 index 000000000..4c7256aca --- /dev/null +++ b/Source/Engine/Platform/Unix/UnixFileSystem.h @@ -0,0 +1,35 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_UNIX + +#include "Engine/Platform/Base/FileSystemBase.h" + +/// +/// Unix platform implementation of filesystem service. +/// +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& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories); + static bool GetChildDirectories(Array& 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& results, const char* path, const char* searchPattern); + static bool getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern); +}; + +#endif diff --git a/Source/Engine/Renderer/GBufferPass.cpp b/Source/Engine/Renderer/GBufferPass.cpp index ee021d0ea..9d3684010 100644 --- a/Source/Engine/Renderer/GBufferPass.cpp +++ b/Source/Engine/Renderer/GBufferPass.cpp @@ -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; } } diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index 968b271dd..fc93202e8 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -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 Atlas; RectPackAtlas StaticAtlas; Dictionary 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 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(TEXT("Shadows"), false)) @@ -1100,11 +1129,14 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch& // Initialize shadow atlas auto& shadows = *renderContext.Buffers->GetCustomBuffer(TEXT("Shadows"), false); + shadows.LinkedShadows = renderContext.Buffers->FindLinkedBuffer(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)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 diff --git a/Source/Engine/Renderer/ShadowsPass.h b/Source/Engine/Renderer/ShadowsPass.h index 748a7c084..8e64a205d 100644 --- a/Source/Engine/Renderer/ShadowsPass.h +++ b/Source/Engine/Renderer/ShadowsPass.h @@ -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) diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index d1203a1f3..cc77f2337 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -108,6 +108,7 @@ namespace MMethod* _method_LateFixedUpdate = nullptr; MMethod* _method_Draw = nullptr; MMethod* _method_Exit = nullptr; + Array> UpdateActions; Dictionary> _nonNativeModules; #if USE_EDITOR bool LastBinariesLoadTriggeredCompilation = false; @@ -245,6 +246,27 @@ void ScriptingService::Update() PROFILE_CPU_NAMED("Scripting::Update"); INVOKE_EVENT(Update); + // Flush update actions + _objectsLocker.Lock(); + int32 count = UpdateActions.Count(); + for (int32 i = 0; i < count; i++) + { + UpdateActions[i](); + } + int32 newlyAdded = UpdateActions.Count() - count; + if (newlyAdded == 0) + UpdateActions.Clear(); + else + { + // Someone added another action within current callback + Array> tmp; + for (int32 i = newlyAdded; i < UpdateActions.Count(); i++) + tmp.Add(UpdateActions[i]); + UpdateActions.Clear(); + UpdateActions.Add(tmp); + } + _objectsLocker.Unlock(); + #if defined(USE_NETCORE) && !USE_EDITOR // Force GC to run in background periodically to avoid large blocking collections causing hitches if (Time::Update.TicksCount % 60 == 0) @@ -311,6 +333,13 @@ void Scripting::ProcessBuildInfoPath(String& path, const String& projectFolderPa path = projectFolderPath / path; } +void Scripting::InvokeOnUpdate(const Function& action) +{ + _objectsLocker.Lock(); + UpdateActions.Add(action); + _objectsLocker.Unlock(); +} + bool Scripting::LoadBinaryModules(const String& path, const String& projectFolderPath) { PROFILE_CPU_NAMED("LoadBinaryModules"); diff --git a/Source/Engine/Scripting/Scripting.h b/Source/Engine/Scripting/Scripting.h index be5e9ca34..1e7daf6f0 100644 --- a/Source/Engine/Scripting/Scripting.h +++ b/Source/Engine/Scripting/Scripting.h @@ -230,6 +230,11 @@ public: static void ProcessBuildInfoPath(String& path, const String& projectFolderPath); + /// + /// Calls the given action on the next scripting update. + /// + /// The action to invoke. + static void InvokeOnUpdate(const Function& action); private: static bool LoadBinaryModules(const String& path, const String& projectFolderPath); diff --git a/Source/Engine/Serialization/MemoryReadStream.cpp b/Source/Engine/Serialization/MemoryReadStream.cpp index b84cb3af6..e6a1acea1 100644 --- a/Source/Engine/Serialization/MemoryReadStream.cpp +++ b/Source/Engine/Serialization/MemoryReadStream.cpp @@ -61,7 +61,11 @@ void MemoryReadStream::ReadBytes(void* data, uint32 bytes) { if (bytes > 0) { - ASSERT(data && GetLength() - GetPosition() >= bytes); + if (!data || GetLength() - GetPosition() < bytes) + { + _hasError = true; + return; + } Platform::MemoryCopy(data, _position, bytes); _position += bytes; } diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index 608f57ec3..6bd88f2ae 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -34,7 +34,6 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B const bool isArray = texture->Type == MaterialParameterType::GPUTextureArray; const bool isVolume = texture->Type == MaterialParameterType::GPUTextureVolume; const bool isNormalMap = texture->Type == MaterialParameterType::NormalMap; - const bool canUseSample = CanUseSample(_treeType); MaterialGraphBox* valueBox = parent->GetBox(1); // Check if has variable assigned @@ -63,6 +62,16 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B // Check if hasn't been sampled during that tree eating if (valueBox->Cache.IsInvalid()) { + bool canUseSample = CanUseSample(_treeType); + String mipLevel = TEXT("0"); + const auto layer = GetRootLayer(); + if (layer && layer->Domain == MaterialDomain::Decal && _treeType == MaterialTreeType::PixelShader) + { + // Decals use computed mip level due to ddx/ddy being unreliable + canUseSample = false; + mipLevel = String::Format(TEXT("CalculateTextureMipmap(input, {})"), texture->ShaderName); + } + // Check if use custom UVs String uv; MaterialGraphBox* uvBox = parent->GetBox(0); @@ -94,10 +103,10 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B // Sample texture if (isNormalMap) { - const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, 0).xyz"); + const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, {3}).xyz"); // Sample encoded normal map - const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv); + const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, mipLevel); const auto normalVector = writeLocal(VariantType::Float3, sampledValue, parent); // Decode normal vector @@ -123,12 +132,12 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B } else*/ { - format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, 0)"); + format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, {3})"); } } // Sample texture - String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, _ddx.Value, _ddy.Value); + String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, mipLevel); valueBox->Cache = writeLocal(VariantType::Float4, sampledValue, parent); } } diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Tools.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Tools.cpp index 975bbabc6..056ddd7ce 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Tools.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Tools.cpp @@ -48,10 +48,8 @@ void MaterialGenerator::ProcessGroupTools(Box* box, Node* node, Value& value) } // Time case 3: - { - value = getTime; + value = box->ID == 1 ? getUnscaledTime : getTime; break; - } // Panner case 6: { diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp index 2aea40c94..9c42d1865 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp @@ -107,6 +107,7 @@ bool FeatureData::Init() MaterialValue MaterialGenerator::getUVs(VariantType::Float2, TEXT("input.TexCoord")); MaterialValue MaterialGenerator::getTime(VariantType::Float, TEXT("TimeParam")); +MaterialValue MaterialGenerator::getUnscaledTime(VariantType::Float, TEXT("UnscaledTimeParam")); MaterialValue MaterialGenerator::getNormal(VariantType::Float3, TEXT("input.TBN[2]")); MaterialValue MaterialGenerator::getNormalZero(VariantType::Float3, TEXT("float3(0, 0, 1)")); MaterialValue MaterialGenerator::getVertexColor(VariantType::Float4, TEXT("GetVertexColor(input)")); diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h index 59c34c55f..706929d75 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h @@ -120,7 +120,6 @@ private: MaterialValue _ddx, _ddy, _cameraVector; public: - MaterialGenerator(); ~MaterialGenerator(); @@ -211,6 +210,7 @@ public: static MaterialValue getUVs; static MaterialValue getTime; + static MaterialValue getUnscaledTime; static MaterialValue getNormal; static MaterialValue getNormalZero; static MaterialValue getVertexColor; diff --git a/Source/Engine/UI/GUI/Brushes/MaterialBrush.cs b/Source/Engine/UI/GUI/Brushes/MaterialBrush.cs index 6210ca2d0..13e9dab24 100644 --- a/Source/Engine/UI/GUI/Brushes/MaterialBrush.cs +++ b/Source/Engine/UI/GUI/Brushes/MaterialBrush.cs @@ -25,7 +25,7 @@ namespace FlaxEngine.GUI /// Initializes a new instance of the struct. /// /// The material. - public MaterialBrush(Material material) + public MaterialBrush(MaterialBase material) { Material = material; } diff --git a/Source/Engine/Utilities/HtmlParser.cs b/Source/Engine/Utilities/HtmlParser.cs index 175b0247a..0b189ff83 100644 --- a/Source/Engine/Utilities/HtmlParser.cs +++ b/Source/Engine/Utilities/HtmlParser.cs @@ -134,6 +134,10 @@ namespace FlaxEngine.Utilities if (isLeadingSlash) Move(); + // Dont process if wrong slash is used. + if (c =='\\') + return false; + // Parse tag bool result = ParseTag(ref tag, name); @@ -206,6 +210,10 @@ namespace FlaxEngine.Utilities SkipWhitespace(); while (Peek() != '>') { + // Return false if start of new html tag is detected. + if (Peek() == '<') + return false; + if (Peek() == '/') { // Handle trailing forward slash diff --git a/Source/Shaders/Lighting.hlsl b/Source/Shaders/Lighting.hlsl index 1429e45d1..9b14db5ed 100644 --- a/Source/Shaders/Lighting.hlsl +++ b/Source/Shaders/Lighting.hlsl @@ -122,6 +122,10 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f // Calculate shadow ShadowSample shadow = GetShadow(lightData, gBuffer, shadowMask); +#if !LIGHTING_NO_DIRECTIONAL + // Directional shadowing + shadow.SurfaceShadow *= NoL; +#endif // Calculate attenuation if (isRadial) @@ -135,11 +139,6 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f shadow.TransmissionShadow *= attenuation; } -#if !LIGHTING_NO_DIRECTIONAL - // Reduce shadow mapping artifacts - shadow.SurfaceShadow *= saturate(NoL * 6.0f - 0.2f) * NoL; -#endif - BRANCH if (shadow.SurfaceShadow + shadow.TransmissionShadow > 0) { diff --git a/Source/Shaders/MaterialCommon.hlsl b/Source/Shaders/MaterialCommon.hlsl index f835b2327..897b01d7e 100644 --- a/Source/Shaders/MaterialCommon.hlsl +++ b/Source/Shaders/MaterialCommon.hlsl @@ -176,6 +176,8 @@ cbuffer ViewData : register(b1) float4 TemporalAAJitter; float3 LargeWorldsChunkIndex; float LargeWorldsChunkSize; + float3 ViewPadding0; + float UnscaledTimeParam; }; #endif diff --git a/Source/Shaders/SSAO.shader b/Source/Shaders/SSAO.shader index 6fc3c523c..5116b3cb9 100644 --- a/Source/Shaders/SSAO.shader +++ b/Source/Shaders/SSAO.shader @@ -56,7 +56,7 @@ static const uint g_numTaps[4] = { 3, 5, 8, 12 }; #define SSAO_NORMAL_BASED_EDGES_ENABLE_AT_QUALITY_PRESET (2) // to disable simply set to 99 or similar #define SSAO_NORMAL_BASED_EDGES_DOT_THRESHOLD (0.5) // use 0-0.1 for super-sharp normal-based edges // -#define SSAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET (1) // whether to use DetailAOStrength; to disable simply set to 99 or similar +#define SSAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET (99) // whether to use DetailAOStrength; to disable simply set to 99 or similar // #define SSAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET (99) // !!warning!! the MIP generation on the C++ side will be enabled on quality preset 2 regardless of this value, so if changing here, change the C++ side too #define SSAO_DEPTH_MIPS_GLOBAL_OFFSET (-4.3) // best noise/quality/performance tradeoff, found empirically diff --git a/Source/Tools/Flax.Build/Build/Builder.Projects.cs b/Source/Tools/Flax.Build/Build/Builder.Projects.cs index 008e9d982..59538364c 100644 --- a/Source/Tools/Flax.Build/Build/Builder.Projects.cs +++ b/Source/Tools/Flax.Build/Build/Builder.Projects.cs @@ -513,6 +513,7 @@ namespace Flax.Build // Combine build options from this module project.CSharp.SystemReferences.AddRange(moduleBuildOptions.ScriptingAPI.SystemReferences); project.CSharp.FileReferences.AddRange(moduleBuildOptions.ScriptingAPI.FileReferences); + project.CSharp.NugetPackageReferences.AddRange(moduleBuildOptions.NugetPackageReferences); // Find references based on the modules dependencies (external or from projects) foreach (var dependencyName in moduleBuildOptions.PublicDependencies.Concat(moduleBuildOptions.PrivateDependencies)) diff --git a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs index 2619f1a17..23646a3d7 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs @@ -109,6 +109,7 @@ namespace Flax.Build // Merge module into target environment buildData.TargetOptions.LinkEnv.InputFiles.AddRange(moduleOptions.OutputFiles); buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles); + buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences); buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles); buildData.TargetOptions.Libraries.AddRange(moduleOptions.Libraries); buildData.TargetOptions.DelayLoadLibraries.AddRange(moduleOptions.DelayLoadLibraries); @@ -141,6 +142,19 @@ namespace Flax.Build var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile)); graph.AddCopyFile(dstFile, srcFile); } + + if (targetBuildOptions.NugetPackageReferences.Any()) + { + var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); + foreach (var reference in targetBuildOptions.NugetPackageReferences) + { + var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll"); + if (!File.Exists(path)) + Utilities.RestoreNugetPackages(graph, target); + var dstFile = Path.Combine(outputPath, Path.GetFileName(path)); + graph.AddCopyFile(dstFile, path); + } + } } } @@ -283,6 +297,18 @@ namespace Flax.Build args.Add(string.Format("/reference:\"{0}{1}.dll\"", referenceAssemblies, reference)); foreach (var reference in fileReferences) args.Add(string.Format("/reference:\"{0}\"", reference)); + + // Reference Nuget package + if (buildData.TargetOptions.NugetPackageReferences.Any()) + { + var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); + foreach (var reference in buildOptions.NugetPackageReferences) + { + var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll"); + args.Add(string.Format("/reference:\"{0}\"", path)); + } + } + #if USE_NETCORE foreach (var systemAnalyzer in buildOptions.ScriptingAPI.SystemAnalyzers) args.Add(string.Format("/analyzer:\"{0}{1}.dll\"", referenceAnalyzers, systemAnalyzer)); diff --git a/Source/Tools/Flax.Build/Build/EngineTarget.cs b/Source/Tools/Flax.Build/Build/EngineTarget.cs index 6a44991a2..827205e91 100644 --- a/Source/Tools/Flax.Build/Build/EngineTarget.cs +++ b/Source/Tools/Flax.Build/Build/EngineTarget.cs @@ -220,6 +220,7 @@ namespace Flax.Build exeBuildOptions.LinkEnv.InputLibraries.Add(Path.Combine(buildOptions.OutputFolder, buildOptions.Platform.GetLinkOutputFileName(LibraryName, engineLibraryType))); exeBuildOptions.LinkEnv.InputFiles.AddRange(mainModuleOptions.OutputFiles); exeBuildOptions.DependencyFiles.AddRange(mainModuleOptions.DependencyFiles); + exeBuildOptions.NugetPackageReferences.AddRange(mainModuleOptions.NugetPackageReferences); exeBuildOptions.OptionalDependencyFiles.AddRange(mainModuleOptions.OptionalDependencyFiles); exeBuildOptions.Libraries.AddRange(mainModuleOptions.Libraries); exeBuildOptions.DelayLoadLibraries.AddRange(mainModuleOptions.DelayLoadLibraries); diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index 95695e203..13ecd1982 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -70,6 +70,40 @@ namespace Flax.Build.NativeCpp Annotations, } + /// + /// Defines a Nuget Package + /// + public struct NugetPackage + { + /// + /// The name of the nuget package. + /// + public string Name; + + /// + /// The version of the nuget package. + /// + public string Version; + + /// + /// The target framework. ex. net8.0, netstandard2.1 + /// + public string Framework; + + /// + /// Initialize the nuget package. + /// + /// The name of the package. + /// The version of the package. + /// The target framework. ex. net8.0, netstandard2.1, etc. + public NugetPackage(string name, string version, string framework) + { + Name = name; + Version = version; + Framework = framework; + } + } + /// /// The native C++ module build settings container. /// @@ -129,6 +163,11 @@ namespace Flax.Build.NativeCpp /// The collection of the modules that are required by this module (for linking). /// public List PrivateDependencies = new List(); + + /// + /// The nuget package references. + /// + public List NugetPackageReferences = new List(); /// /// The collection of defines with preprocessing symbol for a source files of this module. Inherited by the modules that include it. diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs index 1d0d3c695..18459ddb5 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs @@ -425,6 +425,7 @@ namespace Flax.Build moduleOptions.LinkEnv.InputFiles.AddRange(dependencyOptions.OutputFiles); moduleOptions.DependencyFiles.AddRange(dependencyOptions.DependencyFiles); moduleOptions.OptionalDependencyFiles.AddRange(dependencyOptions.OptionalDependencyFiles); + moduleOptions.NugetPackageReferences.AddRange(dependencyOptions.NugetPackageReferences); moduleOptions.PrivateIncludePaths.AddRange(dependencyOptions.PublicIncludePaths); moduleOptions.Libraries.AddRange(dependencyOptions.Libraries); moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries); @@ -440,6 +441,7 @@ namespace Flax.Build moduleOptions.LinkEnv.InputFiles.AddRange(dependencyOptions.OutputFiles); moduleOptions.DependencyFiles.AddRange(dependencyOptions.DependencyFiles); moduleOptions.OptionalDependencyFiles.AddRange(dependencyOptions.OptionalDependencyFiles); + moduleOptions.NugetPackageReferences.AddRange(dependencyOptions.NugetPackageReferences); moduleOptions.PublicIncludePaths.AddRange(dependencyOptions.PublicIncludePaths); moduleOptions.Libraries.AddRange(dependencyOptions.Libraries); moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries); @@ -934,6 +936,7 @@ namespace Flax.Build buildData.TargetOptions.LinkEnv.InputFiles.AddRange(moduleOptions.OutputFiles); buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles); buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles); + buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences); buildData.TargetOptions.Libraries.AddRange(moduleOptions.Libraries); buildData.TargetOptions.DelayLoadLibraries.AddRange(moduleOptions.DelayLoadLibraries); buildData.TargetOptions.ScriptingAPI.Add(moduleOptions.ScriptingAPI); @@ -1054,6 +1057,19 @@ namespace Flax.Build var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile)); graph.AddCopyFile(dstFile, srcFile); } + + if (targetBuildOptions.NugetPackageReferences.Any()) + { + var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); + foreach (var reference in targetBuildOptions.NugetPackageReferences) + { + var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll"); + if (!File.Exists(path)) + Utilities.RestoreNugetPackages(graph, target); + var dstFile = Path.Combine(outputPath, Path.GetFileName(path)); + graph.AddCopyFile(dstFile, path); + } + } } } @@ -1192,6 +1208,7 @@ namespace Flax.Build buildData.TargetOptions.ExternalModules.AddRange(moduleOptions.ExternalModules); buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles); buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles); + buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences); } } } @@ -1253,6 +1270,19 @@ namespace Flax.Build var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile)); graph.AddCopyFile(dstFile, srcFile); } + + if (targetBuildOptions.NugetPackageReferences.Any()) + { + var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); + foreach (var reference in targetBuildOptions.NugetPackageReferences) + { + var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll"); + if (!File.Exists(path)) + Utilities.RestoreNugetPackages(graph, target); + var dstFile = Path.Combine(outputPath, Path.GetFileName(path)); + graph.AddCopyFile(dstFile, path); + } + } } } diff --git a/Source/Tools/Flax.Build/Projects/Project.cs b/Source/Tools/Flax.Build/Projects/Project.cs index 5aa77a453..46ae6102b 100644 --- a/Source/Tools/Flax.Build/Projects/Project.cs +++ b/Source/Tools/Flax.Build/Projects/Project.cs @@ -229,6 +229,11 @@ namespace Flax.Build.Projects /// The .Net libraries references (dll or exe files paths). /// public HashSet FileReferences; + + /// + /// The nuget references. + /// + public HashSet NugetPackageReferences; /// /// The output folder path (optional). @@ -248,6 +253,7 @@ namespace Flax.Build.Projects { SystemReferences = new HashSet(), FileReferences = new HashSet(), + NugetPackageReferences = new HashSet(), }; /// diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSProjectGenerator.cs index da6907f78..35bc7b0ac 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSProjectGenerator.cs @@ -175,6 +175,19 @@ namespace Flax.Build.Projects.VisualStudio csProjectFileContent.AppendLine(" "); } + // Nuget + if (project.CSharp.NugetPackageReferences.Any()) + { + csProjectFileContent.AppendLine(" "); + + foreach (var reference in project.CSharp.NugetPackageReferences) + { + csProjectFileContent.AppendLine(string.Format(" ", reference.Name, reference.Version)); + } + + csProjectFileContent.AppendLine(" "); + } + // References csProjectFileContent.AppendLine(" "); diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs index df1424e95..f126bd3ef 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs @@ -134,6 +134,20 @@ namespace Flax.Build.Projects.VisualStudio } // References + + // Nuget + if (project.CSharp.NugetPackageReferences.Any()) + { + csProjectFileContent.AppendLine(" "); + + foreach (var reference in project.CSharp.NugetPackageReferences) + { + csProjectFileContent.AppendLine(string.Format(" ", reference.Name, reference.Version)); + } + + csProjectFileContent.AppendLine(" "); + csProjectFileContent.AppendLine(""); + } csProjectFileContent.AppendLine(" "); diff --git a/Source/Tools/Flax.Build/Utilities/Utilities.cs b/Source/Tools/Flax.Build/Utilities/Utilities.cs index 85f791d09..0ce5c8b7b 100644 --- a/Source/Tools/Flax.Build/Utilities/Utilities.cs +++ b/Source/Tools/Flax.Build/Utilities/Utilities.cs @@ -16,6 +16,47 @@ namespace Flax.Build /// public static class Utilities { + /// + /// Gets the .Net SDK path. + /// + /// The path. + public static string GetDotNetPath() + { + var buildPlatform = Platform.BuildTargetPlatform; + var dotnetSdk = DotNetSdk.Instance; + if (!dotnetSdk.IsValid) + throw new DotNetSdk.MissingException(); + var dotnetPath = "dotnet"; + switch (buildPlatform) + { + case TargetPlatform.Windows: + dotnetPath = Path.Combine(dotnetSdk.RootPath, "dotnet.exe"); + break; + case TargetPlatform.Linux: break; + case TargetPlatform.Mac: + dotnetPath = Path.Combine(dotnetSdk.RootPath, "dotnet"); + break; + default: throw new InvalidPlatformException(buildPlatform); + } + return dotnetPath; + } + + /// + /// Restores a targets nuget packages. + /// + /// The task graph. + /// The target. + /// The dotnet path. + public static void RestoreNugetPackages(Graph.TaskGraph graph, Target target) + { + var dotNetPath = GetDotNetPath(); + var task = graph.Add(); + task.WorkingDirectory = target.FolderPath; + task.InfoMessage = $"Restoring Nuget Packages for {target.Name}"; + task.CommandPath = dotNetPath; + task.CommandArguments = $"restore"; + } + /// /// Gets the hash code for the string (the same for all platforms). Matches Engine algorithm for string hashing. ///