diff --git a/Content/Editor/Camera/M_Camera.flax b/Content/Editor/Camera/M_Camera.flax
index 7d7213a8d..7d4c71666 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:7edc1b9d2c7fbd32fcf778814deb719c71781f657da050ac0c7c78984aeb360d
+oid sha256:b73d774c71bd7b46c9c4198a4c957055e6447e31d8252813b272db92301475e7
size 29533
diff --git a/Content/Editor/CubeTexturePreviewMaterial.flax b/Content/Editor/CubeTexturePreviewMaterial.flax
index 5969c90fa..2d732c086 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:ac6023e5d6525c3b7c385a380ed9d6fc71ec9d683c587391d14c9daf6653e31a
+oid sha256:c4ec07a3b7e0a2dfd4332598a982c3192c0c357c6bcd128d7a7797fb483780e7
size 31445
diff --git a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax
index fc45d33cc..d082bd8e7 100644
--- a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax
+++ b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ecd573c40f534f293d4827b1a8150d439d4f5e7729552474926208c5814f3d3e
+oid sha256:2830919bea988e1f8bd8299ceac34b8a3695418e2f22ca670f2fec3b3d6d1a2f
size 41149
diff --git a/Content/Editor/DebugMaterials/SingleColor/Decal.flax b/Content/Editor/DebugMaterials/SingleColor/Decal.flax
index 05e99be76..b94f22bc8 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:c0b2ad25738c2bc55bb3e76fc94fc81992b1d65b8b3091b132c75b2ed064c517
-size 10398
+oid sha256:588c29a4b239c32d4b125052e4054a29cf5140562e90ca6fac4d2952e03f66c7
+size 10397
diff --git a/Content/Editor/DebugMaterials/SingleColor/Particle.flax b/Content/Editor/DebugMaterials/SingleColor/Particle.flax
index 7a328e7a0..de2043874 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:02ddea5bcb3fccb697081e47cc26a0b546b23b89ceca299e702a1d431775dfd6
+oid sha256:b39cd76254f341c93e83625475b6e7896ef34f1d6d650da52e649bc055d0d03e
size 33503
diff --git a/Content/Editor/DebugMaterials/SingleColor/Surface.flax b/Content/Editor/DebugMaterials/SingleColor/Surface.flax
index 84e05ee36..7ae8a69c3 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:741a7619b5aebc6c7c6a573a0407e8b7aa42d1b50d0ed5cf6a21026932807d0e
+oid sha256:5861e912cf822c9478f824390f6258d84821b7289e3e993a7dee38b77c5a2f80
size 29398
diff --git a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
index ab4591176..fdcb880df 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:358370943d21a97f8b45ff2181b7c6c2d7a6297e3f166ae7a77363aadf89b152
+oid sha256:b9ed2869a2a754423e0b8c456eed621bd06bdb50cacf7a972a7f024e40a1ea6a
size 32954
diff --git a/Content/Editor/DebugMaterials/SingleColor/Terrain.flax b/Content/Editor/DebugMaterials/SingleColor/Terrain.flax
index 54151179a..ad27a422c 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:486b4db3e1d825d026753d944a04defe4d72eb73eb03a438944c366f19de824e
+oid sha256:05c27ac416ef922ee247adc12a115fd522eb3a1d8873e1056914cd96893a3097
size 21096
diff --git a/Content/Editor/DefaultFontMaterial.flax b/Content/Editor/DefaultFontMaterial.flax
index 8d48c5827..d84425aab 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:ebdfc478caabc84a3a75384a64d262d2d509bbac3540eea462e45911719c288f
+oid sha256:8e3d4ca149e143fee31e2d038b8efec526ca995dbe13258fbb68c89cd43ecbf7
size 29627
diff --git a/Content/Editor/Gizmo/FoliageBrushMaterial.flax b/Content/Editor/Gizmo/FoliageBrushMaterial.flax
index 79385ada6..eb7e784c9 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:aa4f1a733150c62064cac60c07980df7c84bb6163dc9507782aa98df07f48874
+oid sha256:7af1150d6e7cb6ecce5cd039f0edc92967c986a13903a201d6dc15ed0751dc57
size 39637
diff --git a/Content/Editor/Gizmo/Material.flax b/Content/Editor/Gizmo/Material.flax
index ace3bde90..bbb114662 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:26e1832496c01cb31bd4dc9000d3cd326ea4fd54de02910d3801d2641bff685c
+oid sha256:d575ca1b202c84b8268687b391be5fc8d55497ffa23fb3cd4287fa667de654ab
size 34240
diff --git a/Content/Editor/Gizmo/MaterialWire.flax b/Content/Editor/Gizmo/MaterialWire.flax
index 7ea0a596f..fb4b8acca 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:ca8bc1ac9d45534d3efd3b4308d7492fa016726b4ec744be26619069ce911b73
+oid sha256:26f2d88aab9c0cad36ae527b038a36b69755ff3a5a920e8c4563dd5e1ed8ec65
size 32689
diff --git a/Content/Editor/Gizmo/SelectionOutlineMaterial.flax b/Content/Editor/Gizmo/SelectionOutlineMaterial.flax
index 0c1461b72..b5d224d58 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:09f7dff17af9cd055352e0da534f3466c8efa235c40faf5e56da92c788342f6a
-size 17394
+oid sha256:5bb75934622d9251a8a9e72cfe4905091770798ffed22de680a70f98434d0ed7
+size 16241
diff --git a/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax b/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax
index bd4935d96..5a5262e2b 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:1bc0005c64c561a430a17e4707abc000e06498af968890e2c4e223dc07f07c12
+oid sha256:a1afa76c3f9400da065c150a6a58adc904c3596f650e04dfd87b5e1c1b34695e
size 30655
diff --git a/Content/Editor/Highlight Material.flax b/Content/Editor/Highlight Material.flax
index ccecb98aa..9d09ea792 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:95d172cd12bb3c818fbccf737e78ab282bc8d0880aa8f45af0562850b0eabe4b
-size 31616
+oid sha256:1290ae85e4fe41f9d8c1919b33e165287f79377aeddc68f9117c1795ca341003
+size 31267
diff --git a/Content/Editor/Icons/IconsMaterial.flax b/Content/Editor/Icons/IconsMaterial.flax
index b24941463..2ccbce8c9 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:5ca4baa1419080395dcf2b5757676406288f112754bc3cd2f27610b58d199622
+oid sha256:340cc500a160344b43b21ed8c4c22b6d776f406581f606ced62a3e92c5bef18a
size 31300
diff --git a/Content/Editor/IesProfilePreviewMaterial.flax b/Content/Editor/IesProfilePreviewMaterial.flax
index 99bc2662c..b3a382132 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:b3b4c61b04d372d2430a7c08dec612af6caa0e57b1cb47ea44d171d729d3f8f8
+oid sha256:d444cd33ec8d2e1e0e6651c3979260f05c06c8bac33ce2441d6974ae4fa178e4
size 20443
diff --git a/Content/Editor/Particles/Particle Material Color.flax b/Content/Editor/Particles/Particle Material Color.flax
index 19eb7a3c2..91b06b2fb 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:6f3b8a7c48c55e33a41f9fe4dbf9b3109b0e734ff154d6cbd3e4101013b01649
+oid sha256:906443c7db821361b32780c17735bc9477ea96c8979dee371a4899635246af48
size 31708
diff --git a/Content/Editor/Particles/Smoke Material.flax b/Content/Editor/Particles/Smoke Material.flax
index 527d19842..e6396c194 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:2275282d4e3b5e012a0bbc93fca0d6ffdad89e5a5f0c289678f70748f2efab56
-size 40655
+oid sha256:16db9c1a18b64aea2dcdf3e74f9a44c652bf8bd9b33a5bfda39555d8c002a358
+size 39774
diff --git a/Content/Editor/SpriteMaterial.flax b/Content/Editor/SpriteMaterial.flax
index d967a4ea4..2a05418b2 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:6f5e82be7efa6489cfdfd1babeb1fbb90507aaff7c04eb5f64a4971adf0a2164
+oid sha256:56254b02ffc937d61e8e8fa6492d4805e944ca639c7fcfc0f751b4ac2442365d
size 30734
diff --git a/Content/Editor/Terrain/Circle Brush Material.flax b/Content/Editor/Terrain/Circle Brush Material.flax
index 6ddc5f3e9..f481be389 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:2c7fde7be7d6f9876f9c0db02632c098ab95ade7de57c583d2e495c8ae8665bd
+oid sha256:16eefa75a2ae99bba658c4e9b8e8741187b90e577193f76394872764fff2ca0b
size 28232
diff --git a/Content/Editor/Terrain/Highlight Terrain Material.flax b/Content/Editor/Terrain/Highlight Terrain Material.flax
index c573eb3ee..579db477c 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:9369a554ea1776154f5e39d4aaed044f928d98f1f5955b7590b0972015b07438
+oid sha256:e25a3c9e130e51b28dfe5ce43678f52c277c0def83142a2853c4c8ca84dbf417
size 21179
diff --git a/Content/Editor/TexturePreviewMaterial.flax b/Content/Editor/TexturePreviewMaterial.flax
index 2c91f9d8f..d75e19d5e 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:4d61f178e72e4d983a919b76368e03c66995ecf50935f6f55b660e34f58755a2
+oid sha256:79de09ba0616eb6066171c2b80cdb6c4235cb52be4836d23162bb9c2585760a0
size 11058
diff --git a/Content/Editor/Wires Debug Material.flax b/Content/Editor/Wires Debug Material.flax
index 308a6230a..b1f87a7d0 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:c7a42b1bc5a34f9c47d1aeb773ef26ce470b2d88c2b092828f0fcb439583ef27
-size 31616
+oid sha256:02d4c767fb59c67fef16ccc081f6f371bad329a5333047f9f79fd3d50b911f93
+size 31753
diff --git a/Content/Engine/DefaultDeformableMaterial.flax b/Content/Engine/DefaultDeformableMaterial.flax
index a397d1ad8..1244ae3ec 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:be21bb7eecd9c774196dbaa89d1b049b108fc0929d648795056c977fe00364ab
-size 19582
+oid sha256:d1f556b230cea8e83d00bd4357d34a77e5e468389a5f3bb615e30f6a3ce3ace4
+size 19734
diff --git a/Content/Engine/DefaultMaterial.flax b/Content/Engine/DefaultMaterial.flax
index eddcbace8..bd57e7d44 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:0a8a4ad5e763704263b94a7a7e0cc30ab7b1cd1abcb5ccae2d4c6062a65920df
-size 31928
+oid sha256:c4ec872b3433d58f8aed640c6efee3d911f226740b4844cb07ed0bf94c00ea18
+size 32080
diff --git a/Content/Engine/DefaultRadialMenu.flax b/Content/Engine/DefaultRadialMenu.flax
index 60e2ba5f9..5fba9092e 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:c4151a58e5314937efcd3bdcb9fe0bdd5047b8705931e45e0a4e71a4470e16a0
+oid sha256:0da99403c069966d05daea7fc11d32f20f88bac0463fbc08724840e249ee3bd2
size 21700
diff --git a/Content/Engine/DefaultTerrainMaterial.flax b/Content/Engine/DefaultTerrainMaterial.flax
index b302ade35..4147fe0e4 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:c5cf6924809b9bd7ad3c09722a93f327a0d111676060d136df9c14ab34e8475b
-size 23930
+oid sha256:bdfa3b4842a5734d2cd8110af03599b4a5280b33a72b2ba435cd19487cebcde6
+size 24082
diff --git a/Content/Engine/SingleColorMaterial.flax b/Content/Engine/SingleColorMaterial.flax
index d6d179150..6d556af2b 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:750f69ce59ef020d2e2186ed6c4bf7aac67ecb1692287e358eaed969fc36381a
+oid sha256:6ff8f127d46e68e3423339a352f623c079f2c5d93512c5e9b25841edc7cd0f05
size 29615
diff --git a/Content/Engine/SkyboxMaterial.flax b/Content/Engine/SkyboxMaterial.flax
index cc369ceee..b51c5bce7 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:3eecc9556af6c2a79d39a7e1c52e4019bdccfb43b074eaddd18600a5854dbffe
+oid sha256:14c9833ed19302ea7c6e730fff63f1b72dbac71dc2b49c1d62edb61ccaa68b6f
size 31974
diff --git a/Content/Shaders/GI/DDGI.flax b/Content/Shaders/GI/DDGI.flax
index 6739b2436..257953bf9 100644
--- a/Content/Shaders/GI/DDGI.flax
+++ b/Content/Shaders/GI/DDGI.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5577ef4ce821b08a38afe17b9e5d11cb0b409eb05dd89b2ca76ea95d88085dc0
-size 32893
+oid sha256:5b017cf857f443553020e4bc7c8c8c5da3a826a2514322664a023ffa6005f7a5
+size 38217
diff --git a/Content/Shaders/GI/GlobalSurfaceAtlas.flax b/Content/Shaders/GI/GlobalSurfaceAtlas.flax
index 1b0173ba5..57990c249 100644
--- a/Content/Shaders/GI/GlobalSurfaceAtlas.flax
+++ b/Content/Shaders/GI/GlobalSurfaceAtlas.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0f34bf867df5f4296ca66ac691c2bca4efa168fb9e21ca4e613e8086669575cf
-size 13296
+oid sha256:615dff65b01507be6c4de722e126324aba20fc197f8e12dafaa94a05e46cba6e
+size 13222
diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax
index 590e8f3a9..5afcb4bf4 100644
--- a/Content/Shaders/GlobalSignDistanceField.flax
+++ b/Content/Shaders/GlobalSignDistanceField.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:064f54786958f109222c49cbc0358ff4f345b30010fcd5e8cc1fab7bdc68c4fe
-size 13349
+oid sha256:1f07ebb16820897e8598ae7a0627cb75b3d28e9dceea3ad4bd9ff543d5cdd01c
+size 13979
diff --git a/Flax.flaxproj b/Flax.flaxproj
index 7fd591727..5d926a87b 100644
--- a/Flax.flaxproj
+++ b/Flax.flaxproj
@@ -4,7 +4,7 @@
"Major": 1,
"Minor": 11,
"Revision": 0,
- "Build": 6805
+ "Build": 6806
},
"Company": "Flax",
"Copyright": "Copyright (c) 2012-2025 Wojciech Figat. All rights reserved.",
diff --git a/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs b/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs
new file mode 100644
index 000000000..2cbf01e41
--- /dev/null
+++ b/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+using FlaxEngine;
+
+namespace FlaxEditor.CustomEditors.Dedicated
+{
+ ///
+ /// Custom editor for .
+ ///
+ ///
+ [CustomEditor(typeof(NavMeshBoundsVolume)), DefaultEditor]
+ internal class NavMeshBoundsVolumeEditor : ActorEditor
+ {
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ base.Initialize(layout);
+
+ if (Values.HasDifferentTypes == false)
+ {
+ var button = layout.Button("Build");
+ button.Button.Clicked += OnBuildClicked;
+ }
+ }
+
+ private void OnBuildClicked()
+ {
+ foreach (var value in Values)
+ {
+ if (value is NavMeshBoundsVolume volume)
+ {
+ Navigation.BuildNavMesh(volume.Box, volume.Scene);
+ Editor.Instance.Scene.MarkSceneEdited(volume.Scene);
+ }
+ }
+ }
+ }
+}
diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
index 954599347..9844f3fda 100644
--- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
@@ -909,7 +909,8 @@ namespace FlaxEditor.CustomEditors.Dedicated
settingsButton.Tag = script;
settingsButton.Clicked += OnSettingsButtonClicked;
- group.Panel.HeaderTextMargin = new Margin(scriptDrag.Right - 12, 15, 2, 2);
+ // Adjust margin to not overlap with other ui elements in the header
+ group.Panel.HeaderTextMargin = group.Panel.HeaderTextMargin with { Left = scriptDrag.Right - 12, Right = settingsButton.Width + Utilities.Constants.UIMargin };
group.Object(values, editor);
// Remove drop down arrows and containment lines if no objects in the group
if (group.Children.Count == 0)
diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
index b3fff5644..28593a7f5 100644
--- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
@@ -450,6 +450,7 @@ namespace FlaxEditor.CustomEditors.Editors
protected bool NotNullItems;
private IntValueBox _sizeBox;
+ private Label _label;
private Color _background;
private int _elementsCount, _minCount, _maxCount;
private bool _readOnly;
@@ -566,7 +567,7 @@ namespace FlaxEditor.CustomEditors.Editors
Parent = dropPanel,
};
- var label = new Label
+ _label = new Label
{
Text = "Size",
AnchorPreset = AnchorPresets.TopRight,
@@ -672,8 +673,10 @@ namespace FlaxEditor.CustomEditors.Editors
Resize(Count + 1);
};
}
- }
+ Layout.ContainerControl.SizeChanged += OnLayoutSizeChanged;
+ }
+
private void OnSetupContextMenu(ContextMenu menu, DropPanel panel)
{
if (menu.Items.Any(x => x is ContextMenuButton b && b.Text.Equals("Open All", StringComparison.Ordinal)))
@@ -696,10 +699,24 @@ namespace FlaxEditor.CustomEditors.Editors
});
}
+ private void OnLayoutSizeChanged(Control control)
+ {
+ if (Layout.ContainerControl is DropPanel dropPanel)
+ {
+ // Hide "Size" text when array editor title overlaps
+ var headerTextSize = dropPanel.HeaderTextFont.GetFont().MeasureText(dropPanel.HeaderText);
+ if (headerTextSize.X + DropPanel.DropDownIconSize >= _label.Left)
+ _label.TextColor = _label.TextColorHighlighted = Color.Transparent;
+ else
+ _label.TextColor = _label.TextColorHighlighted = FlaxEngine.GUI.Style.Current.Foreground;
+ }
+ }
+
///
protected override void Deinitialize()
{
_sizeBox = null;
+ Layout.ContainerControl.SizeChanged -= OnLayoutSizeChanged;
base.Deinitialize();
}
diff --git a/Source/Editor/CustomEditors/Elements/Container/GroupElement.cs b/Source/Editor/CustomEditors/Elements/Container/GroupElement.cs
index 64bc9080b..055c6a29d 100644
--- a/Source/Editor/CustomEditors/Elements/Container/GroupElement.cs
+++ b/Source/Editor/CustomEditors/Elements/Container/GroupElement.cs
@@ -44,7 +44,8 @@ namespace FlaxEditor.CustomEditors.Elements
{
var style = Style.Current;
var settingsButtonSize = Panel.HeaderHeight;
- return new Image
+ Panel.HeaderTextMargin = Panel.HeaderTextMargin with { Right = settingsButtonSize + Utilities.Constants.UIMargin };
+; return new Image
{
TooltipText = "Settings",
AutoFocus = true,
diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs
index 58466e35d..c881d9dcd 100644
--- a/Source/Editor/Editor.cs
+++ b/Source/Editor/Editor.cs
@@ -23,6 +23,7 @@ using FlaxEngine.Assertions;
using FlaxEngine.GUI;
using FlaxEngine.Interop;
using FlaxEngine.Json;
+using FlaxEngine.Utilities;
#pragma warning disable CS1591
@@ -1370,7 +1371,7 @@ namespace FlaxEditor
public void BuildCSG()
{
var scenes = Level.Scenes;
- scenes.ToList().ForEach(x => x.BuildCSG(0));
+ scenes.ForEach(x => x.BuildCSG(0));
Scene.MarkSceneEdited(scenes);
}
@@ -1380,7 +1381,7 @@ namespace FlaxEditor
public void BuildNavMesh()
{
var scenes = Level.Scenes;
- scenes.ToList().ForEach(x => Navigation.BuildNavMesh(x, 0));
+ Navigation.BuildNavMesh();
Scene.MarkSceneEdited(scenes);
}
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenu.cs
index 896bd5bc2..f5705c6f5 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenu.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenu.cs
@@ -502,6 +502,7 @@ namespace FlaxEditor.GUI.ContextMenu
if (base.OnKeyDown(key))
return true;
+ // Keyboard navigation around the menu
switch (key)
{
case KeyboardKeys.ArrowDown:
@@ -526,6 +527,20 @@ namespace FlaxEditor.GUI.ContextMenu
}
}
break;
+ case KeyboardKeys.ArrowRight:
+ for (int i = 0; i < _panel.Children.Count; i++)
+ {
+ if (_panel.Children[i] is ContextMenuChildMenu item && item.Visible && item.IsFocused && !item.ContextMenu.IsOpened)
+ {
+ item.ShowChild(this);
+ item.ContextMenu._panel.Children.FirstOrDefault(x => x is ContextMenuButton && x.Visible)?.Focus();
+ break;
+ }
+ }
+ break;
+ case KeyboardKeys.ArrowLeft:
+ ParentCM?.RootWindow.Focus();
+ break;
}
return false;
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
index 826baa482..041d4f053 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
@@ -75,6 +75,11 @@ namespace FlaxEditor.GUI.ContextMenu
///
public bool HasChildCMOpened => _childCM != null;
+ ///
+ /// Gets the parent context menu (if exists).
+ ///
+ public ContextMenuBase ParentCM => _parentCM;
+
///
/// Gets the topmost context menu.
///
@@ -84,9 +89,7 @@ namespace FlaxEditor.GUI.ContextMenu
{
var cm = this;
while (cm._parentCM != null && cm._isSubMenu)
- {
cm = cm._parentCM;
- }
return cm;
}
}
@@ -111,6 +114,11 @@ namespace FlaxEditor.GUI.ContextMenu
///
public bool UseInput = true;
+ ///
+ /// Optional flag that can disable UI navigation (tab/enter).
+ ///
+ public bool UseNavigation = true;
+
///
/// Initializes a new instance of the class.
///
@@ -622,6 +630,21 @@ namespace FlaxEditor.GUI.ContextMenu
case KeyboardKeys.Escape:
Hide();
return true;
+ case KeyboardKeys.Return:
+ if (UseNavigation && Root?.FocusedControl != null)
+ {
+ Root.SubmitFocused();
+ return true;
+ }
+ break;
+ case KeyboardKeys.Tab:
+ if (UseNavigation && Root != null)
+ {
+ bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
+ Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next);
+ return true;
+ }
+ break;
}
return false;
}
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs
index 74ab560fb..78337d011 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs
@@ -29,7 +29,7 @@ namespace FlaxEditor.GUI.ContextMenu
CloseMenuOnClick = false;
}
- private void ShowChild(ContextMenu parentContextMenu)
+ internal void ShowChild(ContextMenu parentContextMenu)
{
// Hide parent CM popups and set itself as child
var vAlign = parentContextMenu.ItemsAreaMargin.Top;
diff --git a/Source/Editor/GUI/CurveEditor.Contents.cs b/Source/Editor/GUI/CurveEditor.Contents.cs
index c2831046b..75f37d457 100644
--- a/Source/Editor/GUI/CurveEditor.Contents.cs
+++ b/Source/Editor/GUI/CurveEditor.Contents.cs
@@ -522,6 +522,16 @@ namespace FlaxEditor.GUI
cm.AddButton("Show whole curve", _editor.ShowWholeCurve);
cm.AddButton("Reset view", _editor.ResetView);
}
+ cm.AddSeparator();
+ var presetCm = cm.AddChildMenu("Apply preset");
+ foreach (var value in Enum.GetValues(typeof(CurvePreset)))
+ {
+ CurvePreset preset = (CurvePreset)value;
+ string name = Utilities.Utils.GetPropertyNameUI(preset.ToString());
+ var b = presetCm.ContextMenu.AddButton(name, () => _editor.ApplyPreset(preset));
+ b.Enabled = !(_editor is LinearCurveEditor && (preset != CurvePreset.Constant && preset != CurvePreset.Linear));
+ }
+
_editor.OnShowContextMenu(cm, selectionCount);
cm.Show(this, location);
}
@@ -619,6 +629,33 @@ namespace FlaxEditor.GUI
}
}
+ ///
+ /// A list of avaliable curve presets for the .
+ ///
+ public enum CurvePreset
+ {
+ ///
+ /// A curve where every point has the same value.
+ ///
+ Constant,
+ ///
+ /// A curve linear curve.
+ ///
+ Linear,
+ ///
+ /// A curve that starts a slowly and then accelerates until the end.
+ ///
+ EaseIn,
+ ///
+ /// A curve that starts a steep and then flattens until the end.
+ ///
+ EaseOut,
+ ///
+ /// A combination of the and preset.
+ ///
+ Smoothstep
+ }
+
///
public override void OnKeyframesDeselect(IKeyframesEditor editor)
{
diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs
index 706d07b32..4fb727ea1 100644
--- a/Source/Editor/GUI/CurveEditor.cs
+++ b/Source/Editor/GUI/CurveEditor.cs
@@ -19,6 +19,48 @@ namespace FlaxEditor.GUI
///
public abstract partial class CurveEditor : CurveEditorBase where T : new()
{
+ ///
+ /// Represents a single point in a .
+ ///
+ protected struct CurvePresetPoint
+ {
+ ///
+ /// The time.
+ ///
+ public float Time;
+
+ ///
+ /// The value.
+ ///
+ public float Value;
+
+ ///
+ /// The in tangent. Will be ignored in
+ ///
+ public float TangentIn;
+
+ ///
+ /// The out tangent. Will be ignored in
+ ///
+ public float TangentOut;
+ }
+
+ ///
+ /// A curve preset.
+ ///
+ protected struct CurveEditorPreset()
+ {
+ ///
+ /// If the tangents will be linear or smooth.
+ ///
+ public bool LinearTangents;
+
+ ///
+ /// The points of the preset.
+ ///
+ public List Points;
+ }
+
private class Popup : ContextMenuBase
{
private CustomEditorPresenter _presenter;
@@ -26,11 +68,12 @@ namespace FlaxEditor.GUI
private List _keyframeIndices;
private bool _isDirty;
- public Popup(CurveEditor editor, object[] selection, List keyframeIndices = null, float height = 140.0f)
- : this(editor, height)
+ public Popup(CurveEditor editor, object[] selection, List keyframeIndices = null, float maxHeight = 140.0f)
+ : this(editor, maxHeight)
{
_presenter.Select(selection);
_presenter.OpenAllGroups();
+ Size = new Float2(Size.X, Mathf.Min(_presenter.ContainerControl.Size.Y, maxHeight));
_keyframeIndices = keyframeIndices;
if (keyframeIndices != null && selection.Length != keyframeIndices.Count)
throw new Exception();
@@ -169,7 +212,7 @@ namespace FlaxEditor.GUI
if (IsSelected)
color = Editor.ContainsFocus ? style.SelectionBorder : Color.Lerp(style.ForegroundDisabled, style.SelectionBorder, 0.4f);
if (IsMouseOver)
- color *= 1.1f;
+ color *= 1.5f;
Render2D.FillRectangle(rect, color);
}
@@ -285,7 +328,7 @@ namespace FlaxEditor.GUI
///
/// The keyframes size.
///
- protected static readonly Float2 KeyframesSize = new Float2(7.0f);
+ protected static readonly Float2 KeyframesSize = new Float2(8.0f);
///
/// The colors for the keyframe points.
@@ -326,6 +369,63 @@ namespace FlaxEditor.GUI
private Color _labelsColor;
private Font _labelsFont;
+ ///
+ /// Preset values for to be applied to a .
+ ///
+ protected Dictionary Presets = new Dictionary
+ {
+ { CurvePreset.Constant, new CurveEditorPreset
+ {
+ LinearTangents = true,
+ Points = new List
+ {
+ new CurvePresetPoint { Time = 0f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f },
+ new CurvePresetPoint { Time = 1f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f },
+ }
+ }
+ },
+ { CurvePreset.EaseIn, new CurveEditorPreset
+ {
+ LinearTangents = false,
+ Points = new List
+ {
+ new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
+ new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = -1.4f, TangentOut = 0f },
+ }
+ }
+ },
+ { CurvePreset.EaseOut, new CurveEditorPreset
+ {
+ LinearTangents = false,
+ Points = new List
+ {
+ new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
+ new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 1.4f },
+ }
+ }
+ },
+ { CurvePreset.Linear, new CurveEditorPreset
+ {
+ LinearTangents = true,
+ Points = new List
+ {
+ new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
+ new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
+ }
+ }
+ },
+ { CurvePreset.Smoothstep, new CurveEditorPreset
+ {
+ LinearTangents = false,
+ Points = new List
+ {
+ new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
+ new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
+ }
+ }
+ },
+ };
+
///
/// The keyframe UI points.
///
@@ -568,6 +668,28 @@ namespace FlaxEditor.GUI
/// The list of indices of the keyframes to remove.
protected abstract void RemoveKeyframesInternal(HashSet indicesToRemove);
+ ///
+ /// Tries to convert a float to the type of the type wildcard of the curve editor.
+ ///
+ /// The float.
+ /// The converted value.
+ public static object ConvertCurvePresetValueToCurveEditorType(float value)
+ {
+ if (typeof(T) == typeof(Float2))
+ return new Float2(value);
+ if (typeof(T) == typeof(Float3))
+ return new Float3(value);
+ if (typeof(T) == typeof(Float4))
+ return new Float4(value);
+ if (typeof(T) == typeof(Vector2))
+ return new Vector2(value);
+ if (typeof(T) == typeof(Vector3))
+ return new Vector3(value);
+ if (typeof(T) == typeof(Vector4))
+ return new Vector4(value);
+ return value;
+ }
+
///
/// Called when showing a context menu. Can be used to add custom buttons with actions.
///
@@ -752,6 +874,17 @@ namespace FlaxEditor.GUI
ShowCurve(false);
}
+ ///
+ /// Applies a to the curve editor.
+ ///
+ /// The preset.
+ public virtual void ApplyPreset(CurvePreset preset)
+ {
+ // Remove existing keyframes
+ SelectAll();
+ RemoveKeyframes();
+ }
+
///
public override void Evaluate(out object result, float time, bool loop = false)
{
@@ -1028,6 +1161,31 @@ namespace FlaxEditor.GUI
return true;
}
+ bool left = key == KeyboardKeys.ArrowLeft;
+ bool right = key == KeyboardKeys.ArrowRight;
+ bool up = key == KeyboardKeys.ArrowUp;
+ bool down = key == KeyboardKeys.ArrowDown;
+
+ if (left || right || up || down)
+ {
+ bool shift = Root.GetKey(KeyboardKeys.Shift);
+ bool alt = Root.GetKey(KeyboardKeys.Alt);
+ float deltaValue = 10f;
+ if (shift || alt)
+ deltaValue = shift ? 2.5f : 5f;
+
+ Float2 moveDelta = Float2.Zero;
+ if (left || right)
+ moveDelta.X = left ? -deltaValue : deltaValue;
+ if (up || down)
+ moveDelta.Y = up ? -deltaValue : deltaValue;
+
+ _contents.OnMoveStart(Float2.Zero);
+ _contents.OnMove(moveDelta);
+ _contents.OnMoveEnd(Float2.Zero);
+ return true;
+ }
+
return false;
}
@@ -1526,6 +1684,22 @@ namespace FlaxEditor.GUI
_tangents[i].Visible = false;
}
+ ///
+ public override void ApplyPreset(CurvePreset preset)
+ {
+ base.ApplyPreset(preset);
+
+ CurveEditorPreset data = Presets[preset];
+ foreach (var point in data.Points)
+ {
+ float time = point.Time;
+ object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value);
+ AddKeyframe(time, value);
+ }
+
+ ShowWholeCurve();
+ }
+
///
protected override void DrawCurve(ref Rectangle viewRect)
{
@@ -2312,6 +2486,30 @@ namespace FlaxEditor.GUI
}
}
+ ///
+ public override void ApplyPreset(CurvePreset preset)
+ {
+ base.ApplyPreset(preset);
+
+ CurveEditorPreset data = Presets[preset];
+
+ foreach (var point in data.Points)
+ {
+ float time = point.Time;
+ object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value);
+ object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)point.TangentIn);
+ object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)point.TangentOut);
+
+ AddKeyframe(time, value, tangentIn, tangentOut);
+ }
+
+ SelectAll();
+ if (data.LinearTangents)
+ SetTangentsLinear();
+
+ ShowWholeCurve();
+ }
+
///
protected override void SetScaleInternal(ref Float2 scale)
{
diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs
index bfac161f0..c8900dcba 100644
--- a/Source/Editor/GUI/Docking/DockPanel.cs
+++ b/Source/Editor/GUI/Docking/DockPanel.cs
@@ -469,7 +469,7 @@ namespace FlaxEditor.GUI.Docking
var childPanels = _childPanels.ToArray();
if (childPanels.Length != 0)
{
- // Move tabs from child panels into this one
+ // Fallback: move tabs from child panels into this one.
DockWindow selectedTab = null;
foreach (var childPanel in childPanels)
{
@@ -490,7 +490,8 @@ namespace FlaxEditor.GUI.Docking
{
// Unlink splitter
var splitterParent = splitter.Parent;
- Assert.IsNotNull(splitterParent);
+ if (splitterParent == null)
+ return;
splitter.Parent = null;
// Move controls from second split panel to the split panel parent
@@ -507,17 +508,63 @@ namespace FlaxEditor.GUI.Docking
splitter.Dispose();
}
}
+ else if (IsMaster && _childPanels.Count != 0)
+ {
+ if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
+ return;
+ }
else if (!IsMaster)
{
throw new InvalidOperationException();
}
}
+ else if (_childPanels.Count != 0)
+ {
+ if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
+ return;
+ }
else if (!IsMaster)
{
throw new InvalidOperationException();
}
}
+ internal bool CollapseEmptyTabsProxy()
+ {
+ if (TabsCount == 0 && ChildPanelsCount > 0)
+ {
+ return TryCollapseSplitter(_tabsProxy?.Parent as Panel);
+ }
+ return false;
+ }
+
+ private bool TryCollapseSplitter(Panel removedPanelParent)
+ {
+ if (removedPanelParent == null)
+ return false;
+ if (!(removedPanelParent.Parent is SplitPanel tabsSplitter))
+ return false;
+
+ var splitterParent = tabsSplitter.Parent;
+ if (splitterParent == null)
+ return false;
+ tabsSplitter.Parent = null;
+
+ var scrPanel = removedPanelParent == tabsSplitter.Panel2 ? tabsSplitter.Panel1 : tabsSplitter.Panel2;
+ var srcPanelChildrenCount = scrPanel.ChildrenCount;
+ for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--)
+ {
+ scrPanel.GetChild(i).Parent = splitterParent;
+ }
+ Assert.IsTrue(scrPanel.ChildrenCount == 0);
+ Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount);
+
+ tabsSplitter.Dispose();
+ if (_tabsProxy != null && _tabsProxy.Parent == removedPanelParent)
+ _tabsProxy = null;
+ return true;
+ }
+
internal virtual void DockWindowInternal(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null)
{
DockWindow(state, window, autoSelect, splitterValue);
diff --git a/Source/Editor/GUI/Input/ValueBox.cs b/Source/Editor/GUI/Input/ValueBox.cs
index 2252a7f22..5e1e4aaf4 100644
--- a/Source/Editor/GUI/Input/ValueBox.cs
+++ b/Source/Editor/GUI/Input/ValueBox.cs
@@ -99,6 +99,11 @@ namespace FlaxEditor.GUI.Input
///
public event Action SlidingEnd;
+ ///
+ /// If enabled, pressing the arrow up or down key increments/ decrements the value.
+ ///
+ public bool ArrowKeysIncrement = true;
+
///
/// Gets or sets the slider speed. Use value 0 to disable and hide slider UI.
///
@@ -239,6 +244,27 @@ namespace FlaxEditor.GUI.Input
ResetViewOffset();
}
+ ///
+ public override bool OnKeyDown(KeyboardKeys key)
+ {
+ if (ArrowKeysIncrement && (key == KeyboardKeys.ArrowUp || key == KeyboardKeys.ArrowDown))
+ {
+ bool altDown = Root.GetKey(KeyboardKeys.Alt);
+ bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
+ bool controlDown = Root.GetKey(KeyboardKeys.Control);
+ float deltaValue = altDown ? 0.1f : (shiftDown ? 10f : (controlDown ? 100f : 1f));
+ float slideDelta = key == KeyboardKeys.ArrowUp ? deltaValue : -deltaValue;
+
+ _startSlideValue = Value;
+ ApplySliding(slideDelta);
+ EndSliding();
+ Focus();
+ return true;
+ }
+
+ return base.OnKeyDown(key);
+ }
+
///
public override bool OnMouseDown(Float2 location, MouseButton button)
{
diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs
index ed1257819..b0ce8c251 100644
--- a/Source/Editor/GUI/Tree/TreeNode.cs
+++ b/Source/Editor/GUI/Tree/TreeNode.cs
@@ -1140,8 +1140,11 @@ namespace FlaxEditor.GUI.Tree
ParentTree.DraggedOverNode = this;
// Expand node if mouse goes over arrow
- if (ArrowRect.Contains(location) && HasAnyVisibleChild)
+ if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed)
+ {
Expand(true);
+ ParentTree?.FlushPendingPerformLayout();
+ }
result = OnDragEnterHeader(data);
}
@@ -1172,8 +1175,11 @@ namespace FlaxEditor.GUI.Tree
ParentTree.DraggedOverNode = this;
// Expand node if mouse goes over arrow
- if (ArrowRect.Contains(location) && HasAnyVisibleChild)
+ if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed)
+ {
Expand(true);
+ ParentTree?.FlushPendingPerformLayout();
+ }
if (!_isDragOverHeader)
result = OnDragEnterHeader(data);
diff --git a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs
index 542e8b388..66e835fac 100644
--- a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs
+++ b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs
@@ -36,11 +36,12 @@ public sealed class ViewportRubberBandSelector
/// Triggers the start of a rubber band selection.
///
/// True if selection started, otherwise false.
- public bool TryStartingRubberBandSelection()
+ public bool TryStartingRubberBandSelection(Float2 mousePosition)
{
if (!_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
{
_tryStartRubberBand = true;
+ _cachedStartingMousePosition = mousePosition;
return true;
}
return false;
@@ -82,12 +83,15 @@ public sealed class ViewportRubberBandSelector
return;
}
- if (_tryStartRubberBand && (Mathf.Abs(_owner.MouseDelta.X) > 0.1f || Mathf.Abs(_owner.MouseDelta.Y) > 0.1f) && canStart)
+ if (_tryStartRubberBand && canStart)
{
- _isRubberBandSpanning = true;
- _cachedStartingMousePosition = mousePosition;
- _rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero);
- _tryStartRubberBand = false;
+ var delta = mousePosition - _cachedStartingMousePosition;
+ if (Mathf.Abs(delta.X) > 0.1f || Mathf.Abs(delta.Y) > 0.1f)
+ {
+ _isRubberBandSpanning = true;
+ _rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero);
+ _tryStartRubberBand = false;
+ }
}
else if (_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
{
diff --git a/Source/Editor/Modules/SceneEditingModule.cs b/Source/Editor/Modules/SceneEditingModule.cs
index c36866bc3..b1a5be6f1 100644
--- a/Source/Editor/Modules/SceneEditingModule.cs
+++ b/Source/Editor/Modules/SceneEditingModule.cs
@@ -229,7 +229,7 @@ namespace FlaxEditor.Modules
if (!isPlayMode && options.General.AutoRebuildNavMesh && actor.Scene && node.AffectsNavigationWithChildren)
{
var bounds = actor.BoxWithChildren;
- Navigation.BuildNavMesh(actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs
index 663c81909..7449d4f37 100644
--- a/Source/Editor/Modules/UIModule.cs
+++ b/Source/Editor/Modules/UIModule.cs
@@ -158,6 +158,7 @@ namespace FlaxEditor.Modules
private ContextMenuButton _menuToolsProfilerWindow;
private ContextMenuButton _menuToolsSetTheCurrentSceneViewAsDefault;
private ContextMenuButton _menuToolsTakeScreenshot;
+ private ContextMenuButton _menuToolsOpenLocalFolder;
private ContextMenuChildMenu _menuWindowApplyWindowLayout;
private ToolStripButton _toolStripSaveAll;
@@ -733,6 +734,16 @@ namespace FlaxEditor.Modules
_menuToolsTakeScreenshot = cm.AddButton("Take screenshot", inputOptions.TakeScreenshot, Editor.Windows.TakeScreenshot);
cm.AddSeparator();
cm.AddButton("Plugins", () => Editor.Windows.PluginsWin.Show());
+ cm.AddSeparator();
+ var childMenu = cm.AddChildMenu("Open Product Local folder");
+ childMenu.ContextMenu.AddButton("Editor", () => FileSystem.ShowFileExplorer(Globals.ProductLocalFolder));
+ _menuToolsOpenLocalFolder = childMenu.ContextMenu.AddButton("Game", () =>
+ {
+ string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ GameSettings settings = GameSettings.Load();
+ string path = Path.Combine(localAppData, settings.CompanyName, settings.ProductName);
+ FileSystem.ShowFileExplorer(path);
+ });
// Window
MenuWindow = MainMenu.AddButton("Window");
@@ -1084,6 +1095,10 @@ namespace FlaxEditor.Modules
_menuToolsBuildNavMesh.Enabled = canEdit;
_menuToolsCancelBuilding.Enabled = GameCooker.IsRunning;
_menuToolsSetTheCurrentSceneViewAsDefault.Enabled = Level.ScenesCount > 0;
+ string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ GameSettings settings = GameSettings.Load();
+ string path = Path.Combine(localAppData, settings.CompanyName, settings.ProductName);
+ _menuToolsOpenLocalFolder.Enabled = Directory.Exists(path);
c.PerformLayout();
}
diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs
index 51c88e082..0d1f0e2d7 100644
--- a/Source/Editor/Modules/WindowsModule.cs
+++ b/Source/Editor/Modules/WindowsModule.cs
@@ -490,10 +490,15 @@ namespace FlaxEditor.Modules
Editor.LogWarning("Empty panel inside layout.");
p.RemoveIt();
}
+ else
+ {
+ p.CollapseEmptyTabsProxy();
+ }
}
}
panel.SelectTab(selectedTab);
+ panel.CollapseEmptyTabsProxy();
}
private static void SaveBounds(XmlWriter writer, Window win)
diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs
index ab473ebed..a759b7247 100644
--- a/Source/Editor/Options/InputOptions.cs
+++ b/Source/Editor/Options/InputOptions.cs
@@ -571,6 +571,10 @@ namespace FlaxEditor.Options
[EditorDisplay("View Flags"), EditorOrder(3260)]
public InputBinding DebugDraw = new InputBinding(KeyboardKeys.Alpha4, KeyboardKeys.Control, KeyboardKeys.Shift);
+ [DefaultValue(typeof(InputBinding), "None")]
+ [EditorDisplay("View Flags"), EditorOrder(3270)]
+ public InputBinding Particles = new InputBinding(KeyboardKeys.None);
+
#endregion
#region Interface
diff --git a/Source/Editor/SceneGraph/Actors/BoxColliderNode.cs b/Source/Editor/SceneGraph/Actors/BoxColliderNode.cs
index c4fd47f71..4a7150972 100644
--- a/Source/Editor/SceneGraph/Actors/BoxColliderNode.cs
+++ b/Source/Editor/SceneGraph/Actors/BoxColliderNode.cs
@@ -42,6 +42,7 @@ namespace FlaxEditor.SceneGraph.Actors
if (value is BoxCollider collider)
collider.AutoResize(!_keepLocalOrientation);
}
+ Presenter.OnModified();
}
}
diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs
index de319ca1d..515939526 100644
--- a/Source/Editor/SceneGraph/Actors/SplineNode.cs
+++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs
@@ -555,7 +555,7 @@ namespace FlaxEditor.SceneGraph.Actors
var options = Editor.Instance.Options.Options.General;
if (options.AutoRebuildNavMesh)
{
- Navigation.BuildNavMesh(collider.Scene, collider.Box, options.AutoRebuildNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(collider.Box, options.AutoRebuildNavMeshTimeoutMs);
}
}
}
diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs
index e95364c2d..4cd63f05d 100644
--- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs
+++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs
@@ -47,6 +47,11 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
+ ///
+ /// Gets the model used by this actor.
+ ///
+ public Model Model => ((StaticModel)Actor).Model;
+
///
public StaticModelNode(Actor actor)
: base(actor)
@@ -120,12 +125,12 @@ namespace FlaxEditor.SceneGraph.Actors
{
base.OnContextMenu(contextMenu, window);
- // Check if every selected node is a primitive
+ // Check if every selected node is a primitive or has collision asset
var selection = GetSelection(window);
bool autoOptionEnabled = true;
foreach (var node in selection)
{
- if (node is StaticModelNode staticModelNode && !staticModelNode.IsPrimitive)
+ if (node is StaticModelNode staticModelNode && (!staticModelNode.IsPrimitive && GetCollisionData(staticModelNode.Model) == null))
{
autoOptionEnabled = false;
break;
@@ -201,6 +206,54 @@ namespace FlaxEditor.SceneGraph.Actors
return Array.Empty();
}
+ private static bool TryCollisionData(Model model, BinaryAssetItem assetItem, out CollisionData collisionData)
+ {
+ collisionData = FlaxEngine.Content.Load(assetItem.ID);
+ if (collisionData)
+ {
+ var options = collisionData.Options;
+ if (options.Model == model.ID || options.Model == Guid.Empty)
+ return true;
+ }
+ return false;
+ }
+
+ private CollisionData GetCollisionData(Model model)
+ {
+ if (model == null)
+ return null;
+
+ // Check if there already is collision data for that model to reuse
+ var modelItem = (AssetItem)Editor.Instance.ContentDatabase.Find(model.ID);
+ if (modelItem?.ParentFolder != null)
+ {
+ foreach (var child in modelItem.ParentFolder.Children)
+ {
+ // Check if there is collision that was made with this model
+ if (child is BinaryAssetItem b && b.IsOfType())
+ {
+ if (TryCollisionData(model, b, out var collisionData))
+ return collisionData;
+ }
+
+ // Check if there is an auto-imported collision
+ if (child is ContentFolder childFolder && childFolder.ShortName == modelItem.ShortName)
+ {
+ foreach (var childFolderChild in childFolder.Children)
+ {
+ if (childFolderChild is BinaryAssetItem c && c.IsOfType())
+ {
+ if (TryCollisionData(model, c, out var collisionData))
+ return collisionData;
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
private void CreateAuto(StaticModel actor, Spawner spawner, bool singleNode)
{
// Special case for in-built Editor models that can use analytical collision
@@ -243,6 +296,15 @@ namespace FlaxEditor.SceneGraph.Actors
collider.LocalPosition = new Vector3(0, 50.0f, 0);
collider.LocalOrientation = Quaternion.Euler(0, 0, 90.0f);
}
+ else
+ {
+ var collider = new MeshCollider
+ {
+ Transform = actor.Transform,
+ CollisionData = GetCollisionData(model),
+ };
+ spawner(collider);
+ }
}
private void CreateBox(StaticModel actor, Spawner spawner, bool singleNode)
diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs
index e46038639..9ac43082e 100644
--- a/Source/Editor/Surface/Archetypes/Material.cs
+++ b/Source/Editor/Surface/Archetypes/Material.cs
@@ -304,25 +304,14 @@ namespace FlaxEditor.Surface.Archetypes
}
}
- internal sealed class CustomCodeNode : SurfaceNode
+ internal sealed class CustomCodeNode : ResizableSurfaceNode
{
- private Rectangle _resizeButtonRect;
- private Float2 _startResizingSize;
- private Float2 _startResizingCornerOffset;
- private bool _isResizing;
private CustomCodeTextBox _textBox;
- private int SizeValueIndex => Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array
-
- private Float2 SizeValue
- {
- get => (Float2)Values[SizeValueIndex];
- set => SetValue(SizeValueIndex, value, false);
- }
-
public CustomCodeNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
+ _sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array
Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize), size;
if (nodeArch.TypeID == 8)
{
@@ -345,126 +334,19 @@ namespace FlaxEditor.Surface.Archetypes
_textBox.EditEnd += () => SetValue(0, _textBox.Text);
}
- public override bool CanSelect(ref Float2 location)
- {
- return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
- }
-
public override void OnSurfaceLoaded(SurfaceNodeActions action)
{
base.OnSurfaceLoaded(action);
_textBox.Text = (string)Values[0];
-
- var size = SizeValue;
- if (Surface != null && Surface.GridSnappingEnabled)
- size = Surface.SnapToGrid(size, true);
- Resize(size.X, size.Y);
}
public override void OnValuesChanged()
{
base.OnValuesChanged();
- var size = SizeValue;
- Resize(size.X, size.Y);
_textBox.Text = (string)Values[0];
}
-
- protected override void UpdateRectangles()
- {
- base.UpdateRectangles();
-
- const float buttonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin;
- const float buttonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize;
- _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize);
- }
-
- public override void Draw()
- {
- base.Draw();
-
- var style = Style.Current;
- if (_isResizing)
- {
- Render2D.FillRectangle(_resizeButtonRect, style.Selection);
- Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
- }
- Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
- }
-
- public override void OnLostFocus()
- {
- if (_isResizing)
- EndResizing();
-
- base.OnLostFocus();
- }
-
- public override void OnEndMouseCapture()
- {
- if (_isResizing)
- EndResizing();
-
- base.OnEndMouseCapture();
- }
-
- public override bool OnMouseDown(Float2 location, MouseButton button)
- {
- if (base.OnMouseDown(location, button))
- return true;
-
- if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit)
- {
- // Start sliding
- _isResizing = true;
- _startResizingSize = Size;
- _startResizingCornerOffset = Size - location;
- StartMouseCapture();
- Cursor = CursorType.SizeNWSE;
- return true;
- }
-
- return false;
- }
-
- public override void OnMouseMove(Float2 location)
- {
- if (_isResizing)
- {
- var emptySize = CalculateNodeSize(0, 0);
- var size = Float2.Max(location - emptySize + _startResizingCornerOffset, new Float2(240, 160));
- Resize(size.X, size.Y);
- }
- else
- {
- base.OnMouseMove(location);
- }
- }
-
- public override bool OnMouseUp(Float2 location, MouseButton button)
- {
- if (button == MouseButton.Left && _isResizing)
- {
- EndResizing();
- return true;
- }
-
- return base.OnMouseUp(location, button);
- }
-
- private void EndResizing()
- {
- Cursor = CursorType.Default;
- EndMouseCapture();
- _isResizing = false;
- if (_startResizingSize != Size)
- {
- var emptySize = CalculateNodeSize(0, 0);
- SizeValue = Size - emptySize;
- Surface.MarkAsEdited(false);
- }
- }
}
internal enum MaterialTemplateInputsMapping
diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs
index 56f8154d7..f09ee5015 100644
--- a/Source/Editor/Surface/Archetypes/Textures.cs
+++ b/Source/Editor/Surface/Archetypes/Textures.cs
@@ -23,11 +23,14 @@ namespace FlaxEditor.Surface.Archetypes
TextureGroup = 4,
}
- internal class SampleTextureNode : SurfaceNode
+ internal class TextureSamplerNode : SurfaceNode
{
private ComboBox _textureGroupPicker;
+ protected int _samplerTypeValueIndex = -1;
+ protected int _textureGroupValueIndex = -1;
+ protected int _level = 5;
- public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ protected TextureSamplerNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
}
@@ -48,13 +51,13 @@ namespace FlaxEditor.Surface.Archetypes
private void UpdateUI()
{
- if ((int)Values[0] == (int)CommonSamplerType.TextureGroup)
+ if ((int)Values[_samplerTypeValueIndex] == (int)CommonSamplerType.TextureGroup)
{
if (_textureGroupPicker == null)
{
_textureGroupPicker = new ComboBox
{
- Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * 5),
+ Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * _level),
Width = 100,
Parent = this,
};
@@ -71,7 +74,7 @@ namespace FlaxEditor.Surface.Archetypes
_textureGroupPicker.Visible = true;
}
_textureGroupPicker.SelectedIndexChanged -= OnSelectedTextureGroupChanged;
- _textureGroupPicker.SelectedIndex = (int)Values[2];
+ _textureGroupPicker.SelectedIndex = (int)Values[_textureGroupValueIndex];
_textureGroupPicker.SelectedIndexChanged += OnSelectedTextureGroupChanged;
}
else if (_textureGroupPicker != null)
@@ -83,7 +86,39 @@ namespace FlaxEditor.Surface.Archetypes
private void OnSelectedTextureGroupChanged(ComboBox comboBox)
{
- SetValue(2, _textureGroupPicker.SelectedIndex);
+ SetValue(_textureGroupValueIndex, _textureGroupPicker.SelectedIndex);
+ }
+ }
+
+ internal class SampleTextureNode : TextureSamplerNode
+ {
+ public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(id, context, nodeArch, groupArch)
+ {
+ _samplerTypeValueIndex = 0;
+ _textureGroupValueIndex = 2;
+ }
+ }
+
+ internal class TriplanarSampleTextureNode : TextureSamplerNode
+ {
+ public TriplanarSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(id, context, nodeArch, groupArch)
+ {
+ _samplerTypeValueIndex = 3;
+ _textureGroupValueIndex = 5;
+ _level = 5;
+ }
+ }
+
+ internal class ProceduralSampleTextureNode : TextureSamplerNode
+ {
+ public ProceduralSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(id, context, nodeArch, groupArch)
+ {
+ _samplerTypeValueIndex = 0;
+ _textureGroupValueIndex = 2;
+ _level = 4;
}
}
@@ -280,9 +315,9 @@ namespace FlaxEditor.Surface.Archetypes
ConnectionsHints = ConnectionsHint.Vector,
DefaultValues = new object[]
{
- 0,
- -1.0f,
- 0,
+ (int)CommonSamplerType.LinearClamp, // Sampler
+ -1.0f, // Level
+ 0, // Texture Group
},
Elements = new[]
{
@@ -402,6 +437,7 @@ namespace FlaxEditor.Surface.Archetypes
new NodeArchetype
{
TypeID = 16,
+ Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch),
Title = "Triplanar Texture",
Description = "Projects a texture using world-space coordinates with triplanar mapping.",
Flags = NodeFlags.MaterialGraph,
@@ -411,8 +447,9 @@ namespace FlaxEditor.Surface.Archetypes
Float3.One, // Scale
1.0f, // Blend
Float2.Zero, // Offset
- 2, // Sampler
+ (int)CommonSamplerType.LinearWrap, // Sampler
false, // Local
+ 0, // Texture Group
},
Elements = new[]
{
@@ -430,17 +467,17 @@ namespace FlaxEditor.Surface.Archetypes
new NodeArchetype
{
TypeID = 17,
- Create = (id, context, arch, groupArch) => new SampleTextureNode(id, context, arch, groupArch),
+ Create = (id, context, arch, groupArch) => new ProceduralSampleTextureNode(id, context, arch, groupArch),
Title = "Procedural Sample Texture",
Description = "Samples a texture to create a more natural look with less obvious tiling.",
Flags = NodeFlags.MaterialGraph,
- Size = new Float2(240, 110),
+ Size = new Float2(240, 130),
ConnectionsHints = ConnectionsHint.Vector,
DefaultValues = new object[]
{
- 2,
- -1.0f,
- 0,
+ (int)CommonSamplerType.LinearWrap, // Sampler
+ -1.0f, // Level
+ 0, // Texture Group
},
Elements = new[]
{
@@ -448,8 +485,8 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.Input(1, "UVs", true, null, 1),
NodeElementArchetype.Factory.Input(2, "Offset", true, typeof(Float2), 3),
NodeElementArchetype.Factory.Output(0, "Color", typeof(Float4), 4),
- NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 4, "Sampler"),
- NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 4, 100, 0, typeof(CommonSamplerType))
+ NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 3, "Sampler"),
+ NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 3, 100, 0, typeof(CommonSamplerType))
}
},
new NodeArchetype
@@ -469,6 +506,7 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 23,
Title = "Triplanar Normal Map",
+ Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch),
Description = "Projects a normal map texture using world-space coordinates with triplanar mapping.",
Flags = NodeFlags.MaterialGraph,
Size = new Float2(280, 100),
@@ -477,8 +515,9 @@ namespace FlaxEditor.Surface.Archetypes
Float3.One, // Scale
1.0f, // Blend
Float2.Zero, // Offset
- 2, // Sampler
+ (int)CommonSamplerType.LinearWrap, // Sampler
false, // Local
+ 0, // Texture Group
},
Elements = new[]
{
diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs
index aacebd189..68a733197 100644
--- a/Source/Editor/Surface/Archetypes/Tools.cs
+++ b/Source/Editor/Surface/Archetypes/Tools.cs
@@ -453,7 +453,7 @@ namespace FlaxEditor.Surface.Archetypes
}
}
- private class CurveNode : SurfaceNode where T : struct
+ private class CurveNode : ResizableSurfaceNode where T : struct
{
private BezierCurveEditor _curve;
private bool _isSavingCurve;
@@ -467,7 +467,7 @@ namespace FlaxEditor.Surface.Archetypes
Create = (id, context, arch, groupArch) => new CurveNode(id, context, arch, groupArch),
Description = "An animation spline represented by a set of keyframes, each representing an endpoint of a Bezier curve.",
Flags = NodeFlags.AllGraphs,
- Size = new Float2(400, 180.0f),
+ Size = new Float2(400, 180),
DefaultValues = new object[]
{
// Keyframes count
@@ -491,6 +491,8 @@ namespace FlaxEditor.Surface.Archetypes
0.0f, zero, zero, zero,
0.0f, zero, zero, zero,
0.0f, zero, zero, zero,
+
+ new Float2(400, 180),
},
Elements = new[]
{
@@ -504,30 +506,52 @@ namespace FlaxEditor.Surface.Archetypes
public CurveNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
+ _sizeValueIndex = 29; // Index of the Size stored in Values array
}
-
+
///
public override void OnLoaded(SurfaceNodeActions action)
{
base.OnLoaded(action);
+ // Create curve editor
var upperLeft = GetBox(0).BottomLeft;
var upperRight = GetBox(1).BottomRight;
float curveMargin = 20.0f;
-
_curve = new BezierCurveEditor
{
MaxKeyframes = 7,
Bounds = new Rectangle(upperLeft + new Float2(curveMargin, 10.0f), upperRight.X - upperLeft.X - curveMargin * 2.0f, 140.0f),
- Parent = this
+ Parent = this,
+ AnchorMax = Float2.One,
};
_curve.Edited += OnCurveEdited;
_curve.UnlockChildrenRecursive();
_curve.PerformLayout();
+ // Sync keyframes
UpdateCurveKeyframes();
}
+ ///
+ public override void OnSurfaceLoaded(SurfaceNodeActions action)
+ {
+ base.OnSurfaceLoaded(action);
+
+ // Ensure the whole curve is shown
+ _curve.ShowWholeCurve();
+ }
+
+ public override void OnValuesChanged()
+ {
+ base.OnValuesChanged();
+
+ if (!_isSavingCurve)
+ {
+ UpdateCurveKeyframes();
+ }
+ }
+
private void OnCurveEdited()
{
if (_isSavingCurve)
@@ -553,17 +577,6 @@ namespace FlaxEditor.Surface.Archetypes
_isSavingCurve = false;
}
- ///
- public override void OnValuesChanged()
- {
- base.OnValuesChanged();
-
- if (!_isSavingCurve)
- {
- UpdateCurveKeyframes();
- }
- }
-
private void UpdateCurveKeyframes()
{
var count = (int)Values[0];
@@ -1575,7 +1588,7 @@ namespace FlaxEditor.Surface.Archetypes
DefaultValues = new object[]
{
Guid.Empty,
- string.Empty
+ string.Empty,
},
Elements = new[]
{
diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs
new file mode 100644
index 000000000..259c29836
--- /dev/null
+++ b/Source/Editor/Surface/ResizableSurfaceNode.cs
@@ -0,0 +1,182 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.Surface
+{
+ ///
+ /// Visject Surface node control that cna be resized.
+ ///
+ ///
+ [HideInEditor]
+ public class ResizableSurfaceNode : SurfaceNode
+ {
+ private Float2 _startResizingSize;
+ private Float2 _startResizingCornerOffset;
+
+ ///
+ /// Indicates whether the node is currently being resized.
+ ///
+ protected bool _isResizing;
+
+ ///
+ /// Index of the Float2 value in the node values list to store node size.
+ ///
+ protected int _sizeValueIndex = -1;
+
+ ///
+ /// Minimum node size.
+ ///
+ protected Float2 _sizeMin = new Float2(240, 160);
+
+ ///
+ /// Node resizing rectangle bounds.
+ ///
+ protected Rectangle _resizeButtonRect;
+
+ private Float2 SizeValue
+ {
+ get => (Float2)Values[_sizeValueIndex];
+ set => SetValue(_sizeValueIndex, value, false);
+ }
+
+ ///
+ public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(id, context, nodeArch, groupArch)
+ {
+ }
+
+ ///
+ public override bool CanSelect(ref Float2 location)
+ {
+ return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
+ }
+
+ ///
+ public override void OnSurfaceLoaded(SurfaceNodeActions action)
+ {
+ // Reapply the curve node size
+ var size = SizeValue;
+ if (Surface != null && Surface.GridSnappingEnabled)
+ size = Surface.SnapToGrid(size, true);
+ Resize(size.X, size.Y);
+
+ base.OnSurfaceLoaded(action);
+ }
+
+ ///
+ public override void OnValuesChanged()
+ {
+ base.OnValuesChanged();
+
+ var size = SizeValue;
+ Resize(size.X, size.Y);
+ }
+
+ ///
+ public override void Draw()
+ {
+ base.Draw();
+
+ if (Surface.CanEdit)
+ {
+ var style = Style.Current;
+ if (_isResizing)
+ {
+ Render2D.FillRectangle(_resizeButtonRect, style.Selection);
+ Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
+ }
+ Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey);
+ }
+ }
+
+ ///
+ public override void OnLostFocus()
+ {
+ if (_isResizing)
+ EndResizing();
+
+ base.OnLostFocus();
+ }
+
+ ///
+ public override void OnEndMouseCapture()
+ {
+ if (_isResizing)
+ EndResizing();
+
+ base.OnEndMouseCapture();
+ }
+
+ ///
+ public override bool OnMouseDown(Float2 location, MouseButton button)
+ {
+ if (base.OnMouseDown(location, button))
+ return true;
+
+ if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit)
+ {
+ // Start resizing
+ _isResizing = true;
+ _startResizingSize = Size;
+ _startResizingCornerOffset = Size - location;
+ StartMouseCapture();
+ Cursor = CursorType.SizeNWSE;
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ public override void OnMouseMove(Float2 location)
+ {
+ if (_isResizing)
+ {
+ var emptySize = CalculateNodeSize(0, 0);
+ var size = Float2.Max(location - emptySize + _startResizingCornerOffset, _sizeMin);
+ Resize(size.X, size.Y);
+ }
+ else
+ {
+ base.OnMouseMove(location);
+ }
+ }
+
+ ///
+ public override bool OnMouseUp(Float2 location, MouseButton button)
+ {
+ if (button == MouseButton.Left && _isResizing)
+ {
+ EndResizing();
+ return true;
+ }
+
+ return base.OnMouseUp(location, button);
+ }
+
+ ///
+ protected override void UpdateRectangles()
+ {
+ base.UpdateRectangles();
+
+ const float buttonMargin = Constants.NodeCloseButtonMargin;
+ const float buttonSize = Constants.NodeCloseButtonSize;
+ _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize);
+ }
+
+ private void EndResizing()
+ {
+ Cursor = CursorType.Default;
+ EndMouseCapture();
+ _isResizing = false;
+ if (_startResizingSize != Size)
+ {
+ var emptySize = CalculateNodeSize(0, 0);
+ SizeValue = Size - emptySize;
+ Surface.MarkAsEdited(false);
+ }
+ }
+ }
+}
diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs
index 10e9fc776..a76fa245d 100644
--- a/Source/Editor/Surface/SurfaceComment.cs
+++ b/Source/Editor/Surface/SurfaceComment.cs
@@ -14,18 +14,11 @@ namespace FlaxEditor.Surface
///
///
[HideInEditor]
- public class SurfaceComment : SurfaceNode
+ public class SurfaceComment : ResizableSurfaceNode
{
private Rectangle _colorButtonRect;
- private Rectangle _resizeButtonRect;
- private Float2 _startResizingSize;
private readonly TextBox _renameTextBox;
- ///
- /// True if sizing tool is in use.
- ///
- protected bool _isResizing;
-
///
/// True if rename textbox is active in order to rename comment
///
@@ -52,12 +45,6 @@ namespace FlaxEditor.Surface
set => SetValue(1, value, false);
}
- private Float2 SizeValue
- {
- get => (Float2)Values[2];
- set => SetValue(2, value, false);
- }
-
private int OrderValue
{
get => (int)Values[3];
@@ -68,6 +55,8 @@ namespace FlaxEditor.Surface
public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
+ _sizeValueIndex = 2; // Index of the Size stored in Values array
+ _sizeMin = new Float2(140.0f, Constants.NodeHeaderSize);
_renameTextBox = new TextBox(false, 0, 0, Width)
{
Height = Constants.NodeHeaderSize,
@@ -86,10 +75,6 @@ namespace FlaxEditor.Surface
// Read node data
Title = TitleValue;
Color = ColorValue;
- var size = SizeValue;
- if (Surface != null && Surface.GridSnappingEnabled)
- size = Surface.SnapToGrid(size, true);
- Size = size;
// Order
// Backwards compatibility - When opening with an older version send the old comments to the back
@@ -126,27 +111,6 @@ namespace FlaxEditor.Surface
// Read node data
Title = TitleValue;
Color = ColorValue;
- Size = SizeValue;
- }
-
- private void EndResizing()
- {
- // Clear state
- _isResizing = false;
-
- if (_startResizingSize != Size)
- {
- SizeValue = Size;
- Surface.MarkAsEdited(false);
- }
-
- EndMouseCapture();
- }
-
- ///
- public override bool CanSelect(ref Float2 location)
- {
- return _headerRect.MakeOffsetted(Location).Contains(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
}
///
@@ -158,6 +122,8 @@ namespace FlaxEditor.Surface
///
protected override void UpdateRectangles()
{
+ base.UpdateRectangles();
+
const float headerSize = Constants.NodeHeaderSize;
const float buttonMargin = Constants.NodeCloseButtonMargin;
const float buttonSize = Constants.NodeCloseButtonSize;
@@ -222,16 +188,13 @@ namespace FlaxEditor.Surface
// Color button
Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
- // Check if is resizing
+ // Resize button
if (_isResizing)
{
- // Draw overlay
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
}
-
- // Resize button
- Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
+ Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey);
}
// Selection outline
@@ -247,88 +210,28 @@ namespace FlaxEditor.Surface
///
protected override Float2 CalculateNodeSize(float width, float height)
{
- return Size;
+ // No margins or headers
+ return new Float2(width, height);
}
///
public override void OnLostFocus()
{
- // Check if was resizing
- if (_isResizing)
- {
- EndResizing();
- }
-
- // Check if was renaming
if (_isRenaming)
{
Rename(_renameTextBox.Text);
StopRenaming();
}
- // Base
base.OnLostFocus();
}
- ///
- public override void OnEndMouseCapture()
- {
- // Check if was resizing
- if (_isResizing)
- {
- EndResizing();
- }
- else
- {
- base.OnEndMouseCapture();
- }
- }
-
///
public override bool ContainsPoint(ref Float2 location, bool precise)
{
return _headerRect.Contains(ref location) || _resizeButtonRect.Contains(ref location);
}
- ///
- public override bool OnMouseDown(Float2 location, MouseButton button)
- {
- if (base.OnMouseDown(location, button))
- return true;
-
- // Check if can start resizing
- if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit)
- {
- // Start sliding
- _isResizing = true;
- _startResizingSize = Size;
- StartMouseCapture();
-
- return true;
- }
-
- return false;
- }
-
- ///
- public override void OnMouseMove(Float2 location)
- {
- // Check if is resizing
- if (_isResizing)
- {
- // Update size
- var size = Float2.Max(location, new Float2(140.0f, _headerRect.Bottom));
- if (Surface.GridSnappingEnabled)
- size = Surface.SnapToGrid(size, true);
- Size = size;
- }
- else
- {
- // Base
- base.OnMouseMove(location);
- }
- }
-
///
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
{
@@ -394,12 +297,6 @@ namespace FlaxEditor.Surface
///
public override bool OnMouseUp(Float2 location, MouseButton button)
{
- if (button == MouseButton.Left && _isResizing)
- {
- EndResizing();
- return true;
- }
-
if (base.OnMouseUp(location, button))
return true;
diff --git a/Source/Editor/Tools/Terrain/EditTab.cs b/Source/Editor/Tools/Terrain/EditTab.cs
index 551a47974..6a6191122 100644
--- a/Source/Editor/Tools/Terrain/EditTab.cs
+++ b/Source/Editor/Tools/Terrain/EditTab.cs
@@ -192,7 +192,7 @@ namespace FlaxEditor.Tools.Terrain
{
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
{
- Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
diff --git a/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs b/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs
index 54a6d7fa4..5fc0e894f 100644
--- a/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs
+++ b/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs
@@ -209,7 +209,7 @@ namespace FlaxEditor.Tools.Terrain
{
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
{
- Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
diff --git a/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs b/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs
index 87b0c3cc9..afac0948e 100644
--- a/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs
+++ b/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs
@@ -172,7 +172,7 @@ namespace FlaxEditor.Tools.Terrain.Undo
if (_navmeshBoundsModifications != null)
{
foreach (var bounds in _navmeshBoundsModifications)
- Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs);
}
Editor.Instance.Scene.MarkSceneEdited(scene);
@@ -217,11 +217,10 @@ namespace FlaxEditor.Tools.Terrain.Undo
}
// Update navmesh
- var scene = Terrain.Scene;
if (_navmeshBoundsModifications != null)
{
foreach (var bounds in _navmeshBoundsModifications)
- Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs);
}
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);
diff --git a/Source/Editor/Undo/Actions/DeleteActorsAction.cs b/Source/Editor/Undo/Actions/DeleteActorsAction.cs
index 75594ecb9..19ffb1e3f 100644
--- a/Source/Editor/Undo/Actions/DeleteActorsAction.cs
+++ b/Source/Editor/Undo/Actions/DeleteActorsAction.cs
@@ -303,7 +303,7 @@ namespace FlaxEditor.Actions
if (_nodeParents[i] is ActorNode node && node.Actor && node.Actor.Scene && node.AffectsNavigationWithChildren)
{
var bounds = node.Actor.BoxWithChildren;
- Navigation.BuildNavMesh(node.Actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
diff --git a/Source/Editor/Undo/Actions/TransformObjectsAction.cs b/Source/Editor/Undo/Actions/TransformObjectsAction.cs
index ebed61174..df013e20e 100644
--- a/Source/Editor/Undo/Actions/TransformObjectsAction.cs
+++ b/Source/Editor/Undo/Actions/TransformObjectsAction.cs
@@ -121,12 +121,12 @@ namespace FlaxEditor
// Handle simple case where objects were moved just a little and use one navmesh build request to improve performance
if (data.BeforeBounds.Intersects(ref data.AfterBounds))
{
- Navigation.BuildNavMesh(data.Scene, BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
}
else
{
- Navigation.BuildNavMesh(data.Scene, data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
- Navigation.BuildNavMesh(data.Scene, data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
+ Navigation.BuildNavMesh(data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
diff --git a/Source/Editor/Utilities/ShuntingYardParser.cs b/Source/Editor/Utilities/ShuntingYardParser.cs
index 47e2275e5..fe473389c 100644
--- a/Source/Editor/Utilities/ShuntingYardParser.cs
+++ b/Source/Editor/Utilities/ShuntingYardParser.cs
@@ -444,6 +444,9 @@ namespace FlaxEditor.Utilities
/// The result value.
public static double Parse(string text)
{
+ // Hack to allow parsing numbers while using "_" as a separator (like this: 1_000)
+ text = text.Replace("_", string.Empty);
+
var tokens = Tokenize(text);
var rpn = OrderTokens(tokens);
return EvaluateRPN(rpn);
diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs
index 5fb1c4657..04563e1cd 100644
--- a/Source/Editor/Viewport/EditorViewport.cs
+++ b/Source/Editor/Viewport/EditorViewport.cs
@@ -1069,6 +1069,7 @@ namespace FlaxEditor.Viewport
InputActions.Add(options => options.Fog, () => Task.ViewFlags ^= ViewFlags.Fog);
InputActions.Add(options => options.SpecularLight, () => Task.ViewFlags ^= ViewFlags.SpecularLight);
InputActions.Add(options => options.Decals, () => Task.ViewFlags ^= ViewFlags.Decals);
+ InputActions.Add(options => options.Particles, () => Task.ViewFlags ^= ViewFlags.Particles);
InputActions.Add(options => options.CustomPostProcess, () => Task.ViewFlags ^= ViewFlags.CustomPostProcess);
InputActions.Add(options => options.Bloom, () => Task.ViewFlags ^= ViewFlags.Bloom);
InputActions.Add(options => options.ToneMapping, () => Task.ViewFlags ^= ViewFlags.ToneMapping);
@@ -2147,6 +2148,7 @@ namespace FlaxEditor.Viewport
new ViewFlagOptions(ViewFlags.Fog, "Fog", Editor.Instance.Options.Options.Input.Fog),
new ViewFlagOptions(ViewFlags.SpecularLight, "Specular Light", Editor.Instance.Options.Options.Input.SpecularLight),
new ViewFlagOptions(ViewFlags.Decals, "Decals", Editor.Instance.Options.Options.Input.Decals),
+ new ViewFlagOptions(ViewFlags.Particles, "Particles", Editor.Instance.Options.Options.Input.Particles),
new ViewFlagOptions(ViewFlags.CustomPostProcess, "Custom Post Process", Editor.Instance.Options.Options.Input.CustomPostProcess),
new ViewFlagOptions(ViewFlags.Bloom, "Bloom", Editor.Instance.Options.Options.Input.Bloom),
new ViewFlagOptions(ViewFlags.ToneMapping, "Tone Mapping", Editor.Instance.Options.Options.Input.ToneMapping),
@@ -2166,12 +2168,13 @@ namespace FlaxEditor.Viewport
if (cm.Visible == false)
return;
var ccm = (ContextMenu)cm;
+ var flags = Task.View.Flags;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b && b.Tag != null)
{
var v = (ViewFlags)b.Tag;
- b.Icon = (Task.View.Flags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
+ b.Icon = (flags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
}
}
}
diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
index e9980db8b..743797b68 100644
--- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
@@ -629,7 +629,7 @@ namespace FlaxEditor.Viewport
base.OnLeftMouseButtonDown();
if (!IsAltKeyDown)
- _rubberBandSelector.TryStartingRubberBandSelection();
+ _rubberBandSelector.TryStartingRubberBandSelection(_viewMousePos);
}
///
diff --git a/Source/Editor/Windows/EditorOptionsWindow.cs b/Source/Editor/Windows/EditorOptionsWindow.cs
index 0ee9a92d7..c6bf2fd16 100644
--- a/Source/Editor/Windows/EditorOptionsWindow.cs
+++ b/Source/Editor/Windows/EditorOptionsWindow.cs
@@ -45,7 +45,7 @@ namespace FlaxEditor.Windows
{
Parent = this
};
- _saveButton = (ToolStripButton)toolstrip.AddButton(editor.Icons.Save64, SaveData).LinkTooltip("Save");
+ _saveButton = (ToolStripButton)toolstrip.AddButton(editor.Icons.Save64, SaveData).LinkTooltip("Save.");
_saveButton.Enabled = false;
_tabs = new Tabs
@@ -104,6 +104,8 @@ namespace FlaxEditor.Windows
{
_saveButton.Enabled = true;
_isDataDirty = true;
+ if (!Title.EndsWith('*'))
+ Title += "*";
}
}
@@ -113,6 +115,8 @@ namespace FlaxEditor.Windows
{
_saveButton.Enabled = false;
_isDataDirty = false;
+ if (Title.EndsWith('*'))
+ Title = Title.Remove(Title.Length - 1);
}
}
diff --git a/Source/Engine/Content/Assets/Material.cpp b/Source/Engine/Content/Assets/Material.cpp
index 019fd9dd8..b4cf55d4d 100644
--- a/Source/Engine/Content/Assets/Material.cpp
+++ b/Source/Engine/Content/Assets/Material.cpp
@@ -41,6 +41,35 @@ bool Material::IsMaterialInstance() const
return false;
}
+#if USE_EDITOR
+
+void Material::GetReferences(Array& assets, Array& files) const
+{
+ ShaderAssetTypeBase::GetReferences(assets, files);
+
+ // Collect references from material graph (needs to load it)
+ if (!WaitForLoaded() && HasChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE))
+ {
+ ScopeLock lock(Locker);
+ if (!LoadChunks(GET_CHUNK_FLAG(SHADER_FILE_CHUNK_VISJECT_SURFACE)))
+ {
+ const auto surfaceChunk = GetChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE);
+ if (surfaceChunk)
+ {
+ MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
+ MaterialGraph graph;
+ if (!graph.Load(&stream, false))
+ {
+ graph.GetReferences(assets);
+ }
+ }
+ }
+ }
+
+}
+
+#endif
+
const MaterialInfo& Material::GetInfo() const
{
if (_materialShader)
diff --git a/Source/Engine/Content/Assets/Material.h b/Source/Engine/Content/Assets/Material.h
index 4ce47b154..cd2ae8e97 100644
--- a/Source/Engine/Content/Assets/Material.h
+++ b/Source/Engine/Content/Assets/Material.h
@@ -38,6 +38,9 @@ public:
public:
// [MaterialBase]
bool IsMaterialInstance() const override;
+#if USE_EDITOR
+ void GetReferences(Array& assets, Array& files) const override;
+#endif
// [IMaterial]
const MaterialInfo& GetInfo() const override;
diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp
index 91547dc8d..f3548dc5c 100644
--- a/Source/Engine/ContentImporters/ImportModel.cpp
+++ b/Source/Engine/ContentImporters/ImportModel.cpp
@@ -478,16 +478,23 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
}
// Check if restore local changes on asset reimport
+ constexpr bool RestoreModelOptionsOnReimport = true;
constexpr bool RestoreAnimEventsOnReimport = true;
+ const bool restoreModelOptions = RestoreModelOptionsOnReimport && (options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel);
const bool restoreMaterials = options.RestoreMaterialsOnReimport && data->Materials.HasItems();
const bool restoreAnimEvents = RestoreAnimEventsOnReimport && options.Type == ModelTool::ModelType::Animation && data->Animations.HasItems();
- if ((restoreMaterials || restoreAnimEvents) && FileSystem::FileExists(context.TargetAssetPath))
+ if ((restoreModelOptions || restoreMaterials || restoreAnimEvents) && FileSystem::FileExists(context.TargetAssetPath))
{
AssetReference asset = Content::LoadAsync(context.TargetAssetPath);
if (asset && !asset->WaitForLoaded())
{
auto* model = ScriptingObject::Cast(asset);
auto* animation = ScriptingObject::Cast(asset);
+ if (restoreModelOptions && model)
+ {
+ // Copy general properties
+ data->MinScreenSize = model->MinScreenSize;
+ }
if (restoreMaterials && model)
{
// Copy material settings
diff --git a/Source/Engine/Debug/DebugCommands.cpp b/Source/Engine/Debug/DebugCommands.cpp
index fa171d5dd..58cf2894b 100644
--- a/Source/Engine/Debug/DebugCommands.cpp
+++ b/Source/Engine/Debug/DebugCommands.cpp
@@ -8,6 +8,7 @@
#include "Engine/Threading/Threading.h"
#include "Engine/Threading/Task.h"
#include "Engine/Profiler/ProfilerCPU.h"
+#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/ManagedCLR/MAssembly.h"
@@ -219,6 +220,7 @@ namespace
if (module == GetBinaryModuleCorlib())
return;
PROFILE_CPU();
+ PROFILE_MEM(EngineDebug);
#if USE_CSHARP
if (auto* managedModule = dynamic_cast(module))
@@ -381,6 +383,7 @@ DebugCommandsService DebugCommandsServiceInstance;
void DebugCommands::Execute(StringView command)
{
+ PROFILE_MEM(EngineDebug);
// TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush)
String commandCopy = command;
command = commandCopy;
@@ -423,6 +426,7 @@ void DebugCommands::Search(StringView searchText, Array& matches, bo
{
if (searchText.IsEmpty())
return;
+ PROFILE_MEM(EngineDebug);
// TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush)
String searchTextCopy = searchText;
searchText = searchTextCopy;
diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp
index 7c798f88f..18552dcec 100644
--- a/Source/Engine/Debug/DebugDraw.cpp
+++ b/Source/Engine/Debug/DebugDraw.cpp
@@ -480,6 +480,7 @@ DebugDrawCall WriteLists(int32& vertexCounter, const Array& listA, const Arra
FORCE_INLINE DebugTriangle* AppendTriangles(int32 count, float duration, bool depthTest)
{
+ PROFILE_MEM(EngineDebug);
Array* list;
if (depthTest)
list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles;
@@ -490,6 +491,19 @@ FORCE_INLINE DebugTriangle* AppendTriangles(int32 count, float duration, bool de
return list->Get() + startIndex;
}
+FORCE_INLINE DebugTriangle* AppendWireTriangles(int32 count, float duration, bool depthTest)
+{
+ PROFILE_MEM(EngineDebug);
+ Array* list;
+ if (depthTest)
+ list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultWireTriangles : &Context->DebugDrawDepthTest.OneFrameWireTriangles;
+ else
+ list = duration > 0 ? &Context->DebugDrawDefault.DefaultWireTriangles : &Context->DebugDrawDefault.OneFrameWireTriangles;
+ const int32 startIndex = list->Count();
+ list->AddUninitialized(count);
+ return list->Get() + startIndex;
+}
+
inline void DrawText3D(const DebugText3D& t, const RenderContext& renderContext, const Float3& viewUp, const Matrix& f, const Matrix& vp, const Viewport& viewport, GPUContext* context, GPUTextureView* target, GPUTextureView* depthBuffer)
{
Matrix w, fw, m;
@@ -527,7 +541,7 @@ DebugDrawService DebugDrawServiceInstance;
bool DebugDrawService::Init()
{
- PROFILE_MEM(Graphics);
+ PROFILE_MEM(EngineDebug);
Context = &GlobalContext;
// Init wireframe sphere cache
@@ -646,7 +660,7 @@ void DebugDrawService::Update()
}
PROFILE_CPU();
- PROFILE_MEM(Graphics);
+ PROFILE_MEM(EngineDebug);
// Update lists
float deltaTime = Time::Update.DeltaTime.GetTotalSeconds();
@@ -1102,6 +1116,7 @@ void DebugDraw::DrawRay(const Ray& ray, const Color& color, float length, float
void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& color, float duration, bool depthTest)
{
+ PROFILE_MEM(EngineDebug);
const Float3 startF = start - Context->Origin, endF = end - Context->Origin;
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
if (duration > 0)
@@ -1120,6 +1135,7 @@ void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color&
void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& startColor, const Color& endColor, float duration, bool depthTest)
{
+ PROFILE_MEM(EngineDebug);
const Float3 startF = start - Context->Origin, endF = end - Context->Origin;
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
if (duration > 0)
@@ -1149,6 +1165,7 @@ void DebugDraw::DrawLines(const Span& lines, const Matrix& transform, co
}
// Draw lines
+ PROFILE_MEM(EngineDebug);
const Float3* p = lines.Get();
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
const Matrix transformF = transform * Matrix::Translation(-Context->Origin);
@@ -1188,6 +1205,7 @@ void DebugDraw::DrawLines(GPUBuffer* lines, const Matrix& transform, float durat
}
// Draw lines
+ PROFILE_MEM(EngineDebug);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
auto& geometry = debugDrawData.GeometryBuffers.AddOne();
geometry.Buffer = lines;
@@ -1212,6 +1230,7 @@ void DebugDraw::DrawLines(const Span& lines, const Matrix& transform, c
}
// Draw lines
+ PROFILE_MEM(EngineDebug);
const Double3* p = lines.Get();
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
const Matrix transformF = transform * Matrix::Translation(-Context->Origin);
@@ -1258,6 +1277,7 @@ void DebugDraw::DrawBezier(const Vector3& p1, const Vector3& p2, const Vector3&
const float segmentCountInv = 1.0f / (float)segmentCount;
// Draw segmented curve from lines
+ PROFILE_MEM(EngineDebug);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
if (duration > 0)
{
@@ -1298,6 +1318,7 @@ void DebugDraw::DrawWireBox(const BoundingBox& box, const Color& color, float du
c -= Context->Origin;
// Draw lines
+ PROFILE_MEM(EngineDebug);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
if (duration > 0)
{
@@ -1332,6 +1353,7 @@ void DebugDraw::DrawWireFrustum(const BoundingFrustum& frustum, const Color& col
c -= Context->Origin;
// Draw lines
+ PROFILE_MEM(EngineDebug);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
if (duration > 0)
{
@@ -1366,6 +1388,7 @@ void DebugDraw::DrawWireBox(const OrientedBoundingBox& box, const Color& color,
c -= Context->Origin;
// Draw lines
+ PROFILE_MEM(EngineDebug);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
if (duration > 0)
{
@@ -1407,6 +1430,7 @@ void DebugDraw::DrawWireSphere(const BoundingSphere& sphere, const Color& color,
auto& cache = SphereCache[index];
// Draw lines of the unit sphere after linear transform
+ PROFILE_MEM(EngineDebug);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
if (duration > 0)
{
@@ -1442,6 +1466,7 @@ void DebugDraw::DrawSphere(const BoundingSphere& sphere, const Color& color, flo
list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles;
else
list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles;
+ PROFILE_MEM(EngineDebug);
list->EnsureCapacity(list->Count() + SphereTriangleCache.Count());
const Float3 centerF = sphere.Center - Context->Origin;
@@ -1473,6 +1498,7 @@ void DebugDraw::DrawCircle(const Vector3& position, const Float3& normal, float
Matrix::Multiply(scale, world, matrix);
// Draw lines of the unit circle after linear transform
+ PROFILE_MEM(EngineDebug);
Float3 prev = Float3::Transform(CircleCache[0], matrix);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
for (int32 i = 1; i < DEBUG_DRAW_CIRCLE_VERTICES;)
@@ -1503,6 +1529,7 @@ void DebugDraw::DrawWireTriangle(const Vector3& v0, const Vector3& v1, const Vec
void DebugDraw::DrawTriangle(const Vector3& v0, const Vector3& v1, const Vector3& v2, const Color& color, float duration, bool depthTest)
{
+ PROFILE_MEM(EngineDebug);
DebugTriangle t;
t.Color = Color32(color);
t.TimeLeft = duration;
@@ -1558,6 +1585,7 @@ void DebugDraw::DrawTriangles(GPUBuffer* triangles, const Matrix& transform, flo
DebugLog::ThrowException("Cannot draw debug lines with incorrect amount of items in array");
return;
}
+ PROFILE_MEM(EngineDebug);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
auto& geometry = debugDrawData.GeometryBuffers.AddOne();
geometry.Buffer = triangles;
@@ -1714,7 +1742,7 @@ void DebugDraw::DrawWireTriangles(const Span& vertices, const Color& col
DebugTriangle t;
t.Color = Color32(color);
t.TimeLeft = duration;
- auto dst = AppendTriangles(vertices.Length() / 3, duration, depthTest);
+ auto dst = AppendWireTriangles(vertices.Length() / 3, duration, depthTest);
const Float3 origin = Context->Origin;
for (int32 i = 0; i < vertices.Length();)
{
@@ -1736,7 +1764,7 @@ void DebugDraw::DrawWireTriangles(const Span& vertices, const SpanOrigin;
for (int32 i = 0; i < indices.Length();)
{
@@ -1758,7 +1786,7 @@ void DebugDraw::DrawWireTriangles(const Span& vertices, const Color& co
DebugTriangle t;
t.Color = Color32(color);
t.TimeLeft = duration;
- auto dst = AppendTriangles(vertices.Length() / 3, duration, depthTest);
+ auto dst = AppendWireTriangles(vertices.Length() / 3, duration, depthTest);
const Double3 origin = Context->Origin;
for (int32 i = 0; i < vertices.Length();)
{
@@ -1780,7 +1808,7 @@ void DebugDraw::DrawWireTriangles(const Span& vertices, const SpanOrigin;
for (int32 i = 0; i < indices.Length();)
{
@@ -1847,6 +1875,7 @@ void DebugDraw::DrawWireCapsule(const Vector3& position, const Quaternion& orien
Matrix::Multiply(rotation, translation, world);
// Write vertices
+ PROFILE_MEM(EngineDebug);
auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault;
Color32 color32(color);
if (duration > 0)
@@ -1941,6 +1970,7 @@ namespace
void DrawCylinder(Array* list, const Vector3& position, const Quaternion& orientation, float radius, float height, const Color& color, float duration)
{
// Setup cache
+ PROFILE_MEM(EngineDebug);
Float3 CylinderCache[DEBUG_DRAW_CYLINDER_VERTICES];
const float angleBetweenFacets = TWO_PI / DEBUG_DRAW_CYLINDER_RESOLUTION;
const float verticalOffset = height * 0.5f;
@@ -2012,6 +2042,7 @@ namespace
void DrawCone(Array* list, const Vector3& position, const Quaternion& orientation, float radius, float angleXY, float angleXZ, const Color& color, float duration)
{
+ PROFILE_MEM(EngineDebug);
const float tolerance = 0.001f;
const float angle1 = Math::Clamp(angleXY, tolerance, PI - tolerance);
const float angle2 = Math::Clamp(angleXZ, tolerance, PI - tolerance);
@@ -2101,6 +2132,7 @@ void DebugDraw::DrawArc(const Vector3& position, const Quaternion& orientation,
{
if (angle <= 0)
return;
+ PROFILE_MEM(EngineDebug);
if (angle > TWO_PI)
angle = TWO_PI;
Array* list;
@@ -2133,6 +2165,7 @@ void DebugDraw::DrawWireArc(const Vector3& position, const Quaternion& orientati
{
if (angle <= 0)
return;
+ PROFILE_MEM(EngineDebug);
if (angle > TWO_PI)
angle = TWO_PI;
const int32 resolution = Math::CeilToInt((float)DEBUG_DRAW_CONE_RESOLUTION / TWO_PI * angle);
@@ -2199,6 +2232,7 @@ void DebugDraw::DrawBox(const BoundingBox& box, const Color& color, float durati
list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles;
else
list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles;
+ PROFILE_MEM(EngineDebug);
list->EnsureCapacity(list->Count() + 36);
for (int i0 = 0; i0 < 36;)
{
@@ -2227,6 +2261,7 @@ void DebugDraw::DrawBox(const OrientedBoundingBox& box, const Color& color, floa
list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles;
else
list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles;
+ PROFILE_MEM(EngineDebug);
list->EnsureCapacity(list->Count() + 36);
for (int i0 = 0; i0 < 36;)
{
@@ -2242,6 +2277,7 @@ void DebugDraw::DrawText(const StringView& text, const Float2& position, const C
{
if (text.Length() == 0 || size < 4)
return;
+ PROFILE_MEM(EngineDebug);
Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText2D : &Context->DebugDrawDefault.OneFrameText2D;
auto& t = list->AddOne();
t.Text.Resize(text.Length() + 1);
@@ -2257,6 +2293,7 @@ void DebugDraw::DrawText(const StringView& text, const Vector3& position, const
{
if (text.Length() == 0 || size < 4)
return;
+ PROFILE_MEM(EngineDebug);
Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText3D : &Context->DebugDrawDefault.OneFrameText3D;
auto& t = list->AddOne();
t.Text.Resize(text.Length() + 1);
@@ -2274,6 +2311,7 @@ void DebugDraw::DrawText(const StringView& text, const Transform& transform, con
{
if (text.Length() == 0 || size < 4)
return;
+ PROFILE_MEM(EngineDebug);
Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText3D : &Context->DebugDrawDefault.OneFrameText3D;
auto& t = list->AddOne();
t.Text.Resize(text.Length() + 1);
diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp
index ddc3468f7..f8b9c7b0f 100644
--- a/Source/Engine/Foliage/Foliage.cpp
+++ b/Source/Engine/Foliage/Foliage.cpp
@@ -44,20 +44,39 @@ void Foliage::AddToCluster(ChunkedArray ZeroTolerance);
ASSERT(cluster->Bounds.Intersects(instance.Bounds));
- // Find target cluster
- while (cluster->Children[0])
+ // Minor clusters don't use bounds intersection but try to find the first free cluster instead
+ if (cluster->IsMinor)
{
+ // Insert into the first non-full child cluster or subdivide 1st child
+#define CHECK_CHILD(idx) \
+ if (cluster->Children[idx]->Instances.Count() < FOLIAGE_CLUSTER_CAPACITY) \
+ { \
+ cluster->Children[idx]->Instances.Add(&instance); \
+ return; \
+ }
+ CHECK_CHILD(3);
+ CHECK_CHILD(2);
+ CHECK_CHILD(1);
+ cluster = cluster->Children[0];
+#undef CHECK_CHILD
+ }
+ else
+ {
+ // Find target cluster
+ while (cluster->Children[0])
+ {
#define CHECK_CHILD(idx) \
if (cluster->Children[idx]->Bounds.Intersects(instance.Bounds)) \
{ \
cluster = cluster->Children[idx]; \
continue; \
}
- CHECK_CHILD(0);
- CHECK_CHILD(1);
- CHECK_CHILD(2);
- CHECK_CHILD(3);
+ CHECK_CHILD(0);
+ CHECK_CHILD(1);
+ CHECK_CHILD(2);
+ CHECK_CHILD(3);
#undef CHECK_CHILD
+ }
}
// Check if it's not full
@@ -79,11 +98,20 @@ void Foliage::AddToCluster(ChunkedArrayBounds.Minimum;
const Vector3 max = cluster->Bounds.Maximum;
- const Vector3 size = cluster->Bounds.GetSize();
+ const Vector3 size = max - min;
cluster->Children[0]->Init(BoundingBox(min, min + size * Vector3(0.5f, 1.0f, 0.5f)));
cluster->Children[1]->Init(BoundingBox(min + size * Vector3(0.5f, 0.0f, 0.5f), max));
cluster->Children[2]->Init(BoundingBox(min + size * Vector3(0.5f, 0.0f, 0.0f), min + size * Vector3(1.0f, 1.0f, 0.5f)));
cluster->Children[3]->Init(BoundingBox(min + size * Vector3(0.0f, 0.0f, 0.5f), min + size * Vector3(0.5f, 1.0f, 1.0f)));
+ if (cluster->IsMinor || size.MinValue() < 1.0f)
+ {
+ // Mark children as minor to avoid infinite subdivision
+ cluster->IsMinor = true;
+ cluster->Children[0]->IsMinor = true;
+ cluster->Children[1]->IsMinor = true;
+ cluster->Children[2]->IsMinor = true;
+ cluster->Children[3]->IsMinor = true;
+ }
// Move instances to a proper cells
for (int32 i = 0; i < cluster->Instances.Count(); i++)
diff --git a/Source/Engine/Foliage/FoliageCluster.cpp b/Source/Engine/Foliage/FoliageCluster.cpp
index 1f76e5086..fd4c0f753 100644
--- a/Source/Engine/Foliage/FoliageCluster.cpp
+++ b/Source/Engine/Foliage/FoliageCluster.cpp
@@ -9,6 +9,7 @@ void FoliageCluster::Init(const BoundingBox& bounds)
Bounds = bounds;
TotalBounds = bounds;
MaxCullDistance = 0.0f;
+ IsMinor = false;
Children[0] = nullptr;
Children[1] = nullptr;
diff --git a/Source/Engine/Foliage/FoliageCluster.h b/Source/Engine/Foliage/FoliageCluster.h
index 55cbeb027..c55305c5d 100644
--- a/Source/Engine/Foliage/FoliageCluster.h
+++ b/Source/Engine/Foliage/FoliageCluster.h
@@ -33,6 +33,11 @@ public:
///
float MaxCullDistance;
+ ///
+ /// Flag used by clusters that are not typical quad-tree nodes but have no volume (eg. lots of instances placed on top of each other).
+ ///
+ int32 IsMinor : 1;
+
///
/// The child clusters. If any element is valid then all are created.
///
diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h
index f6af6c16b..107fe3533 100644
--- a/Source/Engine/Graphics/Enums.h
+++ b/Source/Engine/Graphics/Enums.h
@@ -1075,20 +1075,25 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64
///
LightsDebug = 1 << 27,
+ ///
+ /// Shows/hides particle effects.
+ ///
+ Particles = 1 << 28,
+
///
/// Default flags for Game.
///
- DefaultGame = Reflections | DepthOfField | Fog | Decals | MotionBlur | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | GlobalSDF | Sky,
+ DefaultGame = Reflections | DepthOfField | Fog | Decals | MotionBlur | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | GlobalSDF | Sky | Particles,
///
/// Default flags for Editor.
///
- DefaultEditor = Reflections | Fog | Decals | DebugDraw | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | EditorSprites | ContactShadows | GlobalSDF | Sky,
+ DefaultEditor = Reflections | Fog | Decals | DebugDraw | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | EditorSprites | ContactShadows | GlobalSDF | Sky | Particles,
///
/// Default flags for materials/models previews generating.
///
- DefaultAssetPreview = Reflections | Decals | DirectionalLights | PointLights | SpotLights | SkyLights | SpecularLight | AntiAliasing | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | Sky,
+ DefaultAssetPreview = Reflections | Decals | DirectionalLights | PointLights | SpotLights | SkyLights | SpecularLight | AntiAliasing | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | Sky | Particles,
};
DECLARE_ENUM_OPERATORS(ViewFlags);
diff --git a/Source/Engine/Graphics/Materials/MaterialShader.h b/Source/Engine/Graphics/Materials/MaterialShader.h
index 5da4ee04f..bb68520c0 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 178
+#define MATERIAL_GRAPH_VERSION 179
class Material;
class GPUShader;
diff --git a/Source/Engine/Graphics/Materials/MaterialShaderFeatures.cpp b/Source/Engine/Graphics/Materials/MaterialShaderFeatures.cpp
index 64dfe8303..19f2042f4 100644
--- a/Source/Engine/Graphics/Materials/MaterialShaderFeatures.cpp
+++ b/Source/Engine/Graphics/Materials/MaterialShaderFeatures.cpp
@@ -191,7 +191,7 @@ bool GlobalIlluminationFeature::Bind(MaterialShader::BindParameters& params, Spa
{
// Unbind SRVs to prevent issues
data.DDGI.CascadesCount = 0;
- data.DDGI.FallbackIrradiance = Float3::Zero;
+ data.DDGI.FallbackIrradiance = Float4::Zero;
params.GPUContext->UnBindSR(srv + 0);
params.GPUContext->UnBindSR(srv + 1);
params.GPUContext->UnBindSR(srv + 2);
diff --git a/Source/Engine/Graphics/Models/MeshAccessor.h b/Source/Engine/Graphics/Models/MeshAccessor.h
index 67b30e502..25fc01a1a 100644
--- a/Source/Engine/Graphics/Models/MeshAccessor.h
+++ b/Source/Engine/Graphics/Models/MeshAccessor.h
@@ -17,7 +17,7 @@ public:
///
/// Mesh data stream.
///
- struct Stream
+ struct FLAXENGINE_API Stream
{
friend MeshAccessor;
diff --git a/Source/Engine/Graphics/PostProcessSettings.h b/Source/Engine/Graphics/PostProcessSettings.h
index 670d99611..a300063e7 100644
--- a/Source/Engine/Graphics/PostProcessSettings.h
+++ b/Source/Engine/Graphics/PostProcessSettings.h
@@ -378,7 +378,7 @@ API_STRUCT() struct FLAXENGINE_API GlobalIlluminationSettings : ISerializable
/// The irradiance lighting outside the GI range used as a fallback to prevent pure-black scene outside the Global Illumination range.
///
API_FIELD(Attributes="EditorOrder(40), PostProcessSetting((int)GlobalIlluminationSettingsOverride.FallbackIrradiance)")
- Color FallbackIrradiance = Color::Black;
+ Color FallbackIrradiance = Color::Transparent;
public:
///
diff --git a/Source/Engine/Graphics/RenderTools.cpp b/Source/Engine/Graphics/RenderTools.cpp
index b0d587c8d..effbe6e1b 100644
--- a/Source/Engine/Graphics/RenderTools.cpp
+++ b/Source/Engine/Graphics/RenderTools.cpp
@@ -620,6 +620,40 @@ void RenderTools::ComputeSphereModelDrawMatrix(const RenderView& view, const Flo
resultIsViewInside = Float3::DistanceSquared(view.Position, position) < Math::Square(radius * 1.1f); // Manually tweaked bias
}
+Float3 RenderTools::GetColorQuantizationError(PixelFormat format)
+{
+ Float3 mantissaBits;
+ switch (format)
+ {
+ case PixelFormat::R11G11B10_Float:
+ mantissaBits = Float3(6, 6, 5);
+ break;
+ case PixelFormat::R10G10B10A2_UNorm:
+ mantissaBits = Float3(10, 10, 10);
+ break;
+ case PixelFormat::R16G16B16A16_Float:
+ mantissaBits = Float3(16, 16, 16);
+ break;
+ case PixelFormat::R32G32B32A32_Float:
+ mantissaBits = Float3(23, 23, 23);
+ break;
+ case PixelFormat::R9G9B9E5_SharedExp:
+ mantissaBits = Float3(5, 6, 5);
+ break;
+ case PixelFormat::R8G8B8A8_UNorm:
+ case PixelFormat::B8G8R8A8_UNorm:
+ mantissaBits = Float3(8, 8, 8);
+ break;
+ default:
+ return Float3::Zero;
+ }
+ return {
+ Math::Pow(0.5f, mantissaBits.X),
+ Math::Pow(0.5f, mantissaBits.Y),
+ Math::Pow(0.5f, mantissaBits.Z)
+ };
+}
+
int32 MipLevelsCount(int32 width)
{
int32 result = 1;
diff --git a/Source/Engine/Graphics/RenderTools.h b/Source/Engine/Graphics/RenderTools.h
index 18357a13a..5f0dc23dc 100644
--- a/Source/Engine/Graphics/RenderTools.h
+++ b/Source/Engine/Graphics/RenderTools.h
@@ -140,6 +140,9 @@ public:
static void CalculateTangentFrame(Float3& resultNormal, Float4& resultTangent, const Float3& normal, const Float3& tangent);
static void ComputeSphereModelDrawMatrix(const RenderView& view, const Float3& position, float radius, Matrix& resultWorld, bool& resultIsViewInside);
+
+ // Calculates error for a given render target format to reduce floating-point precision artifacts via QuantizeColor (from Noise.hlsl).
+ static Float3 GetColorQuantizationError(PixelFormat format);
};
// Calculates mip levels count for a texture 1D.
diff --git a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp
index 23382673f..05c6d605a 100644
--- a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp
+++ b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp
@@ -59,7 +59,7 @@ namespace
elements.Get()[j].Slot = (byte)slot;
}
}
- GPUVertexLayout* result = anyValid ? GPUVertexLayout::Get(elements) : nullptr;
+ GPUVertexLayout* result = anyValid ? GPUVertexLayout::Get(elements, true) : nullptr;
VertexBufferCache.Add(key, result);
return result;
}
@@ -97,6 +97,7 @@ GPUVertexLayout::GPUVertexLayout()
void GPUVertexLayout::SetElements(const Elements& elements, bool explicitOffsets)
{
uint32 offsets[GPU_MAX_VB_BINDED + 1] = {};
+ uint32 maxOffset[GPU_MAX_VB_BINDED + 1] = {};
_elements = elements;
for (int32 i = 0; i < _elements.Count(); i++)
{
@@ -108,9 +109,10 @@ void GPUVertexLayout::SetElements(const Elements& elements, bool explicitOffsets
else
e.Offset = (byte)offset;
offset += PixelFormatExtensions::SizeInBytes(e.Format);
+ maxOffset[e.Slot] = Math::Max(maxOffset[e.Slot], offset);
}
_stride = 0;
- for (uint32 offset : offsets)
+ for (uint32 offset : maxOffset)
_stride += offset;
}
@@ -139,7 +141,7 @@ VertexElement GPUVertexLayout::FindElement(VertexElement::Types type) const
GPUVertexLayout* GPUVertexLayout::Get(const Elements& elements, bool explicitOffsets)
{
// Hash input layout
- uint32 hash = 0;
+ uint32 hash = explicitOffsets ? 131 : 0;
for (const VertexElement& element : elements)
{
CombineHash(hash, GetHash(element));
diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h
index 319e1a939..567cbb618 100644
--- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h
+++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h
@@ -4,6 +4,7 @@
#include "Engine/Graphics/Shaders/GPUShaderProgram.h"
#include "Engine/Core/Types/DataContainer.h"
+#include "Engine/Core/Collections/Dictionary.h"
#include "../IncludeDirectXHeaders.h"
#if GRAPHICS_API_DIRECTX11
diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp
index 98143c7c3..4dc923234 100644
--- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp
+++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp
@@ -3,16 +3,9 @@
#if GRAPHICS_API_DIRECTX12
#include "Engine/Graphics/Config.h"
+#include "Engine/Platform/Platform.h"
+#include "../IncludeDirectXHeaders.h"
#if USE_PIX && GPU_ALLOW_PROFILE_EVENTS
-// Include these header files before pix3
-#define WIN32_LEAN_AND_MEAN
-#define NOMINMAX
-#define NOGDI
-#define NODRAWTEXT
-//#define NOCTLMGR
-#define NOFLATSBAPIS
-#include
-#include
#include
#endif
#include "GPUContextDX12.h"
diff --git a/Source/Engine/Input/Input.cpp b/Source/Engine/Input/Input.cpp
index 1f33f9fef..6860c3463 100644
--- a/Source/Engine/Input/Input.cpp
+++ b/Source/Engine/Input/Input.cpp
@@ -81,6 +81,8 @@ Delegate Input::MouseWheel;
Delegate Input::MouseMove;
Delegate Input::MouseMoveRelative;
Action Input::MouseLeave;
+Delegate Input::GamepadButtonDown;
+Delegate Input::GamepadButtonUp;
Delegate Input::TouchDown;
Delegate Input::TouchMove;
Delegate Input::TouchUp;
@@ -1045,6 +1047,19 @@ void InputService::Update()
break;
}
}
+ // TODO: route gamepad button events into global InputEvents queue to improve processing
+ for (int32 i = 0; i < Input::Gamepads.Count(); i++)
+ {
+ auto gamepad = Input::Gamepads[i];
+ for (int32 buttonIdx = 1; buttonIdx < (int32)GamepadButton::MAX; buttonIdx++)
+ {
+ GamepadButton button = (GamepadButton)buttonIdx;
+ if (gamepad->GetButtonDown(button))
+ Input::GamepadButtonDown((InputGamepadIndex)i, button);
+ else if (gamepad->GetButtonUp(button))
+ Input::GamepadButtonUp((InputGamepadIndex)i, button);
+ }
+ }
// Update all actions
for (int32 i = 0; i < Input::ActionMappings.Count(); i++)
diff --git a/Source/Engine/Input/Input.h b/Source/Engine/Input/Input.h
index 0021f99a5..964a247d9 100644
--- a/Source/Engine/Input/Input.h
+++ b/Source/Engine/Input/Input.h
@@ -118,6 +118,16 @@ public:
///
API_EVENT() static Action MouseLeave;
+ ///
+ /// Event fired when gamepad button goes down.
+ ///
+ API_EVENT() static Delegate GamepadButtonDown;
+
+ ///
+ /// Event fired when gamepad button goes up.
+ ///
+ API_EVENT() static Delegate GamepadButtonUp;
+
///
/// Event fired when touch action begins.
///
diff --git a/Source/Engine/Level/Actors/Light.cpp b/Source/Engine/Level/Actors/Light.cpp
index ad7fe54a2..6de7977c9 100644
--- a/Source/Engine/Level/Actors/Light.cpp
+++ b/Source/Engine/Level/Actors/Light.cpp
@@ -107,6 +107,7 @@ void LightWithShadow::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE(ContactShadowsLength);
SERIALIZE(ShadowsUpdateRate);
SERIALIZE(ShadowsUpdateRateAtDistance);
+ SERIALIZE(ShadowsResolution);
}
void LightWithShadow::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
@@ -125,4 +126,5 @@ void LightWithShadow::Deserialize(DeserializeStream& stream, ISerializeModifier*
DESERIALIZE(ContactShadowsLength);
DESERIALIZE(ShadowsUpdateRate);
DESERIALIZE(ShadowsUpdateRateAtDistance);
+ DESERIALIZE(ShadowsResolution);
}
diff --git a/Source/Engine/Navigation/NavMesh.cpp b/Source/Engine/Navigation/NavMesh.cpp
index 5593d732a..5932607a0 100644
--- a/Source/Engine/Navigation/NavMesh.cpp
+++ b/Source/Engine/Navigation/NavMesh.cpp
@@ -25,6 +25,7 @@ NavMesh::NavMesh(const SpawnParams& params)
void NavMesh::SaveNavMesh()
{
#if COMPILE_WITH_ASSETS_IMPORTER
+ PROFILE_MEM(NavigationMesh);
// Skip if scene is missing
const auto scene = GetScene();
@@ -111,7 +112,7 @@ void NavMesh::OnAssetLoaded(Asset* asset, void* caller)
if (Data.Tiles.HasItems())
return;
ScopeLock lock(DataAsset->Locker);
- PROFILE_MEM(Navigation);
+ PROFILE_MEM(NavigationMesh);
// Remove added tiles
if (_navMeshActive)
diff --git a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp
index 56351fded..c54f2f072 100644
--- a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp
+++ b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp
@@ -6,7 +6,7 @@
#if USE_EDITOR
#include "Editor/Editor.h"
#include "Editor/Managed/ManagedEditor.h"
-#include "NavMeshBuilder.h"
+#include "Navigation.h"
#endif
NavMeshBoundsVolume::NavMeshBoundsVolume(const SpawnParams& params)
@@ -55,9 +55,30 @@ void NavMeshBoundsVolume::OnBoundsChanged(const BoundingBox& prevBounds)
// Auto-rebuild modified navmesh area
if (IsDuringPlay() && IsActiveInHierarchy() && !Editor::IsPlayMode && Editor::Managed->CanAutoBuildNavMesh())
{
- BoundingBox dirtyBounds;
- BoundingBox::Merge(prevBounds, _box, dirtyBounds);
- NavMeshBuilder::Build(GetScene(), dirtyBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
+ if (_box.Intersects(prevBounds))
+ {
+ // Bounds were moved a bit so merge into a single request (for performance reasons)
+ BoundingBox dirtyBounds;
+ BoundingBox::Merge(prevBounds, _box, dirtyBounds);
+ Navigation::BuildNavMesh(dirtyBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
+ }
+ else
+ {
+ // Dirty each bounds in separate
+ Navigation::BuildNavMesh(prevBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
+ Navigation::BuildNavMesh(_box, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
+ }
+ }
+}
+
+void NavMeshBoundsVolume::OnActiveInTreeChanged()
+{
+ BoxVolume::OnActiveInTreeChanged();
+
+ // Auto-rebuild
+ if (IsDuringPlay() && !Editor::IsPlayMode && Editor::Managed->CanAutoBuildNavMesh())
+ {
+ Navigation::BuildNavMesh(_box, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
}
}
diff --git a/Source/Engine/Navigation/NavMeshBoundsVolume.h b/Source/Engine/Navigation/NavMeshBoundsVolume.h
index c04bc0483..80df5035a 100644
--- a/Source/Engine/Navigation/NavMeshBoundsVolume.h
+++ b/Source/Engine/Navigation/NavMeshBoundsVolume.h
@@ -30,6 +30,7 @@ protected:
void OnDisable() override;
#if USE_EDITOR
void OnBoundsChanged(const BoundingBox& prevBounds) override;
+ void OnActiveInTreeChanged() override;
Color GetWiresColor() override;
#endif
};
diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp
index e92173846..896cf4217 100644
--- a/Source/Engine/Navigation/NavMeshBuilder.cpp
+++ b/Source/Engine/Navigation/NavMeshBuilder.cpp
@@ -3,6 +3,7 @@
#if COMPILE_WITH_NAV_MESH_BUILDER
#include "NavMeshBuilder.h"
+#include "Navigation.h"
#include "NavMesh.h"
#include "NavigationSettings.h"
#include "NavMeshBoundsVolume.h"
@@ -706,6 +707,7 @@ struct BuildRequest
ScriptingObjectReference Scene;
DateTime Time;
BoundingBox DirtyBounds;
+ bool SpecificScene;
};
CriticalSection NavBuildQueueLocker;
@@ -713,6 +715,7 @@ Array NavBuildQueue;
CriticalSection NavBuildTasksLocker;
int32 NavBuildTasksMaxCount = 0;
+bool NavBuildCheckMissingNavMeshes = false;
Array NavBuildTasks;
class NavMeshTileBuildTask : public ThreadPoolTask
@@ -733,7 +736,7 @@ public:
bool Run() override
{
PROFILE_CPU_NAMED("BuildNavMeshTile");
- PROFILE_MEM(Navigation);
+ PROFILE_MEM(NavigationBuilding);
const auto navMesh = NavMesh.Get();
if (!navMesh)
return false;
@@ -776,13 +779,13 @@ void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime)
NavBuildTasksLocker.Unlock();
}
-void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y)
+void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y, NavMesh* navMesh)
{
NavBuildTasksLocker.Lock();
for (int32 i = 0; i < NavBuildTasks.Count(); i++)
{
auto task = NavBuildTasks[i];
- if (task->Runtime == runtime && task->X == x && task->Y == y)
+ if (task->Runtime == runtime && task->X == x && task->Y == y && task->NavMesh == navMesh)
{
NavBuildTasksLocker.Unlock();
@@ -838,7 +841,7 @@ void NavMeshBuilder::Init()
Level::SceneUnloading.Bind();
}
-bool NavMeshBuilder::IsBuildingNavMesh()
+bool Navigation::IsBuildingNavMesh()
{
NavBuildTasksLocker.Lock();
const bool hasAnyTask = NavBuildTasks.HasItems();
@@ -847,7 +850,7 @@ bool NavMeshBuilder::IsBuildingNavMesh()
return hasAnyTask;
}
-float NavMeshBuilder::GetNavMeshBuildingProgress()
+float Navigation::GetNavMeshBuildingProgress()
{
NavBuildTasksLocker.Lock();
float result = 1.0f;
@@ -907,15 +910,13 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo
// Align dirty bounds to tile size
BoundingBox dirtyBoundsNavMesh;
BoundingBox::Transform(dirtyBounds, worldToNavMesh, dirtyBoundsNavMesh);
- BoundingBox dirtyBoundsAligned;
- dirtyBoundsAligned.Minimum = Float3::Floor(dirtyBoundsNavMesh.Minimum / tileSize) * tileSize;
- dirtyBoundsAligned.Maximum = Float3::Ceil(dirtyBoundsNavMesh.Maximum / tileSize) * tileSize;
+ dirtyBoundsNavMesh.Minimum = Float3::Floor(dirtyBoundsNavMesh.Minimum / tileSize) * tileSize;
+ dirtyBoundsNavMesh.Maximum = Float3::Ceil(dirtyBoundsNavMesh.Maximum / tileSize) * tileSize;
// Calculate tiles range for the given navigation dirty bounds (aligned to tiles size)
- const Int3 tilesMin(dirtyBoundsAligned.Minimum / tileSize);
- const Int3 tilesMax(dirtyBoundsAligned.Maximum / tileSize);
- const int32 tilesX = tilesMax.X - tilesMin.X;
- const int32 tilesY = tilesMax.Z - tilesMin.Z;
+ const Int3 tilesMin(dirtyBoundsNavMesh.Minimum / tileSize);
+ const Int3 tilesMax(dirtyBoundsNavMesh.Maximum / tileSize);
+ const int32 tilesXZ = (tilesMax.X - tilesMin.X) * (tilesMax.Z - tilesMin.Z);
{
PROFILE_CPU_NAMED("Prepare");
@@ -932,18 +933,18 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo
// Remove all tiles from navmesh runtime
runtime->RemoveTiles(navMesh);
runtime->SetTileSize(tileSize);
- runtime->EnsureCapacity(tilesX * tilesY);
+ runtime->EnsureCapacity(tilesXZ);
// Remove all tiles from navmesh data
navMesh->Data.TileSize = tileSize;
navMesh->Data.Tiles.Clear();
- navMesh->Data.Tiles.EnsureCapacity(tilesX * tilesX);
+ navMesh->Data.Tiles.EnsureCapacity(tilesXZ);
navMesh->IsDataDirty = true;
}
else
{
// Ensure to have enough memory for tiles
- runtime->EnsureCapacity(tilesX * tilesY);
+ runtime->EnsureCapacity(tilesXZ);
}
runtime->Locker.Unlock();
@@ -959,11 +960,10 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo
// Cache navmesh volumes
Array> volumes;
- for (int32 i = 0; i < scene->Navigation.Volumes.Count(); i++)
+ for (const NavMeshBoundsVolume* volume : scene->Navigation.Volumes)
{
- const auto volume = scene->Navigation.Volumes.Get()[i];
if (!volume->AgentsMask.IsNavMeshSupported(navMesh->Properties) ||
- !volume->GetBox().Intersects(dirtyBoundsAligned))
+ !volume->GetBox().Intersects(dirtyBoundsNavMesh))
continue;
auto& bounds = volumes.AddOne();
BoundingBox::Transform(volume->GetBox(), worldToNavMesh, bounds);
@@ -1026,7 +1026,7 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo
for (const auto& tile : unusedTiles)
{
// Wait for any async tasks that are producing this tile
- CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y);
+ CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y, navMesh);
}
runtime->Locker.Lock();
for (const auto& tile : unusedTiles)
@@ -1095,6 +1095,7 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild
else if (settings->AutoAddMissingNavMeshes)
{
// Spawn missing navmesh
+ PROFILE_MEM(Navigation);
navMesh = New();
navMesh->SetStaticFlags(StaticFlags::FullyStatic);
navMesh->SetName(TEXT("NavMesh.") + navMeshProperties.Name);
@@ -1108,39 +1109,6 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild
{
BuildDirtyBounds(scene, navMesh, dirtyBounds, rebuild);
}
-
- // Remove unused navmeshes
- if (settings->AutoRemoveMissingNavMeshes)
- {
- for (NavMesh* navMesh : scene->Navigation.Meshes)
- {
- // Skip used navmeshes
- if (navMesh->Data.Tiles.HasItems())
- continue;
-
- // Skip navmeshes during async building
- int32 usageCount = 0;
- NavBuildTasksLocker.Lock();
- for (int32 i = 0; i < NavBuildTasks.Count(); i++)
- {
- if (NavBuildTasks.Get()[i]->NavMesh == navMesh)
- usageCount++;
- }
- NavBuildTasksLocker.Unlock();
- if (usageCount != 0)
- continue;
-
- navMesh->DeleteObject();
- }
- }
-}
-
-void BuildWholeScene(Scene* scene)
-{
- // Compute total navigation area bounds
- const BoundingBox worldBounds = scene->Navigation.GetNavigationBounds();
-
- BuildDirtyBounds(scene, worldBounds, true);
}
void ClearNavigation(Scene* scene)
@@ -1154,22 +1122,58 @@ void ClearNavigation(Scene* scene)
}
}
+void BuildNavigation(BuildRequest& request)
+{
+ // If scene is not specified then build all loaded scenes
+ if (!request.Scene)
+ {
+ for (Scene* scene : Level::Scenes)
+ {
+ request.Scene = scene;
+ BuildNavigation(request);
+ }
+ return;
+ }
+
+ // Early out if scene is not using navigation
+ if (request.Scene->Navigation.Volumes.IsEmpty())
+ {
+ ClearNavigation(request.Scene);
+ return;
+ }
+
+ // Check if similar request is already in a queue
+ for (auto& e : NavBuildQueue)
+ {
+ if (e.Scene == request.Scene && (e.DirtyBounds == request.DirtyBounds || request.DirtyBounds == BoundingBox::Empty))
+ {
+ e = request;
+ return;
+ }
+ }
+
+ // Enqueue request
+ NavBuildQueue.Add(request);
+}
+
void NavMeshBuilder::Update()
{
- PROFILE_MEM(Navigation);
+ PROFILE_MEM(NavigationBuilding);
ScopeLock lock(NavBuildQueueLocker);
// Process nav mesh building requests and kick the tasks
const auto now = DateTime::NowUTC();
+ bool didRebuild = false;
for (int32 i = 0; NavBuildQueue.HasItems() && i < NavBuildQueue.Count(); i++)
{
auto req = NavBuildQueue.Get()[i];
if (now - req.Time >= 0)
{
NavBuildQueue.RemoveAt(i--);
- const auto scene = req.Scene.Get();
+ Scene* scene = req.Scene.Get();
if (!scene)
continue;
+ bool rebuild = req.DirtyBounds == BoundingBox::Empty;
// Early out if scene has no bounds volumes to define nav mesh area
if (scene->Navigation.Volumes.IsEmpty())
@@ -1179,80 +1183,69 @@ void NavMeshBuilder::Update()
}
// Check if build a custom dirty bounds or whole scene
- if (req.DirtyBounds == BoundingBox::Empty)
- {
- BuildWholeScene(scene);
- }
+ if (rebuild)
+ req.DirtyBounds = scene->Navigation.GetNavigationBounds(); // Compute total navigation area bounds
+ if (didRebuild)
+ rebuild = false; // When rebuilding navmesh for multiple scenes, rebuild only the first one (other scenes will use additive update)
else
+ didRebuild = true;
+ BuildDirtyBounds(scene, req.DirtyBounds, rebuild);
+ NavBuildCheckMissingNavMeshes = true;
+ }
+ }
+
+ // Remove unused navmeshes (when all active tasks are done)
+ // TODO: ignore AutoRemoveMissingNavMeshes in game and make it editor-only?
+ if (NavBuildCheckMissingNavMeshes && NavBuildTasksMaxCount == 0 && NavigationSettings::Get()->AutoRemoveMissingNavMeshes)
+ {
+ NavBuildCheckMissingNavMeshes = false;
+ NavBuildTasksLocker.Lock();
+ int32 taskCount = NavBuildTasks.Count();
+ NavBuildTasksLocker.Unlock();
+ if (taskCount == 0)
+ {
+ for (Scene* scene : Level::Scenes)
{
- BuildDirtyBounds(scene, req.DirtyBounds, false);
+ for (NavMesh* navMesh : scene->Navigation.Meshes)
+ {
+ if (!navMesh->Data.Tiles.HasItems())
+ {
+ navMesh->DeleteObject();
+ }
+ }
}
}
}
}
-void NavMeshBuilder::Build(Scene* scene, float timeoutMs)
+void Navigation::BuildNavMesh(Scene* scene, float timeoutMs)
{
- if (!scene)
- {
- LOG(Warning, "Could not generate navmesh without scene.");
- return;
- }
-
- // Early out if scene is not using navigation
- if (scene->Navigation.Volumes.IsEmpty())
- {
- ClearNavigation(scene);
- return;
- }
-
- PROFILE_CPU_NAMED("NavMeshBuilder");
- PROFILE_MEM(Navigation);
+ PROFILE_CPU();
+ PROFILE_MEM(NavigationBuilding);
ScopeLock lock(NavBuildQueueLocker);
BuildRequest req;
req.Scene = scene;
req.Time = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs);
req.DirtyBounds = BoundingBox::Empty;
-
- for (int32 i = 0; i < NavBuildQueue.Count(); i++)
- {
- auto& e = NavBuildQueue.Get()[i];
- if (e.Scene == scene && e.DirtyBounds == req.DirtyBounds)
- {
- e = req;
- return;
- }
- }
-
- NavBuildQueue.Add(req);
+ req.SpecificScene = scene != nullptr;
+ BuildNavigation(req);
}
-void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs)
+void Navigation::BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene, float timeoutMs)
{
- if (!scene)
- {
- LOG(Warning, "Could not generate navmesh without scene.");
- return;
- }
-
- // Early out if scene is not using navigation
- if (scene->Navigation.Volumes.IsEmpty())
- {
- ClearNavigation(scene);
- return;
- }
-
- PROFILE_CPU_NAMED("NavMeshBuilder");
- PROFILE_MEM(Navigation);
+ if (dirtyBounds.GetVolume() <= ZeroTolerance)
+ return; // Skip updating empty bounds
+ PROFILE_CPU();
+ PROFILE_MEM(NavigationBuilding);
ScopeLock lock(NavBuildQueueLocker);
BuildRequest req;
req.Scene = scene;
req.Time = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs);
req.DirtyBounds = dirtyBounds;
-
- NavBuildQueue.Add(req);
+ req.SpecificScene = scene != nullptr;
+ BuildNavigation(req);
}
#endif
diff --git a/Source/Engine/Navigation/NavMeshBuilder.h b/Source/Engine/Navigation/NavMeshBuilder.h
index a3477db27..355bac7de 100644
--- a/Source/Engine/Navigation/NavMeshBuilder.h
+++ b/Source/Engine/Navigation/NavMeshBuilder.h
@@ -15,11 +15,7 @@ class FLAXENGINE_API NavMeshBuilder
{
public:
static void Init();
- static bool IsBuildingNavMesh();
- static float GetNavMeshBuildingProgress();
static void Update();
- static void Build(Scene* scene, float timeoutMs);
- static void Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs);
};
#endif
diff --git a/Source/Engine/Navigation/NavMeshRuntime.cpp b/Source/Engine/Navigation/NavMeshRuntime.cpp
index 2758077c6..0ace29415 100644
--- a/Source/Engine/Navigation/NavMeshRuntime.cpp
+++ b/Source/Engine/Navigation/NavMeshRuntime.cpp
@@ -5,6 +5,9 @@
#include "NavMesh.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Random.h"
+#if COMPILE_WITH_DEBUG_DRAW
+#include "Engine/Level/Scene/Scene.h"
+#endif
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Threading/Threading.h"
@@ -326,7 +329,7 @@ void NavMeshRuntime::EnsureCapacity(int32 tilesToAddCount)
if (newTilesCount <= capacity)
return;
PROFILE_CPU_NAMED("NavMeshRuntime.EnsureCapacity");
- PROFILE_MEM(Navigation);
+ PROFILE_MEM(NavigationMesh);
// Navmesh tiles capacity growing rule
int32 newCapacity = capacity ? capacity : 32;
@@ -387,7 +390,7 @@ void NavMeshRuntime::AddTiles(NavMesh* navMesh)
return;
auto& data = navMesh->Data;
PROFILE_CPU_NAMED("NavMeshRuntime.AddTiles");
- PROFILE_MEM(Navigation);
+ PROFILE_MEM(NavigationMesh);
ScopeLock lock(Locker);
// Validate data (must match navmesh) or init navmesh to match the tiles options
@@ -419,7 +422,7 @@ void NavMeshRuntime::AddTile(NavMesh* navMesh, NavMeshTileData& tileData)
ASSERT(navMesh);
auto& data = navMesh->Data;
PROFILE_CPU_NAMED("NavMeshRuntime.AddTile");
- PROFILE_MEM(Navigation);
+ PROFILE_MEM(NavigationMesh);
ScopeLock lock(Locker);
// Validate data (must match navmesh) or init navmesh to match the tiles options
@@ -603,7 +606,21 @@ void NavMeshRuntime::DebugDraw()
if (!tile->header)
continue;
- //DebugDraw::DrawWireBox(*(BoundingBox*)&tile->header->bmin[0], Color::CadetBlue);
+#if 0
+ // Debug draw tile bounds and owner scene name
+ BoundingBox tileBounds = *(BoundingBox*)&tile->header->bmin[0];
+ DebugDraw::DrawWireBox(tileBounds, Color::CadetBlue);
+ // TODO: build map from tile coords to tile data to avoid this loop
+ for (const auto& e : _tiles)
+ {
+ if (e.X == tile->header->x && e.Y == tile->header->y && e.Layer == tile->header->layer)
+ {
+ if (e.NavMesh && e.NavMesh->GetScene())
+ DebugDraw::DrawText(e.NavMesh->GetScene()->GetName(), tileBounds.Minimum + tileBounds.GetSize() * Float3(0.5f, 0.8f, 0.5f), Color::CadetBlue);
+ break;
+ }
+ }
+#endif
for (int i = 0; i < tile->header->polyCount; i++)
{
diff --git a/Source/Engine/Navigation/NavMeshRuntime.h b/Source/Engine/Navigation/NavMeshRuntime.h
index 1ca6607b9..9e1165196 100644
--- a/Source/Engine/Navigation/NavMeshRuntime.h
+++ b/Source/Engine/Navigation/NavMeshRuntime.h
@@ -111,7 +111,7 @@ public:
/// The start position.
/// The result hit information. Valid only when query succeed.
/// The maximum distance to search for wall (search radius).
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() bool FindDistanceToWall(const Vector3& startPosition, NavMeshHit& hitInfo, float maxDistance = MAX_float) const;
///
@@ -187,7 +187,7 @@ public:
/// The start position.
/// The end position.
/// The result hit information. Valid only when query succeed.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo) const;
public:
diff --git a/Source/Engine/Navigation/NavModifierVolume.cpp b/Source/Engine/Navigation/NavModifierVolume.cpp
index 9e1295f70..aa71e7aa1 100644
--- a/Source/Engine/Navigation/NavModifierVolume.cpp
+++ b/Source/Engine/Navigation/NavModifierVolume.cpp
@@ -2,7 +2,7 @@
#include "NavModifierVolume.h"
#include "NavigationSettings.h"
-#include "NavMeshBuilder.h"
+#include "Navigation.h"
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Serialization/Serialization.h"
#if USE_EDITOR
@@ -83,7 +83,7 @@ void NavModifierVolume::OnBoundsChanged(const BoundingBox& prevBounds)
#else
const float timeoutMs = 0.0f;
#endif
- NavMeshBuilder::Build(GetScene(), dirtyBounds, timeoutMs);
+ Navigation::BuildNavMesh(dirtyBounds, GetScene(), timeoutMs);
}
#endif
}
diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp
index 34983652f..908819765 100644
--- a/Source/Engine/Navigation/Navigation.cpp
+++ b/Source/Engine/Navigation/Navigation.cpp
@@ -180,7 +180,7 @@ NavigationService NavigationServiceInstance;
void* dtAllocDefault(size_t size, dtAllocHint)
{
- PROFILE_MEM(Navigation);
+ PROFILE_MEM(NavigationMesh);
return Allocator::Allocate(size);
}
@@ -382,30 +382,6 @@ bool Navigation::RayCast(const Vector3& startPosition, const Vector3& endPositio
return NavMeshes.First()->RayCast(startPosition, endPosition, hitInfo);
}
-#if COMPILE_WITH_NAV_MESH_BUILDER
-
-bool Navigation::IsBuildingNavMesh()
-{
- return NavMeshBuilder::IsBuildingNavMesh();
-}
-
-float Navigation::GetNavMeshBuildingProgress()
-{
- return NavMeshBuilder::GetNavMeshBuildingProgress();
-}
-
-void Navigation::BuildNavMesh(Scene* scene, float timeoutMs)
-{
- NavMeshBuilder::Build(scene, timeoutMs);
-}
-
-void Navigation::BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs)
-{
- NavMeshBuilder::Build(scene, dirtyBounds, timeoutMs);
-}
-
-#endif
-
#if COMPILE_WITH_DEBUG_DRAW
void Navigation::DrawNavMesh()
diff --git a/Source/Engine/Navigation/Navigation.h b/Source/Engine/Navigation/Navigation.h
index 4d8b181e7..80c8eb84a 100644
--- a/Source/Engine/Navigation/Navigation.h
+++ b/Source/Engine/Navigation/Navigation.h
@@ -19,7 +19,7 @@ public:
/// The start position.
/// The result hit information. Valid only when query succeed.
/// The maximum distance to search for wall (search radius).
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() static bool FindDistanceToWall(const Vector3& startPosition, API_PARAM(Out) NavMeshHit& hitInfo, float maxDistance = MAX_float);
///
@@ -81,12 +81,10 @@ public:
/// The start position.
/// The end position.
/// The result hit information. Valid only when query succeed.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() static bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo);
-public:
#if COMPILE_WITH_NAV_MESH_BUILDER
-
///
/// Returns true if navigation system is during navmesh building (any request is valid or async task active).
///
@@ -100,32 +98,49 @@ public:
///
/// Builds the Nav Mesh for the given scene (discards all its tiles).
///
- ///
- /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.
- ///
- /// The scene.
+ /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.
+ /// The scene. Pass null to build navmesh for all loaded scenes.
/// The timeout to wait before building Nav Mesh (in milliseconds).
- API_FUNCTION() static void BuildNavMesh(Scene* scene, float timeoutMs = 50);
+ API_FUNCTION() static void BuildNavMesh(Scene* scene = nullptr, float timeoutMs = 50);
///
/// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box).
///
- ///
- /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.
- ///
+ /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.
+ /// The bounds in world-space to build overlapping tiles.
+ /// The scene. Pass null to build navmesh for all loaded scenes that intersect with a given bounds.
+ /// The timeout to wait before building Nav Mesh (in milliseconds).
+ API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene = nullptr, float timeoutMs = 50);
+
+ ///
+ /// Builds the Nav Mesh for all the loaded scenes (builds only the tiles overlapping the given bounding box).
+ ///
+ /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.
+ /// The bounds in world-space to build overlapping tiles.
+ /// The timeout to wait before building Nav Mesh (in milliseconds).
+ API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, float timeoutMs = 50)
+ {
+ BuildNavMesh(dirtyBounds, nullptr, timeoutMs);
+ }
+
+ ///
+ /// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box).
+ /// [Deprecated in v1.12]
+ ///
+ /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.
/// The scene.
/// The bounds in world-space to build overlapping tiles.
/// The timeout to wait before building Nav Mesh (in milliseconds).
- API_FUNCTION() static void BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs = 50);
-
+ API_FUNCTION() DEPRECATED("Use BuildNavMesh with reordered arguments instead") static void BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs = 50)
+ {
+ BuildNavMesh(dirtyBounds, scene, timeoutMs);
+ }
#endif
#if COMPILE_WITH_DEBUG_DRAW
-
///
/// Draws the navigation for all the scenes (uses DebugDraw interface).
///
static void DrawNavMesh();
-
#endif
};
diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp
index 6e94594b0..14467664c 100644
--- a/Source/Engine/Particles/ParticleEffect.cpp
+++ b/Source/Engine/Particles/ParticleEffect.cpp
@@ -11,6 +11,10 @@
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Engine/Time.h"
#include "Engine/Engine/Engine.h"
+#if USE_EDITOR
+#include "Editor/Editor.h"
+#include "Editor/Managed/ManagedEditor.h"
+#endif
ParticleEffect::ParticleEffect(const SpawnParams& params)
: Actor(params)
@@ -465,7 +469,12 @@ void ParticleEffect::Update()
if (UpdateMode == SimulationUpdateMode::FixedTimestep)
{
// Check if last simulation update was past enough to kick a new on
- const float time = Time::Update.Time.GetTotalSeconds();
+ bool useTimeScale = UseTimeScale;
+#if USE_EDITOR
+ if (!Editor::IsPlayMode && IsDuringPlay())
+ useTimeScale = false;
+#endif
+ const float time = (useTimeScale ? Time::Update.Time : Time::Update.UnscaledTime).GetTotalSeconds();
if (time - Instance.LastUpdateTime < FixedTimestep)
return;
}
@@ -475,9 +484,6 @@ void ParticleEffect::Update()
#if USE_EDITOR
-#include "Editor/Editor.h"
-#include "Editor/Managed/ManagedEditor.h"
-
void ParticleEffect::UpdateExecuteInEditor()
{
// Auto-play in Editor
@@ -601,7 +607,9 @@ bool ParticleEffect::HasContentLoaded() const
void ParticleEffect::Draw(RenderContext& renderContext)
{
- if (renderContext.View.Pass == DrawPass::GlobalSDF || renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas)
+ if (renderContext.View.Pass == DrawPass::GlobalSDF ||
+ renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas ||
+ EnumHasNoneFlags(renderContext.View.Flags, ViewFlags::Particles))
return;
_lastMinDstSqr = Math::Min(_lastMinDstSqr, Vector3::DistanceSquared(GetPosition(), renderContext.View.WorldPosition));
RenderContextBatch renderContextBatch(renderContext);
@@ -610,10 +618,12 @@ void ParticleEffect::Draw(RenderContext& renderContext)
void ParticleEffect::Draw(RenderContextBatch& renderContextBatch)
{
+ const RenderView& mainView = renderContextBatch.GetMainContext().View;
+ if (EnumHasNoneFlags(mainView.Flags, ViewFlags::Particles))
+ return;
Particles::DrawParticles(renderContextBatch, this);
// Cull again against the main context (if using multiple ones) to skip caching draw distance from shadow projections
- const RenderView& mainView = renderContextBatch.GetMainContext().View;
const BoundingSphere bounds(_sphere.Center - mainView.Origin, _sphere.Radius);
if (renderContextBatch.Contexts.Count() > 1 && !mainView.CullingFrustum.Intersects(bounds))
return;
diff --git a/Source/Engine/Physics/Colliders/BoxCollider.cpp b/Source/Engine/Physics/Colliders/BoxCollider.cpp
index 1e90cb91f..47e551b37 100644
--- a/Source/Engine/Physics/Colliders/BoxCollider.cpp
+++ b/Source/Engine/Physics/Colliders/BoxCollider.cpp
@@ -23,15 +23,15 @@ void BoxCollider::SetSize(const Float3& value)
void BoxCollider::AutoResize(bool globalOrientation = true)
{
Actor* parent = GetParent();
- if (Cast(parent))
+ if (parent == nullptr || Cast(parent))
return;
// Get bounds of all siblings (excluding itself)
const Vector3 parentScale = parent->GetScale();
if (parentScale.IsAnyZero())
- return; // Avoid division by zero
+ return;
- // Hacky way to get unrotated bounded box of parent.
+ // Hacky way to get unrotated bounded box of parent
const Quaternion parentOrientation = parent->GetOrientation();
parent->SetOrientation(Quaternion::Identity);
BoundingBox parentBox = parent->GetBox();
diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
index 3edfaa0c6..8c3d3610e 100644
--- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
+++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
@@ -2784,6 +2784,69 @@ float PhysicsBackend::ComputeShapeSqrDistanceToPoint(void* shape, const Vector3&
{
auto shapePhysX = (PxShape*)shape;
const PxTransform trans(C2P(position), C2P(orientation));
+
+ // Special case for heightfield collider (not implemented in PhysX)
+ if (shapePhysX->getGeometryType() == PxGeometryType::eHEIGHTFIELD)
+ {
+ // Do a bunch of raycasts in all directions to find the closest point on the heightfield
+ PxVec3 origin = C2P(point);
+ Array unitDirections;
+ constexpr int32 resolution = 32;
+ unitDirections.EnsureCapacity((resolution + 1) * (resolution + 1));
+ for (int32 i = 0; i <= resolution; i++)
+ {
+ float phi = PI * (float)i / resolution;
+ float sinPhi = Math::Sin(phi);
+ float cosPhi = Math::Cos(phi);
+ for (int32 j = 0; j <= resolution; j++)
+ {
+ float theta = 2.0f * PI * (float)j / resolution;
+ float cosTheta = Math::Cos(theta);
+ float sinTheta = Math::Sin(theta);
+
+ PxVec3 v;
+ v.x = cosTheta * sinPhi;
+ v.y = cosPhi;
+ v.z = sinTheta * sinPhi;
+
+ // All generated vectors are unit vectors (length 1)
+ unitDirections.Add(v);
+ }
+ }
+
+ PxReal maxDistance = PX_MAX_REAL; // Search indefinitely
+ PxQueryFilterData filterData;
+ filterData.data.word0 = (PxU32)shapePhysX->getSimulationFilterData().word0;
+ PxHitFlags hitFlags = PxHitFlag::ePOSITION | PxHitFlag::eMESH_BOTH_SIDES; // Both sides added for if it is underneath the height field
+ PxRaycastBuffer buffer;
+ auto scene = shapePhysX->getActor()->getScene();
+
+ PxReal closestDistance = maxDistance;
+ PxVec3 tempClosestPoint;
+ for (PxVec3& unitDir : unitDirections)
+ {
+ bool hitResult = scene->raycast(origin, unitDir, maxDistance, buffer, hitFlags, filterData);
+ if (hitResult)
+ {
+ auto& hit = buffer.getAnyHit(0);
+ if (hit.distance < closestDistance && hit.distance > 0.0f)
+ {
+ tempClosestPoint = hit.position;
+ closestDistance = hit.distance;
+ }
+ }
+ }
+
+ if (closestDistance < maxDistance)
+ {
+ *closestPoint = P2C(tempClosestPoint);
+ return closestDistance * closestDistance; // Result is squared distance
+ }
+
+ return -1.0f;
+ }
+
+ // Default point distance for other collider queries
#if USE_LARGE_WORLDS
PxVec3 closestPointPx;
float result = PxGeometryQuery::pointDistance(C2P(point), shapePhysX->getGeometry(), trans, &closestPointPx);
diff --git a/Source/Engine/Physics/Physics.h b/Source/Engine/Physics/Physics.h
index 2fc116020..85cd5e77b 100644
--- a/Source/Engine/Physics/Physics.h
+++ b/Source/Engine/Physics/Physics.h
@@ -102,7 +102,7 @@ public:
/// The end position of the line.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() static bool LineCast(const Vector3& start, const Vector3& end, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -113,18 +113,18 @@ public:
/// The result hit information. Valid only when method returns true.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() static bool LineCast(const Vector3& start, const Vector3& end, API_PARAM(Out) RayCastHit& hitInfo, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
//
- /// Performs a line between two points in the scene, returns all hitpoints infos.
+ /// Performs a line between two points in the scene, returns all hit points info.
///
/// The origin of the ray.
/// The end position of the line.
/// The result hits. Valid only when method returns true.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() static bool LineCastAll(const Vector3& start, const Vector3& end, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -135,7 +135,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() static bool RayCast(const Vector3& origin, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -147,7 +147,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() static bool RayCast(const Vector3& origin, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -159,7 +159,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() static bool RayCastAll(const Vector3& origin, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -172,7 +172,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if box hits an matching object, otherwise false.
+ /// True if box hits a matching object, otherwise false.
API_FUNCTION() static bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -186,7 +186,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if box hits an matching object, otherwise false.
+ /// True if box hits a matching object, otherwise false.
API_FUNCTION() static bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -200,7 +200,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if box hits an matching object, otherwise false.
+ /// True if box hits a matching object, otherwise false.
API_FUNCTION() static bool BoxCastAll(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -212,7 +212,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if sphere hits an matching object, otherwise false.
+ /// True if sphere hits a matching object, otherwise false.
API_FUNCTION() static bool SphereCast(const Vector3& center, float radius, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -225,7 +225,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if sphere hits an matching object, otherwise false.
+ /// True if sphere hits a matching object, otherwise false.
API_FUNCTION() static bool SphereCast(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -238,7 +238,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if sphere hits an matching object, otherwise false.
+ /// True if sphere hits a matching object, otherwise false.
API_FUNCTION() static bool SphereCastAll(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -252,7 +252,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if capsule hits an matching object, otherwise false.
+ /// True if capsule hits a matching object, otherwise false.
API_FUNCTION() static bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -267,7 +267,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if capsule hits an matching object, otherwise false.
+ /// True if capsule hits a matching object, otherwise false.
API_FUNCTION() static bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -282,7 +282,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if capsule hits an matching object, otherwise false.
+ /// True if capsule hits a matching object, otherwise false.
API_FUNCTION() static bool CapsuleCastAll(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -296,7 +296,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if convex mesh hits an matching object, otherwise false.
+ /// True if convex mesh hits a matching object, otherwise false.
API_FUNCTION() static bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -311,7 +311,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if convex mesh hits an matching object, otherwise false.
+ /// True if convex mesh hits a matching object, otherwise false.
API_FUNCTION() static bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -326,7 +326,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if convex mesh hits an matching object, otherwise false.
+ /// True if convex mesh hits a matching object, otherwise false.
API_FUNCTION() static bool ConvexCastAll(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -375,7 +375,7 @@ public:
API_FUNCTION() static bool CheckConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given box.
+ /// Finds all colliders touching or inside the given box.
///
/// The box center.
/// The half size of the box in each direction.
@@ -387,7 +387,7 @@ public:
API_FUNCTION() static bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given sphere.
+ /// Finds all colliders touching or inside the given sphere.
///
/// The sphere center.
/// The radius of the sphere.
@@ -398,7 +398,7 @@ public:
API_FUNCTION() static bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given capsule.
+ /// Finds all colliders touching or inside the given capsule.
///
/// The capsule center.
/// The radius of the capsule.
@@ -411,7 +411,7 @@ public:
API_FUNCTION() static bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given convex mesh.
+ /// Finds all colliders touching or inside the given convex mesh.
///
/// The convex mesh center.
/// Collision data of the convex mesh.
@@ -424,7 +424,7 @@ public:
API_FUNCTION() static bool OverlapConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given box.
+ /// Finds all colliders touching or inside the given box.
///
/// The box center.
/// The half size of the box in each direction.
@@ -436,7 +436,7 @@ public:
API_FUNCTION() static bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given sphere.
+ /// Finds all colliders touching or inside the given sphere.
///
/// The sphere center.
/// The radius of the sphere.
@@ -447,7 +447,7 @@ public:
API_FUNCTION() static bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given capsule.
+ /// Finds all colliders touching or inside the given capsule.
///
/// The capsule center.
/// The radius of the capsule.
@@ -460,7 +460,7 @@ public:
API_FUNCTION() static bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given convex mesh.
+ /// Finds all colliders touching or inside the given convex mesh.
///
/// The convex mesh center.
/// Collision data of the convex mesh.
diff --git a/Source/Engine/Physics/PhysicsScene.h b/Source/Engine/Physics/PhysicsScene.h
index 602e6f713..a7cb91cbe 100644
--- a/Source/Engine/Physics/PhysicsScene.h
+++ b/Source/Engine/Physics/PhysicsScene.h
@@ -140,7 +140,7 @@ public:
/// The end position of the line.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() bool LineCast(const Vector3& start, const Vector3& end, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -151,18 +151,18 @@ public:
/// The result hit information. Valid only when method returns true.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() bool LineCast(const Vector3& start, const Vector3& end, API_PARAM(Out) RayCastHit& hitInfo, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
//
- /// Performs a line between two points in the scene, returns all hitpoints infos.
+ /// Performs a line between two points in the scene, returns all hit points info.
///
/// The origin of the ray.
/// The normalized direction of the ray.
/// The result hits. Valid only when method returns true.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() bool LineCastAll(const Vector3& start, const Vector3& end, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -173,7 +173,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() bool RayCast(const Vector3& origin, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -185,7 +185,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() bool RayCast(const Vector3& origin, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -197,7 +197,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if ray hits an matching object, otherwise false.
+ /// True if ray hits a matching object, otherwise false.
API_FUNCTION() bool RayCastAll(const Vector3& origin, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -210,7 +210,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if box hits an matching object, otherwise false.
+ /// True if box hits a matching object, otherwise false.
API_FUNCTION() bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -224,7 +224,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if box hits an matching object, otherwise false.
+ /// True if box hits a matching object, otherwise false.
API_FUNCTION() bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -238,7 +238,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if box hits an matching object, otherwise false.
+ /// True if box hits a matching object, otherwise false.
API_FUNCTION() bool BoxCastAll(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -250,7 +250,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if sphere hits an matching object, otherwise false.
+ /// True if sphere hits a matching object, otherwise false.
API_FUNCTION() bool SphereCast(const Vector3& center, float radius, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -263,7 +263,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if sphere hits an matching object, otherwise false.
+ /// True if sphere hits a matching object, otherwise false.
API_FUNCTION() bool SphereCast(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -276,7 +276,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if sphere hits an matching object, otherwise false.
+ /// True if sphere hits a matching object, otherwise false.
API_FUNCTION() bool SphereCastAll(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -290,7 +290,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if capsule hits an matching object, otherwise false.
+ /// True if capsule hits a matching object, otherwise false.
API_FUNCTION() bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -305,7 +305,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if capsule hits an matching object, otherwise false.
+ /// True if capsule hits a matching object, otherwise false.
API_FUNCTION() bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -320,7 +320,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if capsule hits an matching object, otherwise false.
+ /// True if capsule hits a matching object, otherwise false.
API_FUNCTION() bool CapsuleCastAll(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -334,7 +334,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if convex mesh hits an matching object, otherwise false.
+ /// True if convex mesh hits a matching object, otherwise false.
API_FUNCTION() bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -349,7 +349,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if convex mesh hits an matching object, otherwise false.
+ /// True if convex mesh hits a matching object, otherwise false.
API_FUNCTION() bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -364,7 +364,7 @@ public:
/// The maximum distance the ray should check for collisions.
/// The layer mask used to filter the results.
/// If set to true triggers will be hit, otherwise will skip them.
- /// True if convex mesh hits an matching object, otherwise false.
+ /// True if convex mesh hits a matching object, otherwise false.
API_FUNCTION() bool ConvexCastAll(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
@@ -413,7 +413,7 @@ public:
API_FUNCTION() bool CheckConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given box.
+ /// Finds all colliders touching or inside the given box.
///
/// The box center.
/// The half size of the box in each direction.
@@ -425,7 +425,7 @@ public:
API_FUNCTION() bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given sphere.
+ /// Finds all colliders touching or inside the given sphere.
///
/// The sphere center.
/// The radius of the sphere.
@@ -436,7 +436,7 @@ public:
API_FUNCTION() bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given capsule.
+ /// Finds all colliders touching or inside the given capsule.
///
/// The capsule center.
/// The radius of the capsule.
@@ -449,7 +449,7 @@ public:
API_FUNCTION() bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given convex mesh.
+ /// Finds all colliders touching or inside the given convex mesh.
///
/// The convex mesh center.
/// Collision data of the convex mesh.
@@ -462,7 +462,7 @@ public:
API_FUNCTION() bool OverlapConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given box.
+ /// Finds all colliders touching or inside the given box.
///
/// The box center.
/// The half size of the box in each direction.
@@ -474,7 +474,7 @@ public:
API_FUNCTION() bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given sphere.
+ /// Finds all colliders touching or inside the given sphere.
///
/// The sphere center.
/// The radius of the sphere.
@@ -485,7 +485,7 @@ public:
API_FUNCTION() bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given capsule.
+ /// Finds all colliders touching or inside the given capsule.
///
/// The capsule center.
/// The radius of the capsule.
@@ -498,7 +498,7 @@ public:
API_FUNCTION() bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true);
///
- /// Finds all colliders touching or inside of the given convex mesh.
+ /// Finds all colliders touching or inside the given convex mesh.
///
/// The convex mesh center.
/// Collision data of the convex mesh.
diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.cpp b/Source/Engine/Platform/Windows/WindowsPlatform.cpp
index 1f7683354..5fd6540ee 100644
--- a/Source/Engine/Platform/Windows/WindowsPlatform.cpp
+++ b/Source/Engine/Platform/Windows/WindowsPlatform.cpp
@@ -549,7 +549,6 @@ void WindowsPlatform::ReleaseMutex()
}
}
-PRAGMA_DISABLE_OPTIMIZATION;
void CheckInstructionSet()
{
#if PLATFORM_ARCH_X86 || PLATFORM_ARCH_X64
diff --git a/Source/Engine/Profiler/ProfilerMemory.cpp b/Source/Engine/Profiler/ProfilerMemory.cpp
index c936ff5b2..6b8f18ce3 100644
--- a/Source/Engine/Profiler/ProfilerMemory.cpp
+++ b/Source/Engine/Profiler/ProfilerMemory.cpp
@@ -243,6 +243,7 @@ void InitProfilerMemory(const Char* cmdLine, int32 stage)
#define INIT_PARENT(parent, child) GroupParents[(int32)ProfilerMemory::Groups::child] = (uint8)ProfilerMemory::Groups::parent
INIT_PARENT(Engine, EngineThreading);
INIT_PARENT(Engine, EngineDelegate);
+ INIT_PARENT(Engine, EngineDebug);
INIT_PARENT(Malloc, MallocArena);
INIT_PARENT(Graphics, GraphicsTextures);
INIT_PARENT(Graphics, GraphicsRenderTargets);
@@ -260,6 +261,8 @@ void InitProfilerMemory(const Char* cmdLine, int32 stage)
INIT_PARENT(Content, ContentFiles);
INIT_PARENT(Level, LevelFoliage);
INIT_PARENT(Level, LevelTerrain);
+ INIT_PARENT(Navigation, NavigationMesh);
+ INIT_PARENT(Navigation, NavigationBuilding);
INIT_PARENT(Scripting, ScriptingVisual);
INIT_PARENT(Scripting, ScriptingCSharp);
INIT_PARENT(ScriptingCSharp, ScriptingCSharpGCCommitted);
diff --git a/Source/Engine/Profiler/ProfilerMemory.h b/Source/Engine/Profiler/ProfilerMemory.h
index 5dddb912b..9177ae6e7 100644
--- a/Source/Engine/Profiler/ProfilerMemory.h
+++ b/Source/Engine/Profiler/ProfilerMemory.h
@@ -44,6 +44,8 @@ public:
EngineThreading,
// Memory used by Delegate (engine events system to store all references).
EngineDelegate,
+ // Memory used by debug tools (eg. DebugDraw, DebugCommands or DebugLog).
+ EngineDebug,
// Total graphics memory usage.
Graphics,
@@ -105,6 +107,10 @@ public:
// Total navigation system memory.
Navigation,
+ // Navigation mesh memory.
+ NavigationMesh,
+ // Navigation mesh builder memory.
+ NavigationBuilding,
// Total networking system memory.
Networking,
diff --git a/Source/Engine/Renderer/ColorGradingPass.cpp b/Source/Engine/Renderer/ColorGradingPass.cpp
index d6e164622..c0b40d3f6 100644
--- a/Source/Engine/Renderer/ColorGradingPass.cpp
+++ b/Source/Engine/Renderer/ColorGradingPass.cpp
@@ -37,8 +37,45 @@ GPU_CB_STRUCT(Data {
Float3 Dummy;
float LutWeight;
+
+ void Init(const PostProcessSettings& settings, GPUTexture*& lut)
+ {
+ Dummy = Float2::Zero;
+ auto& toneMapping = settings.ToneMapping;
+ auto& colorGrading = settings.ColorGrading;
+ // White Balance
+ WhiteTemp = toneMapping.WhiteTemperature;
+ WhiteTint = toneMapping.WhiteTint;
+ // Shadows
+ ColorSaturationShadows = colorGrading.ColorSaturationShadows * colorGrading.ColorSaturation;
+ ColorContrastShadows = colorGrading.ColorContrastShadows * colorGrading.ColorContrast;
+ ColorGammaShadows = colorGrading.ColorGammaShadows * colorGrading.ColorGamma;
+ ColorGainShadows = colorGrading.ColorGainShadows * colorGrading.ColorGain;
+ ColorOffsetShadows = colorGrading.ColorOffsetShadows + colorGrading.ColorOffset;
+ ColorCorrectionShadowsMax = colorGrading.ShadowsMax;
+ // Midtones
+ ColorSaturationMidtones = colorGrading.ColorSaturationMidtones * colorGrading.ColorSaturation;
+ ColorContrastMidtones = colorGrading.ColorContrastMidtones * colorGrading.ColorContrast;
+ ColorGammaMidtones = colorGrading.ColorGammaMidtones * colorGrading.ColorGamma;
+ ColorGainMidtones = colorGrading.ColorGainMidtones * colorGrading.ColorGain;
+ ColorOffsetMidtones = colorGrading.ColorOffsetMidtones + colorGrading.ColorOffset;
+ // Highlights
+ ColorSaturationHighlights = colorGrading.ColorSaturationHighlights * colorGrading.ColorSaturation;
+ ColorContrastHighlights = colorGrading.ColorContrastHighlights * colorGrading.ColorContrast;
+ ColorGammaHighlights = colorGrading.ColorGammaHighlights * colorGrading.ColorGamma;
+ ColorGainHighlights = colorGrading.ColorGainHighlights * colorGrading.ColorGain;
+ ColorOffsetHighlights = colorGrading.ColorOffsetHighlights + colorGrading.ColorOffset;
+ ColorCorrectionHighlightsMin = colorGrading.HighlightsMin;
+ //
+ Texture* lutTexture = colorGrading.LutTexture.Get();
+ const bool useLut = lutTexture && lutTexture->IsLoaded() && lutTexture->GetResidentMipLevels() > 0 && colorGrading.LutWeight > ZeroTolerance;
+ LutWeight = useLut ? colorGrading.LutWeight : 0.0f;
+ lut = useLut ? lutTexture->GetTexture() : nullptr;
+ }
});
+Data DefaultData;
+
// Custom render buffer for caching Color Grading LUT.
class ColorGradingCustomBuffer : public RenderBuffers::CustomBuffer
{
@@ -46,7 +83,7 @@ public:
GPUTexture* LUT = nullptr;
Data CachedData;
ToneMappingMode Mode = ToneMappingMode::None;
- Texture* LutTexture = nullptr;
+ GPUTexture* LutTexture = nullptr;
#if COMPILE_WITH_DEV_ENV
uint64 FrameRendered = 0;
#endif
@@ -82,6 +119,9 @@ bool ColorGradingPass::Init()
#if COMPILE_WITH_DEV_ENV
_shader.Get()->OnReloading.Bind(this);
#endif
+ PostProcessSettings defaultSettings;
+ GPUTexture* defaultLut;
+ DefaultData.Init(defaultSettings, defaultLut);
return false;
}
@@ -125,6 +165,18 @@ GPUTexture* ColorGradingPass::RenderLUT(RenderContext& renderContext)
{
PROFILE_CPU();
+ // Prepare the parameters
+ Data data;
+ GPUTexture* lutTexture;
+ auto& toneMapping = renderContext.List->Settings.ToneMapping;
+ data.Init(renderContext.List->Settings, lutTexture);
+
+ // Skip if color grading is unsued
+ if (Platform::MemoryCompare(&DefaultData, &data, sizeof(Data)) == 0 &&
+ lutTexture == nullptr &&
+ toneMapping.Mode == ToneMappingMode::None)
+ return nullptr;
+
// Check if can use volume texture (3D) for a LUT (faster on modern platforms, requires geometry shader)
const auto device = GPUDevice::Instance;
bool use3D = GPU_ALLOW_GEOMETRY_SHADERS && Graphics::PostProcessing::ColorGradingVolumeLUT;
@@ -172,41 +224,8 @@ GPUTexture* ColorGradingPass::RenderLUT(RenderContext& renderContext)
RENDER_TARGET_POOL_SET_NAME(colorGradingBuffer.LUT, "ColorGrading.LUT");
}
- // Prepare the parameters
- Data data;
- data.Dummy = Float2::Zero;
- auto& toneMapping = renderContext.List->Settings.ToneMapping;
- auto& colorGrading = renderContext.List->Settings.ColorGrading;
- // White Balance
- data.WhiteTemp = toneMapping.WhiteTemperature;
- data.WhiteTint = toneMapping.WhiteTint;
- // Shadows
- data.ColorSaturationShadows = colorGrading.ColorSaturationShadows * colorGrading.ColorSaturation;
- data.ColorContrastShadows = colorGrading.ColorContrastShadows * colorGrading.ColorContrast;
- data.ColorGammaShadows = colorGrading.ColorGammaShadows * colorGrading.ColorGamma;
- data.ColorGainShadows = colorGrading.ColorGainShadows * colorGrading.ColorGain;
- data.ColorOffsetShadows = colorGrading.ColorOffsetShadows + colorGrading.ColorOffset;
- data.ColorCorrectionShadowsMax = colorGrading.ShadowsMax;
- // Midtones
- data.ColorSaturationMidtones = colorGrading.ColorSaturationMidtones * colorGrading.ColorSaturation;
- data.ColorContrastMidtones = colorGrading.ColorContrastMidtones * colorGrading.ColorContrast;
- data.ColorGammaMidtones = colorGrading.ColorGammaMidtones * colorGrading.ColorGamma;
- data.ColorGainMidtones = colorGrading.ColorGainMidtones * colorGrading.ColorGain;
- data.ColorOffsetMidtones = colorGrading.ColorOffsetMidtones + colorGrading.ColorOffset;
- // Highlights
- data.ColorSaturationHighlights = colorGrading.ColorSaturationHighlights * colorGrading.ColorSaturation;
- data.ColorContrastHighlights = colorGrading.ColorContrastHighlights * colorGrading.ColorContrast;
- data.ColorGammaHighlights = colorGrading.ColorGammaHighlights * colorGrading.ColorGamma;
- data.ColorGainHighlights = colorGrading.ColorGainHighlights * colorGrading.ColorGain;
- data.ColorOffsetHighlights = colorGrading.ColorOffsetHighlights + colorGrading.ColorOffset;
- data.ColorCorrectionHighlightsMin = colorGrading.HighlightsMin;
- //
- Texture* lutTexture = colorGrading.LutTexture.Get();
- const bool useLut = lutTexture && lutTexture->IsLoaded() && lutTexture->GetResidentMipLevels() > 0 && colorGrading.LutWeight > ZeroTolerance;
- data.LutWeight = useLut ? colorGrading.LutWeight : 0.0f;
-
// Check if LUT parameter hasn't been changed since the last time
- if (Platform::MemoryCompare(&colorGradingBuffer.CachedData , &data, sizeof(Data)) == 0 &&
+ if (Platform::MemoryCompare(&colorGradingBuffer.CachedData, &data, sizeof(Data)) == 0 &&
colorGradingBuffer.Mode == toneMapping.Mode &&
#if COMPILE_WITH_DEV_ENV
colorGradingBuffer.FrameRendered > _reloadedFrame &&
@@ -232,7 +251,7 @@ GPUTexture* ColorGradingPass::RenderLUT(RenderContext& renderContext)
context->BindCB(0, cb);
context->SetViewportAndScissors((float)lutDesc.Width, (float)lutDesc.Height);
context->SetState(_psLut.Get((int32)toneMapping.Mode));
- context->BindSR(0, useLut ? lutTexture->GetTexture() : nullptr);
+ context->BindSR(0, lutTexture);
#if GPU_ALLOW_GEOMETRY_SHADERS
if (use3D)
{
diff --git a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
index a954cf31f..25550ecd8 100644
--- a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
+++ b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
@@ -11,6 +11,7 @@
#include "Engine/Core/Math/Quaternion.h"
#include "Engine/Core/Config/GraphicsSettings.h"
#include "Engine/Engine/Engine.h"
+#include "Engine/Engine/Units.h"
#include "Engine/Content/Content.h"
#include "Engine/Debug/DebugDraw.h"
#include "Engine/Graphics/GPUContext.h"
@@ -41,6 +42,7 @@
#define DDGI_PROBE_RESOLUTION_DISTANCE 14 // Resolution (in texels) for probe distance data (excluding 1px padding on each side)
#define DDGI_PROBE_UPDATE_BORDERS_GROUP_SIZE 8
#define DDGI_PROBE_CLASSIFY_GROUP_SIZE 32
+#define DDGI_PROBE_EMPTY_AREA_DENSITY 8 // Spacing (in probe grid) between fallback probes placed into empty areas to provide valid GI for nearby dynamic objects or transparency
#define DDGI_DEBUG_STATS 0 // Enables additional GPU-driven stats for probe/rays count
#define DDGI_DEBUG_INSTABILITY 0 // Enables additional probe irradiance instability debugging
@@ -68,11 +70,14 @@ GPU_CB_STRUCT(Data0 {
Int4 ProbeScrollClears[4];
Float3 ViewDir;
float Padding1;
+ Float3 QuantizationError;
+ int32 FrameIndexMod8;
});
GPU_CB_STRUCT(Data1 {
// TODO: use push constants on Vulkan or root signature data on DX12 to reduce overhead of changing single DWORD
- Float2 Padding2;
+ float Padding2;
+ int32 StepSize;
uint32 CascadeIndex;
uint32 ProbeIndexOffset;
});
@@ -214,6 +219,7 @@ bool DynamicDiffuseGlobalIlluminationPass::setupResources()
return true;
_csClassify = shader->GetCS("CS_Classify");
_csUpdateProbesInitArgs = shader->GetCS("CS_UpdateProbesInitArgs");
+ _csUpdateInactiveProbes = shader->GetCS("CS_UpdateInactiveProbes");
_csTraceRays[0] = shader->GetCS("CS_TraceRays", 0);
_csTraceRays[1] = shader->GetCS("CS_TraceRays", 1);
_csTraceRays[2] = shader->GetCS("CS_TraceRays", 2);
@@ -245,6 +251,7 @@ void DynamicDiffuseGlobalIlluminationPass::OnShaderReloading(Asset* obj)
LastFrameShaderReload = Engine::FrameCount;
_csClassify = nullptr;
_csUpdateProbesInitArgs = nullptr;
+ _csUpdateInactiveProbes = nullptr;
_csTraceRays[0] = nullptr;
_csTraceRays[1] = nullptr;
_csTraceRays[2] = nullptr;
@@ -322,7 +329,6 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
const float indirectLightingIntensity = settings.Intensity;
const float probeHistoryWeight = Math::Clamp(settings.TemporalResponse, 0.0f, 0.98f);
const float distance = settings.Distance;
- const Color fallbackIrradiance = settings.FallbackIrradiance;
// Automatically calculate amount of cascades to cover the GI distance at the current probes spacing
const int32 idealProbesCount = 20; // Ideal amount of probes per-cascade to try to fit in order to cover whole distance
@@ -335,7 +341,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
}
// Calculate the probes count based on the amount of cascades and the distance to cover
- const float cascadesDistanceScales[] = { 1.0f, 3.0f, 6.0f, 10.0f }; // Scales each cascade further away from the camera origin
+ const float cascadesDistanceScales[] = { 1.0f, 3.0f, 5.0f, 10.0f }; // Scales each cascade further away from the camera origin
const float distanceExtent = distance / cascadesDistanceScales[cascadesCount - 1];
const float verticalRangeScale = 0.8f; // Scales the probes volume size at Y axis (horizontal aspect ratio makes the DDGI use less probes vertically to cover whole screen)
Int3 probesCounts(Float3::Ceil(Float3(distanceExtent, distanceExtent * verticalRangeScale, distanceExtent) / probesSpacing));
@@ -351,6 +357,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
// Initialize cascades
float probesSpacings[4];
Float3 viewOrigins[4];
+ Float3 blendOrigins[4];
for (int32 cascadeIndex = 0; cascadeIndex < cascadesCount; cascadeIndex++)
{
// Each cascade has higher spacing between probes
@@ -361,14 +368,15 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
// Calculate view origin for cascade by shifting it towards the view direction to account for better view frustum coverage
Float3 viewOrigin = renderContext.View.Position;
Float3 viewDirection = renderContext.View.Direction;
- const Float3 probesDistance = Float3(probesCounts) * cascadeProbesSpacing;
+ const Float3 probesDistance = Float3(probesCounts - 1) * cascadeProbesSpacing;
const float probesDistanceMax = probesDistance.MaxValue();
const Float3 viewRayHit = CollisionsHelper::LineHitsBox(viewOrigin, viewOrigin + viewDirection * (probesDistanceMax * 2.0f), viewOrigin - probesDistance, viewOrigin + probesDistance);
const float viewOriginOffset = viewRayHit.Y * probesDistanceMax * 0.6f;
viewOrigin += viewDirection * viewOriginOffset;
+ //viewOrigin = Float3::Zero;
+ blendOrigins[cascadeIndex] = viewOrigin;
const float viewOriginSnapping = cascadeProbesSpacing;
viewOrigin = Float3::Floor(viewOrigin / viewOriginSnapping) * viewOriginSnapping;
- //viewOrigin = Float3::Zero;
viewOrigins[cascadeIndex] = viewOrigin;
}
@@ -500,6 +508,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
{
auto& cascade = ddgiData.Cascades[cascadeIndex];
ddgiData.Result.Constants.ProbesOriginAndSpacing[cascadeIndex] = Float4(cascade.ProbesOrigin, cascade.ProbesSpacing);
+ ddgiData.Result.Constants.BlendOrigin[cascadeIndex] = Float4(blendOrigins[cascadeIndex], 0.0f);
ddgiData.Result.Constants.ProbesScrollOffsets[cascadeIndex] = Int4(cascade.ProbeScrollOffsets, 0);
}
ddgiData.Result.Constants.RayMaxDistance = distance;
@@ -508,7 +517,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
ddgiData.Result.Constants.ProbeHistoryWeight = probeHistoryWeight;
ddgiData.Result.Constants.IrradianceGamma = 1.5f;
ddgiData.Result.Constants.IndirectLightingIntensity = indirectLightingIntensity;
- ddgiData.Result.Constants.FallbackIrradiance = fallbackIrradiance.ToFloat3() * fallbackIrradiance.A;
+ ddgiData.Result.Constants.FallbackIrradiance = settings.FallbackIrradiance.ToFloat4();
ddgiData.Result.ProbesData = ddgiData.ProbesData->View();
ddgiData.Result.ProbesDistance = ddgiData.ProbesDistance->View();
ddgiData.Result.ProbesIrradiance = ddgiData.ProbesIrradiance->View();
@@ -535,6 +544,8 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
data.TemporalTime = renderContext.List->Setup.UseTemporalAAJitter ? RenderTools::ComputeTemporalTime() : 0.0f;
data.ViewDir = renderContext.View.Direction;
data.SkyboxIntensity = renderContext.List->Sky ? renderContext.List->Sky->GetIndirectLightingIntensity() : 1.0f;
+ data.QuantizationError = RenderTools::GetColorQuantizationError(ddgiData.ProbesIrradiance->Format());
+ data.FrameIndexMod8 = (int32)(Engine::FrameCount % 8);
GBufferPass::SetInputs(renderContext.View, data.GBuffer);
context->UpdateCB(_cb0, &data);
context->BindCB(0, _cb0);
@@ -581,6 +592,23 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
context->ResetUA();
}
+ // For inactive probes, search nearby ones to find the closest valid for quick fallback when sampling irradiance
+ {
+ PROFILE_GPU_CPU_NAMED("Update Inactive Probes");
+ // TODO: this could run within GPUComputePass during Trace Rays or Update Probes to overlap compute works
+ context->BindUA(0, ddgiData.Result.ProbesData);
+ Data1 data;
+ data.CascadeIndex = cascadeIndex;
+ int32 iterations = Math::CeilToInt(Math::Log2((float)Math::Min(probesCounts.MaxValue(), DDGI_PROBE_EMPTY_AREA_DENSITY) + 1.0f));
+ for (int32 i = iterations - 1; i >= 0; i--)
+ {
+ data.StepSize = Math::FloorToInt(Math::Pow(2, (float)i) + 0.5f); // Jump Flood step size
+ context->UpdateCB(_cb1, &data);
+ context->Dispatch(_csUpdateInactiveProbes, threadGroupsX, 1, 1);
+ }
+ context->ResetUA();
+ }
+
// Update probes in batches so ProbesTrace texture can be smaller
uint32 arg = 0;
// TODO: use rays allocator to dispatch raytracing in packets (eg. 8 threads in a group instead of hardcoded limit)
diff --git a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h
index e6ace0373..5953da887 100644
--- a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h
+++ b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h
@@ -15,7 +15,8 @@ public:
// Constant buffer data for DDGI access on a GPU.
GPU_CB_STRUCT(ConstantsData {
Float4 ProbesOriginAndSpacing[4];
- Int4 ProbesScrollOffsets[4];
+ Float4 BlendOrigin[4]; // w is unused
+ Int4 ProbesScrollOffsets[4]; // w is unused
uint32 ProbesCounts[3];
uint32 CascadesCount;
float IrradianceGamma;
@@ -24,8 +25,7 @@ public:
float IndirectLightingIntensity;
Float3 ViewPos;
uint32 RaysCount;
- Float3 FallbackIrradiance;
- float Padding0;
+ Float4 FallbackIrradiance;
});
// Binding data for the GPU.
@@ -44,6 +44,7 @@ private:
GPUConstantBuffer* _cb1 = nullptr;
GPUShaderProgramCS* _csClassify;
GPUShaderProgramCS* _csUpdateProbesInitArgs;
+ GPUShaderProgramCS* _csUpdateInactiveProbes;
GPUShaderProgramCS* _csTraceRays[4];
GPUShaderProgramCS* _csUpdateProbesIrradiance;
GPUShaderProgramCS* _csUpdateProbesDistance;
diff --git a/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp b/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp
index ce0ec1881..7216a8fa8 100644
--- a/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp
+++ b/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp
@@ -428,6 +428,7 @@ public:
// Write to objects buffer (this must match unpacking logic in HLSL)
uint32 objectAddress = ObjectsBuffer.Data.Count() / sizeof(Float4);
ObjectsListBuffer.Write(objectAddress);
+ ObjectsBuffer.Data.EnsureCapacity(ObjectsBuffer.Data.Count() + sizeof(Float4) * (GLOBAL_SURFACE_ATLAS_OBJECT_DATA_STRIDE + 6 * GLOBAL_SURFACE_ATLAS_TILE_DATA_STRIDE));
auto* objectData = ObjectsBuffer.WriteReserve(GLOBAL_SURFACE_ATLAS_OBJECT_DATA_STRIDE);
objectData[0] = Float4(object.Position, object.Radius);
objectData[1] = Float4::Zero;
@@ -511,6 +512,7 @@ public:
{
// Dirty object to redraw
object->LastFrameUpdated = 0;
+ return;
}
GlobalSurfaceAtlasLight* light = Lights.TryGet(a->GetID());
if (light)
diff --git a/Source/Engine/Renderer/PostProcessingPass.cpp b/Source/Engine/Renderer/PostProcessingPass.cpp
index 030541e4c..5ac204523 100644
--- a/Source/Engine/Renderer/PostProcessingPass.cpp
+++ b/Source/Engine/Renderer/PostProcessingPass.cpp
@@ -269,7 +269,7 @@ void PostProcessingPass::Render(RenderContext& renderContext, GPUTexture* input,
int32 bloomMipCount = CalculateBloomMipCount(w1, h1);
// Ensure to have valid data and if at least one effect should be applied
- if (!(useBloom || useToneMapping || useCameraArtifacts) || checkIfSkipPass() || w8 <= 1 || h8 <= 1)
+ if (!(useBloom || useToneMapping || useCameraArtifacts || colorGradingLUT) || checkIfSkipPass() || w8 <= 1 || h8 <= 1)
{
// Resources are missing. Do not perform rendering. Just copy raw frame
context->SetViewportAndScissors((float)output->Width(), (float)output->Height());
diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp
index 7a72cd923..fd7d43c8b 100644
--- a/Source/Engine/Renderer/Renderer.cpp
+++ b/Source/Engine/Renderer/Renderer.cpp
@@ -402,6 +402,8 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
case ViewMode::MaterialComplexity:
case ViewMode::Wireframe:
case ViewMode::NoPostFx:
+ case ViewMode::VertexColors:
+ case ViewMode::QuadOverdraw:
setup.UseTemporalAAJitter = false;
break;
}
diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs
index 7f9f2980c..229e411f3 100644
--- a/Source/Engine/Scripting/Scripting.cs
+++ b/Source/Engine/Scripting/Scripting.cs
@@ -137,8 +137,8 @@ namespace FlaxEngine
{
Debug.LogError($"Unhandled Exception: {exception.Message}");
Debug.LogException(exception);
- if (e.IsTerminating && !System.Diagnostics.Debugger.IsAttached)
- Platform.Fatal($"Unhandled Exception: {exception}");
+ //if (e.IsTerminating && !System.Diagnostics.Debugger.IsAttached)
+ // Platform.Fatal($"Unhandled Exception: {exception}");
}
}
diff --git a/Source/Engine/Threading/Task.cpp b/Source/Engine/Threading/Task.cpp
index a640019d1..cef08b0bc 100644
--- a/Source/Engine/Threading/Task.cpp
+++ b/Source/Engine/Threading/Task.cpp
@@ -148,9 +148,8 @@ Task* Task::StartNew(Function::Signature& action, Object* target)
void Task::Execute()
{
- if (IsCanceled())
+ if (!IsQueued())
return;
- ASSERT(IsQueued());
SetState(TaskState::Running);
// Perform an operation
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
index c48a4c569..7978b4f9e 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
@@ -534,7 +534,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
}
// Sample Texture
case 9:
- // Procedural Texture Sample
+ // Procedural Sample Texture
case 17:
{
// Get input boxes
@@ -739,7 +739,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
const int32 samplerIndex = node->Values.Count() >= 4 ? node->Values[3].AsInt : LinearWrap;
if (samplerIndex == TextureGroup)
{
- auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[3].AsInt);
+ auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[5].AsInt);
samplerName = *textureGroupSampler.ShaderName;
}
else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames))
@@ -828,7 +828,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
const int32 samplerIndex = node->Values[3].AsInt;
if (samplerIndex == TextureGroup)
{
- auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[3].AsInt);
+ auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[5].AsInt);
samplerName = *textureGroupSampler.ShaderName;
}
else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames))
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp
index 843822b98..57afeb7a5 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp
@@ -567,6 +567,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(CalculateBoneOffsetMatrices);
SERIALIZE(LightmapUVsSource);
SERIALIZE(CollisionMeshesPrefix);
+ SERIALIZE(CollisionMeshesPostfix);
SERIALIZE(CollisionType);
SERIALIZE(PositionFormat);
SERIALIZE(TexCoordFormat);
@@ -621,6 +622,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(CalculateBoneOffsetMatrices);
DESERIALIZE(LightmapUVsSource);
DESERIALIZE(CollisionMeshesPrefix);
+ DESERIALIZE(CollisionMeshesPostfix);
DESERIALIZE(CollisionType);
DESERIALIZE(PositionFormat);
DESERIALIZE(TexCoordFormat);
@@ -1830,7 +1832,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
// Collision mesh output
- if (options.CollisionMeshesPrefix.HasChars())
+ if (options.CollisionMeshesPrefix.HasChars() || options.CollisionMeshesPostfix.HasChars())
{
// Extract collision meshes from the model
ModelData collisionModel;
@@ -1839,7 +1841,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
for (int32 i = lod.Meshes.Count() - 1; i >= 0; i--)
{
auto mesh = lod.Meshes[i];
- if (mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase))
+ if ((options.CollisionMeshesPrefix.HasChars() && mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase)) ||
+ (options.CollisionMeshesPostfix.HasChars() && mesh->Name.EndsWith(options.CollisionMeshesPostfix, StringSearchCase::IgnoreCase)))
{
// Remove material slot used by this mesh (if no other mesh else uses it)
int32 materialSlotUsageCount = 0;
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h
index d7545b92e..bc96e8308 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.h
+++ b/Source/Engine/Tools/ModelTool/ModelTool.h
@@ -221,6 +221,9 @@ public:
// If specified, all meshes that name starts with this prefix in the name will be imported as a separate collision data asset (excluded used for rendering).
API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))")
String CollisionMeshesPrefix = TEXT("");
+ // If specified, all meshes that name ends with this postfix in the name will be imported as a separate collision data asset (excluded used for rendering).
+ API_FIELD(Attributes="EditorOrder(101), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))")
+ String CollisionMeshesPostfix = TEXT("");
// The type of collision that should be generated if the mesh has a collision prefix specified.
API_FIELD(Attributes="EditorOrder(105), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))")
CollisionDataType CollisionType = CollisionDataType::ConvexMesh;
diff --git a/Source/Engine/UI/GUI/CanvasScaler.cs b/Source/Engine/UI/GUI/CanvasScaler.cs
index 6bd18ea51..1e30fd22f 100644
--- a/Source/Engine/UI/GUI/CanvasScaler.cs
+++ b/Source/Engine/UI/GUI/CanvasScaler.cs
@@ -449,8 +449,7 @@ namespace FlaxEngine.GUI
///
public override bool RayCast(ref Float2 location, out Control hit)
{
- var p = location / _scale;
- if (RayCastChildren(ref p, out hit))
+ if (RayCastChildren(ref location, out hit))
return true;
return base.RayCast(ref location, out hit);
}
diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
index 20ef1c401..bb6ee22a5 100644
--- a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
+++ b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
@@ -143,6 +143,40 @@ namespace FlaxEngine.GUI
context.Caret.X = 0;
OnLineAdded(ref context, _text.Length - 1);
}
+
+ // Organize lines vertically
+ if (_textBlocks.Count != 0)
+ {
+ var lastBlock = _textBlocks[_textBlocks.Count - 1];
+
+ // Get style (global or leftover from style stack or the last lime)
+ var verticalAlignments = _textStyle.Alignment;
+ if (context.StyleStack.Count > 1)
+ verticalAlignments = context.StyleStack.Peek().Alignment;
+ else if ((lastBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask) != TextBlockStyle.Alignments.Baseline)
+ verticalAlignments = lastBlock.Style.Alignment;
+
+ var totalSize = lastBlock.Bounds.BottomRight;
+ var sizeOffset = Size - totalSize;
+ var textBlocks = CollectionsMarshal.AsSpan(_textBlocks);
+ if ((verticalAlignments & TextBlockStyle.Alignments.Middle) == TextBlockStyle.Alignments.Middle)
+ {
+ sizeOffset.Y *= 0.5f;
+ for (int i = 0; i < _textBlocks.Count; i++)
+ {
+ ref TextBlock textBlock = ref textBlocks[i];
+ textBlock.Bounds.Location.Y += sizeOffset.Y;
+ }
+ }
+ else if ((verticalAlignments & TextBlockStyle.Alignments.Bottom) == TextBlockStyle.Alignments.Bottom)
+ {
+ for (int i = 0; i < _textBlocks.Count; i++)
+ {
+ ref TextBlock textBlock = ref textBlocks[i];
+ textBlock.Bounds.Location.Y += sizeOffset.Y;
+ }
+ }
+ }
}
///
@@ -239,14 +273,15 @@ namespace FlaxEngine.GUI
}
// Organize text blocks within line
- var horizontalAlignments = TextBlockStyle.Alignments.Baseline;
- var verticalAlignments = TextBlockStyle.Alignments.Baseline;
+ var lineAlignments = TextBlockStyle.Alignments.Baseline;
for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++)
{
ref TextBlock textBlock = ref textBlocks[i];
var vOffset = lineSize.Y - textBlock.Bounds.Height;
- horizontalAlignments |= textBlock.Style.Alignment & TextBlockStyle.Alignments.HorizontalMask;
- verticalAlignments |= textBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask;
+ if (i == context.LineStartTextBlockIndex)
+ lineAlignments = textBlock.Style.Alignment;
+ else
+ lineAlignments &= textBlock.Style.Alignment;
switch (textBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask)
{
case TextBlockStyle.Alignments.Baseline:
@@ -275,9 +310,9 @@ namespace FlaxEngine.GUI
}
}
- // Organize blocks within whole container
+ // Organize whole line horizontally
var sizeOffset = Size - lineSize;
- if ((horizontalAlignments & TextBlockStyle.Alignments.Center) == TextBlockStyle.Alignments.Center)
+ if ((lineAlignments & TextBlockStyle.Alignments.Center) == TextBlockStyle.Alignments.Center)
{
sizeOffset.X *= 0.5f;
for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++)
@@ -286,7 +321,7 @@ namespace FlaxEngine.GUI
textBlock.Bounds.Location.X += sizeOffset.X;
}
}
- else if ((horizontalAlignments & TextBlockStyle.Alignments.Right) == TextBlockStyle.Alignments.Right)
+ else if ((lineAlignments & TextBlockStyle.Alignments.Right) == TextBlockStyle.Alignments.Right)
{
for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++)
{
@@ -294,23 +329,6 @@ namespace FlaxEngine.GUI
textBlock.Bounds.Location.X += sizeOffset.X;
}
}
- if ((verticalAlignments & TextBlockStyle.Alignments.Middle) == TextBlockStyle.Alignments.Middle)
- {
- sizeOffset.Y *= 0.5f;
- for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++)
- {
- ref TextBlock textBlock = ref textBlocks[i];
- textBlock.Bounds.Location.Y += sizeOffset.Y;
- }
- }
- else if ((verticalAlignments & TextBlockStyle.Alignments.Bottom) == TextBlockStyle.Alignments.Bottom)
- {
- for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++)
- {
- ref TextBlock textBlock = ref textBlocks[i];
- textBlock.Bounds.Location.Y += sizeOffset.Y;
- }
- }
// Move to the next line
context.LineStartCharacterIndex = lineEnd + 1;
diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs
index b57fac47d..3bb99762f 100644
--- a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs
+++ b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs
@@ -175,7 +175,7 @@ namespace FlaxEngine.GUI
// Setup size
var font = imageBlock.Style.Font.GetFont();
if (font)
- imageBlock.Bounds.Size = new Float2(font.Height);
+ imageBlock.Bounds.Size = new Float2(font.Ascender);
var imageSize = image.Size;
imageBlock.Bounds.Size.X *= imageSize.X / imageSize.Y; // Keep original aspect ratio
bool hasWidth = TryParseNumberTag(ref tag, "width", imageBlock.Bounds.Width, out var width);
@@ -215,16 +215,16 @@ namespace FlaxEngine.GUI
switch (valign)
{
case "top":
- style.Alignment = TextBlockStyle.Alignments.Top;
+ style.Alignment |= TextBlockStyle.Alignments.Top;
break;
case "bottom":
- style.Alignment = TextBlockStyle.Alignments.Bottom;
+ style.Alignment |= TextBlockStyle.Alignments.Bottom;
break;
case "middle":
- style.Alignment = TextBlockStyle.Alignments.Middle;
+ style.Alignment |= TextBlockStyle.Alignments.Middle;
break;
case "baseline":
- style.Alignment = TextBlockStyle.Alignments.Baseline;
+ style.Alignment |= TextBlockStyle.Alignments.Baseline;
break;
}
}
@@ -243,17 +243,17 @@ namespace FlaxEngine.GUI
var style = context.StyleStack.Peek();
if (tag.Attributes.TryGetValue(string.Empty, out var valign))
{
- style.Alignment &= ~TextBlockStyle.Alignments.VerticalMask;
+ style.Alignment &= ~TextBlockStyle.Alignments.HorizontalMask;
switch (valign)
{
case "left":
- style.Alignment = TextBlockStyle.Alignments.Left;
+ style.Alignment |= TextBlockStyle.Alignments.Left;
break;
case "right":
- style.Alignment = TextBlockStyle.Alignments.Right;
+ style.Alignment |= TextBlockStyle.Alignments.Right;
break;
case "center":
- style.Alignment = TextBlockStyle.Alignments.Center;
+ style.Alignment |= TextBlockStyle.Alignments.Center;
break;
}
}
@@ -270,7 +270,8 @@ namespace FlaxEngine.GUI
else
{
var style = context.StyleStack.Peek();
- style.Alignment = TextBlockStyle.Alignments.Center;
+ style.Alignment &= ~TextBlockStyle.Alignments.HorizontalMask;
+ style.Alignment |= TextBlockStyle.Alignments.Center;
context.StyleStack.Push(style);
}
}
diff --git a/Source/Engine/UI/GUI/Panels/DropPanel.cs b/Source/Engine/UI/GUI/Panels/DropPanel.cs
index de80f9fc5..308272218 100644
--- a/Source/Engine/UI/GUI/Panels/DropPanel.cs
+++ b/Source/Engine/UI/GUI/Panels/DropPanel.cs
@@ -11,6 +11,11 @@ namespace FlaxEngine.GUI
[ActorToolbox("GUI")]
public class DropPanel : ContainerControl
{
+ ///
+ /// Size of the drop down icon.
+ ///
+ public const float DropDownIconSize = 14.0f;
+
///
/// The header height.
///
@@ -368,7 +373,7 @@ namespace FlaxEngine.GUI
var style = Style.Current;
var enabled = EnabledInHierarchy;
- // Paint Background
+ // Draw Background
var backgroundColor = BackgroundColor;
if (backgroundColor.A > 0.0f)
{
@@ -386,7 +391,7 @@ namespace FlaxEngine.GUI
float textLeft = 0;
if (EnableDropDownIcon)
{
- textLeft += 14;
+ textLeft += DropDownIconSize;
var dropDownRect = new Rectangle(2, (HeaderHeight - 12) / 2, 12, 12);
var arrowColor = _mouseOverHeader ? style.Foreground : style.ForegroundGrey;
if (_isClosed)
@@ -395,7 +400,7 @@ namespace FlaxEngine.GUI
ArrowImageOpened?.Draw(dropDownRect, arrowColor);
}
- // Text
+ // Header text
var textRect = new Rectangle(textLeft, 0, Width - textLeft, HeaderHeight);
_headerTextMargin.ShrinkRectangle(ref textRect);
var textColor = HeaderTextColor;
@@ -404,7 +409,9 @@ namespace FlaxEngine.GUI
textColor *= 0.6f;
}
+ Render2D.PushClip(textRect);
Render2D.DrawText(HeaderTextFont.GetFont(), HeaderTextMaterial, HeaderText, textRect, textColor, TextAlignment.Near, TextAlignment.Center);
+ Render2D.PopClip();
if (!_isClosed && EnableContainmentLines)
{
diff --git a/Source/Shaders/GI/DDGI.hlsl b/Source/Shaders/GI/DDGI.hlsl
index c116b597a..b88b846a6 100644
--- a/Source/Shaders/GI/DDGI.hlsl
+++ b/Source/Shaders/GI/DDGI.hlsl
@@ -20,17 +20,23 @@
#define DDGI_PROBE_ATTENTION_MAX 0.98f // Maximum probe attention value that still makes it active (but not activated which is 1.0f).
#define DDGI_PROBE_RESOLUTION_IRRADIANCE 6 // Resolution (in texels) for probe irradiance data (excluding 1px padding on each side)
#define DDGI_PROBE_RESOLUTION_DISTANCE 14 // Resolution (in texels) for probe distance data (excluding 1px padding on each side)
-#define DDGI_CASCADE_BLEND_SIZE 2.5f // Distance in probes over which cascades blending happens
+#define DDGI_CASCADE_BLEND_SIZE 2.0f // Distance in probes over which cascades blending happens
#ifndef DDGI_CASCADE_BLEND_SMOOTH
#define DDGI_CASCADE_BLEND_SMOOTH 0 // Enables smooth cascade blending, otherwise dithering will be used
#endif
#define DDGI_SRGB_BLENDING 1 // Enables blending in sRGB color space, otherwise irradiance blending is done in linear space
+#define DDGI_DEFAULT_BIAS 0.2f // Default value for DDGI sampling bias
+#define DDGI_FALLBACK_COORDS_ENCODE(coord) ((float3)(coord + 1) / 128.0f)
+#define DDGI_FALLBACK_COORDS_DECODE(data) (uint3)(data.xyz * 128.0f - 1)
+#define DDGI_FALLBACK_COORDS_VALID(data) (length(data.xyz) > 0)
+//#define DDGI_DEBUG_CASCADE 0 // Forces a specific cascade to be only in use (for debugging)
// DDGI data for a constant buffer
struct DDGIData
{
float4 ProbesOriginAndSpacing[4];
- int4 ProbesScrollOffsets[4]; // w unused
+ float4 BlendOrigin[4]; // w is unused
+ int4 ProbesScrollOffsets[4]; // w is unused
uint3 ProbesCounts;
uint CascadesCount;
float IrradianceGamma;
@@ -39,8 +45,7 @@ struct DDGIData
float IndirectLightingIntensity;
float3 ViewPos;
uint RaysCount;
- float3 FallbackIrradiance;
- float Padding0;
+ float4 FallbackIrradiance;
};
uint GetDDGIProbeIndex(DDGIData data, uint3 probeCoords)
@@ -159,6 +164,8 @@ float2 GetDDGIProbeUV(DDGIData data, uint cascadeIndex, uint probeIndex, float2
float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probesData, Texture2D probesDistance, Texture2D probesIrradiance, float3 worldPosition, float3 worldNormal, uint cascadeIndex, float3 probesOrigin, float3 probesExtent, float probesSpacing, float3 biasedWorldPosition)
{
+ bool invalidCascade = cascadeIndex >= data.CascadesCount;
+ cascadeIndex = min(cascadeIndex, data.CascadesCount - 1);
uint3 probeCoordsEnd = data.ProbesCounts - uint3(1, 1, 1);
uint3 baseProbeCoords = clamp(uint3((worldPosition - probesOrigin + probesExtent) / probesSpacing), uint3(0, 0, 0), probeCoordsEnd);
@@ -168,7 +175,6 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes
// Loop over the closest probes to accumulate their contributions
float4 irradiance = float4(0, 0, 0, 0);
- const int3 SearchAxisMasks[3] = { int3(1, 0, 0), int3(0, 1, 0), int3(0, 0, 1) };
for (uint i = 0; i < 8; i++)
{
uint3 probeCoordsOffset = uint3(i, i >> 1, i >> 2) & 1;
@@ -178,33 +184,23 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes
// Load probe position and state
float4 probeData = LoadDDGIProbeData(data, probesData, cascadeIndex, probeIndex);
uint probeState = DecodeDDGIProbeState(probeData);
+ uint useVisibility = true;
+ float minWight = 0.000001f;
if (probeState == DDGI_PROBE_STATE_INACTIVE)
{
- // Search nearby probes to find any nearby GI sample
- for (int searchDistance = 1; searchDistance < 3 && probeState == DDGI_PROBE_STATE_INACTIVE; searchDistance++)
- for (uint searchAxis = 0; searchAxis < 3; searchAxis++)
- {
- int searchAxisDir = probeCoordsOffset[searchAxis] ? 1 : -1;
- int3 searchCoordsOffset = SearchAxisMasks[searchAxis] * searchAxisDir * searchDistance;
- uint3 searchCoords = clamp((int3)probeCoords + searchCoordsOffset, int3(0, 0, 0), (int3)probeCoordsEnd);
- uint searchIndex = GetDDGIScrollingProbeIndex(data, cascadeIndex, searchCoords);
- float4 searchData = LoadDDGIProbeData(data, probesData, cascadeIndex, searchIndex);
- uint searchState = DecodeDDGIProbeState(searchData);
- if (searchState != DDGI_PROBE_STATE_INACTIVE)
- {
- // Use nearby probe as a fallback (visibility test might ignore it but with smooth gradient)
- probeCoords = searchCoords;
- probeIndex = searchIndex;
- probeData = searchData;
- probeState = searchState;
- break;
- }
- }
- if (probeState == DDGI_PROBE_STATE_INACTIVE)
- continue;
+ // Use fallback probe that is closest to this one
+ uint3 fallbackCoords = DDGI_FALLBACK_COORDS_DECODE(probeData);
+ float fallbackToProbeDist = length((float3)probeCoords - (float3)fallbackCoords);
+ useVisibility = fallbackToProbeDist <= 1.0f; // Skip visibility test that blocks too far probes due to limiting max distance to 1.5 of probe spacing
+ if (fallbackToProbeDist > 2.0f) minWight = 1.0f;
+ probeCoords = fallbackCoords;
+ probeIndex = GetDDGIScrollingProbeIndex(data, cascadeIndex, fallbackCoords);
+ probeData = LoadDDGIProbeData(data, probesData, cascadeIndex, probeIndex);
+ //if (DecodeDDGIProbeState(probeData) == DDGI_PROBE_STATE_INACTIVE) continue;
}
- float3 probeBasePosition = baseProbeWorldPosition + ((probeCoords - baseProbeCoords) * probesSpacing);
- float3 probePosition = probeBasePosition + probeData.xyz * probesSpacing; // Probe offset is [-1;1] within probes spacing
+
+ // Calculate probe position
+ float3 probePosition = baseProbeWorldPosition + (((float3)probeCoords - (float3)baseProbeCoords) * probesSpacing) + probeData.xyz * probesSpacing;
// Calculate the distance and direction from the (biased and non-biased) shading point and the probe
float3 worldPosToProbe = normalize(probePosition - worldPosition);
@@ -213,6 +209,7 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes
// Smooth backface test
float weight = Square(dot(worldPosToProbe, worldNormal) * 0.5f + 0.5f);
+ weight = max(weight, 0.1f);
// Sample distance texture
float2 octahedralCoords = GetOctahedralCoords(-biasedPosToProbe);
@@ -220,24 +217,23 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes
float2 probeDistance = probesDistance.SampleLevel(SamplerLinearClamp, uv, 0).rg * 2.0f;
// Visibility weight (Chebyshev)
- if (biasedPosToProbeDist > probeDistance.x)
+ if (biasedPosToProbeDist > probeDistance.x && useVisibility)
{
float variance = abs(Square(probeDistance.x) - probeDistance.y);
float visibilityWeight = variance / (variance + Square(biasedPosToProbeDist - probeDistance.x));
- weight *= max(visibilityWeight * visibilityWeight * visibilityWeight, 0.05f);
+ weight *= max(visibilityWeight * visibilityWeight * visibilityWeight, 0.0f);
}
// Avoid a weight of zero
- weight = max(weight, 0.000001f);
+ weight = max(weight, minWight);
// Adjust weight curve to inject a small portion of light
const float minWeightThreshold = 0.2f;
- if (weight < minWeightThreshold)
- weight *= Square(weight) / Square(minWeightThreshold);
+ if (weight < minWeightThreshold) weight *= (weight * weight) * (1.0f / (minWeightThreshold * minWeightThreshold));
// Calculate trilinear weights based on the distance to each probe to smoothly transition between grid of 8 probes
float3 trilinear = lerp(1.0f - biasAlpha, biasAlpha, (float3)probeCoordsOffset);
- weight *= max(trilinear.x * trilinear.y * trilinear.z, 0.001f);
+ weight *= saturate(trilinear.x * trilinear.y * trilinear.z * 2.0f);
// Sample irradiance texture
octahedralCoords = GetOctahedralCoords(worldNormal);
@@ -269,7 +265,9 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes
if (irradiance.a > 0.0f)
{
// Normalize irradiance
- irradiance.rgb /= irradiance.a;
+ //irradiance.rgb /= irradiance.a;
+ //irradiance.rgb /= lerp(1, irradiance.a, saturate(irradiance.a * irradiance.a + 0.9f));
+ irradiance.rgb /= invalidCascade ? irradiance.a : lerp(1, irradiance.a, saturate(irradiance.a * irradiance.a + 0.9f));
#if DDGI_SRGB_BLENDING
irradiance.rgb *= irradiance.rgb;
#endif
@@ -281,22 +279,34 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes
float3 GetDDGISurfaceBias(float3 viewDir, float probesSpacing, float3 worldNormal, float bias)
{
// Bias the world-space position to reduce artifacts
- return (worldNormal * 0.2f + viewDir * 0.8f) * (0.75f * probesSpacing * bias);
+ return (worldNormal * 0.2f + viewDir * 0.8f) * (0.6f * probesSpacing * bias);
+}
+
+// [Inigo Quilez, https://iquilezles.org/articles/distfunctions/]
+float sdRoundBox(float3 p, float3 b, float r)
+{
+ float3 q = abs(p) - b + r;
+ return length(max(q, 0.0f)) + min(max(q.x, max(q.y, q.z)), 0.0f) - r;
}
// Samples DDGI probes volume at the given world-space position and returns the irradiance.
// bias - scales the bias vector to the initial sample point to reduce self-shading artifacts
// dither - randomized per-pixel value in range 0-1, used to smooth dithering for cascades blending
-float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, Texture2D probesDistance, Texture2D probesIrradiance, float3 worldPosition, float3 worldNormal, float bias = 0.2f, float dither = 0.0f)
+float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, Texture2D probesDistance, Texture2D probesIrradiance, float3 worldPosition, float3 worldNormal, float bias = DDGI_DEFAULT_BIAS, float dither = 0.0f)
{
// Select the highest cascade that contains the sample location
- uint cascadeIndex = 0;
float probesSpacing = 0, cascadeWeight = 0;
float3 probesOrigin = (float3)0, probesExtent = (float3)0, biasedWorldPosition = (float3)0;
float3 viewDir = normalize(data.ViewPos - worldPosition);
#if DDGI_CASCADE_BLEND_SMOOTH
dither = 0.0f;
#endif
+#ifdef DDGI_DEBUG_CASCADE
+ uint cascadeIndex = DDGI_DEBUG_CASCADE;
+#else
+ uint cascadeIndex = 0;
+ if (data.CascadesCount == 0)
+ return float3(0, 0, 0);
for (; cascadeIndex < data.CascadesCount; cascadeIndex++)
{
// Get cascade data
@@ -306,26 +316,21 @@ float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, T
biasedWorldPosition = worldPosition + GetDDGISurfaceBias(viewDir, probesSpacing, worldNormal, bias);
// Calculate cascade blending weight (use input bias to smooth transition)
- float cascadeBlendSmooth = frac(max(distance(data.ViewPos, worldPosition) - probesExtent.x, 0) / probesSpacing) * 0.1f;
- float3 cascadeBlendPoint = worldPosition - probesOrigin - cascadeBlendSmooth * probesSpacing;
float fadeDistance = probesSpacing * DDGI_CASCADE_BLEND_SIZE;
-#if DDGI_CASCADE_BLEND_SMOOTH
- fadeDistance *= 2.0f; // Make it even smoother when using linear blending
-#endif
- cascadeWeight = saturate(Min3(probesExtent - abs(cascadeBlendPoint)) / fadeDistance);
+ float3 blendPos = worldPosition - data.BlendOrigin[cascadeIndex].xyz;
+ cascadeWeight = sdRoundBox(blendPos, probesExtent - probesSpacing, probesSpacing * 2) + fadeDistance;
+ cascadeWeight = 1 - saturate(cascadeWeight / fadeDistance);
if (cascadeWeight > dither)
break;
}
- if (cascadeIndex == data.CascadesCount)
- return data.FallbackIrradiance;
+#endif
// Sample cascade
float3 result = SampleDDGIIrradianceCascade(data, probesData, probesDistance, probesIrradiance, worldPosition, worldNormal, cascadeIndex, probesOrigin, probesExtent, probesSpacing, biasedWorldPosition);
// Blend with the next cascade (or fallback irradiance outside the volume)
+#if DDGI_CASCADE_BLEND_SMOOTH && !defined(DDGI_DEBUG_CASCADE)
cascadeIndex++;
-#if DDGI_CASCADE_BLEND_SMOOTH
- result *= cascadeWeight;
if (cascadeIndex < data.CascadesCount && cascadeWeight < 0.99f)
{
probesSpacing = data.ProbesOriginAndSpacing[cascadeIndex].w;
@@ -333,18 +338,16 @@ float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, T
probesExtent = (data.ProbesCounts - 1) * (probesSpacing * 0.5f);
biasedWorldPosition = worldPosition + GetDDGISurfaceBias(viewDir, probesSpacing, worldNormal, bias);
float3 resultNext = SampleDDGIIrradianceCascade(data, probesData, probesDistance, probesIrradiance, worldPosition, worldNormal, cascadeIndex, probesOrigin, probesExtent, probesSpacing, biasedWorldPosition);
+ result *= cascadeWeight;
result += resultNext * (1 - cascadeWeight);
}
- else
- {
- result += data.FallbackIrradiance * (1 - cascadeWeight);
- }
-#else
- if (cascadeIndex == data.CascadesCount)
- {
- result += data.FallbackIrradiance * (1 - cascadeWeight);
- }
#endif
+ if (cascadeIndex >= data.CascadesCount)
+ {
+ // Blend between the last cascade and the fallback irradiance
+ float fallbackWeight = (1 - cascadeWeight) * data.FallbackIrradiance.a;
+ result = lerp(result, data.FallbackIrradiance.rgb, fallbackWeight);
+ }
return result;
}
diff --git a/Source/Shaders/GI/DDGI.shader b/Source/Shaders/GI/DDGI.shader
index daad2018d..b080efc0b 100644
--- a/Source/Shaders/GI/DDGI.shader
+++ b/Source/Shaders/GI/DDGI.shader
@@ -13,6 +13,7 @@
#include "./Flax/Math.hlsl"
#include "./Flax/Noise.hlsl"
#include "./Flax/Quaternion.hlsl"
+#include "./Flax/MonteCarlo.hlsl"
#include "./Flax/GlobalSignDistanceField.hlsl"
#include "./Flax/GI/GlobalSurfaceAtlas.hlsl"
#include "./Flax/GI/DDGI.hlsl"
@@ -26,6 +27,7 @@
#define DDGI_PROBE_CLASSIFY_GROUP_SIZE 32
#define DDGI_PROBE_RELOCATE_ITERATIVE 1 // If true, probes relocation algorithm tries to move them in additive way, otherwise all nearby locations are checked to find the best position
#define DDGI_PROBE_RELOCATE_FIND_BEST 1 // If true, probes relocation algorithm tries to move to the best matching location within nearby area
+#define DDGI_PROBE_EMPTY_AREA_DENSITY 8 // Spacing (in probe grid) between fallback probes placed into empty areas to provide valid GI for nearby dynamic objects or transparency
#define DDGI_DEBUG_STATS 0 // Enables additional GPU-driven stats for probe/rays count
#define DDGI_DEBUG_INSTABILITY 0 // Enables additional probe irradiance instability debugging
@@ -42,10 +44,13 @@ float TemporalTime;
int4 ProbeScrollClears[4];
float3 ViewDir;
float Padding1;
+float3 QuantizationError;
+uint FrameIndexMod8;
META_CB_END
META_CB_BEGIN(1, Data1)
-float2 Padding2;
+float Padding2;
+int StepSize;
uint CascadeIndex;
uint ProbeIndexOffset;
META_CB_END
@@ -98,6 +103,11 @@ float3 Remap(float3 value, float3 fromMin, float3 fromMax, float3 toMin, float3
return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin;
}
+bool IsProbeAtBorder(uint3 probeCoords)
+{
+ return min(probeCoords.x, min(probeCoords.y, probeCoords.z)) == 0 || probeCoords.x == DDGI.ProbesCounts.x - 1 || probeCoords.y == DDGI.ProbesCounts.y - 1 || probeCoords.z == DDGI.ProbesCounts.z - 1;
+}
+
// Compute shader for updating probes state between active and inactive and performing probes relocation.
META_CS(true, FEATURE_LEVEL_SM5)
[numthreads(DDGI_PROBE_CLASSIFY_GROUP_SIZE, 1, 1)]
@@ -112,6 +122,14 @@ void CS_Classify(uint3 DispatchThreadId : SV_DispatchThreadID)
float probesSpacing = DDGI.ProbesOriginAndSpacing[CascadeIndex].w;
float3 probeBasePosition = GetDDGIProbeWorldPosition(DDGI, CascadeIndex, probeCoords);
+#ifdef DDGI_DEBUG_CASCADE
+ // Single cascade-only debugging
+ if (CascadeIndex != DDGI_DEBUG_CASCADE)
+ {
+ RWProbesData[probeDataCoords] = EncodeDDGIProbeData(float3(0, 0, 0), DDGI_PROBE_STATE_INACTIVE, 0.0f);
+ return;
+ }
+#else
// Disable probes that are is in the range of higher-quality cascade
if (CascadeIndex > 0)
{
@@ -119,15 +137,15 @@ void CS_Classify(uint3 DispatchThreadId : SV_DispatchThreadID)
float prevProbesSpacing = DDGI.ProbesOriginAndSpacing[prevCascade].w;
float3 prevProbesOrigin = DDGI.ProbesScrollOffsets[prevCascade].xyz * prevProbesSpacing + DDGI.ProbesOriginAndSpacing[prevCascade].xyz;
float3 prevProbesExtent = (DDGI.ProbesCounts - 1) * (prevProbesSpacing * 0.5f);
- prevProbesExtent -= probesSpacing * ceil(DDGI_CASCADE_BLEND_SIZE); // Apply safe margin to allow probes on cascade edges
+ prevProbesExtent -= probesSpacing * ceil(DDGI_CASCADE_BLEND_SIZE) * 2; // Apply safe margin to allow probes on cascade edges
float prevCascadeWeight = Min3(prevProbesExtent - abs(probeBasePosition - prevProbesOrigin));
if (prevCascadeWeight > 0.1f)
{
- // Disable probe
RWProbesData[probeDataCoords] = EncodeDDGIProbeData(float3(0, 0, 0), DDGI_PROBE_STATE_INACTIVE, 0.0f);
return;
}
}
+#endif
// Check if probe was scrolled
int3 probeScrollClears = ProbeScrollClears[CascadeIndex].xyz;
@@ -171,9 +189,29 @@ void CS_Classify(uint3 DispatchThreadId : SV_DispatchThreadID)
float voxelLimit = GlobalSDF.CascadeVoxelSize[CascadeIndex] * 0.8f;
float distanceLimit = probesSpacing * ProbesDistanceLimits[CascadeIndex];
float relocateLimit = probesSpacing * ProbesRelocateLimits[CascadeIndex];
- if (sdfDst > distanceLimit + length(probeOffset)) // Probe is too far from geometry (or deep inside)
+#ifdef DDGI_PROBE_EMPTY_AREA_DENSITY
+ uint3 probeCoordsStable = GetDDGIProbeCoords(DDGI, probeIndex);
+ if (sdf > probesSpacing * DDGI.ProbesCounts.x * 0.3f
+#if DDGI_PROBE_EMPTY_AREA_DENSITY > 1
+ && (
+ // Low-density grid grid
+ (probeCoordsStable.x % DDGI_PROBE_EMPTY_AREA_DENSITY == 0 && probeCoordsStable.y % DDGI_PROBE_EMPTY_AREA_DENSITY == 0 && probeCoordsStable.z % DDGI_PROBE_EMPTY_AREA_DENSITY == 0)
+ // Edge probes at the last cascade (for good fallback irradiance outside the GI distance)
+ //|| (CascadeIndex + 1 == DDGI.CascadesCount && IsProbeAtBorder(probeCoords))
+ )
+#endif
+ )
{
- // Disable it
+ // Addd some fallback probes in empty areas to provide valid GI for nearby dynamic objects or transparency
+ probeOffset = float3(0, 0, 0);
+ probeState = wasScrolled || probeStateOld == DDGI_PROBE_STATE_INACTIVE ? DDGI_PROBE_STATE_ACTIVATED : DDGI_PROBE_STATE_ACTIVE;
+ probeAttention = DDGI_PROBE_ATTENTION_MIN;
+ }
+ else
+#endif
+ if (sdfDst > distanceLimit + length(probeOffset))
+ {
+ // Probe is too far from geometry (or deep inside) so disable it
probeOffset = float3(0, 0, 0);
probeState = DDGI_PROBE_STATE_INACTIVE;
probeAttention = 0.0f;
@@ -194,6 +232,7 @@ void CS_Classify(uint3 DispatchThreadId : SV_DispatchThreadID)
probeAttention = clamp(probeAttention, DDGI_PROBE_ATTENTION_MIN, DDGI_PROBE_ATTENTION_MAX);
// Relocate only if probe location is not good enough
+ BRANCH
if (sdf <= voxelLimit)
{
#if DDGI_PROBE_RELOCATE_ITERATIVE
@@ -265,6 +304,7 @@ void CS_Classify(uint3 DispatchThreadId : SV_DispatchThreadID)
bool wasActivated = probeStateOld == DDGI_PROBE_STATE_INACTIVE;
bool wasRelocated = distance(probeOffset, probeOffsetOld) > 2.0f;
#if DDGI_PROBE_RELOCATE_FIND_BEST || DDGI_PROBE_RELOCATE_ITERATIVE
+ BRANCH
if (wasRelocated && !wasActivated)
{
// If probe was relocated but the previous location is visible from the new one, then don't re-activate it for smoother blend
@@ -323,6 +363,78 @@ void CS_UpdateProbesInitArgs()
#endif
+#ifdef _CS_UpdateInactiveProbes
+
+RWTexture2D RWProbesData : register(u0);
+
+void CheckNearbyProbe(inout uint3 fallbackCoords, inout uint probeState, inout float minDistance, uint3 probeCoords, int3 probeCoordsEnd, int3 offset)
+{
+ uint3 nearbyCoords = (uint3)clamp(((int3)probeCoords + offset), int3(0, 0, 0), probeCoordsEnd);
+ uint nearbyIndex = GetDDGIScrollingProbeIndex(DDGI, CascadeIndex, nearbyCoords);
+ float4 nearbyData = RWProbesData[GetDDGIProbeTexelCoords(DDGI, CascadeIndex, nearbyIndex)];
+ float nearbyDist = distance((float3)nearbyCoords, (float3)probeCoords);
+ if (DecodeDDGIProbeState(nearbyData) != DDGI_PROBE_STATE_INACTIVE && nearbyDist < minDistance)
+ {
+ // Use nearby probe
+ fallbackCoords = nearbyCoords;
+ probeState = DDGI_PROBE_STATE_ACTIVE;
+ minDistance = nearbyDist;
+ return;
+ }
+ nearbyCoords = DDGI_FALLBACK_COORDS_DECODE(nearbyData);
+ nearbyDist = distance((float3)nearbyCoords, (float3)probeCoords);
+ if (DDGI_FALLBACK_COORDS_VALID(nearbyData) && nearbyDist < minDistance)
+ {
+ // Use fallback probe
+ fallbackCoords = nearbyCoords;
+ probeState = DDGI_PROBE_STATE_ACTIVE;
+ minDistance = nearbyDist;
+ }
+}
+
+// Compute shader to store closest valid probe coords inside inactive probes data for quick fallback lookup when sampling irradiance.
+// Uses Jump Flood algorithm.
+META_CS(true, FEATURE_LEVEL_SM5)
+[numthreads(DDGI_PROBE_CLASSIFY_GROUP_SIZE, 1, 1)]
+void CS_UpdateInactiveProbes(uint3 DispatchThreadId : SV_DispatchThreadID)
+{
+ uint probeIndex = min(DispatchThreadId.x, ProbesCount - 1);
+ uint3 fallbackCoords = uint3(1000, 1000, 1000);
+
+ // Load probe data for the current thread
+ uint3 probeCoords = GetDDGIProbeCoords(DDGI, probeIndex);
+ probeIndex = GetDDGIScrollingProbeIndex(DDGI, CascadeIndex, probeCoords);
+ int2 probeDataCoords = GetDDGIProbeTexelCoords(DDGI, CascadeIndex, probeIndex);
+ float4 probeData = RWProbesData[probeDataCoords];
+ uint probeState = DecodeDDGIProbeState(probeData);
+ BRANCH
+ if (probeState == DDGI_PROBE_STATE_INACTIVE)
+ {
+ // Find the closest active probe (Jump Flood)
+ int3 probeCoordsEnd = (int3)DDGI.ProbesCounts - int3(1, 1, 1);
+ float minDistance = 1e27f;
+ UNROLL for (int z = -1; z <= 1; z++)
+ UNROLL for (int y = -1; y <= 1; y++)
+ UNROLL for (int x = -1; x <= 1; x++)
+ {
+ int3 offset = int3(x, y, z) * StepSize;
+ CheckNearbyProbe(fallbackCoords, probeState, minDistance, probeCoords, probeCoordsEnd, offset);
+ }
+ }
+
+ // Ensure all threads (within dispatch) got proper data before writing back to the same memory
+ AllMemoryBarrierWithGroupSync();
+
+ // Write modified probe data back (remain inactive)
+ BRANCH
+ if (probeState != DDGI_PROBE_STATE_INACTIVE && DispatchThreadId.x < ProbesCount && fallbackCoords.x != 1000)
+ {
+ RWProbesData[probeDataCoords] = EncodeDDGIProbeData(DDGI_FALLBACK_COORDS_ENCODE(fallbackCoords), DDGI_PROBE_STATE_INACTIVE, 0.0f);
+ }
+}
+
+#endif
+
#ifdef _CS_TraceRays
RWTexture2D RWProbesTrace : register(u0);
@@ -392,6 +504,8 @@ void CS_TraceRays(uint3 DispatchThreadId : SV_DispatchThreadID)
// Add some bias to prevent self occlusion artifacts in Chebyshev due to Global SDF being very incorrect in small scale
radiance.w = max(radiance.w + GlobalSDF.CascadeVoxelSize[hit.HitCascade] * 0.5f, 0);
+ float probesSpacing = DDGI.ProbesOriginAndSpacing[CascadeIndex].w;
+ radiance.w += probesSpacing * 0.05f;
}
}
else
@@ -639,7 +753,7 @@ void CS_UpdateProbes(uint3 GroupThreadId : SV_GroupThreadID, uint3 GroupId : SV_
// Add distance (R), distance^2 (G) and weight (A)
float rayDistance = CachedProbesTraceDistance[rayIndex];
- result += float4(rayDistance * rayWeight, (rayDistance * rayDistance) * rayWeight, 0.0f, rayWeight);
+ result += float4(rayDistance, rayDistance * rayDistance, 0.0f, 1.0f) * rayWeight;
#endif
}
@@ -700,13 +814,17 @@ void CS_UpdateProbes(uint3 GroupThreadId : SV_GroupThreadID, uint3 GroupId : SV_
//result.rgb = previous + (irradianceDelta * 0.25f);
}
result = float4(lerp(result.rgb, previous.rgb, historyWeight), 1.0f);
+
+ // Apply quantization error to reduce yellowish artifacts due to R11G11B10 format
+ float noise = InterleavedGradientNoise(octahedralCoords, FrameIndexMod8);
+ result.rgb = QuantizeColor(result.rgb, noise, QuantizationError);
#else
result = float4(lerp(result.rg, previous.rg, historyWeight), 0.0f, 1.0f);
#endif
RWOutput[outputCoords] = result;
-
GroupMemoryBarrierWithGroupSync();
+
uint2 baseCoords = GetDDGIProbeTexelCoords(DDGI, CascadeIndex, probeIndex) * (DDGI_PROBE_RESOLUTION + 2);
#if DDGI_PROBE_UPDATE_MODE == 0
@@ -786,10 +904,10 @@ void PS_IndirectLighting(Quad_VS2PS input, out float4 output : SV_Target0)
}
// Sample irradiance
- float bias = 0.2f;
float dither = RandN2(input.TexCoord + TemporalTime).x;
- float3 irradiance = SampleDDGIIrradiance(DDGI, ProbesData, ProbesDistance, ProbesIrradiance, gBuffer.WorldPos, gBuffer.Normal, bias, dither);
-
+ float3 samplePos = gBuffer.WorldPos + gBuffer.Normal * (dither * 0.1f + 0.1f);
+ float3 irradiance = SampleDDGIIrradiance(DDGI, ProbesData, ProbesDistance, ProbesIrradiance, samplePos, gBuffer.Normal, DDGI_DEFAULT_BIAS, dither);
+
// Calculate lighting
float3 diffuseColor = GetDiffuseColor(gBuffer);
float3 diffuse = Diffuse_Lambert(diffuseColor);
diff --git a/Source/Shaders/GI/GlobalSurfaceAtlas.shader b/Source/Shaders/GI/GlobalSurfaceAtlas.shader
index 6778a7cd7..6930107d1 100644
--- a/Source/Shaders/GI/GlobalSurfaceAtlas.shader
+++ b/Source/Shaders/GI/GlobalSurfaceAtlas.shader
@@ -328,7 +328,6 @@ float4 PS_Debug(Quad_VS2PS input) : SV_Target
float3 viewRay = lerp(lerp(ViewFrustumWorldRays[3], ViewFrustumWorldRays[0], input.TexCoord.x), lerp(ViewFrustumWorldRays[2], ViewFrustumWorldRays[1], input.TexCoord.x), 1 - input.TexCoord.y).xyz;
viewRay = normalize(viewRay - ViewWorldPos);
trace.Init(ViewWorldPos, viewRay, ViewNearPlane, ViewFarPlane);
- trace.NeedsHitNormal = true;
GlobalSDFHit hit = RayTraceGlobalSDF(GlobalSDF, GlobalSDFTex, GlobalSDFMip, trace);
float3 color;
@@ -337,7 +336,6 @@ float4 PS_Debug(Quad_VS2PS input) : SV_Target
// Sample Global Surface Atlas at the hit location
float surfaceThreshold = GetGlobalSurfaceAtlasThreshold(GlobalSDF, hit);
color = SampleGlobalSurfaceAtlas(GlobalSurfaceAtlas, GlobalSurfaceAtlasChunks, GlobalSurfaceAtlasCulledObjects, GlobalSurfaceAtlasObjects, GlobalSurfaceAtlasDepth, GlobalSurfaceAtlasTex, hit.GetHitPosition(trace), -viewRay, surfaceThreshold).rgb;
- //color = hit.HitNormal * 0.5f + 0.5f;
}
else
{
diff --git a/Source/Shaders/GlobalSignDistanceField.hlsl b/Source/Shaders/GlobalSignDistanceField.hlsl
index 8075c081d..c1bd4250b 100644
--- a/Source/Shaders/GlobalSignDistanceField.hlsl
+++ b/Source/Shaders/GlobalSignDistanceField.hlsl
@@ -32,17 +32,13 @@ struct GlobalSDFTrace
float MinDistance;
float3 WorldDirection;
float MaxDistance;
- float StepScale;
- bool NeedsHitNormal;
- void Init(float3 worldPosition, float3 worldDirection, float minDistance, float maxDistance, float stepScale = 1.0f)
+ void Init(float3 worldPosition, float3 worldDirection, float minDistance, float maxDistance)
{
WorldPosition = worldPosition;
WorldDirection = worldDirection;
MinDistance = minDistance;
MaxDistance = maxDistance;
- StepScale = stepScale;
- NeedsHitNormal = false;
}
};
@@ -75,12 +71,23 @@ void GetGlobalSDFCascadeUV(const GlobalSDFData data, uint cascade, float3 worldP
textureUV = float3(((float)cascade + cascadeUV.x) / (float)data.CascadesCount, cascadeUV.y, cascadeUV.z); // Cascades are placed next to each other on X axis
}
-// Clamps Global SDF cascade UV to ensure it can be sued for gradient sampling (clamps first and last pixels).
+void GetGlobalSDFCascadeUV(const GlobalSDFData data, uint cascade, float3 worldPosition, out float3 cascadeUV, out float3 textureUV, out float3 textureMipUV)
+{
+ float4 cascadePosDistance = data.CascadePosDistance[cascade];
+ float3 posInCascade = worldPosition - cascadePosDistance.xyz;
+ float cascadeSize = cascadePosDistance.w * 2;
+ cascadeUV = saturate(posInCascade / cascadeSize + 0.5f);
+ textureUV = float3(((float)cascade + cascadeUV.x) / (float)data.CascadesCount, cascadeUV.y, cascadeUV.z); // Cascades are placed next to each other on X axis
+ float halfTexelOffsetMip = (GLOBAL_SDF_RASTERIZE_MIP_FACTOR * 0.5f) / data.Resolution;
+ textureMipUV = textureUV + float3(halfTexelOffsetMip / (float)data.CascadesCount, halfTexelOffsetMip, halfTexelOffsetMip); // Mipmaps are offset by half texel to sample correctly
+}
+
+// Clamps Global SDF cascade UV to ensure it can be used for gradient sampling (clamps first and last pixels).
void ClampGlobalSDFTextureGradientUV(const GlobalSDFData data, uint cascade, float texelOffset, inout float3 textureUV)
{
float cascadeSizeUV = 1.0f / data.CascadesCount;
- float cascadeUVStart = cascadeSizeUV * cascade + texelOffset;
- float cascadeUVEnd = cascadeUVStart + cascadeSizeUV - texelOffset * 3;
+ float cascadeUVStart = cascadeSizeUV * cascade + texelOffset * 2;
+ float cascadeUVEnd = cascadeUVStart + cascadeSizeUV - texelOffset * 4;
textureUV.x = clamp(textureUV.x, cascadeUVStart, cascadeUVEnd);
}
@@ -144,13 +151,13 @@ float SampleGlobalSDF(const GlobalSDFData data, Texture3D tex, Text
startCascade = min(startCascade, data.CascadesCount - 1);
for (uint cascade = startCascade; cascade < data.CascadesCount; cascade++)
{
- float3 cascadeUV, textureUV;
- GetGlobalSDFCascadeUV(data, cascade, worldPosition, cascadeUV, textureUV);
+ float3 cascadeUV, textureUV, textureMipUV;
+ GetGlobalSDFCascadeUV(data, cascade, worldPosition, cascadeUV, textureUV, textureMipUV);
float voxelSize = data.CascadeVoxelSize[cascade];
float chunkSize = voxelSize * GLOBAL_SDF_RASTERIZE_CHUNK_SIZE;
float chunkMargin = voxelSize * (GLOBAL_SDF_CHUNK_MARGIN_SCALE * GLOBAL_SDF_RASTERIZE_CHUNK_MARGIN);
float maxDistanceMip = data.CascadeMaxDistanceMip[cascade];
- float distanceMip = mip.SampleLevel(GLOBAL_SDF_SAMPLER, textureUV, 0);
+ float distanceMip = mip.SampleLevel(GLOBAL_SDF_SAMPLER, textureMipUV, 0);
if (distanceMip < chunkSize && all(cascadeUV > 0) && all(cascadeUV < 1))
{
distance = distanceMip * maxDistanceMip;
@@ -208,13 +215,13 @@ float3 SampleGlobalSDFGradient(const GlobalSDFData data, Texture3D
startCascade = min(startCascade, data.CascadesCount - 1);
for (uint cascade = startCascade; cascade < data.CascadesCount; cascade++)
{
- float3 cascadeUV, textureUV;
- GetGlobalSDFCascadeUV(data, cascade, worldPosition, cascadeUV, textureUV);
+ float3 cascadeUV, textureUV, textureMipUV;
+ GetGlobalSDFCascadeUV(data, cascade, worldPosition, cascadeUV, textureUV, textureMipUV);
float voxelSize = data.CascadeVoxelSize[cascade];
float chunkSize = voxelSize * GLOBAL_SDF_RASTERIZE_CHUNK_SIZE;
float chunkMargin = voxelSize * (GLOBAL_SDF_CHUNK_MARGIN_SCALE * GLOBAL_SDF_RASTERIZE_CHUNK_MARGIN);
float maxDistanceMip = data.CascadeMaxDistanceMip[cascade];
- float distanceMip = mip.SampleLevel(GLOBAL_SDF_SAMPLER, textureUV, 0) * maxDistanceMip;
+ float distanceMip = mip.SampleLevel(GLOBAL_SDF_SAMPLER, textureMipUV, 0) * maxDistanceMip;
if (distanceMip < chunkSize && all(cascadeUV > 0) && all(cascadeUV < 1))
{
float maxDistanceTex = data.CascadeMaxDistanceTex[cascade];
@@ -236,13 +243,13 @@ float3 SampleGlobalSDFGradient(const GlobalSDFData data, Texture3D
{
distance = distanceMip;
float texelOffset = (float)GLOBAL_SDF_RASTERIZE_MIP_FACTOR / data.Resolution;
- ClampGlobalSDFTextureGradientUV(data, cascade, texelOffset, textureUV);
- float xp = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x + texelOffset, textureUV.y, textureUV.z), 0).x;
- float xn = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x - texelOffset, textureUV.y, textureUV.z), 0).x;
- float yp = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x, textureUV.y + texelOffset, textureUV.z), 0).x;
- float yn = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x, textureUV.y - texelOffset, textureUV.z), 0).x;
- float zp = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x, textureUV.y, textureUV.z + texelOffset), 0).x;
- float zn = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x, textureUV.y, textureUV.z - texelOffset), 0).x;
+ ClampGlobalSDFTextureGradientUV(data, cascade, texelOffset, textureMipUV);
+ float xp = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureMipUV.x + texelOffset, textureMipUV.y, textureMipUV.z), 0).x;
+ float xn = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureMipUV.x - texelOffset, textureMipUV.y, textureMipUV.z), 0).x;
+ float yp = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureMipUV.x, textureMipUV.y + texelOffset, textureMipUV.z), 0).x;
+ float yn = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureMipUV.x, textureMipUV.y - texelOffset, textureMipUV.z), 0).x;
+ float zp = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureMipUV.x, textureMipUV.y, textureMipUV.z + texelOffset), 0).x;
+ float zn = mip.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureMipUV.x, textureMipUV.y, textureMipUV.z - texelOffset), 0).x;
gradient = float3(xp - xn, yp - yn, zp - zn) * maxDistanceMip;
}
break;
@@ -290,59 +297,32 @@ GlobalSDFHit RayTraceGlobalSDF(const GlobalSDFData data, Texture3D
float maxDistanceTex = data.CascadeMaxDistanceTex[cascade];
float maxDistanceMip = data.CascadeMaxDistanceMip[cascade];
LOOP
- for (; step < 250 && stepTime < intersections.y && hit.HitTime < 0.0f; step++)
+ for (; step < 100 && stepTime < intersections.y && hit.HitTime < 0.0f; step++)
{
float3 stepPosition = trace.WorldPosition + trace.WorldDirection * stepTime;
- float stepScale = trace.StepScale;
// Sample SDF
- float stepDistance, voxelSizeScale = (float)GLOBAL_SDF_RASTERIZE_MIP_FACTOR;
- float3 cascadeUV, textureUV;
- GetGlobalSDFCascadeUV(data, cascade, stepPosition, cascadeUV, textureUV);
- float distanceMip = mip.SampleLevel(GLOBAL_SDF_SAMPLER, textureUV, 0) * maxDistanceMip;
- if (distanceMip < chunkSize)
- {
- stepDistance = distanceMip;
- float distanceTex = tex.SampleLevel(GLOBAL_SDF_SAMPLER, textureUV, 0) * maxDistanceTex;
- if (distanceTex < chunkMargin)
- {
- stepDistance = distanceTex;
- voxelSizeScale = 1.0f;
- stepScale *= 0.63f; // Perform smaller steps nearby geometry
- }
- }
- else
- {
- // Assume no SDF nearby so perform a jump to the next chunk
- stepDistance = chunkSize;
- voxelSizeScale = 1.0f;
- }
+ float stepDistance;
+ float3 cascadeUV, textureUV, textureMipUV;
+ GetGlobalSDFCascadeUV(data, cascade, stepPosition, cascadeUV, textureUV, textureMipUV);
+ stepDistance = min(mip.SampleLevel(GLOBAL_SDF_SAMPLER, textureMipUV, 0) * maxDistanceMip, chunkSize);
+ float distanceTex = tex.SampleLevel(GLOBAL_SDF_SAMPLER, textureUV, 0) * maxDistanceTex;
+ FLATTEN
+ if (distanceTex < chunkMargin)
+ stepDistance = distanceTex;
// Detect surface hit
- float minSurfaceThickness = voxelSizeScale * voxelExtent * saturate(stepTime / voxelSize);
+ float minSurfaceThickness = voxelExtent * saturate(stepTime / voxelSize);
if (stepDistance < minSurfaceThickness)
{
// Surface hit
hit.HitTime = max(stepTime + stepDistance - minSurfaceThickness, 0.0f);
hit.HitCascade = cascade;
hit.HitSDF = stepDistance;
- if (trace.NeedsHitNormal)
- {
- // Calculate hit normal from SDF gradient
- float texelOffset = 1.0f / data.Resolution;
- ClampGlobalSDFTextureGradientUV(data, cascade, texelOffset, textureUV);
- float xp = tex.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x + texelOffset, textureUV.y, textureUV.z), 0).x;
- float xn = tex.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x - texelOffset, textureUV.y, textureUV.z), 0).x;
- float yp = tex.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x, textureUV.y + texelOffset, textureUV.z), 0).x;
- float yn = tex.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x, textureUV.y - texelOffset, textureUV.z), 0).x;
- float zp = tex.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x, textureUV.y, textureUV.z + texelOffset), 0).x;
- float zn = tex.SampleLevel(GLOBAL_SDF_SAMPLER, float3(textureUV.x, textureUV.y, textureUV.z - texelOffset), 0).x;
- hit.HitNormal = normalize(float3(xp - xn, yp - yn, zp - zn));
- }
}
// Move forward
- stepTime += max(stepDistance * stepScale, voxelSize);
+ stepTime += max(stepDistance, voxelSize);
}
hit.StepsCount += step;
}
diff --git a/Source/Shaders/GlobalSignDistanceField.shader b/Source/Shaders/GlobalSignDistanceField.shader
index 461dba08d..fe4bafda5 100644
--- a/Source/Shaders/GlobalSignDistanceField.shader
+++ b/Source/Shaders/GlobalSignDistanceField.shader
@@ -311,26 +311,39 @@ float4 PS_Debug(Quad_VS2PS input) : SV_Target
float3 viewRay = lerp(lerp(ViewFrustumWorldRays[3], ViewFrustumWorldRays[0], input.TexCoord.x), lerp(ViewFrustumWorldRays[2], ViewFrustumWorldRays[1], input.TexCoord.x), 1 - input.TexCoord.y).xyz;
viewRay = normalize(viewRay - ViewWorldPos);
trace.Init(ViewWorldPos, viewRay, ViewNearPlane, ViewFarPlane);
- trace.NeedsHitNormal = true;
GlobalSDFHit hit = RayTraceGlobalSDF(GlobalSDF, GlobalSDFTex, GlobalSDFMip, trace);
// Debug draw
- float3 color = saturate(hit.StepsCount / 80.0f).xxx;
- if (!hit.IsHit())
- color.rg *= 0.4f;
-#if 0
- else
- {
+ float3 color = saturate(hit.StepsCount / 50.0f).xxx;
+ if (hit.IsHit())
+ {
+#if 1
+ float3 hitPosition = hit.GetHitPosition(trace);
+ float hitSDF;
+ float3 hitNormal = SampleGlobalSDFGradient(GlobalSDF, GlobalSDFTex, GlobalSDFMip, hitPosition, hitSDF, hit.HitCascade);
+#if 1
+ // Composite step count with SDF normals
+ //color.rgb *= saturate(normalize(hitNormal) * 0.5f + 0.7f) + 0.3f;
+ color = lerp(normalize(hitNormal) * 0.5f + 0.5f, 1 - color, saturate(hit.StepsCount / 80.0f));
+#else
// Debug draw SDF normals
- color.rgb = normalize(hit.HitNormal) * 0.5f + 0.5f;
- }
-#elif 1
+ color = normalize(hitNormal) * 0.5f + 0.5f;
+#endif
+#else
+ // Heatmap with step count
+ if (hit.StepsCount > 40)
+ color = float3(saturate(hit.StepsCount / 80.0f), 0, 0);
+ else if (hit.StepsCount > 20)
+ color = float3(saturate(hit.StepsCount / 40.0f).xx, 0);
+ else
+ color = float3(0, saturate(hit.StepsCount / 20.0f), 0);
+#endif
+ }
else
{
- // Composite with SDF normals
- color.rgb *= saturate(normalize(hit.HitNormal) * 0.5f + 0.7f) + 0.1f;
+ // Bluish sky
+ color.rg *= 0.4f;
}
-#endif
return float4(color, 1);
}
diff --git a/Source/Shaders/Noise.hlsl b/Source/Shaders/Noise.hlsl
index dc35f1efc..df5a041fa 100644
--- a/Source/Shaders/Noise.hlsl
+++ b/Source/Shaders/Noise.hlsl
@@ -54,6 +54,26 @@ float2 PerlinNoiseFade(float2 t)
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
+// "Next Generation Post Processing in Call of Duty: Advanced Warfare"
+// http://advances.realtimerendering.com/s2014/index.html
+float InterleavedGradientNoise(float2 uv, uint frameCount)
+{
+ const float2 magicFrameScale = float2(47, 17) * 0.695;
+ uv += frameCount * magicFrameScale;
+ const float3 magic = float3(0.06711056, 0.00583715, 52.9829189);
+ return frac(magic.z * frac(dot(uv, magic.xy)));
+}
+
+// Removes error from the color to properly store it in lower precision formats (error = 2^(-mantissaBits))
+float3 QuantizeColor(float3 color, float noise, float3 error)
+{
+ float3 delta = color * error;
+ delta.x = asfloat(asuint(delta.x) & ~0x007fffff);
+ delta.y = asfloat(asuint(delta.y) & ~0x007fffff);
+ delta.z = asfloat(asuint(delta.z) & ~0x007fffff);
+ return color + delta * noise;
+}
+
float rand2dTo1d(float2 value, float2 dotDir = float2(12.9898, 78.233))
{
// https://www.ronja-tutorials.com/post/024-white-noise/
diff --git a/Source/ThirdParty/meshoptimizer/allocator.cpp b/Source/ThirdParty/meshoptimizer/allocator.cpp
index 12eda3872..6b6083da2 100644
--- a/Source/ThirdParty/meshoptimizer/allocator.cpp
+++ b/Source/ThirdParty/meshoptimizer/allocator.cpp
@@ -1,8 +1,17 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
-void meshopt_setAllocator(void*(MESHOPTIMIZER_ALLOC_CALLCONV* allocate)(size_t), void(MESHOPTIMIZER_ALLOC_CALLCONV* deallocate)(void*))
+#ifdef MESHOPTIMIZER_ALLOC_EXPORT
+meshopt_Allocator::Storage& meshopt_Allocator::storage()
{
- meshopt_Allocator::Storage::allocate = allocate;
- meshopt_Allocator::Storage::deallocate = deallocate;
+ static Storage s = {::operator new, ::operator delete };
+ return s;
+}
+#endif
+
+void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV* allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV* deallocate)(void*))
+{
+ meshopt_Allocator::Storage& s = meshopt_Allocator::storage();
+ s.allocate = allocate;
+ s.deallocate = deallocate;
}
diff --git a/Source/ThirdParty/meshoptimizer/clusterizer.cpp b/Source/ThirdParty/meshoptimizer/clusterizer.cpp
index 52fe5a362..73cc0ab53 100644
--- a/Source/ThirdParty/meshoptimizer/clusterizer.cpp
+++ b/Source/ThirdParty/meshoptimizer/clusterizer.cpp
@@ -6,19 +6,39 @@
#include
#include
+// The block below auto-detects SIMD ISA that can be used on the target platform
+#ifndef MESHOPTIMIZER_NO_SIMD
+#if defined(__SSE2__) || (defined(_MSC_VER) && defined(_M_X64))
+#define SIMD_SSE
+#include
+#elif defined(__aarch64__) || (defined(_MSC_VER) && defined(_M_ARM64) && _MSC_VER >= 1922)
+#define SIMD_NEON
+#include
+#endif
+#endif // !MESHOPTIMIZER_NO_SIMD
+
// This work is based on:
// Graham Wihlidal. Optimizing the Graphics Pipeline with Compute. 2016
// Matthaeus Chajdas. GeometryFX 1.2 - Cluster Culling. 2016
// Jack Ritter. An Efficient Bounding Sphere. 1990
+// Thomas Larsson. Fast and Tight Fitting Bounding Spheres. 2008
+// Ingo Wald, Vlastimil Havran. On building fast kd-Trees for Ray Tracing, and on doing that in O(N log N). 2006
namespace meshopt
{
-// This must be <= 255 since index 0xff is used internally to indice a vertex that doesn't belong to a meshlet
-const size_t kMeshletMaxVertices = 255;
+// This must be <= 256 since meshlet indices are stored as bytes
+const size_t kMeshletMaxVertices = 256;
// A reasonable limit is around 2*max_vertices or less
const size_t kMeshletMaxTriangles = 512;
+// We keep a limited number of seed triangles and add a few triangles per finished meshlet
+const size_t kMeshletMaxSeeds = 256;
+const size_t kMeshletAddSeeds = 4;
+
+// To avoid excessive recursion for malformed inputs, we limit the maximum depth of the tree
+const int kMeshletMaxTreeDepth = 50;
+
struct TriangleAdjacency2
{
unsigned int* counts;
@@ -70,72 +90,190 @@ static void buildTriangleAdjacency(TriangleAdjacency2& adjacency, const unsigned
for (size_t i = 0; i < vertex_count; ++i)
{
assert(adjacency.offsets[i] >= adjacency.counts[i]);
-
adjacency.offsets[i] -= adjacency.counts[i];
}
}
-static void computeBoundingSphere(float result[4], const float points[][3], size_t count)
+static void buildTriangleAdjacencySparse(TriangleAdjacency2& adjacency, const unsigned int* indices, size_t index_count, size_t vertex_count, meshopt_Allocator& allocator)
{
- assert(count > 0);
+ size_t face_count = index_count / 3;
- // find extremum points along all 3 axes; for each axis we get a pair of points with min/max coordinates
- size_t pmin[3] = {0, 0, 0};
- size_t pmax[3] = {0, 0, 0};
+ // sparse mode can build adjacency more quickly by ignoring unused vertices, using a bit to mark visited vertices
+ const unsigned int sparse_seen = 1u << 31;
+ assert(index_count < sparse_seen);
+
+ // allocate arrays
+ adjacency.counts = allocator.allocate(vertex_count);
+ adjacency.offsets = allocator.allocate(vertex_count);
+ adjacency.data = allocator.allocate(index_count);
+
+ // fill triangle counts
+ for (size_t i = 0; i < index_count; ++i)
+ assert(indices[i] < vertex_count);
+
+ for (size_t i = 0; i < index_count; ++i)
+ adjacency.counts[indices[i]] = 0;
+
+ for (size_t i = 0; i < index_count; ++i)
+ adjacency.counts[indices[i]]++;
+
+ // fill offset table; uses sparse_seen bit to tag visited vertices
+ unsigned int offset = 0;
+
+ for (size_t i = 0; i < index_count; ++i)
+ {
+ unsigned int v = indices[i];
+
+ if ((adjacency.counts[v] & sparse_seen) == 0)
+ {
+ adjacency.offsets[v] = offset;
+ offset += adjacency.counts[v];
+ adjacency.counts[v] |= sparse_seen;
+ }
+ }
+
+ assert(offset == index_count);
+
+ // fill triangle data
+ for (size_t i = 0; i < face_count; ++i)
+ {
+ unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2];
+
+ adjacency.data[adjacency.offsets[a]++] = unsigned(i);
+ adjacency.data[adjacency.offsets[b]++] = unsigned(i);
+ adjacency.data[adjacency.offsets[c]++] = unsigned(i);
+ }
+
+ // fix offsets that have been disturbed by the previous pass
+ // also fix counts (that were marked with sparse_seen by the first pass)
+ for (size_t i = 0; i < index_count; ++i)
+ {
+ unsigned int v = indices[i];
+
+ if (adjacency.counts[v] & sparse_seen)
+ {
+ adjacency.counts[v] &= ~sparse_seen;
+
+ assert(adjacency.offsets[v] >= adjacency.counts[v]);
+ adjacency.offsets[v] -= adjacency.counts[v];
+ }
+ }
+}
+
+static void clearUsed(short* used, size_t vertex_count, const unsigned int* indices, size_t index_count)
+{
+ // for sparse inputs, it's faster to only clear vertices referenced by the index buffer
+ if (vertex_count <= index_count)
+ memset(used, -1, vertex_count * sizeof(short));
+ else
+ for (size_t i = 0; i < index_count; ++i)
+ {
+ assert(indices[i] < vertex_count);
+ used[indices[i]] = -1;
+ }
+}
+
+static void computeBoundingSphere(float result[4], const float* points, size_t count, size_t points_stride, const float* radii, size_t radii_stride, size_t axis_count)
+{
+ static const float kAxes[7][3] = {
+ // X, Y, Z
+ {1, 0, 0},
+ {0, 1, 0},
+ {0, 0, 1},
+
+ // XYZ, -XYZ, X-YZ, XY-Z; normalized to unit length
+ {0.57735026f, 0.57735026f, 0.57735026f},
+ {-0.57735026f, 0.57735026f, 0.57735026f},
+ {0.57735026f, -0.57735026f, 0.57735026f},
+ {0.57735026f, 0.57735026f, -0.57735026f},
+ };
+
+ assert(count > 0);
+ assert(axis_count <= sizeof(kAxes) / sizeof(kAxes[0]));
+
+ size_t points_stride_float = points_stride / sizeof(float);
+ size_t radii_stride_float = radii_stride / sizeof(float);
+
+ // find extremum points along all axes; for each axis we get a pair of points with min/max coordinates
+ size_t pmin[7], pmax[7];
+ float tmin[7], tmax[7];
+
+ for (size_t axis = 0; axis < axis_count; ++axis)
+ {
+ pmin[axis] = pmax[axis] = 0;
+ tmin[axis] = FLT_MAX;
+ tmax[axis] = -FLT_MAX;
+ }
for (size_t i = 0; i < count; ++i)
{
- const float* p = points[i];
+ const float* p = points + i * points_stride_float;
+ float r = radii[i * radii_stride_float];
- for (int axis = 0; axis < 3; ++axis)
+ for (size_t axis = 0; axis < axis_count; ++axis)
{
- pmin[axis] = (p[axis] < points[pmin[axis]][axis]) ? i : pmin[axis];
- pmax[axis] = (p[axis] > points[pmax[axis]][axis]) ? i : pmax[axis];
+ const float* ax = kAxes[axis];
+
+ float tp = ax[0] * p[0] + ax[1] * p[1] + ax[2] * p[2];
+ float tpmin = tp - r, tpmax = tp + r;
+
+ pmin[axis] = (tpmin < tmin[axis]) ? i : pmin[axis];
+ pmax[axis] = (tpmax > tmax[axis]) ? i : pmax[axis];
+ tmin[axis] = (tpmin < tmin[axis]) ? tpmin : tmin[axis];
+ tmax[axis] = (tpmax > tmax[axis]) ? tpmax : tmax[axis];
}
}
// find the pair of points with largest distance
- float paxisd2 = 0;
- int paxis = 0;
+ size_t paxis = 0;
+ float paxisdr = 0;
- for (int axis = 0; axis < 3; ++axis)
+ for (size_t axis = 0; axis < axis_count; ++axis)
{
- const float* p1 = points[pmin[axis]];
- const float* p2 = points[pmax[axis]];
+ const float* p1 = points + pmin[axis] * points_stride_float;
+ const float* p2 = points + pmax[axis] * points_stride_float;
+ float r1 = radii[pmin[axis] * radii_stride_float];
+ float r2 = radii[pmax[axis] * radii_stride_float];
float d2 = (p2[0] - p1[0]) * (p2[0] - p1[0]) + (p2[1] - p1[1]) * (p2[1] - p1[1]) + (p2[2] - p1[2]) * (p2[2] - p1[2]);
+ float dr = sqrtf(d2) + r1 + r2;
- if (d2 > paxisd2)
+ if (dr > paxisdr)
{
- paxisd2 = d2;
+ paxisdr = dr;
paxis = axis;
}
}
// use the longest segment as the initial sphere diameter
- const float* p1 = points[pmin[paxis]];
- const float* p2 = points[pmax[paxis]];
+ const float* p1 = points + pmin[paxis] * points_stride_float;
+ const float* p2 = points + pmax[paxis] * points_stride_float;
+ float r1 = radii[pmin[paxis] * radii_stride_float];
+ float r2 = radii[pmax[paxis] * radii_stride_float];
- float center[3] = {(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2, (p1[2] + p2[2]) / 2};
- float radius = sqrtf(paxisd2) / 2;
+ float paxisd = sqrtf((p2[0] - p1[0]) * (p2[0] - p1[0]) + (p2[1] - p1[1]) * (p2[1] - p1[1]) + (p2[2] - p1[2]) * (p2[2] - p1[2]));
+ float paxisk = paxisd > 0 ? (paxisd + r2 - r1) / (2 * paxisd) : 0.f;
+
+ float center[3] = {p1[0] + (p2[0] - p1[0]) * paxisk, p1[1] + (p2[1] - p1[1]) * paxisk, p1[2] + (p2[2] - p1[2]) * paxisk};
+ float radius = paxisdr / 2;
// iteratively adjust the sphere up until all points fit
for (size_t i = 0; i < count; ++i)
{
- const float* p = points[i];
+ const float* p = points + i * points_stride_float;
+ float r = radii[i * radii_stride_float];
+
float d2 = (p[0] - center[0]) * (p[0] - center[0]) + (p[1] - center[1]) * (p[1] - center[1]) + (p[2] - center[2]) * (p[2] - center[2]);
+ float d = sqrtf(d2);
- if (d2 > radius * radius)
+ if (d + r > radius)
{
- float d = sqrtf(d2);
- assert(d > 0);
+ float k = d > 0 ? (d + r - radius) / (2 * d) : 0.f;
- float k = 0.5f + (radius / d) / 2;
-
- center[0] = center[0] * k + p[0] * (1 - k);
- center[1] = center[1] * k + p[1] * (1 - k);
- center[2] = center[2] * k + p[2] * (1 - k);
- radius = (radius + d) / 2;
+ center[0] += k * (p[0] - center[0]);
+ center[1] += k * (p[1] - center[1]);
+ center[2] += k * (p[2] - center[2]);
+ radius = (radius + d + r) / 2;
}
}
@@ -151,12 +289,12 @@ struct Cone
float nx, ny, nz;
};
-static float getMeshletScore(float distance2, float spread, float cone_weight, float expected_radius)
+static float getMeshletScore(float distance, float spread, float cone_weight, float expected_radius)
{
float cone = 1.f - spread * cone_weight;
float cone_clamped = cone < 1e-3f ? 1e-3f : cone;
- return (1 + sqrtf(distance2) / expected_radius * (1 - cone_weight)) * cone_clamped;
+ return (1 + distance / expected_radius * (1 - cone_weight)) * cone_clamped;
}
static Cone getMeshletCone(const Cone& acc, unsigned int triangle_count)
@@ -221,72 +359,61 @@ static float computeTriangleCones(Cone* triangles, const unsigned int* indices,
return mesh_area;
}
-static void finishMeshlet(meshopt_Meshlet& meshlet, unsigned char* meshlet_triangles)
+static bool appendMeshlet(meshopt_Meshlet& meshlet, unsigned int a, unsigned int b, unsigned int c, short* used, meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t meshlet_offset, size_t max_vertices, size_t max_triangles, bool split = false)
{
- size_t offset = meshlet.triangle_offset + meshlet.triangle_count * 3;
-
- // fill 4b padding with 0
- while (offset & 3)
- meshlet_triangles[offset++] = 0;
-}
-
-static bool appendMeshlet(meshopt_Meshlet& meshlet, unsigned int a, unsigned int b, unsigned int c, unsigned char* used, meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t meshlet_offset, size_t max_vertices, size_t max_triangles)
-{
- unsigned char& av = used[a];
- unsigned char& bv = used[b];
- unsigned char& cv = used[c];
+ short& av = used[a];
+ short& bv = used[b];
+ short& cv = used[c];
bool result = false;
- unsigned int used_extra = (av == 0xff) + (bv == 0xff) + (cv == 0xff);
+ int used_extra = (av < 0) + (bv < 0) + (cv < 0);
- if (meshlet.vertex_count + used_extra > max_vertices || meshlet.triangle_count >= max_triangles)
+ if (meshlet.vertex_count + used_extra > max_vertices || meshlet.triangle_count >= max_triangles || split)
{
meshlets[meshlet_offset] = meshlet;
for (size_t j = 0; j < meshlet.vertex_count; ++j)
- used[meshlet_vertices[meshlet.vertex_offset + j]] = 0xff;
-
- finishMeshlet(meshlet, meshlet_triangles);
+ used[meshlet_vertices[meshlet.vertex_offset + j]] = -1;
meshlet.vertex_offset += meshlet.vertex_count;
- meshlet.triangle_offset += (meshlet.triangle_count * 3 + 3) & ~3; // 4b padding
+ meshlet.triangle_offset += meshlet.triangle_count * 3;
meshlet.vertex_count = 0;
meshlet.triangle_count = 0;
result = true;
}
- if (av == 0xff)
+ if (av < 0)
{
- av = (unsigned char)meshlet.vertex_count;
+ av = short(meshlet.vertex_count);
meshlet_vertices[meshlet.vertex_offset + meshlet.vertex_count++] = a;
}
- if (bv == 0xff)
+ if (bv < 0)
{
- bv = (unsigned char)meshlet.vertex_count;
+ bv = short(meshlet.vertex_count);
meshlet_vertices[meshlet.vertex_offset + meshlet.vertex_count++] = b;
}
- if (cv == 0xff)
+ if (cv < 0)
{
- cv = (unsigned char)meshlet.vertex_count;
+ cv = short(meshlet.vertex_count);
meshlet_vertices[meshlet.vertex_offset + meshlet.vertex_count++] = c;
}
- meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 0] = av;
- meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 1] = bv;
- meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 2] = cv;
+ meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 0] = (unsigned char)av;
+ meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 1] = (unsigned char)bv;
+ meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 2] = (unsigned char)cv;
meshlet.triangle_count++;
return result;
}
-static unsigned int getNeighborTriangle(const meshopt_Meshlet& meshlet, const Cone* meshlet_cone, unsigned int* meshlet_vertices, const unsigned int* indices, const TriangleAdjacency2& adjacency, const Cone* triangles, const unsigned int* live_triangles, const unsigned char* used, float meshlet_expected_radius, float cone_weight, unsigned int* out_extra)
+static unsigned int getNeighborTriangle(const meshopt_Meshlet& meshlet, const Cone& meshlet_cone, const unsigned int* meshlet_vertices, const unsigned int* indices, const TriangleAdjacency2& adjacency, const Cone* triangles, const unsigned int* live_triangles, const short* used, float meshlet_expected_radius, float cone_weight)
{
unsigned int best_triangle = ~0u;
- unsigned int best_extra = 5;
+ int best_priority = 5;
float best_score = FLT_MAX;
for (size_t i = 0; i < meshlet.vertex_count; ++i)
@@ -301,61 +428,159 @@ static unsigned int getNeighborTriangle(const meshopt_Meshlet& meshlet, const Co
unsigned int triangle = neighbors[j];
unsigned int a = indices[triangle * 3 + 0], b = indices[triangle * 3 + 1], c = indices[triangle * 3 + 2];
- unsigned int extra = (used[a] == 0xff) + (used[b] == 0xff) + (used[c] == 0xff);
+ int extra = (used[a] < 0) + (used[b] < 0) + (used[c] < 0);
+ assert(extra <= 2);
+
+ int priority = -1;
// triangles that don't add new vertices to meshlets are max. priority
- if (extra != 0)
- {
- // artificially increase the priority of dangling triangles as they're expensive to add to new meshlets
- if (live_triangles[a] == 1 || live_triangles[b] == 1 || live_triangles[c] == 1)
- extra = 0;
-
- extra++;
- }
+ if (extra == 0)
+ priority = 0;
+ // artificially increase the priority of dangling triangles as they're expensive to add to new meshlets
+ else if (live_triangles[a] == 1 || live_triangles[b] == 1 || live_triangles[c] == 1)
+ priority = 1;
+ // if two vertices have live count of 2, removing this triangle will make another triangle dangling which is good for overall flow
+ else if ((live_triangles[a] == 2) + (live_triangles[b] == 2) + (live_triangles[c] == 2) >= 2)
+ priority = 1 + extra;
+ // otherwise adjust priority to be after the above cases, 3 or 4 based on used[] count
+ else
+ priority = 2 + extra;
// since topology-based priority is always more important than the score, we can skip scoring in some cases
- if (extra > best_extra)
+ if (priority > best_priority)
continue;
- float score = 0;
+ const Cone& tri_cone = triangles[triangle];
- // caller selects one of two scoring functions: geometrical (based on meshlet cone) or topological (based on remaining triangles)
- if (meshlet_cone)
- {
- const Cone& tri_cone = triangles[triangle];
+ float dx = tri_cone.px - meshlet_cone.px, dy = tri_cone.py - meshlet_cone.py, dz = tri_cone.pz - meshlet_cone.pz;
+ float distance = sqrtf(dx * dx + dy * dy + dz * dz);
+ float spread = tri_cone.nx * meshlet_cone.nx + tri_cone.ny * meshlet_cone.ny + tri_cone.nz * meshlet_cone.nz;
- float distance2 =
- (tri_cone.px - meshlet_cone->px) * (tri_cone.px - meshlet_cone->px) +
- (tri_cone.py - meshlet_cone->py) * (tri_cone.py - meshlet_cone->py) +
- (tri_cone.pz - meshlet_cone->pz) * (tri_cone.pz - meshlet_cone->pz);
-
- float spread = tri_cone.nx * meshlet_cone->nx + tri_cone.ny * meshlet_cone->ny + tri_cone.nz * meshlet_cone->nz;
-
- score = getMeshletScore(distance2, spread, cone_weight, meshlet_expected_radius);
- }
- else
- {
- // each live_triangles entry is >= 1 since it includes the current triangle we're processing
- score = float(live_triangles[a] + live_triangles[b] + live_triangles[c] - 3);
- }
+ float score = getMeshletScore(distance, spread, cone_weight, meshlet_expected_radius);
// note that topology-based priority is always more important than the score
// this helps maintain reasonable effectiveness of meshlet data and reduces scoring cost
- if (extra < best_extra || score < best_score)
+ if (priority < best_priority || score < best_score)
{
best_triangle = triangle;
- best_extra = extra;
+ best_priority = priority;
best_score = score;
}
}
}
- if (out_extra)
- *out_extra = best_extra;
-
return best_triangle;
}
+static size_t appendSeedTriangles(unsigned int* seeds, const meshopt_Meshlet& meshlet, const unsigned int* meshlet_vertices, const unsigned int* indices, const TriangleAdjacency2& adjacency, const Cone* triangles, const unsigned int* live_triangles, float cornerx, float cornery, float cornerz)
+{
+ unsigned int best_seeds[kMeshletAddSeeds];
+ unsigned int best_live[kMeshletAddSeeds];
+ float best_score[kMeshletAddSeeds];
+
+ for (size_t i = 0; i < kMeshletAddSeeds; ++i)
+ {
+ best_seeds[i] = ~0u;
+ best_live[i] = ~0u;
+ best_score[i] = FLT_MAX;
+ }
+
+ for (size_t i = 0; i < meshlet.vertex_count; ++i)
+ {
+ unsigned int index = meshlet_vertices[meshlet.vertex_offset + i];
+
+ unsigned int best_neighbor = ~0u;
+ unsigned int best_neighbor_live = ~0u;
+
+ // find the neighbor with the smallest live metric
+ unsigned int* neighbors = &adjacency.data[0] + adjacency.offsets[index];
+ size_t neighbors_size = adjacency.counts[index];
+
+ for (size_t j = 0; j < neighbors_size; ++j)
+ {
+ unsigned int triangle = neighbors[j];
+ unsigned int a = indices[triangle * 3 + 0], b = indices[triangle * 3 + 1], c = indices[triangle * 3 + 2];
+
+ unsigned int live = live_triangles[a] + live_triangles[b] + live_triangles[c];
+
+ if (live < best_neighbor_live)
+ {
+ best_neighbor = triangle;
+ best_neighbor_live = live;
+ }
+ }
+
+ // add the neighbor to the list of seeds; the list is unsorted and the replacement criteria is approximate
+ if (best_neighbor == ~0u)
+ continue;
+
+ float dx = triangles[best_neighbor].px - cornerx, dy = triangles[best_neighbor].py - cornery, dz = triangles[best_neighbor].pz - cornerz;
+ float best_neighbor_score = sqrtf(dx * dx + dy * dy + dz * dz);
+
+ for (size_t j = 0; j < kMeshletAddSeeds; ++j)
+ {
+ // non-strict comparison reduces the number of duplicate seeds (triangles adjacent to multiple vertices)
+ if (best_neighbor_live < best_live[j] || (best_neighbor_live == best_live[j] && best_neighbor_score <= best_score[j]))
+ {
+ best_seeds[j] = best_neighbor;
+ best_live[j] = best_neighbor_live;
+ best_score[j] = best_neighbor_score;
+ break;
+ }
+ }
+ }
+
+ // add surviving seeds to the meshlet
+ size_t seed_count = 0;
+
+ for (size_t i = 0; i < kMeshletAddSeeds; ++i)
+ if (best_seeds[i] != ~0u)
+ seeds[seed_count++] = best_seeds[i];
+
+ return seed_count;
+}
+
+static size_t pruneSeedTriangles(unsigned int* seeds, size_t seed_count, const unsigned char* emitted_flags)
+{
+ size_t result = 0;
+
+ for (size_t i = 0; i < seed_count; ++i)
+ {
+ unsigned int index = seeds[i];
+
+ seeds[result] = index;
+ result += emitted_flags[index] == 0;
+ }
+
+ return result;
+}
+
+static unsigned int selectSeedTriangle(const unsigned int* seeds, size_t seed_count, const unsigned int* indices, const Cone* triangles, const unsigned int* live_triangles, float cornerx, float cornery, float cornerz)
+{
+ unsigned int best_seed = ~0u;
+ unsigned int best_live = ~0u;
+ float best_score = FLT_MAX;
+
+ for (size_t i = 0; i < seed_count; ++i)
+ {
+ unsigned int index = seeds[i];
+ unsigned int a = indices[index * 3 + 0], b = indices[index * 3 + 1], c = indices[index * 3 + 2];
+
+ unsigned int live = live_triangles[a] + live_triangles[b] + live_triangles[c];
+ float dx = triangles[index].px - cornerx, dy = triangles[index].py - cornery, dz = triangles[index].pz - cornerz;
+ float score = sqrtf(dx * dx + dy * dy + dz * dz);
+
+ if (live < best_live || (live == best_live && score < best_score))
+ {
+ best_seed = index;
+ best_live = live;
+ best_score = score;
+ }
+ }
+
+ return best_seed;
+}
+
struct KDNode
{
union
@@ -364,13 +589,13 @@ struct KDNode
unsigned int index;
};
- // leaves: axis = 3, children = number of extra points after this one (0 if 'index' is the only point)
+ // leaves: axis = 3, children = number of points including this one
// branches: axis != 3, left subtree = skip 1, right subtree = skip 1+children
unsigned int axis : 2;
unsigned int children : 30;
};
-static size_t kdtreePartition(unsigned int* indices, size_t count, const float* points, size_t stride, unsigned int axis, float pivot)
+static size_t kdtreePartition(unsigned int* indices, size_t count, const float* points, size_t stride, int axis, float pivot)
{
size_t m = 0;
@@ -400,7 +625,7 @@ static size_t kdtreeBuildLeaf(size_t offset, KDNode* nodes, size_t node_count, u
result.index = indices[0];
result.axis = 3;
- result.children = unsigned(count - 1);
+ result.children = unsigned(count);
// all remaining points are stored in nodes immediately following the leaf
for (size_t i = 1; i < count; ++i)
@@ -415,7 +640,7 @@ static size_t kdtreeBuildLeaf(size_t offset, KDNode* nodes, size_t node_count, u
return offset + count;
}
-static size_t kdtreeBuild(size_t offset, KDNode* nodes, size_t node_count, const float* points, size_t stride, unsigned int* indices, size_t count, size_t leaf_size)
+static size_t kdtreeBuild(size_t offset, KDNode* nodes, size_t node_count, const float* points, size_t stride, unsigned int* indices, size_t count, size_t leaf_size, int depth)
{
assert(count > 0);
assert(offset < node_count);
@@ -441,13 +666,14 @@ static size_t kdtreeBuild(size_t offset, KDNode* nodes, size_t node_count, const
}
// split axis is one where the variance is largest
- unsigned int axis = (vars[0] >= vars[1] && vars[0] >= vars[2]) ? 0 : (vars[1] >= vars[2] ? 1 : 2);
+ int axis = (vars[0] >= vars[1] && vars[0] >= vars[2]) ? 0 : (vars[1] >= vars[2] ? 1 : 2);
float split = mean[axis];
size_t middle = kdtreePartition(indices, count, points, stride, axis, split);
// when the partition is degenerate simply consolidate the points into a single node
- if (middle <= leaf_size / 2 || middle >= count - leaf_size / 2)
+ // this also ensures recursion depth is bounded on pathological inputs
+ if (middle <= leaf_size / 2 || middle >= count - leaf_size / 2 || depth >= kMeshletMaxTreeDepth)
return kdtreeBuildLeaf(offset, nodes, node_count, indices, count);
KDNode& result = nodes[offset];
@@ -456,35 +682,40 @@ static size_t kdtreeBuild(size_t offset, KDNode* nodes, size_t node_count, const
result.axis = axis;
// left subtree is right after our node
- size_t next_offset = kdtreeBuild(offset + 1, nodes, node_count, points, stride, indices, middle, leaf_size);
+ size_t next_offset = kdtreeBuild(offset + 1, nodes, node_count, points, stride, indices, middle, leaf_size, depth + 1);
// distance to the right subtree is represented explicitly
+ assert(next_offset - offset > 1);
result.children = unsigned(next_offset - offset - 1);
- return kdtreeBuild(next_offset, nodes, node_count, points, stride, indices + middle, count - middle, leaf_size);
+ return kdtreeBuild(next_offset, nodes, node_count, points, stride, indices + middle, count - middle, leaf_size, depth + 1);
}
static void kdtreeNearest(KDNode* nodes, unsigned int root, const float* points, size_t stride, const unsigned char* emitted_flags, const float* position, unsigned int& result, float& limit)
{
const KDNode& node = nodes[root];
+ if (node.children == 0)
+ return;
+
if (node.axis == 3)
{
// leaf
- for (unsigned int i = 0; i <= node.children; ++i)
+ bool inactive = true;
+
+ for (unsigned int i = 0; i < node.children; ++i)
{
unsigned int index = nodes[root + i].index;
if (emitted_flags[index])
continue;
+ inactive = false;
+
const float* point = points + index * stride;
- float distance2 =
- (point[0] - position[0]) * (point[0] - position[0]) +
- (point[1] - position[1]) * (point[1] - position[1]) +
- (point[2] - position[2]) * (point[2] - position[2]);
- float distance = sqrtf(distance2);
+ float dx = point[0] - position[0], dy = point[1] - position[1], dz = point[2] - position[2];
+ float distance = sqrtf(dx * dx + dy * dy + dz * dz);
if (distance < limit)
{
@@ -492,6 +723,10 @@ static void kdtreeNearest(KDNode* nodes, unsigned int root, const float* points,
limit = distance;
}
}
+
+ // deactivate leaves that no longer have items to emit
+ if (inactive)
+ nodes[root].children = 0;
}
else
{
@@ -500,6 +735,12 @@ static void kdtreeNearest(KDNode* nodes, unsigned int root, const float* points,
unsigned int first = (delta <= 0) ? 0 : node.children;
unsigned int second = first ^ node.children;
+ // deactivate branches that no longer have items to emit to accelerate traversal
+ // note that we do this *before* recursing which delays deactivation but keeps tail calls
+ if ((nodes[root + 1 + first].children | nodes[root + 1 + second].children) == 0)
+ nodes[root].children = 0;
+
+ // recursion depth is bounded by tree depth (which is limited by construction)
kdtreeNearest(nodes, root + 1 + first, points, stride, emitted_flags, position, result, limit);
// only process the other node if it can have a match based on closest distance so far
@@ -508,6 +749,380 @@ static void kdtreeNearest(KDNode* nodes, unsigned int root, const float* points,
}
}
+struct BVHBoxT
+{
+ float min[4];
+ float max[4];
+};
+
+struct BVHBox
+{
+ float min[3];
+ float max[3];
+};
+
+#if defined(SIMD_SSE)
+static float boxMerge(BVHBoxT& box, const BVHBox& other)
+{
+ __m128 min = _mm_loadu_ps(box.min);
+ __m128 max = _mm_loadu_ps(box.max);
+
+ // note: over-read is safe because BVHBox array is allocated with padding
+ min = _mm_min_ps(min, _mm_loadu_ps(other.min));
+ max = _mm_max_ps(max, _mm_loadu_ps(other.max));
+
+ _mm_storeu_ps(box.min, min);
+ _mm_storeu_ps(box.max, max);
+
+ __m128 size = _mm_sub_ps(max, min);
+ __m128 size_yzx = _mm_shuffle_ps(size, size, _MM_SHUFFLE(0, 0, 2, 1));
+ __m128 mul = _mm_mul_ps(size, size_yzx);
+ __m128 sum_xy = _mm_add_ss(mul, _mm_shuffle_ps(mul, mul, _MM_SHUFFLE(1, 1, 1, 1)));
+ __m128 sum_xyz = _mm_add_ss(sum_xy, _mm_shuffle_ps(mul, mul, _MM_SHUFFLE(2, 2, 2, 2)));
+
+ return _mm_cvtss_f32(sum_xyz);
+}
+#elif defined(SIMD_NEON)
+static float boxMerge(BVHBoxT& box, const BVHBox& other)
+{
+ float32x4_t min = vld1q_f32(box.min);
+ float32x4_t max = vld1q_f32(box.max);
+
+ // note: over-read is safe because BVHBox array is allocated with padding
+ min = vminq_f32(min, vld1q_f32(other.min));
+ max = vmaxq_f32(max, vld1q_f32(other.max));
+
+ vst1q_f32(box.min, min);
+ vst1q_f32(box.max, max);
+
+ float32x4_t size = vsubq_f32(max, min);
+ float32x4_t size_yzx = vextq_f32(vextq_f32(size, size, 3), size, 2);
+ float32x4_t mul = vmulq_f32(size, size_yzx);
+ float sum_xy = vgetq_lane_f32(mul, 0) + vgetq_lane_f32(mul, 1);
+ float sum_xyz = sum_xy + vgetq_lane_f32(mul, 2);
+
+ return sum_xyz;
+}
+#else
+static float boxMerge(BVHBoxT& box, const BVHBox& other)
+{
+ for (int k = 0; k < 3; ++k)
+ {
+ box.min[k] = other.min[k] < box.min[k] ? other.min[k] : box.min[k];
+ box.max[k] = other.max[k] > box.max[k] ? other.max[k] : box.max[k];
+ }
+
+ float sx = box.max[0] - box.min[0], sy = box.max[1] - box.min[1], sz = box.max[2] - box.min[2];
+ return sx * sy + sx * sz + sy * sz;
+}
+#endif
+
+inline unsigned int radixFloat(unsigned int v)
+{
+ // if sign bit is 0, flip sign bit
+ // if sign bit is 1, flip everything
+ unsigned int mask = (int(v) >> 31) | 0x80000000;
+ return v ^ mask;
+}
+
+static void computeHistogram(unsigned int (&hist)[1024][3], const float* data, size_t count)
+{
+ memset(hist, 0, sizeof(hist));
+
+ const unsigned int* bits = reinterpret_cast(data);
+
+ // compute 3 10-bit histograms in parallel (dropping 2 LSB)
+ for (size_t i = 0; i < count; ++i)
+ {
+ unsigned int id = radixFloat(bits[i]);
+
+ hist[(id >> 2) & 1023][0]++;
+ hist[(id >> 12) & 1023][1]++;
+ hist[(id >> 22) & 1023][2]++;
+ }
+
+ unsigned int sum0 = 0, sum1 = 0, sum2 = 0;
+
+ // replace histogram data with prefix histogram sums in-place
+ for (int i = 0; i < 1024; ++i)
+ {
+ unsigned int hx = hist[i][0], hy = hist[i][1], hz = hist[i][2];
+
+ hist[i][0] = sum0;
+ hist[i][1] = sum1;
+ hist[i][2] = sum2;
+
+ sum0 += hx;
+ sum1 += hy;
+ sum2 += hz;
+ }
+
+ assert(sum0 == count && sum1 == count && sum2 == count);
+}
+
+static void radixPass(unsigned int* destination, const unsigned int* source, const float* keys, size_t count, unsigned int (&hist)[1024][3], int pass)
+{
+ const unsigned int* bits = reinterpret_cast(keys);
+ int bitoff = pass * 10 + 2; // drop 2 LSB to be able to use 3 10-bit passes
+
+ for (size_t i = 0; i < count; ++i)
+ {
+ unsigned int id = (radixFloat(bits[source[i]]) >> bitoff) & 1023;
+
+ destination[hist[id][pass]++] = source[i];
+ }
+}
+
+static void bvhPrepare(BVHBox* boxes, float* centroids, const unsigned int* indices, size_t face_count, const float* vertex_positions, size_t vertex_count, size_t vertex_stride_float)
+{
+ (void)vertex_count;
+
+ for (size_t i = 0; i < face_count; ++i)
+ {
+ unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2];
+ assert(a < vertex_count && b < vertex_count && c < vertex_count);
+
+ const float* va = vertex_positions + vertex_stride_float * a;
+ const float* vb = vertex_positions + vertex_stride_float * b;
+ const float* vc = vertex_positions + vertex_stride_float * c;
+
+ BVHBox& box = boxes[i];
+
+ for (int k = 0; k < 3; ++k)
+ {
+ box.min[k] = va[k] < vb[k] ? va[k] : vb[k];
+ box.min[k] = vc[k] < box.min[k] ? vc[k] : box.min[k];
+
+ box.max[k] = va[k] > vb[k] ? va[k] : vb[k];
+ box.max[k] = vc[k] > box.max[k] ? vc[k] : box.max[k];
+
+ centroids[i + face_count * k] = (box.min[k] + box.max[k]) / 2.f;
+ }
+ }
+}
+
+static size_t bvhCountVertices(const unsigned int* order, size_t count, short* used, const unsigned int* indices, unsigned int* out = NULL)
+{
+ // count number of unique vertices
+ size_t used_vertices = 0;
+ for (size_t i = 0; i < count; ++i)
+ {
+ unsigned int index = order[i];
+ unsigned int a = indices[index * 3 + 0], b = indices[index * 3 + 1], c = indices[index * 3 + 2];
+
+ used_vertices += (used[a] < 0) + (used[b] < 0) + (used[c] < 0);
+ used[a] = used[b] = used[c] = 1;
+
+ if (out)
+ out[i] = unsigned(used_vertices);
+ }
+
+ // reset used[] for future invocations
+ for (size_t i = 0; i < count; ++i)
+ {
+ unsigned int index = order[i];
+ unsigned int a = indices[index * 3 + 0], b = indices[index * 3 + 1], c = indices[index * 3 + 2];
+
+ used[a] = used[b] = used[c] = -1;
+ }
+
+ return used_vertices;
+}
+
+static void bvhPackLeaf(unsigned char* boundary, size_t count)
+{
+ // mark meshlet boundary for future reassembly
+ assert(count > 0);
+
+ boundary[0] = 1;
+ memset(boundary + 1, 0, count - 1);
+}
+
+static void bvhPackTail(unsigned char* boundary, const unsigned int* order, size_t count, short* used, const unsigned int* indices, size_t max_vertices, size_t max_triangles)
+{
+ for (size_t i = 0; i < count;)
+ {
+ size_t chunk = i + max_triangles <= count ? max_triangles : count - i;
+
+ if (bvhCountVertices(order + i, chunk, used, indices) <= max_vertices)
+ {
+ bvhPackLeaf(boundary + i, chunk);
+ i += chunk;
+ continue;
+ }
+
+ // chunk is vertex bound, split it into smaller meshlets
+ assert(chunk > max_vertices / 3);
+
+ bvhPackLeaf(boundary + i, max_vertices / 3);
+ i += max_vertices / 3;
+ }
+}
+
+static bool bvhDivisible(size_t count, size_t min, size_t max)
+{
+ // count is representable as a sum of values in [min..max] if if it in range of [k*min..k*min+k*(max-min)]
+ // equivalent to ceil(count / max) <= floor(count / min), but the form below allows using idiv (see nv_cluster_builder)
+ // we avoid expensive integer divisions in the common case where min is <= max/2
+ return min * 2 <= max ? count >= min : count % min <= (count / min) * (max - min);
+}
+
+static void bvhComputeArea(float* areas, const BVHBox* boxes, const unsigned int* order, size_t count)
+{
+ BVHBoxT accuml = {{FLT_MAX, FLT_MAX, FLT_MAX, 0}, {-FLT_MAX, -FLT_MAX, -FLT_MAX, 0}};
+ BVHBoxT accumr = accuml;
+
+ for (size_t i = 0; i < count; ++i)
+ {
+ float larea = boxMerge(accuml, boxes[order[i]]);
+ float rarea = boxMerge(accumr, boxes[order[count - 1 - i]]);
+
+ areas[i] = larea;
+ areas[i + count] = rarea;
+ }
+}
+
+static size_t bvhPivot(const float* areas, const unsigned int* vertices, size_t count, size_t step, size_t min, size_t max, float fill, size_t maxfill, float* out_cost)
+{
+ bool aligned = count >= min * 2 && bvhDivisible(count, min, max);
+ size_t end = aligned ? count - min : count - 1;
+
+ float rmaxfill = 1.f / float(int(maxfill));
+
+ // find best split that minimizes SAH
+ size_t bestsplit = 0;
+ float bestcost = FLT_MAX;
+
+ for (size_t i = min - 1; i < end; i += step)
+ {
+ size_t lsplit = i + 1, rsplit = count - (i + 1);
+
+ if (!bvhDivisible(lsplit, min, max))
+ continue;
+ if (aligned && !bvhDivisible(rsplit, min, max))
+ continue;
+
+ // areas[x] = inclusive surface area of boxes[0..x]
+ // areas[count-1-x] = inclusive surface area of boxes[x..count-1]
+ float larea = areas[i], rarea = areas[(count - 1 - (i + 1)) + count];
+ float cost = larea * float(int(lsplit)) + rarea * float(int(rsplit));
+
+ if (cost > bestcost)
+ continue;
+
+ // use vertex fill when splitting vertex limited clusters; note that we use the same (left->right) vertex count
+ // using bidirectional vertex counts is a little more expensive to compute and produces slightly worse results in practice
+ size_t lfill = vertices ? vertices[i] : lsplit;
+ size_t rfill = vertices ? vertices[i] : rsplit;
+
+ // fill cost; use floating point math to round up to maxfill to avoid expensive integer modulo
+ int lrest = int(float(int(lfill + maxfill - 1)) * rmaxfill) * int(maxfill) - int(lfill);
+ int rrest = int(float(int(rfill + maxfill - 1)) * rmaxfill) * int(maxfill) - int(rfill);
+
+ cost += fill * (float(lrest) * larea + float(rrest) * rarea);
+
+ if (cost < bestcost)
+ {
+ bestcost = cost;
+ bestsplit = i + 1;
+ }
+ }
+
+ *out_cost = bestcost;
+ return bestsplit;
+}
+
+static void bvhPartition(unsigned int* target, const unsigned int* order, const unsigned char* sides, size_t split, size_t count)
+{
+ size_t l = 0, r = split;
+
+ for (size_t i = 0; i < count; ++i)
+ {
+ unsigned char side = sides[order[i]];
+ target[side ? r : l] = order[i];
+ l += 1;
+ l -= side;
+ r += side;
+ }
+
+ assert(l == split && r == count);
+}
+
+static void bvhSplit(const BVHBox* boxes, unsigned int* orderx, unsigned int* ordery, unsigned int* orderz, unsigned char* boundary, size_t count, int depth, void* scratch, short* used, const unsigned int* indices, size_t max_vertices, size_t min_triangles, size_t max_triangles, float fill_weight)
+{
+ if (count <= max_triangles && bvhCountVertices(orderx, count, used, indices) <= max_vertices)
+ return bvhPackLeaf(boundary, count);
+
+ unsigned int* axes[3] = {orderx, ordery, orderz};
+
+ // we can use step=1 unconditionally but to reduce the cost for min=max case we use step=max
+ size_t step = min_triangles == max_triangles && count > max_triangles ? max_triangles : 1;
+
+ // if we could not pack the meshlet, we must be vertex bound
+ size_t mint = count <= max_triangles && max_vertices / 3 < min_triangles ? max_vertices / 3 : min_triangles;
+ size_t maxfill = count <= max_triangles ? max_vertices : max_triangles;
+
+ // find best split that minimizes SAH
+ int bestk = -1;
+ size_t bestsplit = 0;
+ float bestcost = FLT_MAX;
+
+ for (int k = 0; k < 3; ++k)
+ {
+ float* areas = static_cast(scratch);
+ unsigned int* vertices = NULL;
+
+ bvhComputeArea(areas, boxes, axes[k], count);
+
+ if (count <= max_triangles)
+ {
+ // for vertex bound clusters, count number of unique vertices for each split
+ vertices = reinterpret_cast(areas + 2 * count);
+ bvhCountVertices(axes[k], count, used, indices, vertices);
+ }
+
+ float axiscost = FLT_MAX;
+ size_t axissplit = bvhPivot(areas, vertices, count, step, mint, max_triangles, fill_weight, maxfill, &axiscost);
+
+ if (axissplit && axiscost < bestcost)
+ {
+ bestk = k;
+ bestcost = axiscost;
+ bestsplit = axissplit;
+ }
+ }
+
+ // this may happen if SAH costs along the admissible splits are NaN, or due to imbalanced splits on pathological inputs
+ if (bestk < 0 || depth >= kMeshletMaxTreeDepth)
+ return bvhPackTail(boundary, orderx, count, used, indices, max_vertices, max_triangles);
+
+ // mark sides of split for partitioning
+ unsigned char* sides = static_cast(scratch) + count * sizeof(unsigned int);
+
+ for (size_t i = 0; i < bestsplit; ++i)
+ sides[axes[bestk][i]] = 0;
+
+ for (size_t i = bestsplit; i < count; ++i)
+ sides[axes[bestk][i]] = 1;
+
+ // partition all axes into two sides, maintaining order
+ unsigned int* temp = static_cast(scratch);
+
+ for (int k = 0; k < 3; ++k)
+ {
+ if (k == bestk)
+ continue;
+
+ unsigned int* axis = axes[k];
+ memcpy(temp, axis, sizeof(unsigned int) * count);
+ bvhPartition(axis, temp, sides, bestsplit, count);
+ }
+
+ // recursion depth is bounded due to max depth check above
+ bvhSplit(boxes, orderx, ordery, orderz, boundary, bestsplit, depth + 1, scratch, used, indices, max_vertices, min_triangles, max_triangles, fill_weight);
+ bvhSplit(boxes, orderx + bestsplit, ordery + bestsplit, orderz + bestsplit, boundary + bestsplit, count - bestsplit, depth + 1, scratch, used, indices, max_vertices, min_triangles, max_triangles, fill_weight);
+}
+
} // namespace meshopt
size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_t max_triangles)
@@ -517,7 +1132,6 @@ size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_
assert(index_count % 3 == 0);
assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices);
assert(max_triangles >= 1 && max_triangles <= kMeshletMaxTriangles);
- assert(max_triangles % 4 == 0); // ensures the caller will compute output space properly as index data is 4b aligned
(void)kMeshletMaxVertices;
(void)kMeshletMaxTriangles;
@@ -532,7 +1146,7 @@ size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_
return meshlet_limit_vertices > meshlet_limit_triangles ? meshlet_limit_vertices : meshlet_limit_triangles;
}
-size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight)
+size_t meshopt_buildMeshletsFlex(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t min_triangles, size_t max_triangles, float cone_weight, float split_factor)
{
using namespace meshopt;
@@ -541,18 +1155,24 @@ size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_ve
assert(vertex_positions_stride % sizeof(float) == 0);
assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices);
- assert(max_triangles >= 1 && max_triangles <= kMeshletMaxTriangles);
- assert(max_triangles % 4 == 0); // ensures the caller will compute output space properly as index data is 4b aligned
+ assert(min_triangles >= 1 && min_triangles <= max_triangles && max_triangles <= kMeshletMaxTriangles);
assert(cone_weight >= 0 && cone_weight <= 1);
+ assert(split_factor >= 0);
+
+ if (index_count == 0)
+ return 0;
meshopt_Allocator allocator;
TriangleAdjacency2 adjacency = {};
- buildTriangleAdjacency(adjacency, indices, index_count, vertex_count, allocator);
+ if (vertex_count > index_count && index_count < (1u << 31))
+ buildTriangleAdjacencySparse(adjacency, indices, index_count, vertex_count, allocator);
+ else
+ buildTriangleAdjacency(adjacency, indices, index_count, vertex_count, allocator);
- unsigned int* live_triangles = allocator.allocate(vertex_count);
- memcpy(live_triangles, adjacency.counts, vertex_count * sizeof(unsigned int));
+ // live triangle counts; note, we alias adjacency.counts as we remove triangles after emitting them so the counts always match
+ unsigned int* live_triangles = adjacency.counts;
size_t face_count = index_count / 3;
@@ -573,11 +1193,45 @@ size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_ve
kdindices[i] = unsigned(i);
KDNode* nodes = allocator.allocate(face_count * 2);
- kdtreeBuild(0, nodes, face_count * 2, &triangles[0].px, sizeof(Cone) / sizeof(float), kdindices, face_count, /* leaf_size= */ 8);
+ kdtreeBuild(0, nodes, face_count * 2, &triangles[0].px, sizeof(Cone) / sizeof(float), kdindices, face_count, /* leaf_size= */ 8, 0);
- // index of the vertex in the meshlet, 0xff if the vertex isn't used
- unsigned char* used = allocator.allocate(vertex_count);
- memset(used, -1, vertex_count);
+ // find a specific corner of the mesh to use as a starting point for meshlet flow
+ float cornerx = FLT_MAX, cornery = FLT_MAX, cornerz = FLT_MAX;
+
+ for (size_t i = 0; i < face_count; ++i)
+ {
+ const Cone& tri = triangles[i];
+
+ cornerx = cornerx > tri.px ? tri.px : cornerx;
+ cornery = cornery > tri.py ? tri.py : cornery;
+ cornerz = cornerz > tri.pz ? tri.pz : cornerz;
+ }
+
+ // index of the vertex in the meshlet, -1 if the vertex isn't used
+ short* used = allocator.allocate(vertex_count);
+ clearUsed(used, vertex_count, indices, index_count);
+
+ // initial seed triangle is the one closest to the corner
+ unsigned int initial_seed = ~0u;
+ float initial_score = FLT_MAX;
+
+ for (size_t i = 0; i < face_count; ++i)
+ {
+ const Cone& tri = triangles[i];
+
+ float dx = tri.px - cornerx, dy = tri.py - cornery, dz = tri.pz - cornerz;
+ float score = sqrtf(dx * dx + dy * dy + dz * dz);
+
+ if (initial_seed == ~0u || score < initial_score)
+ {
+ initial_seed = unsigned(i);
+ initial_score = score;
+ }
+ }
+
+ // seed triangles to continue meshlet flow
+ unsigned int seeds[kMeshletMaxSeeds] = {};
+ size_t seed_count = 0;
meshopt_Meshlet meshlet = {};
size_t meshlet_offset = 0;
@@ -588,46 +1242,61 @@ size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_ve
{
Cone meshlet_cone = getMeshletCone(meshlet_cone_acc, meshlet.triangle_count);
- unsigned int best_extra = 0;
- unsigned int best_triangle = getNeighborTriangle(meshlet, &meshlet_cone, meshlet_vertices, indices, adjacency, triangles, live_triangles, used, meshlet_expected_radius, cone_weight, &best_extra);
+ unsigned int best_triangle = ~0u;
- // if the best triangle doesn't fit into current meshlet, the spatial scoring we've used is not very meaningful, so we re-select using topological scoring
- if (best_triangle != ~0u && (meshlet.vertex_count + best_extra > max_vertices || meshlet.triangle_count >= max_triangles))
- {
- best_triangle = getNeighborTriangle(meshlet, NULL, meshlet_vertices, indices, adjacency, triangles, live_triangles, used, meshlet_expected_radius, 0.f, NULL);
- }
+ // for the first triangle, we don't have a meshlet cone yet, so we use the initial seed
+ // to continue the meshlet, we select an adjacent triangle based on connectivity and spatial scoring
+ if (meshlet_offset == 0 && meshlet.triangle_count == 0)
+ best_triangle = initial_seed;
+ else
+ best_triangle = getNeighborTriangle(meshlet, meshlet_cone, meshlet_vertices, indices, adjacency, triangles, live_triangles, used, meshlet_expected_radius, cone_weight);
- // when we run out of neighboring triangles we need to switch to spatial search; we currently just pick the closest triangle irrespective of connectivity
+ bool split = false;
+
+ // when we run out of adjacent triangles we need to switch to spatial search; we currently just pick the closest triangle irrespective of connectivity
if (best_triangle == ~0u)
{
float position[3] = {meshlet_cone.px, meshlet_cone.py, meshlet_cone.pz};
unsigned int index = ~0u;
- float limit = FLT_MAX;
+ float distance = FLT_MAX;
- kdtreeNearest(nodes, 0, &triangles[0].px, sizeof(Cone) / sizeof(float), emitted_flags, position, index, limit);
+ kdtreeNearest(nodes, 0, &triangles[0].px, sizeof(Cone) / sizeof(float), emitted_flags, position, index, distance);
best_triangle = index;
+ split = meshlet.triangle_count >= min_triangles && split_factor > 0 && distance > meshlet_expected_radius * split_factor;
}
if (best_triangle == ~0u)
break;
+ int best_extra = (used[indices[best_triangle * 3 + 0]] < 0) + (used[indices[best_triangle * 3 + 1]] < 0) + (used[indices[best_triangle * 3 + 2]] < 0);
+
+ // if the best triangle doesn't fit into current meshlet, we re-select using seeds to maintain global flow
+ if (split || (meshlet.vertex_count + best_extra > max_vertices || meshlet.triangle_count >= max_triangles))
+ {
+ seed_count = pruneSeedTriangles(seeds, seed_count, emitted_flags);
+ seed_count = (seed_count + kMeshletAddSeeds <= kMeshletMaxSeeds) ? seed_count : kMeshletMaxSeeds - kMeshletAddSeeds;
+ seed_count += appendSeedTriangles(seeds + seed_count, meshlet, meshlet_vertices, indices, adjacency, triangles, live_triangles, cornerx, cornery, cornerz);
+
+ unsigned int best_seed = selectSeedTriangle(seeds, seed_count, indices, triangles, live_triangles, cornerx, cornery, cornerz);
+
+ // we may not find a valid seed triangle if the mesh is disconnected as seeds are based on adjacency
+ best_triangle = best_seed != ~0u ? best_seed : best_triangle;
+ }
+
unsigned int a = indices[best_triangle * 3 + 0], b = indices[best_triangle * 3 + 1], c = indices[best_triangle * 3 + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
// add meshlet to the output; when the current meshlet is full we reset the accumulated bounds
- if (appendMeshlet(meshlet, a, b, c, used, meshlets, meshlet_vertices, meshlet_triangles, meshlet_offset, max_vertices, max_triangles))
+ if (appendMeshlet(meshlet, a, b, c, used, meshlets, meshlet_vertices, meshlet_triangles, meshlet_offset, max_vertices, max_triangles, split))
{
meshlet_offset++;
memset(&meshlet_cone_acc, 0, sizeof(meshlet_cone_acc));
}
- live_triangles[a]--;
- live_triangles[b]--;
- live_triangles[c]--;
-
// remove emitted triangle from adjacency data
// this makes sure that we spend less time traversing these lists on subsequent iterations
+ // live triangle counts are updated as a byproduct of these adjustments
for (size_t k = 0; k < 3; ++k)
{
unsigned int index = indices[best_triangle * 3 + k];
@@ -656,20 +1325,23 @@ size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_ve
meshlet_cone_acc.ny += triangles[best_triangle].ny;
meshlet_cone_acc.nz += triangles[best_triangle].nz;
+ assert(!emitted_flags[best_triangle]);
emitted_flags[best_triangle] = 1;
}
if (meshlet.triangle_count)
- {
- finishMeshlet(meshlet, meshlet_triangles);
-
meshlets[meshlet_offset++] = meshlet;
- }
- assert(meshlet_offset <= meshopt_buildMeshletsBound(index_count, max_vertices, max_triangles));
+ assert(meshlet_offset <= meshopt_buildMeshletsBound(index_count, max_vertices, min_triangles));
+ assert(meshlet.triangle_offset + meshlet.triangle_count * 3 <= index_count && meshlet.vertex_offset + meshlet.vertex_count <= index_count);
return meshlet_offset;
}
+size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight)
+{
+ return meshopt_buildMeshletsFlex(meshlets, meshlet_vertices, meshlet_triangles, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride, max_vertices, max_triangles, max_triangles, cone_weight, 0.0f);
+}
+
size_t meshopt_buildMeshletsScan(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles)
{
using namespace meshopt;
@@ -678,13 +1350,12 @@ size_t meshopt_buildMeshletsScan(meshopt_Meshlet* meshlets, unsigned int* meshle
assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices);
assert(max_triangles >= 1 && max_triangles <= kMeshletMaxTriangles);
- assert(max_triangles % 4 == 0); // ensures the caller will compute output space properly as index data is 4b aligned
meshopt_Allocator allocator;
- // index of the vertex in the meshlet, 0xff if the vertex isn't used
- unsigned char* used = allocator.allocate(vertex_count);
- memset(used, -1, vertex_count);
+ // index of the vertex in the meshlet, -1 if the vertex isn't used
+ short* used = allocator.allocate(vertex_count);
+ clearUsed(used, vertex_count, indices, index_count);
meshopt_Meshlet meshlet = {};
size_t meshlet_offset = 0;
@@ -699,13 +1370,109 @@ size_t meshopt_buildMeshletsScan(meshopt_Meshlet* meshlets, unsigned int* meshle
}
if (meshlet.triangle_count)
- {
- finishMeshlet(meshlet, meshlet_triangles);
-
meshlets[meshlet_offset++] = meshlet;
- }
assert(meshlet_offset <= meshopt_buildMeshletsBound(index_count, max_vertices, max_triangles));
+ assert(meshlet.triangle_offset + meshlet.triangle_count * 3 <= index_count && meshlet.vertex_offset + meshlet.vertex_count <= index_count);
+ return meshlet_offset;
+}
+
+size_t meshopt_buildMeshletsSpatial(struct meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t min_triangles, size_t max_triangles, float fill_weight)
+{
+ using namespace meshopt;
+
+ assert(index_count % 3 == 0);
+ assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
+ assert(vertex_positions_stride % sizeof(float) == 0);
+
+ assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices);
+ assert(min_triangles >= 1 && min_triangles <= max_triangles && max_triangles <= kMeshletMaxTriangles);
+
+ if (index_count == 0)
+ return 0;
+
+ size_t face_count = index_count / 3;
+ size_t vertex_stride_float = vertex_positions_stride / sizeof(float);
+
+ meshopt_Allocator allocator;
+
+ // 3 floats plus 1 uint for sorting, or
+ // 2 floats plus 1 uint for pivoting, or
+ // 1 uint plus 1 byte for partitioning
+ float* scratch = allocator.allocate(face_count * 4);
+
+ // compute bounding boxes and centroids for sorting
+ BVHBox* boxes = allocator.allocate(face_count + 1); // padding for SIMD
+ bvhPrepare(boxes, scratch, indices, face_count, vertex_positions, vertex_count, vertex_stride_float);
+ memset(boxes + face_count, 0, sizeof(BVHBox));
+
+ unsigned int* axes = allocator.allocate(face_count * 3);
+ unsigned int* temp = reinterpret_cast(scratch) + face_count * 3;
+
+ for (int k = 0; k < 3; ++k)
+ {
+ unsigned int* order = axes + k * face_count;
+ const float* keys = scratch + k * face_count;
+
+ unsigned int hist[1024][3];
+ computeHistogram(hist, keys, face_count);
+
+ // 3-pass radix sort computes the resulting order into axes
+ for (size_t i = 0; i < face_count; ++i)
+ temp[i] = unsigned(i);
+
+ radixPass(order, temp, keys, face_count, hist, 0);
+ radixPass(temp, order, keys, face_count, hist, 1);
+ radixPass(order, temp, keys, face_count, hist, 2);
+ }
+
+ // index of the vertex in the meshlet, -1 if the vertex isn't used
+ short* used = allocator.allocate(vertex_count);
+ clearUsed(used, vertex_count, indices, index_count);
+
+ unsigned char* boundary = allocator.allocate(face_count);
+
+ bvhSplit(boxes, &axes[0], &axes[face_count], &axes[face_count * 2], boundary, face_count, 0, scratch, used, indices, max_vertices, min_triangles, max_triangles, fill_weight);
+
+ // compute the desired number of meshlets; note that on some meshes with a lot of vertex bound clusters this might go over the bound
+ size_t meshlet_count = 0;
+ for (size_t i = 0; i < face_count; ++i)
+ {
+ assert(boundary[i] <= 1);
+ meshlet_count += boundary[i];
+ }
+
+ size_t meshlet_bound = meshopt_buildMeshletsBound(index_count, max_vertices, min_triangles);
+
+ // pack triangles into meshlets according to the order and boundaries marked by bvhSplit
+ meshopt_Meshlet meshlet = {};
+ size_t meshlet_offset = 0;
+ size_t meshlet_pending = meshlet_count;
+
+ for (size_t i = 0; i < face_count; ++i)
+ {
+ assert(boundary[i] <= 1);
+ bool split = i > 0 && boundary[i] == 1;
+
+ // while we are over the limit, we ignore boundary[] data and disable splits until we free up enough space
+ if (split && meshlet_count > meshlet_bound && meshlet_offset + meshlet_pending >= meshlet_bound)
+ split = false;
+
+ unsigned int index = axes[i];
+ assert(index < face_count);
+
+ unsigned int a = indices[index * 3 + 0], b = indices[index * 3 + 1], c = indices[index * 3 + 2];
+
+ // appends triangle to the meshlet and writes previous meshlet to the output if full
+ meshlet_offset += appendMeshlet(meshlet, a, b, c, used, meshlets, meshlet_vertices, meshlet_triangles, meshlet_offset, max_vertices, max_triangles, split);
+ meshlet_pending -= boundary[i];
+ }
+
+ if (meshlet.triangle_count)
+ meshlets[meshlet_offset++] = meshlet;
+
+ assert(meshlet_offset <= meshlet_bound);
+ assert(meshlet.triangle_offset + meshlet.triangle_count * 3 <= index_count && meshlet.vertex_offset + meshlet.vertex_count <= index_count);
return meshlet_offset;
}
@@ -765,15 +1532,17 @@ meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t
if (triangles == 0)
return bounds;
+ const float rzero = 0.f;
+
// compute cluster bounding sphere; we'll use the center to determine normal cone apex as well
float psphere[4] = {};
- computeBoundingSphere(psphere, corners[0], triangles * 3);
+ computeBoundingSphere(psphere, corners[0][0], triangles * 3, sizeof(float) * 3, &rzero, 0, 7);
float center[3] = {psphere[0], psphere[1], psphere[2]};
// treating triangle normals as points, find the bounding sphere - the sphere center determines the optimal cone axis
float nsphere[4] = {};
- computeBoundingSphere(nsphere, normals, triangles);
+ computeBoundingSphere(nsphere, normals[0], triangles, sizeof(float) * 3, &rzero, 0, 3);
float axis[3] = {nsphere[0], nsphere[1], nsphere[2]};
float axislength = sqrtf(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
@@ -883,6 +1652,33 @@ meshopt_Bounds meshopt_computeMeshletBounds(const unsigned int* meshlet_vertices
return meshopt_computeClusterBounds(indices, triangle_count * 3, vertex_positions, vertex_count, vertex_positions_stride);
}
+meshopt_Bounds meshopt_computeSphereBounds(const float* positions, size_t count, size_t positions_stride, const float* radii, size_t radii_stride)
+{
+ using namespace meshopt;
+
+ assert(positions_stride >= 12 && positions_stride <= 256);
+ assert(positions_stride % sizeof(float) == 0);
+ assert((radii_stride >= 4 && radii_stride <= 256) || radii == NULL);
+ assert(radii_stride % sizeof(float) == 0);
+
+ meshopt_Bounds bounds = {};
+
+ if (count == 0)
+ return bounds;
+
+ const float rzero = 0.f;
+
+ float psphere[4] = {};
+ computeBoundingSphere(psphere, positions, count, positions_stride, radii ? radii : &rzero, radii ? radii_stride : 0, 7);
+
+ bounds.center[0] = psphere[0];
+ bounds.center[1] = psphere[1];
+ bounds.center[2] = psphere[2];
+ bounds.radius = psphere[3];
+
+ return bounds;
+}
+
void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count)
{
using namespace meshopt;
@@ -950,25 +1746,28 @@ void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* mesh
// reorder meshlet vertices for access locality assuming index buffer is scanned sequentially
unsigned int order[kMeshletMaxVertices];
- unsigned char remap[kMeshletMaxVertices];
- memset(remap, -1, vertex_count);
+ short remap[kMeshletMaxVertices];
+ memset(remap, -1, vertex_count * sizeof(short));
size_t vertex_offset = 0;
for (size_t i = 0; i < triangle_count * 3; ++i)
{
- unsigned char& r = remap[indices[i]];
+ short& r = remap[indices[i]];
- if (r == 0xff)
+ if (r < 0)
{
- r = (unsigned char)(vertex_offset);
+ r = short(vertex_offset);
order[vertex_offset] = vertices[indices[i]];
vertex_offset++;
}
- indices[i] = r;
+ indices[i] = (unsigned char)r;
}
assert(vertex_offset <= vertex_count);
memcpy(vertices, order, vertex_offset * sizeof(unsigned int));
}
+
+#undef SIMD_SSE
+#undef SIMD_NEON
diff --git a/Source/ThirdParty/meshoptimizer/vcacheanalyzer.cpp b/Source/ThirdParty/meshoptimizer/indexanalyzer.cpp
similarity index 58%
rename from Source/ThirdParty/meshoptimizer/vcacheanalyzer.cpp
rename to Source/ThirdParty/meshoptimizer/indexanalyzer.cpp
index 368274382..87ceeae66 100644
--- a/Source/ThirdParty/meshoptimizer/vcacheanalyzer.cpp
+++ b/Source/ThirdParty/meshoptimizer/indexanalyzer.cpp
@@ -71,3 +71,56 @@ meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const unsigned int* ind
return result;
}
+
+meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const unsigned int* indices, size_t index_count, size_t vertex_count, size_t vertex_size)
+{
+ assert(index_count % 3 == 0);
+ assert(vertex_size > 0 && vertex_size <= 256);
+
+ meshopt_Allocator allocator;
+
+ meshopt_VertexFetchStatistics result = {};
+
+ unsigned char* vertex_visited = allocator.allocate(vertex_count);
+ memset(vertex_visited, 0, vertex_count);
+
+ const size_t kCacheLine = 64;
+ const size_t kCacheSize = 128 * 1024;
+
+ // simple direct mapped cache; on typical mesh data this is close to 4-way cache, and this model is a gross approximation anyway
+ size_t cache[kCacheSize / kCacheLine] = {};
+
+ for (size_t i = 0; i < index_count; ++i)
+ {
+ unsigned int index = indices[i];
+ assert(index < vertex_count);
+
+ vertex_visited[index] = 1;
+
+ size_t start_address = index * vertex_size;
+ size_t end_address = start_address + vertex_size;
+
+ size_t start_tag = start_address / kCacheLine;
+ size_t end_tag = (end_address + kCacheLine - 1) / kCacheLine;
+
+ assert(start_tag < end_tag);
+
+ for (size_t tag = start_tag; tag < end_tag; ++tag)
+ {
+ size_t line = tag % (sizeof(cache) / sizeof(cache[0]));
+
+ // we store +1 since cache is filled with 0 by default
+ result.bytes_fetched += (cache[line] != tag + 1) * kCacheLine;
+ cache[line] = tag + 1;
+ }
+ }
+
+ size_t unique_vertex_count = 0;
+
+ for (size_t i = 0; i < vertex_count; ++i)
+ unique_vertex_count += vertex_visited[i];
+
+ result.overfetch = unique_vertex_count == 0 ? 0 : float(result.bytes_fetched) / float(unique_vertex_count * vertex_size);
+
+ return result;
+}
diff --git a/Source/ThirdParty/meshoptimizer/indexcodec.cpp b/Source/ThirdParty/meshoptimizer/indexcodec.cpp
index b30046005..7a8fd6867 100644
--- a/Source/ThirdParty/meshoptimizer/indexcodec.cpp
+++ b/Source/ThirdParty/meshoptimizer/indexcodec.cpp
@@ -14,6 +14,7 @@ const unsigned char kIndexHeader = 0xe0;
const unsigned char kSequenceHeader = 0xd0;
static int gEncodeIndexVersion = 1;
+const int kDecodeIndexVersion = 1;
typedef unsigned int VertexFifo[16];
typedef unsigned int EdgeFifo[16][2];
@@ -209,6 +210,7 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons
if (fer >= 0 && (fer >> 2) < 15)
{
+ // note: getEdgeFifo implicitly rotates triangles by matching a/b to existing edge
const unsigned int* order = kTriangleIndexOrder[fer & 3];
unsigned int a = indices[i + order[0]], b = indices[i + order[1]], c = indices[i + order[2]];
@@ -266,6 +268,7 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons
int fc = getVertexFifo(vertexfifo, c, vertexfifooffset);
// after rotation, a is almost always equal to next, so we don't waste bits on FIFO encoding for a
+ // note: decoder implicitly assumes that if feb=fec=0, then fea=0 (reset code); this is enforced by rotation
int fea = (a == next) ? (next++, 0) : 15;
int feb = (fb >= 0 && fb < 14) ? fb + 1 : (b == next ? (next++, 0) : 15);
int fec = (fc >= 0 && fc < 14) ? fc + 1 : (c == next ? (next++, 0) : 15);
@@ -354,11 +357,28 @@ size_t meshopt_encodeIndexBufferBound(size_t index_count, size_t vertex_count)
void meshopt_encodeIndexVersion(int version)
{
- assert(unsigned(version) <= 1);
+ assert(unsigned(version) <= unsigned(meshopt::kDecodeIndexVersion));
meshopt::gEncodeIndexVersion = version;
}
+int meshopt_decodeIndexVersion(const unsigned char* buffer, size_t buffer_size)
+{
+ if (buffer_size < 1)
+ return -1;
+
+ unsigned char header = buffer[0];
+
+ if ((header & 0xf0) != meshopt::kIndexHeader && (header & 0xf0) != meshopt::kSequenceHeader)
+ return -1;
+
+ int version = header & 0x0f;
+ if (version > meshopt::kDecodeIndexVersion)
+ return -1;
+
+ return version;
+}
+
int meshopt_decodeIndexBuffer(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size)
{
using namespace meshopt;
@@ -374,7 +394,7 @@ int meshopt_decodeIndexBuffer(void* destination, size_t index_count, size_t inde
return -1;
int version = buffer[0] & 0x0f;
- if (version > 1)
+ if (version > kDecodeIndexVersion)
return -1;
EdgeFifo edgefifo;
@@ -415,6 +435,7 @@ int meshopt_decodeIndexBuffer(void* destination, size_t index_count, size_t inde
// fifo reads are wrapped around 16 entry buffer
unsigned int a = edgefifo[(edgefifooffset - 1 - fe) & 15][0];
unsigned int b = edgefifo[(edgefifooffset - 1 - fe) & 15][1];
+ unsigned int c = 0;
int fec = codetri & 15;
@@ -424,37 +445,30 @@ int meshopt_decodeIndexBuffer(void* destination, size_t index_count, size_t inde
{
// fifo reads are wrapped around 16 entry buffer
unsigned int cf = vertexfifo[(vertexfifooffset - 1 - fec) & 15];
- unsigned int c = (fec == 0) ? next : cf;
+ c = (fec == 0) ? next : cf;
int fec0 = fec == 0;
next += fec0;
- // output triangle
- writeTriangle(destination, i, index_size, a, b, c);
-
- // push vertex/edge fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
+ // push vertex fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
pushVertexFifo(vertexfifo, c, vertexfifooffset, fec0);
-
- pushEdgeFifo(edgefifo, c, b, edgefifooffset);
- pushEdgeFifo(edgefifo, a, c, edgefifooffset);
}
else
{
- unsigned int c = 0;
-
// fec - (fec ^ 3) decodes 13, 14 into -1, 1
// note that we need to update the last index since free indices are delta-encoded
last = c = (fec != 15) ? last + (fec - (fec ^ 3)) : decodeIndex(data, last);
- // output triangle
- writeTriangle(destination, i, index_size, a, b, c);
-
// push vertex/edge fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
pushVertexFifo(vertexfifo, c, vertexfifooffset);
-
- pushEdgeFifo(edgefifo, c, b, edgefifooffset);
- pushEdgeFifo(edgefifo, a, c, edgefifooffset);
}
+
+ // push edge fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
+ pushEdgeFifo(edgefifo, c, b, edgefifooffset);
+ pushEdgeFifo(edgefifo, a, c, edgefifooffset);
+
+ // output triangle
+ writeTriangle(destination, i, index_size, a, b, c);
}
else
{
@@ -627,7 +641,7 @@ int meshopt_decodeIndexSequence(void* destination, size_t index_count, size_t in
return -1;
int version = buffer[0] & 0x0f;
- if (version > 1)
+ if (version > kDecodeIndexVersion)
return -1;
const unsigned char* data = buffer + 1;
diff --git a/Source/ThirdParty/meshoptimizer/indexgenerator.cpp b/Source/ThirdParty/meshoptimizer/indexgenerator.cpp
index f6728345a..4bf9fccad 100644
--- a/Source/ThirdParty/meshoptimizer/indexgenerator.cpp
+++ b/Source/ThirdParty/meshoptimizer/indexgenerator.cpp
@@ -5,7 +5,9 @@
#include
// This work is based on:
+// Matthias Teschner, Bruno Heidelberger, Matthias Mueller, Danat Pomeranets, Markus Gross. Optimized Spatial Hashing for Collision Detection of Deformable Objects. 2003
// John McDonald, Mark Kilgard. Crack-Free Point-Normal Triangles using Adjacent Edge Normals. 2010
+// John Hable. Variable Rate Shading with Visibility Buffer Rendering. 2024
namespace meshopt
{
@@ -85,6 +87,46 @@ struct VertexStreamHasher
}
};
+struct VertexCustomHasher
+{
+ const float* vertex_positions;
+ size_t vertex_stride_float;
+
+ int (*callback)(void*, unsigned int, unsigned int);
+ void* context;
+
+ size_t hash(unsigned int index) const
+ {
+ const unsigned int* key = reinterpret_cast(vertex_positions + index * vertex_stride_float);
+
+ unsigned int x = key[0], y = key[1], z = key[2];
+
+ // replace negative zero with zero
+ x = (x == 0x80000000) ? 0 : x;
+ y = (y == 0x80000000) ? 0 : y;
+ z = (z == 0x80000000) ? 0 : z;
+
+ // scramble bits to make sure that integer coordinates have entropy in lower bits
+ x ^= x >> 17;
+ y ^= y >> 17;
+ z ^= z >> 17;
+
+ // Optimized Spatial Hashing for Collision Detection of Deformable Objects
+ return (x * 73856093) ^ (y * 19349663) ^ (z * 83492791);
+ }
+
+ bool equal(unsigned int lhs, unsigned int rhs) const
+ {
+ const float* lp = vertex_positions + lhs * vertex_stride_float;
+ const float* rp = vertex_positions + rhs * vertex_stride_float;
+
+ if (lp[0] != rp[0] || lp[1] != rp[1] || lp[2] != rp[2])
+ return false;
+
+ return callback ? callback(context, lhs, rhs) : true;
+ }
+};
+
struct EdgeHasher
{
const unsigned int* remap;
@@ -182,6 +224,43 @@ static void buildPositionRemap(unsigned int* remap, const float* vertex_position
allocator.deallocate(vertex_table);
}
+template
+static size_t generateVertexRemap(unsigned int* remap, const unsigned int* indices, size_t index_count, size_t vertex_count, const Hash& hash, meshopt_Allocator& allocator)
+{
+ memset(remap, -1, vertex_count * sizeof(unsigned int));
+
+ size_t table_size = hashBuckets(vertex_count);
+ unsigned int* table = allocator.allocate(table_size);
+ memset(table, -1, table_size * sizeof(unsigned int));
+
+ unsigned int next_vertex = 0;
+
+ for (size_t i = 0; i < index_count; ++i)
+ {
+ unsigned int index = indices ? indices[i] : unsigned(i);
+ assert(index < vertex_count);
+
+ if (remap[index] != ~0u)
+ continue;
+
+ unsigned int* entry = hashLookup(table, table_size, hash, index, ~0u);
+
+ if (*entry == ~0u)
+ {
+ *entry = index;
+ remap[index] = next_vertex++;
+ }
+ else
+ {
+ assert(remap[*entry] != ~0u);
+ remap[index] = remap[*entry];
+ }
+ }
+
+ assert(next_vertex <= vertex_count);
+ return next_vertex;
+}
+
template
static void remapVertices(void* destination, const void* vertices, size_t vertex_count, size_t vertex_size, const unsigned int* remap)
{
@@ -196,6 +275,35 @@ static void remapVertices(void* destination, const void* vertices, size_t vertex
}
}
+template
+static void generateShadowBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const Hash& hash, meshopt_Allocator& allocator)
+{
+ unsigned int* remap = allocator.allocate(vertex_count);
+ memset(remap, -1, vertex_count * sizeof(unsigned int));
+
+ size_t table_size = hashBuckets(vertex_count);
+ unsigned int* table = allocator.allocate(table_size);
+ memset(table, -1, table_size * sizeof(unsigned int));
+
+ for (size_t i = 0; i < index_count; ++i)
+ {
+ unsigned int index = indices[i];
+ assert(index < vertex_count);
+
+ if (remap[index] == ~0u)
+ {
+ unsigned int* entry = hashLookup(table, table_size, hash, index, ~0u);
+
+ if (*entry == ~0u)
+ *entry = index;
+
+ remap[index] = *entry;
+ }
+
+ destination[i] = remap[index];
+ }
+}
+
} // namespace meshopt
size_t meshopt_generateVertexRemap(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size)
@@ -207,44 +315,9 @@ size_t meshopt_generateVertexRemap(unsigned int* destination, const unsigned int
assert(vertex_size > 0 && vertex_size <= 256);
meshopt_Allocator allocator;
-
- memset(destination, -1, vertex_count * sizeof(unsigned int));
-
VertexHasher hasher = {static_cast(vertices), vertex_size, vertex_size};
- size_t table_size = hashBuckets(vertex_count);
- unsigned int* table = allocator.allocate(table_size);
- memset(table, -1, table_size * sizeof(unsigned int));
-
- unsigned int next_vertex = 0;
-
- for (size_t i = 0; i < index_count; ++i)
- {
- unsigned int index = indices ? indices[i] : unsigned(i);
- assert(index < vertex_count);
-
- if (destination[index] == ~0u)
- {
- unsigned int* entry = hashLookup(table, table_size, hasher, index, ~0u);
-
- if (*entry == ~0u)
- {
- *entry = index;
-
- destination[index] = next_vertex++;
- }
- else
- {
- assert(destination[*entry] != ~0u);
-
- destination[index] = destination[*entry];
- }
- }
- }
-
- assert(next_vertex <= vertex_count);
-
- return next_vertex;
+ return generateVertexRemap(destination, indices, index_count, vertex_count, hasher, allocator);
}
size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count)
@@ -262,44 +335,24 @@ size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigne
}
meshopt_Allocator allocator;
-
- memset(destination, -1, vertex_count * sizeof(unsigned int));
-
VertexStreamHasher hasher = {streams, stream_count};
- size_t table_size = hashBuckets(vertex_count);
- unsigned int* table = allocator.allocate(table_size);
- memset(table, -1, table_size * sizeof(unsigned int));
+ return generateVertexRemap(destination, indices, index_count, vertex_count, hasher, allocator);
+}
- unsigned int next_vertex = 0;
+size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int (*callback)(void*, unsigned int, unsigned int), void* context)
+{
+ using namespace meshopt;
- for (size_t i = 0; i < index_count; ++i)
- {
- unsigned int index = indices ? indices[i] : unsigned(i);
- assert(index < vertex_count);
+ assert(indices || index_count == vertex_count);
+ assert(!indices || index_count % 3 == 0);
+ assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
+ assert(vertex_positions_stride % sizeof(float) == 0);
- if (destination[index] == ~0u)
- {
- unsigned int* entry = hashLookup(table, table_size, hasher, index, ~0u);
+ meshopt_Allocator allocator;
+ VertexCustomHasher hasher = {vertex_positions, vertex_positions_stride / sizeof(float), callback, context};
- if (*entry == ~0u)
- {
- *entry = index;
-
- destination[index] = next_vertex++;
- }
- else
- {
- assert(destination[*entry] != ~0u);
-
- destination[index] = destination[*entry];
- }
- }
- }
-
- assert(next_vertex <= vertex_count);
-
- return next_vertex;
+ return generateVertexRemap(destination, indices, index_count, vertex_count, hasher, allocator);
}
void meshopt_remapVertexBuffer(void* destination, const void* vertices, size_t vertex_count, size_t vertex_size, const unsigned int* remap)
@@ -361,33 +414,9 @@ void meshopt_generateShadowIndexBuffer(unsigned int* destination, const unsigned
assert(vertex_size <= vertex_stride);
meshopt_Allocator allocator;
-
- unsigned int* remap = allocator.allocate(vertex_count);
- memset(remap, -1, vertex_count * sizeof(unsigned int));
-
VertexHasher hasher = {static_cast(vertices), vertex_size, vertex_stride};
- size_t table_size = hashBuckets(vertex_count);
- unsigned int* table = allocator.allocate(table_size);
- memset(table, -1, table_size * sizeof(unsigned int));
-
- for (size_t i = 0; i < index_count; ++i)
- {
- unsigned int index = indices[i];
- assert(index < vertex_count);
-
- if (remap[index] == ~0u)
- {
- unsigned int* entry = hashLookup(table, table_size, hasher, index, ~0u);
-
- if (*entry == ~0u)
- *entry = index;
-
- remap[index] = *entry;
- }
-
- destination[i] = remap[index];
- }
+ generateShadowBuffer(destination, indices, index_count, vertex_count, hasher, allocator);
}
void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count)
@@ -405,32 +434,33 @@ void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const uns
}
meshopt_Allocator allocator;
-
- unsigned int* remap = allocator.allocate(vertex_count);
- memset(remap, -1, vertex_count * sizeof(unsigned int));
-
VertexStreamHasher hasher = {streams, stream_count};
+ generateShadowBuffer(destination, indices, index_count, vertex_count, hasher, allocator);
+}
+
+void meshopt_generatePositionRemap(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
+{
+ using namespace meshopt;
+
+ assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
+ assert(vertex_positions_stride % sizeof(float) == 0);
+
+ meshopt_Allocator allocator;
+ VertexCustomHasher hasher = {vertex_positions, vertex_positions_stride / sizeof(float), NULL, NULL};
+
size_t table_size = hashBuckets(vertex_count);
unsigned int* table = allocator.allocate(table_size);
memset(table, -1, table_size * sizeof(unsigned int));
- for (size_t i = 0; i < index_count; ++i)
+ for (size_t i = 0; i < vertex_count; ++i)
{
- unsigned int index = indices[i];
- assert(index < vertex_count);
+ unsigned int* entry = hashLookup(table, table_size, hasher, unsigned(i), ~0u);
- if (remap[index] == ~0u)
- {
- unsigned int* entry = hashLookup(table, table_size, hasher, index, ~0u);
+ if (*entry == ~0u)
+ *entry = unsigned(i);
- if (*entry == ~0u)
- *entry = index;
-
- remap[index] = *entry;
- }
-
- destination[i] = remap[index];
+ destination[i] = *entry;
}
}
@@ -576,3 +606,99 @@ void meshopt_generateTessellationIndexBuffer(unsigned int* destination, const un
memcpy(destination + i * 4, patch, sizeof(patch));
}
}
+
+size_t meshopt_generateProvokingIndexBuffer(unsigned int* destination, unsigned int* reorder, const unsigned int* indices, size_t index_count, size_t vertex_count)
+{
+ assert(index_count % 3 == 0);
+
+ meshopt_Allocator allocator;
+
+ unsigned int* remap = allocator.allocate(vertex_count);
+ memset(remap, -1, vertex_count * sizeof(unsigned int));
+
+ // compute vertex valence; this is used to prioritize least used corner
+ // note: we use 8-bit counters for performance; for outlier vertices the valence is incorrect but that just affects the heuristic
+ unsigned char* valence = allocator.allocate(vertex_count);
+ memset(valence, 0, vertex_count);
+
+ for (size_t i = 0; i < index_count; ++i)
+ {
+ unsigned int index = indices[i];
+ assert(index < vertex_count);
+
+ valence[index]++;
+ }
+
+ unsigned int reorder_offset = 0;
+
+ // assign provoking vertices; leave the rest for the next pass
+ for (size_t i = 0; i < index_count; i += 3)
+ {
+ unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2];
+ assert(a < vertex_count && b < vertex_count && c < vertex_count);
+
+ // try to rotate triangle such that provoking vertex hasn't been seen before
+ // if multiple vertices are new, prioritize the one with least valence
+ // this reduces the risk that a future triangle will have all three vertices seen
+ unsigned int va = remap[a] == ~0u ? valence[a] : ~0u;
+ unsigned int vb = remap[b] == ~0u ? valence[b] : ~0u;
+ unsigned int vc = remap[c] == ~0u ? valence[c] : ~0u;
+
+ if (vb != ~0u && vb <= va && vb <= vc)
+ {
+ // abc -> bca
+ unsigned int t = a;
+ a = b, b = c, c = t;
+ }
+ else if (vc != ~0u && vc <= va && vc <= vb)
+ {
+ // abc -> cab
+ unsigned int t = c;
+ c = b, b = a, a = t;
+ }
+
+ unsigned int newidx = reorder_offset;
+
+ // now remap[a] = ~0u or all three vertices are old
+ // recording remap[a] makes it possible to remap future references to the same index, conserving space
+ if (remap[a] == ~0u)
+ remap[a] = newidx;
+
+ // we need to clone the provoking vertex to get a unique index
+ // if all three are used the choice is arbitrary since no future triangle will be able to reuse any of these
+ reorder[reorder_offset++] = a;
+
+ // note: first vertex is final, the other two will be fixed up in next pass
+ destination[i + 0] = newidx;
+ destination[i + 1] = b;
+ destination[i + 2] = c;
+
+ // update vertex valences for corner heuristic
+ valence[a]--;
+ valence[b]--;
+ valence[c]--;
+ }
+
+ // remap or clone non-provoking vertices (iterating to skip provoking vertices)
+ int step = 1;
+
+ for (size_t i = 1; i < index_count; i += step, step ^= 3)
+ {
+ unsigned int index = destination[i];
+
+ if (remap[index] == ~0u)
+ {
+ // we haven't seen the vertex before as a provoking vertex
+ // to maintain the reference to the original vertex we need to clone it
+ unsigned int newidx = reorder_offset;
+
+ remap[index] = newidx;
+ reorder[reorder_offset++] = index;
+ }
+
+ destination[i] = remap[index];
+ }
+
+ assert(reorder_offset <= vertex_count + index_count / 3);
+ return reorder_offset;
+}
diff --git a/Source/ThirdParty/meshoptimizer/meshoptimizer.h b/Source/ThirdParty/meshoptimizer/meshoptimizer.h
index 6c8dcd7e8..c9239bc30 100644
--- a/Source/ThirdParty/meshoptimizer/meshoptimizer.h
+++ b/Source/ThirdParty/meshoptimizer/meshoptimizer.h
@@ -1,7 +1,7 @@
/**
- * meshoptimizer - version 0.21
+ * meshoptimizer - version 1.0
*
- * Copyright (C) 2016-2024, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
+ * Copyright (C) 2016-2025, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
* Report bugs and download new versions at https://github.com/zeux/meshoptimizer
*
* This library is distributed under the MIT License. See notice at the end of this file.
@@ -12,7 +12,7 @@
#include
/* Version macro; major * 1000 + minor * 10 + patch */
-#define MESHOPTIMIZER_VERSION 210 /* 0.21 */
+#define MESHOPTIMIZER_VERSION 1000 /* 1.0 */
/* If no API is defined, assume default */
#ifndef MESHOPTIMIZER_API
@@ -29,11 +29,14 @@
#endif
/* Experimental APIs have unstable interface and might have implementation that's not fully tested or optimized */
+#ifndef MESHOPTIMIZER_EXPERIMENTAL
#define MESHOPTIMIZER_EXPERIMENTAL MESHOPTIMIZER_API
+#endif
/* C interface */
#ifdef __cplusplus
-extern "C" {
+extern "C"
+{
#endif
/**
@@ -71,6 +74,19 @@ MESHOPTIMIZER_API size_t meshopt_generateVertexRemap(unsigned int* destination,
*/
MESHOPTIMIZER_API size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count);
+/**
+ * Generates a vertex remap table from the vertex buffer and an optional index buffer and returns number of unique vertices
+ * As a result, all vertices that are equivalent map to the same (new) location, with no gaps in the resulting sequence.
+ * Equivalence is checked in two steps: vertex positions are compared for equality, and then the user-specified equality function is called (if provided).
+ * Resulting remap table maps old vertices to new vertices and can be used in meshopt_remapVertexBuffer/meshopt_remapIndexBuffer.
+ *
+ * destination must contain enough space for the resulting remap table (vertex_count elements)
+ * indices can be NULL if the input is unindexed
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
+ * callback can be NULL if no additional equality check is needed; otherwise, it should return 1 if vertices with specified indices are equivalent and 0 if they are not
+ */
+MESHOPTIMIZER_API size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int (*callback)(void*, unsigned int, unsigned int), void* context);
+
/**
* Generates vertex buffer from the source vertex buffer and remap table generated by meshopt_generateVertexRemap
*
@@ -108,6 +124,16 @@ MESHOPTIMIZER_API void meshopt_generateShadowIndexBuffer(unsigned int* destinati
*/
MESHOPTIMIZER_API void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count);
+/**
+ * Generates a remap table that maps all vertices with the same position to the same (existing) index.
+ * Similarly to meshopt_generateShadowIndexBuffer, this can be helpful to pre-process meshes for position-only rendering.
+ * This can also be used to implement algorithms that require positional-only connectivity, such as hierarchical simplification.
+ *
+ * destination must contain enough space for the resulting remap table (vertex_count elements)
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
+ */
+MESHOPTIMIZER_API void meshopt_generatePositionRemap(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
+
/**
* Generate index buffer that can be used as a geometry shader input with triangle adjacency topology
* Each triangle is converted into a 6-vertex patch with the following layout:
@@ -137,10 +163,23 @@ MESHOPTIMIZER_API void meshopt_generateAdjacencyIndexBuffer(unsigned int* destin
*/
MESHOPTIMIZER_API void meshopt_generateTessellationIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
+/**
+ * Generate index buffer that can be used for visibility buffer rendering and returns the size of the reorder table
+ * Each triangle's provoking vertex index is equal to primitive id; this allows passing it to the fragment shader using flat/nointerpolation attribute.
+ * This is important for performance on hardware where primitive id can't be accessed efficiently in fragment shader.
+ * The reorder table stores the original vertex id for each vertex in the new index buffer, and should be used in the vertex shader to load vertex data.
+ * The provoking vertex is assumed to be the first vertex in the triangle; if this is not the case (OpenGL), rotate each triangle (abc -> bca) before rendering.
+ * For maximum efficiency the input index buffer should be optimized for vertex cache first.
+ *
+ * destination must contain enough space for the resulting index buffer (index_count elements)
+ * reorder must contain enough space for the worst case reorder table (vertex_count + index_count/3 elements)
+ */
+MESHOPTIMIZER_API size_t meshopt_generateProvokingIndexBuffer(unsigned int* destination, unsigned int* reorder, const unsigned int* indices, size_t index_count, size_t vertex_count);
+
/**
* Vertex transform cache optimizer
* Reorders indices to reduce the number of GPU vertex shader invocations
- * If index buffer contains multiple ranges for multiple draw calls, this functions needs to be called on each range individually.
+ * If index buffer contains multiple ranges for multiple draw calls, this function needs to be called on each range individually.
*
* destination must contain enough space for the resulting index buffer (index_count elements)
*/
@@ -159,7 +198,7 @@ MESHOPTIMIZER_API void meshopt_optimizeVertexCacheStrip(unsigned int* destinatio
* Vertex transform cache optimizer for FIFO caches
* Reorders indices to reduce the number of GPU vertex shader invocations
* Generally takes ~3x less time to optimize meshes but produces inferior results compared to meshopt_optimizeVertexCache
- * If index buffer contains multiple ranges for multiple draw calls, this functions needs to be called on each range individually.
+ * If index buffer contains multiple ranges for multiple draw calls, this function needs to be called on each range individually.
*
* destination must contain enough space for the resulting index buffer (index_count elements)
* cache_size should be less than the actual GPU cache size to avoid cache thrashing
@@ -169,7 +208,7 @@ MESHOPTIMIZER_API void meshopt_optimizeVertexCacheFifo(unsigned int* destination
/**
* Overdraw optimizer
* Reorders indices to reduce the number of GPU vertex shader invocations and the pixel overdraw
- * If index buffer contains multiple ranges for multiple draw calls, this functions needs to be called on each range individually.
+ * If index buffer contains multiple ranges for multiple draw calls, this function needs to be called on each range individually.
*
* destination must contain enough space for the resulting index buffer (index_count elements)
* indices must contain index data that is the result of meshopt_optimizeVertexCache (*not* the original mesh indices!)
@@ -182,7 +221,7 @@ MESHOPTIMIZER_API void meshopt_optimizeOverdraw(unsigned int* destination, const
* Vertex fetch cache optimizer
* Reorders vertices and changes indices to reduce the amount of GPU memory fetches during vertex processing
* Returns the number of unique vertices, which is the same as input vertex count unless some vertices are unused
- * This functions works for a single vertex stream; for multiple vertex streams, use meshopt_optimizeVertexFetchRemap + meshopt_remapVertexBuffer for each stream.
+ * This function works for a single vertex stream; for multiple vertex streams, use meshopt_optimizeVertexFetchRemap + meshopt_remapVertexBuffer for each stream.
*
* destination must contain enough space for the resulting vertex buffer (vertex_count elements)
* indices is used both as an input and as an output index buffer
@@ -212,7 +251,8 @@ MESHOPTIMIZER_API size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t
MESHOPTIMIZER_API size_t meshopt_encodeIndexBufferBound(size_t index_count, size_t vertex_count);
/**
- * Set index encoder format version
+ * Set index encoder format version (defaults to 1)
+ *
* version must specify the data format version to encode; valid values are 0 (decodable by all library versions) and 1 (decodable by 0.14+)
*/
MESHOPTIMIZER_API void meshopt_encodeIndexVersion(int version);
@@ -227,6 +267,13 @@ MESHOPTIMIZER_API void meshopt_encodeIndexVersion(int version);
*/
MESHOPTIMIZER_API int meshopt_decodeIndexBuffer(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size);
+/**
+ * Get encoded index format version
+ * Returns format version of the encoded index buffer/sequence, or -1 if the buffer header is invalid
+ * Note that a non-negative value doesn't guarantee that the buffer will be decoded correctly if the input is malformed.
+ */
+MESHOPTIMIZER_API int meshopt_decodeIndexVersion(const unsigned char* buffer, size_t buffer_size);
+
/**
* Index sequence encoder
* Encodes index sequence into an array of bytes that is generally smaller and compresses better compared to original.
@@ -254,15 +301,31 @@ MESHOPTIMIZER_API int meshopt_decodeIndexSequence(void* destination, size_t inde
* Returns encoded data size on success, 0 on error; the only error condition is if buffer doesn't have enough space
* This function works for a single vertex stream; for multiple vertex streams, call meshopt_encodeVertexBuffer for each stream.
* Note that all vertex_size bytes of each vertex are encoded verbatim, including padding which should be zero-initialized.
+ * For maximum efficiency the vertex buffer being encoded has to be quantized and optimized for locality of reference (cache/fetch) first.
*
* buffer must contain enough space for the encoded vertex buffer (use meshopt_encodeVertexBufferBound to compute worst case size)
+ * vertex_size must be a multiple of 4 (and <= 256)
*/
MESHOPTIMIZER_API size_t meshopt_encodeVertexBuffer(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size);
MESHOPTIMIZER_API size_t meshopt_encodeVertexBufferBound(size_t vertex_count, size_t vertex_size);
/**
- * Set vertex encoder format version
- * version must specify the data format version to encode; valid values are 0 (decodable by all library versions)
+ * Vertex buffer encoder
+ * Encodes vertex data just like meshopt_encodeVertexBuffer, but allows to override compression level.
+ * For compression level to take effect, the vertex encoding version must be set to 1.
+ * The default compression level implied by meshopt_encodeVertexBuffer is 2.
+ *
+ * buffer must contain enough space for the encoded vertex buffer (use meshopt_encodeVertexBufferBound to compute worst case size)
+ * vertex_size must be a multiple of 4 (and <= 256)
+ * level should be in the range [0, 3] with 0 being the fastest and 3 being the slowest and producing the best compression ratio.
+ * version should be -1 to use the default version (specified via meshopt_encodeVertexVersion), or 0/1 to override the version; per above, level won't take effect if version is 0.
+ */
+MESHOPTIMIZER_API size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level, int version);
+
+/**
+ * Set vertex encoder format version (defaults to 1)
+ *
+ * version must specify the data format version to encode; valid values are 0 (decodable by all library versions) and 1 (decodable by 0.23+)
*/
MESHOPTIMIZER_API void meshopt_encodeVertexVersion(int version);
@@ -273,32 +336,44 @@ MESHOPTIMIZER_API void meshopt_encodeVertexVersion(int version);
* The decoder is safe to use for untrusted input, but it may produce garbage data.
*
* destination must contain enough space for the resulting vertex buffer (vertex_count * vertex_size bytes)
+ * vertex_size must be a multiple of 4 (and <= 256)
*/
MESHOPTIMIZER_API int meshopt_decodeVertexBuffer(void* destination, size_t vertex_count, size_t vertex_size, const unsigned char* buffer, size_t buffer_size);
+/**
+ * Get encoded vertex format version
+ * Returns format version of the encoded vertex buffer, or -1 if the buffer header is invalid
+ * Note that a non-negative value doesn't guarantee that the buffer will be decoded correctly if the input is malformed.
+ */
+MESHOPTIMIZER_API int meshopt_decodeVertexVersion(const unsigned char* buffer, size_t buffer_size);
+
/**
* Vertex buffer filters
* These functions can be used to filter output of meshopt_decodeVertexBuffer in-place.
*
- * meshopt_decodeFilterOct decodes octahedral encoding of a unit vector with K-bit (K <= 16) signed X/Y as an input; Z must store 1.0f.
+ * meshopt_decodeFilterOct decodes octahedral encoding of a unit vector with K-bit signed X/Y as an input; Z must store 1.0f.
* Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. W is preserved as is.
*
- * meshopt_decodeFilterQuat decodes 3-component quaternion encoding with K-bit (4 <= K <= 16) component encoding and a 2-bit component index indicating which component to reconstruct.
+ * meshopt_decodeFilterQuat decodes 3-component quaternion encoding with K-bit component encoding and a 2-bit component index indicating which component to reconstruct.
* Each component is stored as an 16-bit integer; stride must be equal to 8.
*
* meshopt_decodeFilterExp decodes exponential encoding of floating-point data with 8-bit exponent and 24-bit integer mantissa as 2^E*M.
* Each 32-bit component is decoded in isolation; stride must be divisible by 4.
+ *
+ * meshopt_decodeFilterColor decodes RGBA colors from YCoCg (+A) color encoding where RGB is converted to YCoCg space with K-bit component encoding, and A is stored using K-1 bits.
+ * Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8.
*/
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterOct(void* buffer, size_t count, size_t stride);
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterQuat(void* buffer, size_t count, size_t stride);
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterExp(void* buffer, size_t count, size_t stride);
+MESHOPTIMIZER_API void meshopt_decodeFilterOct(void* buffer, size_t count, size_t stride);
+MESHOPTIMIZER_API void meshopt_decodeFilterQuat(void* buffer, size_t count, size_t stride);
+MESHOPTIMIZER_API void meshopt_decodeFilterExp(void* buffer, size_t count, size_t stride);
+MESHOPTIMIZER_API void meshopt_decodeFilterColor(void* buffer, size_t count, size_t stride);
/**
* Vertex buffer filter encoders
* These functions can be used to encode data in a format that meshopt_decodeFilter can decode
*
- * meshopt_encodeFilterOct encodes unit vectors with K-bit (K <= 16) signed X/Y as an output.
- * Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. W is preserved as is.
+ * meshopt_encodeFilterOct encodes unit vectors with K-bit (2 <= K <= 16) signed X/Y as an output.
+ * Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. Z will store 1.0f, W is preserved as is.
* Input data must contain 4 floats for every vector (count*4 total).
*
* meshopt_encodeFilterQuat encodes unit quaternions with K-bit (4 <= K <= 16) component encoding.
@@ -308,6 +383,10 @@ MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterExp(void* buffer, size_t cou
* meshopt_encodeFilterExp encodes arbitrary (finite) floating-point data with 8-bit exponent and K-bit integer mantissa (1 <= K <= 24).
* Exponent can be shared between all components of a given vector as defined by stride or all values of a given component; stride must be divisible by 4.
* Input data must contain stride/4 floats for every vector (count*stride/4 total).
+ *
+ * meshopt_encodeFilterColor encodes RGBA color data by converting RGB to YCoCg color space with K-bit (2 <= K <= 16) component encoding; A is stored using K-1 bits.
+ * Each component is stored as an 8-bit or 16-bit integer; stride must be equal to 4 or 8.
+ * Input data must contain 4 floats for every color (count*4 total).
*/
enum meshopt_EncodeExpMode
{
@@ -317,11 +396,14 @@ enum meshopt_EncodeExpMode
meshopt_EncodeExpSharedVector,
/* When encoding exponents, use shared value for each component of all vectors (best compression) */
meshopt_EncodeExpSharedComponent,
+ /* When encoding exponents, use separate values for each component, but clamp to 0 (good quality if very small values are not important) */
+ meshopt_EncodeExpClamped,
};
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeFilterOct(void* destination, size_t count, size_t stride, int bits, const float* data);
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeFilterQuat(void* destination, size_t count, size_t stride, int bits, const float* data);
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeFilterExp(void* destination, size_t count, size_t stride, int bits, const float* data, enum meshopt_EncodeExpMode mode);
+MESHOPTIMIZER_API void meshopt_encodeFilterOct(void* destination, size_t count, size_t stride, int bits, const float* data);
+MESHOPTIMIZER_API void meshopt_encodeFilterQuat(void* destination, size_t count, size_t stride, int bits, const float* data);
+MESHOPTIMIZER_API void meshopt_encodeFilterExp(void* destination, size_t count, size_t stride, int bits, const float* data, enum meshopt_EncodeExpMode mode);
+MESHOPTIMIZER_API void meshopt_encodeFilterColor(void* destination, size_t count, size_t stride, int bits, const float* data);
/**
* Simplification options
@@ -334,16 +416,34 @@ enum
meshopt_SimplifySparse = 1 << 1,
/* Treat error limit and resulting error as absolute instead of relative to mesh extents. */
meshopt_SimplifyErrorAbsolute = 1 << 2,
+ /* Remove disconnected parts of the mesh during simplification incrementally, regardless of the topological restrictions inside components. */
+ meshopt_SimplifyPrune = 1 << 3,
+ /* Produce more regular triangle sizes and shapes during simplification, at some cost to geometric and attribute quality. */
+ meshopt_SimplifyRegularize = 1 << 4,
+ /* Experimental: Allow collapses across attribute discontinuities, except for vertices that are tagged with meshopt_SimplifyVertex_Protect in vertex_lock. */
+ meshopt_SimplifyPermissive = 1 << 5,
+};
+
+/**
+ * Experimental: Simplification vertex flags/locks, for use in `vertex_lock` arrays in simplification APIs
+ */
+enum
+{
+ /* Do not move this vertex. */
+ meshopt_SimplifyVertex_Lock = 1 << 0,
+ /* Protect attribute discontinuity at this vertex; must be used together with meshopt_SimplifyPermissive option. */
+ meshopt_SimplifyVertex_Protect = 1 << 1,
};
/**
* Mesh simplifier
* Reduces the number of triangles in the mesh, attempting to preserve mesh appearance as much as possible
* The algorithm tries to preserve mesh topology and can stop short of the target goal based on topology constraints or target error.
- * If not all attributes from the input mesh are required, it's recommended to reindex the mesh using meshopt_generateShadowIndexBuffer prior to simplification.
+ * If not all attributes from the input mesh are needed, it's recommended to reindex the mesh without them prior to simplification.
* Returns the number of indices after simplification, with destination containing new index data
+ *
* The resulting index buffer references vertices from the original vertex buffer.
- * If the original vertex data isn't required, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
+ * If the original vertex data isn't needed, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
*
* destination must contain enough space for the target index buffer, worst case is index_count elements (*not* target_index_count)!
* vertex_positions should have float3 position in the first 12 bytes of each vertex
@@ -354,45 +454,94 @@ enum
MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options, float* result_error);
/**
- * Experimental: Mesh simplifier with attribute metric
- * The algorithm ehnahces meshopt_simplify by incorporating attribute values into the error metric used to prioritize simplification order; see meshopt_simplify documentation for details.
- * Note that the number of attributes affects memory requirements and running time; this algorithm requires ~1.5x more memory and time compared to meshopt_simplify when using 4 scalar attributes.
+ * Mesh simplifier with attribute metric
+ * Reduces the number of triangles in the mesh, attempting to preserve mesh appearance as much as possible.
+ * Similar to meshopt_simplify, but incorporates attribute values into the error metric used to prioritize simplification order.
+ * The algorithm tries to preserve mesh topology and can stop short of the target goal based on topology constraints or target error.
+ * If not all attributes from the input mesh are needed, it's recommended to reindex the mesh without them prior to simplification.
+ * Returns the number of indices after simplification, with destination containing new index data
*
+ * The resulting index buffer references vertices from the original vertex buffer.
+ * If the original vertex data isn't needed, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
+ * Note that the number of attributes with non-zero weights affects memory requirements and running time.
+ *
+ * destination must contain enough space for the target index buffer, worst case is index_count elements (*not* target_index_count)!
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
* vertex_attributes should have attribute_count floats for each vertex
- * attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position. The recommended weight range is [1e-3..1e-1], assuming attribute data is in [0..1] range.
- * attribute_count must be <= 16
+ * attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position
+ * attribute_count must be <= 32
* vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; 1 denotes vertices that can't be moved
- * TODO target_error/result_error currently use combined distance+attribute error; this may change in the future
+ * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1]
+ * options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default
+ * result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification
*/
-MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifyWithAttributes(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options, float* result_error);
+MESHOPTIMIZER_API size_t meshopt_simplifyWithAttributes(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options, float* result_error);
/**
- * Experimental: Mesh simplifier (sloppy)
+ * Mesh simplifier with position/attribute update
+ * Reduces the number of triangles in the mesh, attempting to preserve mesh appearance as much as possible.
+ * Similar to meshopt_simplifyWithAttributes, but destructively updates positions and attribute values for optimal appearance.
+ * The algorithm tries to preserve mesh topology and can stop short of the target goal based on topology constraints or target error.
+ * If not all attributes from the input mesh are needed, it's recommended to reindex the mesh without them prior to simplification.
+ * Returns the number of indices after simplification, indices are destructively updated with new index data
+ *
+ * The updated index buffer references vertices from the original vertex buffer, however the vertex positions and attributes are updated in-place.
+ * Creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended; if the original vertex data is needed, it should be copied before simplification.
+ * Note that the number of attributes with non-zero weights affects memory requirements and running time. Attributes with zero weights are not updated.
+ *
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
+ * vertex_attributes should have attribute_count floats for each vertex
+ * attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position
+ * attribute_count must be <= 32
+ * vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; 1 denotes vertices that can't be moved
+ * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1]
+ * options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default
+ * result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification
+ */
+MESHOPTIMIZER_API size_t meshopt_simplifyWithUpdate(unsigned int* indices, size_t index_count, float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options, float* result_error);
+
+/**
+ * Mesh simplifier (sloppy)
* Reduces the number of triangles in the mesh, sacrificing mesh appearance for simplification performance
* The algorithm doesn't preserve mesh topology but can stop short of the target goal based on target error.
* Returns the number of indices after simplification, with destination containing new index data
* The resulting index buffer references vertices from the original vertex buffer.
- * If the original vertex data isn't required, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
+ * If the original vertex data isn't needed, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
*
* destination must contain enough space for the target index buffer, worst case is index_count elements (*not* target_index_count)!
* vertex_positions should have float3 position in the first 12 bytes of each vertex
+ * vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; vertices that can't be moved should set 1 consistently for all indices with the same position
* target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1]
* result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification
*/
-MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, float* result_error);
+MESHOPTIMIZER_API size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const unsigned char* vertex_lock, size_t target_index_count, float target_error, float* result_error);
/**
- * Experimental: Point cloud simplifier
+ * Mesh simplifier (pruner)
+ * Reduces the number of triangles in the mesh by removing small isolated parts of the mesh
+ * Returns the number of indices after simplification, with destination containing new index data
+ * The resulting index buffer references vertices from the original vertex buffer.
+ * If the original vertex data isn't needed, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
+ *
+ * destination must contain enough space for the target index buffer, worst case is index_count elements
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
+ * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1]
+ */
+MESHOPTIMIZER_API size_t meshopt_simplifyPrune(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float target_error);
+
+/**
+ * Point cloud simplifier
* Reduces the number of points in the cloud to reach the given target
* Returns the number of points after simplification, with destination containing new index data
* The resulting index buffer references vertices from the original vertex buffer.
- * If the original vertex data isn't required, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
+ * If the original vertex data isn't needed, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
*
* destination must contain enough space for the target index buffer (target_vertex_count elements)
* vertex_positions should have float3 position in the first 12 bytes of each vertex
- * vertex_colors should can be NULL; when it's not NULL, it should have float3 color in the first 12 bytes of each vertex
+ * vertex_colors can be NULL; when it's not NULL, it should have float3 color in the first 12 bytes of each vertex
+ * color_weight determines relative priority of color wrt position; 1.0 is a safe default
*/
-MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_colors, size_t vertex_colors_stride, float color_weight, size_t target_vertex_count);
+MESHOPTIMIZER_API size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_colors, size_t vertex_colors_stride, float color_weight, size_t target_vertex_count);
/**
* Returns the error scaling factor used by the simplifier to convert between absolute and relative extents
@@ -440,6 +589,19 @@ struct meshopt_VertexCacheStatistics
*/
MESHOPTIMIZER_API struct meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int cache_size, unsigned int warp_size, unsigned int primgroup_size);
+struct meshopt_VertexFetchStatistics
+{
+ unsigned int bytes_fetched;
+ float overfetch; /* fetched bytes / vertex buffer size; best case 1.0 (each byte is fetched once) */
+};
+
+/**
+ * Vertex fetch cache analyzer
+ * Returns cache hit statistics using a simplified direct mapped model
+ * Results may not match actual GPU performance
+ */
+MESHOPTIMIZER_API struct meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const unsigned int* indices, size_t index_count, size_t vertex_count, size_t vertex_size);
+
struct meshopt_OverdrawStatistics
{
unsigned int pixels_covered;
@@ -456,26 +618,34 @@ struct meshopt_OverdrawStatistics
*/
MESHOPTIMIZER_API struct meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
-struct meshopt_VertexFetchStatistics
+struct meshopt_CoverageStatistics
{
- unsigned int bytes_fetched;
- float overfetch; /* fetched bytes / vertex buffer size; best case 1.0 (each byte is fetched once) */
+ float coverage[3];
+ float extent; /* viewport size in mesh coordinates */
};
/**
- * Vertex fetch cache analyzer
- * Returns cache hit statistics using a simplified direct mapped model
- * Results may not match actual GPU performance
+ * Coverage analyzer
+ * Returns coverage statistics (ratio of viewport pixels covered from each axis) using a software rasterizer
+ *
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
*/
-MESHOPTIMIZER_API struct meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const unsigned int* indices, size_t index_count, size_t vertex_count, size_t vertex_size);
+MESHOPTIMIZER_API struct meshopt_CoverageStatistics meshopt_analyzeCoverage(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
+/**
+ * Meshlet is a small mesh cluster (subset) that consists of:
+ * - triangles, an 8-bit micro triangle (index) buffer, that for each triangle specifies three local vertices to use;
+ * - vertices, a 32-bit vertex indirection buffer, that for each local vertex specifies which mesh vertex to fetch vertex attributes from.
+ *
+ * For efficiency, meshlet triangles and vertices are packed into two large arrays; this structure contains offsets and counts to access the data.
+ */
struct meshopt_Meshlet
{
/* offsets within meshlet_vertices and meshlet_triangles arrays with meshlet data */
unsigned int vertex_offset;
unsigned int triangle_offset;
- /* number of vertices and triangles used in the meshlet; data is stored in consecutive range defined by offset and count */
+ /* number of vertices and triangles used in the meshlet; data is stored in consecutive range [offset..offset+count) for vertices and [offset..offset+count*3) for triangles */
unsigned int vertex_count;
unsigned int triangle_count;
};
@@ -484,14 +654,15 @@ struct meshopt_Meshlet
* Meshlet builder
* Splits the mesh into a set of meshlets where each meshlet has a micro index buffer indexing into meshlet vertices that refer to the original vertex buffer
* The resulting data can be used to render meshes using NVidia programmable mesh shading pipeline, or in other cluster-based renderers.
+ * When targeting mesh shading hardware, for maximum efficiency meshlets should be further optimized using meshopt_optimizeMeshlet.
* When using buildMeshlets, vertex positions need to be provided to minimize the size of the resulting clusters.
* When using buildMeshletsScan, for maximum efficiency the index buffer being converted has to be optimized for vertex cache first.
*
* meshlets must contain enough space for all meshlets, worst case size can be computed with meshopt_buildMeshletsBound
- * meshlet_vertices must contain enough space for all meshlets, worst case size is equal to max_meshlets * max_vertices
- * meshlet_triangles must contain enough space for all meshlets, worst case size is equal to max_meshlets * max_triangles * 3
+ * meshlet_vertices must contain enough space for all meshlets, worst case is index_count elements (*not* vertex_count!)
+ * meshlet_triangles must contain enough space for all meshlets, worst case is index_count elements
* vertex_positions should have float3 position in the first 12 bytes of each vertex
- * max_vertices and max_triangles must not exceed implementation limits (max_vertices <= 255 - not 256!, max_triangles <= 512; max_triangles must be divisible by 4)
+ * max_vertices and max_triangles must not exceed implementation limits (max_vertices <= 256, max_triangles <= 512)
* cone_weight should be set to 0 when cone culling is not used, and a value between 0 and 1 otherwise to balance between cluster size and cone culling efficiency
*/
MESHOPTIMIZER_API size_t meshopt_buildMeshlets(struct meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight);
@@ -499,14 +670,41 @@ MESHOPTIMIZER_API size_t meshopt_buildMeshletsScan(struct meshopt_Meshlet* meshl
MESHOPTIMIZER_API size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_t max_triangles);
/**
- * Experimental: Meshlet optimizer
- * Reorders meshlet vertices and triangles to maximize locality to improve rasterizer throughput
+ * Meshlet builder with flexible cluster sizes
+ * Splits the mesh into a set of meshlets, similarly to meshopt_buildMeshlets, but allows to specify minimum and maximum number of triangles per meshlet.
+ * Clusters between min and max triangle counts are split when the cluster size would have exceeded the expected cluster size by more than split_factor.
*
- * meshlet_triangles and meshlet_vertices must refer to meshlet triangle and vertex index data; when buildMeshlets* is used, these
- * need to be computed from meshlet's vertex_offset and triangle_offset
- * triangle_count and vertex_count must not exceed implementation limits (vertex_count <= 255 - not 256!, triangle_count <= 512)
+ * meshlets must contain enough space for all meshlets, worst case size can be computed with meshopt_buildMeshletsBound using min_triangles (*not* max!)
+ * meshlet_vertices must contain enough space for all meshlets, worst case is index_count elements (*not* vertex_count!)
+ * meshlet_triangles must contain enough space for all meshlets, worst case is index_count elements
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
+ * max_vertices, min_triangles and max_triangles must not exceed implementation limits (max_vertices <= 256, max_triangles <= 512; min_triangles <= max_triangles)
+ * cone_weight should be set to 0 when cone culling is not used, and a value between 0 and 1 otherwise to balance between cluster size and cone culling efficiency
+ * split_factor should be set to a non-negative value; when greater than 0, clusters that have large bounds may be split unless they are under the min_triangles threshold
*/
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count);
+MESHOPTIMIZER_API size_t meshopt_buildMeshletsFlex(struct meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t min_triangles, size_t max_triangles, float cone_weight, float split_factor);
+
+/**
+ * Meshlet builder that produces clusters optimized for raytracing
+ * Splits the mesh into a set of meshlets, similarly to meshopt_buildMeshlets, but optimizes cluster subdivision for raytracing and allows to specify minimum and maximum number of triangles per meshlet.
+ *
+ * meshlets must contain enough space for all meshlets, worst case size can be computed with meshopt_buildMeshletsBound using min_triangles (*not* max!)
+ * meshlet_vertices must contain enough space for all meshlets, worst case is index_count elements (*not* vertex_count!)
+ * meshlet_triangles must contain enough space for all meshlets, worst case is index_count elements
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
+ * max_vertices, min_triangles and max_triangles must not exceed implementation limits (max_vertices <= 256, max_triangles <= 512; min_triangles <= max_triangles)
+ * fill_weight allows to prioritize clusters that are closer to maximum size at some cost to SAH quality; 0.5 is a safe default
+ */
+MESHOPTIMIZER_API size_t meshopt_buildMeshletsSpatial(struct meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t min_triangles, size_t max_triangles, float fill_weight);
+
+/**
+ * Meshlet optimizer
+ * Reorders meshlet vertices and triangles to maximize locality which can improve rasterizer throughput or ray tracing performance when using fast-build modes.
+ *
+ * meshlet_triangles and meshlet_vertices must refer to meshlet data; when buildMeshlets* is used, these need to be computed from meshlet's vertex_offset and triangle_offset
+ * triangle_count and vertex_count must not exceed implementation limits (vertex_count <= 256, triangle_count <= 512)
+ */
+MESHOPTIMIZER_API void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count);
struct meshopt_Bounds
{
@@ -544,11 +742,35 @@ struct meshopt_Bounds
* Real-Time Rendering 4th Edition, section 19.3).
*
* vertex_positions should have float3 position in the first 12 bytes of each vertex
- * index_count/3 should be less than or equal to 512 (the function assumes clusters of limited size)
+ * vertex_count should specify the number of vertices in the entire mesh, not cluster or meshlet
+ * index_count/3 and triangle_count must not exceed implementation limits (<= 512)
*/
MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeMeshletBounds(const unsigned int* meshlet_vertices, const unsigned char* meshlet_triangles, size_t triangle_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
+/**
+ * Sphere bounds generator
+ * Creates bounding sphere around a set of points or a set of spheres; returns the center and radius of the sphere, with other fields of the result set to 0.
+ *
+ * positions should have float3 position in the first 12 bytes of each element
+ * radii can be NULL; when it's not NULL, it should have a non-negative float radius in the first 4 bytes of each element
+ */
+MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeSphereBounds(const float* positions, size_t count, size_t positions_stride, const float* radii, size_t radii_stride);
+
+/**
+ * Cluster partitioner
+ * Partitions clusters into groups of similar size, prioritizing grouping clusters that share vertices or are close to each other.
+ * When vertex positions are not provided, only clusters that share vertices will be grouped together, which may result in small partitions for some inputs.
+ *
+ * destination must contain enough space for the resulting partition data (cluster_count elements)
+ * destination[i] will contain the partition id for cluster i, with the total number of partitions returned by the function
+ * cluster_indices should have the vertex indices referenced by each cluster, stored sequentially
+ * cluster_index_counts should have the number of indices in each cluster; sum of all cluster_index_counts must be equal to total_index_count
+ * vertex_positions can be NULL; when it's not NULL, it should have float3 position in the first 12 bytes of each vertex
+ * target_partition_size is a target size for each partition, in clusters; the resulting partitions may be smaller or larger (up to target + target/3)
+ */
+MESHOPTIMIZER_API size_t meshopt_partitionClusters(unsigned int* destination, const unsigned int* cluster_indices, size_t total_index_count, const unsigned int* cluster_index_counts, size_t cluster_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_partition_size);
+
/**
* Spatial sorter
* Generates a remap table that can be used to reorder points for spatial locality.
@@ -560,13 +782,44 @@ MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeMeshletBounds(const unsig
MESHOPTIMIZER_API void meshopt_spatialSortRemap(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
/**
- * Experimental: Spatial sorter
+ * Spatial sorter
* Reorders triangles for spatial locality, and generates a new index buffer. The resulting index buffer can be used with other functions like optimizeVertexCache.
*
* destination must contain enough space for the resulting index buffer (index_count elements)
* vertex_positions should have float3 position in the first 12 bytes of each vertex
*/
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_spatialSortTriangles(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
+MESHOPTIMIZER_API void meshopt_spatialSortTriangles(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
+
+/**
+ * Spatial clusterizer
+ * Reorders points into clusters optimized for spatial locality, and generates a new index buffer.
+ * Ensures the output can be split into cluster_size chunks where each chunk has good positional locality. Only the last chunk will be smaller than cluster_size.
+ *
+ * destination must contain enough space for the resulting index buffer (vertex_count elements)
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex
+ */
+MESHOPTIMIZER_API void meshopt_spatialClusterPoints(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t cluster_size);
+
+/**
+ * Quantize a float into half-precision (as defined by IEEE-754 fp16) floating point value
+ * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest
+ * Representable magnitude range: [6e-5; 65504]
+ * Maximum relative reconstruction error: 5e-4
+ */
+MESHOPTIMIZER_API unsigned short meshopt_quantizeHalf(float v);
+
+/**
+ * Quantize a float into a floating point value with a limited number of significant mantissa bits, preserving the IEEE-754 fp32 binary representation
+ * Preserves infinities/NaN, flushes denormals to zero, rounds to nearest
+ * Assumes N is in a valid mantissa precision range, which is 1..23
+ */
+MESHOPTIMIZER_API float meshopt_quantizeFloat(float v, int N);
+
+/**
+ * Reverse quantization of a half-precision (as defined by IEEE-754 fp16) floating point value
+ * Preserves Inf/NaN, flushes denormals to zero
+ */
+MESHOPTIMIZER_API float meshopt_dequantizeHalf(unsigned short h);
/**
* Set allocation callbacks
@@ -574,13 +827,13 @@ MESHOPTIMIZER_EXPERIMENTAL void meshopt_spatialSortTriangles(unsigned int* desti
* Note that all algorithms only allocate memory for temporary use.
* allocate/deallocate are always called in a stack-like order - last pointer to be allocated is deallocated first.
*/
-MESHOPTIMIZER_API void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV *allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV *deallocate)(void*));
+MESHOPTIMIZER_API void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV* allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV* deallocate)(void*));
#ifdef __cplusplus
} /* extern "C" */
#endif
-/* Quantization into commonly supported data formats */
+/* Quantization into fixed point normalized formats; these are only available as inline C++ functions */
#ifdef __cplusplus
/**
* Quantize a float in [0..1] range into an N-bit fixed point unorm value
@@ -595,27 +848,6 @@ inline int meshopt_quantizeUnorm(float v, int N);
* Maximum reconstruction error: 1/2^N
*/
inline int meshopt_quantizeSnorm(float v, int N);
-
-/**
- * Quantize a float into half-precision (as defined by IEEE-754 fp16) floating point value
- * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest
- * Representable magnitude range: [6e-5; 65504]
- * Maximum relative reconstruction error: 5e-4
- */
-MESHOPTIMIZER_API unsigned short meshopt_quantizeHalf(float v);
-
-/**
- * Quantize a float into a floating point value with a limited number of significant mantissa bits, preserving the IEEE-754 fp32 binary representation
- * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest
- * Assumes N is in a valid mantissa precision range, which is 1..23
- */
-MESHOPTIMIZER_API float meshopt_quantizeFloat(float v, int N);
-
-/**
- * Reverse quantization of a half-precision (as defined by IEEE-754 fp16) floating point value
- * Preserves Inf/NaN, flushes denormals to zero
- */
-MESHOPTIMIZER_API float meshopt_dequantizeHalf(unsigned short h);
#endif
/**
@@ -631,6 +863,10 @@ template
inline size_t meshopt_generateVertexRemap(unsigned int* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size);
template
inline size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const T* indices, size_t index_count, size_t vertex_count, const meshopt_Stream* streams, size_t stream_count);
+template
+inline size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, F callback);
+template
+inline size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, F callback);
template
inline void meshopt_remapIndexBuffer(T* destination, const T* indices, size_t index_count, const unsigned int* remap);
template
@@ -642,6 +878,8 @@ inline void meshopt_generateAdjacencyIndexBuffer(T* destination, const T* indice
template
inline void meshopt_generateTessellationIndexBuffer(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
template
+inline size_t meshopt_generateProvokingIndexBuffer(T* destination, unsigned int* reorder, const T* indices, size_t index_count, size_t vertex_count);
+template
inline void meshopt_optimizeVertexCache(T* destination, const T* indices, size_t index_count, size_t vertex_count);
template
inline void meshopt_optimizeVertexCacheStrip(T* destination, const T* indices, size_t index_count, size_t vertex_count);
@@ -661,29 +899,44 @@ template
inline size_t meshopt_encodeIndexSequence(unsigned char* buffer, size_t buffer_size, const T* indices, size_t index_count);
template
inline int meshopt_decodeIndexSequence(T* destination, size_t index_count, const unsigned char* buffer, size_t buffer_size);
+inline size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level);
template
inline size_t meshopt_simplify(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options = 0, float* result_error = NULL);
template
inline size_t meshopt_simplifyWithAttributes(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options = 0, float* result_error = NULL);
template
+inline size_t meshopt_simplifyWithUpdate(T* indices, size_t index_count, float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options = 0, float* result_error = NULL);
+template
inline size_t meshopt_simplifySloppy(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, float* result_error = NULL);
template
+inline size_t meshopt_simplifySloppy(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const unsigned char* vertex_lock, size_t target_index_count, float target_error, float* result_error = NULL);
+template
+inline size_t meshopt_simplifyPrune(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float target_error);
+template
inline size_t meshopt_stripify(T* destination, const T* indices, size_t index_count, size_t vertex_count, T restart_index);
template
inline size_t meshopt_unstripify(T* destination, const T* indices, size_t index_count, T restart_index);
template
-inline meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const T* indices, size_t index_count, size_t vertex_count, unsigned int cache_size, unsigned int warp_size, unsigned int buffer_size);
+inline meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const T* indices, size_t index_count, size_t vertex_count, unsigned int cache_size, unsigned int warp_size, unsigned int primgroup_size);
+template
+inline meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const T* indices, size_t index_count, size_t vertex_count, size_t vertex_size);
template
inline meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
template
-inline meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const T* indices, size_t index_count, size_t vertex_count, size_t vertex_size);
+inline meshopt_CoverageStatistics meshopt_analyzeCoverage(const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
template
inline size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight);
template
inline size_t meshopt_buildMeshletsScan(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const T* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles);
template
+inline size_t meshopt_buildMeshletsFlex(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t min_triangles, size_t max_triangles, float cone_weight, float split_factor);
+template
+inline size_t meshopt_buildMeshletsSpatial(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t min_triangles, size_t max_triangles, float fill_weight);
+template
inline meshopt_Bounds meshopt_computeClusterBounds(const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
template
+inline size_t meshopt_partitionClusters(unsigned int* destination, const T* cluster_indices, size_t total_index_count, const unsigned int* cluster_index_counts, size_t cluster_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_partition_size);
+template
inline void meshopt_spatialSortTriangles(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
#endif
@@ -717,31 +970,39 @@ inline int meshopt_quantizeSnorm(float v, int N)
class meshopt_Allocator
{
public:
- template
- struct StorageT
+ struct Storage
{
- static void* (MESHOPTIMIZER_ALLOC_CALLCONV *allocate)(size_t);
- static void (MESHOPTIMIZER_ALLOC_CALLCONV *deallocate)(void*);
+ void* (MESHOPTIMIZER_ALLOC_CALLCONV* allocate)(size_t);
+ void (MESHOPTIMIZER_ALLOC_CALLCONV* deallocate)(void*);
};
- typedef StorageT Storage;
+#ifdef MESHOPTIMIZER_ALLOC_EXPORT
+ MESHOPTIMIZER_API static Storage& storage();
+#else
+ static Storage& storage()
+ {
+ static Storage s = {::operator new, ::operator delete };
+ return s;
+ }
+#endif
meshopt_Allocator()
- : blocks()
- , count(0)
+ : blocks()
+ , count(0)
{
}
~meshopt_Allocator()
{
for (size_t i = count; i > 0; --i)
- Storage::deallocate(blocks[i - 1]);
+ storage().deallocate(blocks[i - 1]);
}
- template T* allocate(size_t size)
+ template
+ T* allocate(size_t size)
{
assert(count < sizeof(blocks) / sizeof(blocks[0]));
- T* result = static_cast(Storage::allocate(size > size_t(-1) / sizeof(T) ? size_t(-1) : size * sizeof(T)));
+ T* result = static_cast(storage().allocate(size > size_t(-1) / sizeof(T) ? size_t(-1) : size * sizeof(T)));
blocks[count++] = result;
return result;
}
@@ -749,7 +1010,7 @@ public:
void deallocate(void* ptr)
{
assert(count > 0 && blocks[count - 1] == ptr);
- Storage::deallocate(ptr);
+ storage().deallocate(ptr);
count--;
}
@@ -757,10 +1018,6 @@ private:
void* blocks[24];
size_t count;
};
-
-// This makes sure that allocate/deallocate are lazily generated in translation units that need them and are deduplicated by the linker
-template void* (MESHOPTIMIZER_ALLOC_CALLCONV *meshopt_Allocator::StorageT::allocate)(size_t) = operator new;
-template void (MESHOPTIMIZER_ALLOC_CALLCONV *meshopt_Allocator::StorageT::deallocate)(void*) = operator delete;
#endif
/* Inline implementation for C++ templated wrappers */
@@ -782,7 +1039,7 @@ struct meshopt_IndexAdapter
{
size_t size = count > size_t(-1) / sizeof(unsigned int) ? size_t(-1) : count * sizeof(unsigned int);
- data = static_cast(meshopt_Allocator::Storage::allocate(size));
+ data = static_cast(meshopt_Allocator::storage().allocate(size));
if (input)
{
@@ -799,7 +1056,7 @@ struct meshopt_IndexAdapter
result[i] = T(data[i]);
}
- meshopt_Allocator::Storage::deallocate(data);
+ meshopt_Allocator::storage().deallocate(data);
}
};
@@ -830,6 +1087,30 @@ inline size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const
return meshopt_generateVertexRemapMulti(destination, indices ? in.data : NULL, index_count, vertex_count, streams, stream_count);
}
+template
+inline size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, F callback)
+{
+ struct Call
+ {
+ static int compare(void* context, unsigned int lhs, unsigned int rhs) { return (*static_cast(context))(lhs, rhs) ? 1 : 0; }
+ };
+
+ return meshopt_generateVertexRemapCustom(destination, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride, &Call::compare, &callback);
+}
+
+template
+inline size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, F callback)
+{
+ struct Call
+ {
+ static int compare(void* context, unsigned int lhs, unsigned int rhs) { return (*static_cast(context))(lhs, rhs) ? 1 : 0; }
+ };
+
+ meshopt_IndexAdapter in(NULL, indices, indices ? index_count : 0);
+
+ return meshopt_generateVertexRemapCustom(destination, indices ? in.data : NULL, index_count, vertex_positions, vertex_count, vertex_positions_stride, &Call::compare, &callback);
+}
+
template
inline void meshopt_remapIndexBuffer(T* destination, const T* indices, size_t index_count, const unsigned int* remap)
{
@@ -875,6 +1156,19 @@ inline void meshopt_generateTessellationIndexBuffer(T* destination, const T* ind
meshopt_generateTessellationIndexBuffer(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride);
}
+template
+inline size_t meshopt_generateProvokingIndexBuffer(T* destination, unsigned int* reorder, const T* indices, size_t index_count, size_t vertex_count)
+{
+ meshopt_IndexAdapter in(NULL, indices, index_count);
+ meshopt_IndexAdapter out(destination, NULL, index_count);
+
+ size_t bound = vertex_count + (index_count / 3);
+ assert(size_t(T(bound - 1)) == bound - 1); // bound - 1 must fit in T
+ (void)bound;
+
+ return meshopt_generateProvokingIndexBuffer(out.data, reorder, in.data, index_count, vertex_count);
+}
+
template
inline void meshopt_optimizeVertexCache(T* destination, const T* indices, size_t index_count, size_t vertex_count)
{
@@ -961,6 +1255,11 @@ inline int meshopt_decodeIndexSequence(T* destination, size_t index_count, const
return meshopt_decodeIndexSequence(destination, index_count, sizeof(T), buffer, buffer_size);
}
+inline size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level)
+{
+ return meshopt_encodeVertexBufferLevel(buffer, buffer_size, vertices, vertex_count, vertex_size, level, -1);
+}
+
template
inline size_t meshopt_simplify(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options, float* result_error)
{
@@ -979,13 +1278,39 @@ inline size_t meshopt_simplifyWithAttributes(T* destination, const T* indices, s
return meshopt_simplifyWithAttributes(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, vertex_attributes, vertex_attributes_stride, attribute_weights, attribute_count, vertex_lock, target_index_count, target_error, options, result_error);
}
+template
+inline size_t meshopt_simplifyWithUpdate(T* indices, size_t index_count, float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options, float* result_error)
+{
+ meshopt_IndexAdapter inout(indices, indices, index_count);
+
+ return meshopt_simplifyWithUpdate(inout.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, vertex_attributes, vertex_attributes_stride, attribute_weights, attribute_count, vertex_lock, target_index_count, target_error, options, result_error);
+}
+
template
inline size_t meshopt_simplifySloppy(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, float* result_error)
{
meshopt_IndexAdapter in(NULL, indices, index_count);
meshopt_IndexAdapter